2011년 8월 30일 화요일

PHP로 Excel 데이터 읽고 쓰기

http://www.ibm.com/developerworks/kr/library/os-phpexcel/


PHP로 Excel 데이터 읽고 쓰기

XML 지원 사용하기
Jack Herrington, 소프트웨어 엔지니어, Leverage Software Inc.
요약: Microsoft® Excel® 2003에서 내보낸 XML에서 데이터를 읽기 위해 PHP에서 XML 지원을 사용하는 방법에 대해 알아봅니다. 또한 Excel XML로 PHP 애플리케이션에서부터 데이터를 내보내는 것을 배워서 사용자가 실제 스프레드시트에서 데이터를 확인할 수 있습니다.
이 기사에 테그:  php_(hypertext_preprocessor), xml
원문 게재일:  2010 년 8 월 26 일 (출판일: 2005 년 10 월 04 일) 번역 게재일:   2010 년 12 월 21 일
난이도:  중급 원문:  보기
페이지뷰: 3368 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 5표)
Microsoft Windows® 운영 체제용 Microsoft Office 2003은 비Microsoft 엔지니어가 아직 인식하지 못한 새로운 전체적인 기회의 세트를 열었다. 물론 새 기능의 일반 세트는 보유했었다. 하지만 새로운 중대한 향상은 XML 파일 형식이 추가된 것이다. Office 2003을 통해 Microsoft Excel 스프레드시트를 XML로 저장하고, 2진과 동등하게 파일을 사용할 수 있다. 이는 Microsoft Word에도 적용된다.
XML 파일 형식이 그렇게 중요한 이유는 무엇인가? 수 년 동안 Excel 또는 Word의 실제 성능은 정교한 변환기가 액세스해야 하는 2진 파일 형식에 묻혀 있었다. 이제는 PHP 프로그래밍 언어에 내장된 Extensible Stylesheet Language Transformation(XSLT)이나 XML Document Object Model(DOM) 함수와 같이 XML 도구를 사용하여 Excel 또는 Word 파일을 읽거나 쓸 수 있다.
이 기사에서는 이러한 형식을 사용하여 Excel 스프레드시트에서부터 데이터를 데이터베이스로 읽고 데이터베이스 테이블의 내용을 Excel 스프레드시트로 내보내도록 PHP 웹 애플리케이션을 빌드하는 방법을 알려준다.
데이터베이스 작성
이 기사에서 필자가 간단한 웹 애플리케이션을 사용하여 Excel XML 메커니즘을 명확히 확인할 수 있다. 이 애플리케이션은 이름과 이메일 주소의 테이블이다.
MySQL 구문에서 스키마는 목록 1에서 코드의 모양과 같다.

목록 1. 데이터베이스용 SQL
DROP TABLE IF EXISTS names;
CREATE TABLE names (
	id INT NOT NULL AUTO_INCREMENT,
	first TEXT,
	middle TEXT,
	last TEXT,
	email TEXT,
	PRIMARY KEY( id )
);

이 파일은 싱글 테이블 데이터베이스이며, 테이블 -- names -- 에는 자동 증분하는 ID 필드에 이어서 이름, 중간 이름 및 성 필드와 이메일 필드 등 5개의 필드가 있다.
데이터베이스를 설정하려면 Mysqladmin 명령행 도구인 mysqladmin --user=root create names를 사용하는 데이터베이스를 작성한다. 그 다음에 스키마 파일인 mysql --user=root names < schema.sql에서부터 데이터베이스를 로드한다. 사용하는 사용자 및 비밀번호 인증은 설치에 따라 다르지만, 그 개념은 동일하다. 먼저 데이터베이스를 작성한다. 그 다음에 SQL 파일을 사용하여 필수 필드로 테이블을 작성한다.
가져오기 데이터 작성
다음 단계는 가져오기를 위한 일부 데이터를 작성하는 것이다. 새 Excel 파일을 작성한다. 첫 번째 워크북에서 First, Middle, LastEmail 열의 첫 번째 행을 호출한다. 그 다음에 데이터의 몇 개의 행을 목록에 추가한다(그림 1 참조).

그림 1. 가져오기용 데이터
가져오기용 데이터
필드를 원하거나 변경하는 대로 목록을 만들 수 있지만 맞는 것을 확인한다. 이 기사에서 PHP 가져오기 스크립트는 데이터의 첫 행이 헤더 행이라고 가정하기 때문에 이를 무조건 무시한다. 프로덕션 애플리케이션에서는 어느 필드가 어느 열인지 결정하고 가져오기 논리에 맞도록 적절하게 변경하기 위한 헤더 행을 읽고 구문 분석하려 할 것이다.
마지막 단계는 File > Save As를 클릭한 다음에 Save As 창에서 Save as type 드롭다운 목록에서부터 XML Spreadsheet를 선택하여 파일을 XML로 저장하는 것이다(그림 2 참조).

그림 2. 파일을 XML 스프레드시트로 저장
파일을 XML 스프레드시트로 저장
XML 파일을 가지고 이를 통해 PHP 애플리케이션 개발을 시작할 수 있다.
데이터 가져오기
가져오기 시스템은 입력 Excel XML 파일을 지정하는 페이지를 사용하여 간편하게 시작할 수 있다(그림 3 참조).

그림 3. 입력 Excel XML 파일 지정
입력 Excel XML 파일 지정
페이지 논리는 목록 2에 보여주는 대로 간단하다.

목록 2. 업로드 페이지 코드
<html>
<body>
<form enctype="multipart/form-data"
  action="import.php" method="post">
  <input type="hidden" name="MAX_FILE_SIZE" value="2000000" />
  <table width="600">
  <tr>
  <td>Names file:</td>
  <td><input type="file" name="file" /></td>
  <td><input type="submit" value="Upload" /></td>
  </tr>
  </table>
  </form>
  </body>
  </html>
  

파일을 .php 확장자로 이름을 지정했지만 실제로는 PHP가 아니다. 이는 사용자가 파일을 지정할 수 있고 이 파일을 실제로 중요한 일이 발생하는 import.php 페이지로 제출하는 HTML 파일에 불과하다.
Excel XML 데이터 읽기
필자는 더 쉽게 따라하기 위해 두 단계에서 import.php 페이지를 썼다. 첫 번째 단계에서 필자는 간단하게 XML 데이터를 구문분석하고 이를 테이블로 출력한다. 두 번째 단계에서는 레코드를 데이터베이스로 삽입하는 논리를 추가한다.
목록 3에는 예제 Excel 2003 XML 파일을 보여준다.

목록 3. 샘플 Excel XML 파일
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:html="http://www.w3.org/TR/REC-html40">
 <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
  <Author>Jack Herrington</Author>
  <LastAuthor>Jack Herrington</LastAuthor>
  <Created>2005-08-02T04:06:26Z</Created>
  <LastSaved>2005-08-02T04:30:11Z</LastSaved>
  <Company>My Software Company, Inc.</Company>
  <Version>11.6360</Version>
  </DocumentProperties>
  <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
  <WindowHeight>8535</WindowHeight>
  <WindowWidth>12345</WindowWidth>
  <WindowTopX>480</WindowTopX>
  <WindowTopY>90</WindowTopY>
  <ProtectStructure>False</ProtectStructure>
  <ProtectWindows>False</ProtectWindows>
  </ExcelWorkbook>
  <Styles>
  <Style ss:ID="Default" ss:Name="Normal">
  <Alignment ss:Vertical="Bottom"/>
  <Borders/>
  <Font/>
  <Interior/>
  <NumberFormat/>
  <Protection/>
  </Style>
  <Style ss:ID="s21" ss:Name="Hyperlink">
  <Font ss:Color="#0000FF" ss:Underline="Single"/>
  </Style>
  <Style ss:ID="s23">
  <Font x:Family="Swiss" ss:Bold="1"/>
  </Style>
  </Styles>
  <Worksheet ss:Name="Sheet1">
  <Table ss:ExpandedColumnCount="4"
  ss:ExpandedRowCount="5" x:FullColumns="1"
  x:FullRows="1">
  <Column ss:Index="4" ss:AutoFitWidth="0" ss:Width="154.5"/>
  <Row ss:StyleID="s23">
  <Cell><Data ss:Type="String">First</Data></Cell>
  <Cell><Data ss:Type="String">Middle</Data></Cell>
  <Cell><Data ss:Type="String">Last</Data></Cell>
  <Cell><Data ss:Type="String">Email</Data></Cell>
  </Row>
  <Row>
  <Cell><Data ss:Type="String">Molly</Data></Cell>
  <Cell ss:Index="3"><Data
  ss:Type="String">Katzen</Data></Cell>
  <Cell ss:StyleID="s21" ss:HRef="mailto:molly@katzen.com">
  <Data ss:Type="String">molly@katzen.com</Data></Cell>
  </Row>
  ...
  </Table>
  <WorksheetOptions
  xmlns="urn:schemas-microsoft-com:office:excel">
  <Print>
  <ValidPrinterInfo/>
  <HorizontalResolution>300</HorizontalResolution>
  <VerticalResolution>300</VerticalResolution>
  </Print>
  <Selected/>
  <Panes>
  <Pane>
  <Number>3</Number>
  <ActiveRow>5</ActiveRow>
  </Pane>
  </Panes>
  <ProtectObjects>False</ProtectObjects>
  <ProtectScenarios>False</ProtectScenarios>
  </WorksheetOptions>
  </Worksheet>
  <Worksheet ss:Name="Sheet2">
  <WorksheetOptions
  xmlns="urn:schemas-microsoft-com:office:excel">
  <ProtectObjects>False</ProtectObjects>
  <ProtectScenarios>False</ProtectScenarios>
  </WorksheetOptions>
  </Worksheet>
  <Worksheet ss:Name="Sheet3">
  <WorksheetOptions
  xmlns="urn:schemas-microsoft-com:office:excel">
  <ProtectObjects>False</ProtectObjects>
  <ProtectScenarios>False</ProtectScenarios>
  </WorksheetOptions>
  </Worksheet>
  </Workbook>

필자는 중간에 몇 개의 행을 잘라냈지만, 그렇지 않으면 파일은 Excel에서 나온 그대로이다. 이는 상대적으로 정리된 XML이다. 초반부의 문서 헤더 부분에 문서와 그 저자를 설명하고 일부 시각적 정보를 늘어놓고 스타일을 나열하는 것을 유의하자. 그 다음에 데이터가 기본 Workbook 오브젝트에서 워크시트의 세트로 제공된다.
첫 번째 Worksheet 오브젝트에는 실제 데이터가 들어있다. 이 오브젝트에서 데이터는 Table 태그에서 RowCell 태그의 세트로 남아있다. 각 Cell 태그에는 셀에 대한 데이터를 보유하는 이와 연관된 Data 태그가 있다. 이 경우에 데이터는 언제나 String 유형으로 형식화된다.
기본값으로 새 문서를 작성할 때에 Excel은 Sheet1, Sheet2Sheet3이라는 세 개의 워크시트를 작성한다. 필자는 두 번째와 세 번째 워크시트를 삭제하지 않았기 때문에 문서의 끝부분에 이러한 빈 워크북이 보인다.
목록 4에는 import.php 스크립트의 첫 번째 버전을 보여준다.

목록 4. 가져오기 스크립트의 첫 번째 버전
<?php
  $data = array();
  
  function add_person( $first, $middle, $last, $email )
  {
  global $data;
  
  $data []= array(
  'first' => $first,
  'middle' => $middle,
  'last' => $last,
  'email' => $email 
  );
  }
  
  if ( $_FILES['file']['tmp_name'] )
  {
  $dom = DOMDocument::load( $_FILES['file']['tmp_name'] );
  $rows = $dom->getElementsByTagName( 'Row' );
  $first_row = true;
  foreach ($rows as $row)
  {
  if ( !$first_row )
  {
  $first = "";
  $middle = "";
  $last = "";
  $email = "";
  
  $index = 1;
  $cells = $row->getElementsByTagName( 'Cell' );
  foreach( $cells as $cell )
  { 
  $ind = $cell->getAttribute( 'Index' );
  if ( $ind != null ) $index = $ind;
  
  if ( $index == 1 ) $first = $cell->nodeValue;
  if ( $index == 2 ) $middle = $cell->nodeValue;
  if ( $index == 3 ) $last = $cell->nodeValue;
  if ( $index == 4 ) $email = $cell->nodeValue;
  
  $index += 1;
  }
  add_person( $first, $middle, $last, $email );
  }
  $first_row = false;
  }
  }
  ?>
  <html>
  <body>
  <table>
  <tr>
  <th>First</th>
  <th>Middle</th>
  <th>Last</th>
  <th>Email</th>
  </tr>
  <?php foreach( $data as $row ) { ?>
  <tr>
  <td><?php echo( $row['first'] ); ?></td>
  <td><?php echo( $row['middle'] ); ?></td>
  <td><?php echo( $row['last'] ); ?></td>
  <td><?php echo( $row['email'] ); ?></td>
  </tr>
  <?php } ?>
  </table>
  </body>
  </html>

스크립트는 업로드된 임시 파일에서 DOMDocument 오브젝트로 읽으면서 시작된다. 그 다음에 스크립트가 각 Row 태그를 찾는다. 첫 번째 행은 $first_row 변수와 연관된 논리를 사용하여 무시된다. 첫 번째 행 이후에 내부 루프는 행에서 각 Cell 태그를 구문 분석한다.
그 다음에 까다로운 것은 어느 열에 있는지를 파악하는 것이다. XML에서 확인 가능한 대로 Cell 태그는 열이나 행 수를 지정하지 않는다. 스크립트는 이를 그 자체로 추적해야 한다. 실제로는 이보다 훨씬 더 복잡하다. 사실 Cell 태그는 이 행에 빈 열이 있는 경우 셀이 어느 열에 있는지 알려주는 ss:Index 속성이 있다. 이는 getAttribute('index') 코드가 찾고 있는 것이다.
색인을 결정한 후에 코드는 간단하다. 셀 값을 해당 필드와 연관된 로컬 값에 놓는다. 그 다음에 행의 끝에서 add_person 함수를 호출하여 그 사람을 데이터 세트에 추가한다.
PHP는 페이지의 끝에서 익숙한 PHP 메커니즘을 사용하여 HTML 테이블로 찾은 데이터를 출력한다(그림 4 참조).

그림 4. HTML 테이블로 데이터 출력
HTML 테이블로 데이터 출력
다음 단계는 이 데이터를 데이터베이스로 로드하는 것이다.
데이터를 데이터베이스에 추가
스크립트는 PHP 데이터 구조에서 행 데이터를 가진 후에 해당 데이터를 데이터베이스로 추가해야 한다. 이를 수행하기 위해서 Pear DB 모듈을 사용하는 일부 코드를 추가했다(목록 5 참조).

목록 5. 가져오기 스크립트의 두 번째 버전
<?php
require_once( "db.php" );

$data = array();

$db =& DB::connect("mysql://root@localhost/names", array());
if (PEAR::isError($db)) { die($db->getMessage()); }

function add_person( $first, $middle, $last, $email )
{
 global $data, $db;

 $sth = $db->prepare( "INSERT INTO names VALUES( 0, ?, ?, ?, ? )" );
 $db->execute( $sth, array( $first, $middle, $last, $email ) );

 $data []= array(
   'first' => $first,
   'middle' => $middle,
   'last' => $last,
   'email' => $email
 );
}

if ( $_FILES['file']['tmp_name'] )
{
 $dom = DOMDocument::load( $_FILES['file']['tmp_name'] );
 $rows = $dom->getElementsByTagName( 'Row' );
 $first_row = true;
 foreach ($rows as $row)
 {
   if ( !$first_row )
   {
     $first = "";
     $middle = "";
     $last = "";
     $email = "";

     $index = 1;
     $cells = $row->getElementsByTagName( 'Cell' );
     foreach( $cells as $cell )
     {
       $ind = $cell->getAttribute( 'Index' );
       if ( $ind != null ) $index = $ind;

       if ( $index == 1 ) $first = $cell->nodeValue;
       if ( $index == 2 ) $middle = $cell->nodeValue;
       if ( $index == 3 ) $last = $cell->nodeValue;
       if ( $index == 4 ) $email = $cell->nodeValue;

       $index += 1;
     }
     add_person( $first, $middle, $last, $email );
   }
   $first_row = false;
 }
}
?>
<html>
<body>
These records have been added to the database:
<table>
<tr>
<th>First</th>
<th>Middle</th>
<th>Last</th>
<th>Email</th>
</tr>
<?php foreach( $data as $row ) { ?>
<tr>
<td><?php echo( $row['first'] ); ?></td><
<td><?php echo( $row['middle'] ); ?></td><
<td><?php echo( $row['last'] ); ?></td><
<td><?php echo( $row['email'] ); ?></td><
</tr>
<?php } ?>
</table>
Click <a href="list.php">here</a> for the entire table.
    </body>
</html>

그림 5에는 Firefox에서 결과물을 보여준다.

그림 5. 데이터베이스
데이터베이스
특별히 볼 것은 없지만, 그 점은 중요하지 않다. 중요한 점은 데이터베이스 오브젝트의 prepareexecute 명령문의 사용을 통해 데이터를 데이터베이스로 추가할 수 있다는 점이다. 이를 증명하기 위해 데이터베이스에서 데이터를 보여주는 list.php라는 또다른 페이지를 작성했다(목록 6 참조).

목록 6. List.php
<?php
  // Install the DB module using 'pear install DB'
  require_once( "db.php" );

  $data = array();

  $db =& DB::connect("mysql://root@localhost/names", array());
  if (PEAR::isError($db)) { die($db->getMessage()); }

  $res = $db->query( "SELECT * FROM names ORDER BY last" );
  ?>
  <html>
  <body>
  <table>
  <tr>
  <th>ID</th>
  <th>First</th>
  <th>Middle</th>
  <th>Last</th>
  <th>Email</th>
  </tr>
  <?php while( $res->fetchInto( $row,
            DB_FETCHMODE_ASSOC ) ) { ?>
  <tr>
  <td><?php echo( $row['id'] ); ?></td>
  <td><?php echo( $row['first'] ); ?></td>
  <td><?php echo( $row['middle'] ); ?></td>
  <td><?php echo( $row['last'] ); ?></td>
  <td><?php echo( $row['email'] ); ?></td>
  </tr>
  <?php } ?>
  </table>
  Download as an
  <a href="listxl.php">Excel spreadsheet</a>.
 </body>
  </html>

이 간단한 페이지는 이름 테이블에 대해 SQL select 조작을 실행하여 시작된다. 그러면 이는 테이블을 작성하고 fetchInto 메소드를 사용하여 테이블의 모든 행을 추가하여 원시 데이터를 얻는다.
그림 6에는 페이지의 결과물을 보여준다.

그림 6. list.php의 결과물
list.php의 결과물
다시 말해 미인 대회의 우승자가 아니라 이 페이지를 이용하여 데이터베이스로 데이터를 얻는 방법의 기본을 설명하였다. 결과적으로 이는 내보내기 위한 Excel XML 파일을 생성할 스크립트의 기초를 제공한다.
내보내기 Excel XML 생성
마지막 단계는 Excel XML을 생성하는 것이다. 필자는 이를 Excel XML을 PHP 스크립트로 복사하여 시작했다(목록 7 참조). 이러한 작업이 게으른 것은 알지만, 정확하게 구문 분석하는 Excel XML 파일을 얻는 가장 간편한 방법이다. (Excel은 XML에 대해 까다롭다.)

목록 7. XML 내보내기 페이지
<?php
  header( "content-type: text/xml" );
  // Install the DB module using 'pear install DB'
  require_once( "db.php" );

  $data = array();

  $db =& DB::connect("mysql://root@localhost/names", array());
  if (PEAR::isError($db)) { die($db->getMessage()); }

  $res = $db->query( "SELECT * FROM names ORDER BY last" );

  $rows = array();
  while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) )
  { $rows []= $row; }
  print "<?xml version=\"1.0\"?>\n";
  print "<?mso-application progid=\"Excel.Sheet\"?>\n";
  ?>
  <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:x="urn:schemas-microsoft-com:office:excel"
  xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:html="http://www.w3.org/TR/REC-html40">
  <DocumentProperties
     xmlns="urn:schemas-microsoft-com:office:office">
 <Author>Jack Herrington</Author>
  <LastAuthor>Jack Herrington</LastAuthor>
  <Created>2005-08-02T04:06:26Z</Created>
  <LastSaved>2005-08-02T04:30:11Z</LastSaved>
  <Company>My Company, Inc.</Company>
  <Version>11.6360</Version>
  </DocumentProperties>
  <ExcelWorkbook
     xmlns="urn:schemas-microsoft-com:office:excel">
  <WindowHeight>8535</WindowHeight>
  <WindowWidth>12345</WindowWidth>
  <WindowTopX>480</WindowTopX>
  <WindowTopY>90</WindowTopY>
  <ProtectStructure>False</ProtectStructure>
  <ProtectWindows>False</ProtectWindows>
  </ExcelWorkbook>
  <Styles>
  <Style ss:ID="Default" ss:Name="Normal">
  <Alignment ss:Vertical="Bottom"/>
  <Borders/>
  <Font/>
  <Interior/>
  <NumberFormat/>
  <Protection/>
  </Style>
  <Style ss:ID="s21" ss:Name="Hyperlink">
  <Font ss:Color="#0000FF" ss:Underline="Single"/>
  </Style>
  <Style ss:ID="s23">
  <Font x:Family="Swiss" ss:Bold="1"/>
  </Style>
  </Styles>
  <Worksheet ss:Name="Names">
  <Table ss:ExpandedColumnCount="4"
  ss:ExpandedRowCount="<?php echo( count( $rows ) + 1 ); ?>"
  x:FullColumns="1" x:FullRows="1">
  <Column ss:Index="4" ss:AutoFitWidth="0" ss:Width="154.5"/>
  <Row ss:StyleID="s23">
  <Cell><Data
    ss:Type="String">First</Data></Cell>
  <Cell><Data
   ss:Type="String">Middle</Data></Cell>
  <Cell><Data
    ss:Type="String">Last</Data></Cell>
  <Cell><Data
    ss:Type="String">Email</Data></Cell>
  </Row>
  <?php foreach( $rows as $row ) { ?>
  <Row>
  <Cell><Data
     ss:Type="String"><?php echo( $row['first'] ); ?>
  </Data></Cell>
  <Cell><Data
     ss:Type="String"><?php echo( $row['middle'] ); ?>
  </Data></Cell>
  <Cell><Data
    ss:Type="String"><?php echo( $row['last'] ); ?>
  </Data></Cell>
  <Cell ss:StyleID="s21"><Data ss:Type="String">
  <?php echo( $row['email'] ); ?></Data></Cell>
  </Row>
  <?php } ?>
  </Table>
  <WorksheetOptions
     xmlns="urn:schemas-microsoft-com:office:excel">
  <Print>
  <ValidPrinterInfo/>
  <HorizontalResolution>300</HorizontalResolution>
  <VerticalResolution>300</VerticalResolution>
  </Print>
  <Selected/>
  <Panes>
  <Pane>
  <Number>3</Number>
  <ActiveRow>1</ActiveRow>
  </Pane>
  </Panes>
  <ProtectObjects>False</ProtectObjects>
  <ProtectScenarios>False</ProtectScenarios>
  </WorksheetOptions>
  </Worksheet>
  </Workbook>

스크립트는 XML로 결과물의 내용 유형을 설정하여 시작한다. 이렇게 하지 않으면 브라우저가 이 코드를 단순히 불량 HTML이라고 간주하기 때문에 이 작업이 중요하다.
코드의 SQL 쿼리 부분을 변경하여 쿼리의 결과를 배열로 저장했다. 대개 필자는 이러한 보고 페이지 유형으로 이렇게 작업하지 않지만, 이 경우에 행의 숫자인 1을 추가하여 ss:ExpandedRowCount 속성에 넣어야 한다. 1의 추가(+1)는 헤더 행을 세는 것이다.
그림 7에는 링크를 클릭한 결과를 보여준다.

그림 7. Firefox에서 XML 내보내기
Firefox에서 XML 내보내기
엄청나게 대단하지는 않다. 하지만 Internet Explorer에서 동일한 링크를 클릭할 때 나타나는 것을 보자(그림 8 참조).

그림 8. Internet Explorer에서 내보낸 XML
Internet Explorer에서 내보낸 XML
굉장한 차이가 있다. 이는 브라우저 내에서 전체 스프레드시트 -- 형식화 및 기타 등등 -- 이다. (물론 Firefox에서 링크를 마우스 오른쪽 단추로 클릭하고 XML을 파일로 저장하여 이렇게 시작할 수도 있다.)
가능성의 기술
다른 최첨단 기술과 마찬가지로 이 기술에도 약점은 있다. 예를 들어, 최신 Mac용 Office 버전이 XML 파일을 지원하지 않기 때문에 Macintosh에서는 작동하지 않는다.
또다른 문제점은 이러한 파일을 디버깅하면 문제가 될 수 있다는 점이다. XML이 약간만 잘못되어도 내장 Excel 오브젝트는 Excel에서 이는 실행 중이고 시작을 거부한다고 미리 간주하는 불량 상태로 들어가게 된다. 이는 애플리케이션을 다시 시작해야만 수정할 수 있다.
즉, 이 기술은 PHP 프로그래머에게 비할 데 없는 통합 가능성을 제공한다. 데이터의 소스가 Excel이나 Word 등이며 수동으로 웹 애플리케이션으로 마이그레이션 되어야 하는지 -- 셀마다 또는 문단마다 -- 얼마나 자주 알게 되는가? 이와 같은 가져오기 기술을 통해 이 문제가 해결된다. 데이터를 워크시트나 문서에서부터 직접 읽을 수 있다.
이는 내보내기 면에서도 마찬가지이다. HTML은 기사와 글에 대해서는 훌륭하지만, 스프레드시트 정보를 정확하게 렌더링하도록 제작되지는 않았다. 여기에서 보여준 기술을 사용하여 사용자가 보기를 예상하는 방식으로 -- 공식, 형식화 등등 -- 스프레드시트를 생성할 수 있다.

참고자료
교육
  • PHP.net는 PHP에 대한 최신 소식을 보고 다운로드를 찾으며 다른 사용자에게서 배울 수 있는 장소이다.
  • Microsoft Office Online은 지원을 비롯하여 Office 정보를 얻기 위해 시작하기에 최고의 장소이다.
  • XML 표준의 권위 있는 소스는 World Wide Web Consortium(W3C)이다.
  • XML in Office 2003: Information Sharing with Desktop XML (Prentice Hall, 2003)은 Charles F. Goldfarb과 Priscilla Walmsley이 지은 완벽한 광범위한 안내서이다.
  • 이 2003년 11월의 developerWorks 기사인 "Convert Excel data to XML"은 Excel 파일에서부터 데이터를 잠금 해제하여 XML에서 이를 처리하는 방법을 보여주고 다른 솔루션의 장단점을 조사한다. 이는 Excel 2002 및 Excel XP 클라이언트 소프트웨어에 적합하다.
  • developerWorks XML 영역에서 XML 개발자를 위한 더 많은 자원을 찾아보자.
  • developerWorks 오픈 소스 영역에서 오픈 소스 기술을 활용하여 개발 작업을 수행하고 이러한 기술을 IBM 제품과 함께 사용하는 데 도움이 되는 사용법 정보, 도구 및 프로젝트 업데이트를 확인할 수 있다.
제품 및 기술 얻기
  • DVD로 제공되거나 다운로드할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 구현해 보자.
토론
필자소개
Jack D. Herrington은 20년 경력의 소프트웨어 엔지니어이다. Code Generation in Action, Podcasting Hacks, PHP Hacks(출간 예정)의 저자이기도 하다. 30개 이상의 기술자료도 집필했다. (jack_d_herrington@codegeneration.net)

Java development 2.0: MongoDB: (적절한) RDBMS 이동 기능을 제공하는 NoSQL 데이터 저장소

http://www.ibm.com/developerworks/kr/library/j-javadev2-12/


Java development 2.0: MongoDB: (적절한) RDBMS 이동 기능을 제공하는 NoSQL 데이터 저장소

Java 코드 및 Groovy를 사용하여 문서를 작성하고 쿼리하기
Andrew Glover, Author and developer, Beacon50
요약: NoSQL 데이터베이스에 대해 알아보면 NoSQL RDBMS라고도 하는 MongoDB를 만날 수 있습니다. 이 기사에서는 MongoDB의 사용자 정의 API, 대화식 쉘 및 RDBMS 스타일 동적 쿼리 지원과 더불어 빠르고 쉬운 MapReduce 계산에 대해서도 살펴봅니다. 그런 다음 MongoDB의 네이티브 Java™ 언어 드라이버와 사용하기 쉬운 Groovy 랩퍼인 GMongo를 사용하여 데이터를 작성하고, 찾고, 조작하는 방법에 대해 설명합니다.
이 기사에 테그:  애플리케이션_개발
원문 게재일:  2010 년 9 월 28 일 번역 게재일:   2011 년 2 월 15 일
난이도:  중급 원문:  보기 PDF:  A4 and Letter (56KB | 13 pages)Get Adobe® Reader®
페이지뷰: 8446 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 11표)
MongoDB 및 CouchDB와 같은 문서 지향적 데이터베이스는 데이터를 테이블에 저장하지 않고 문서 형식으로 저장한다는 점에서 관계형 데이터베이스와 큰 차이가 있다. 개발자의 관점에서 문서 지향적(또는 스키마리스) 데이터는 관계형 데이터에 비해 단순해서 관리 유연성이 훨씬 높다. 관계를 통해 결합된 테이블, 행 및 열로 구성된 엄격한 스키마에 데이터를 저장하기 보다는 필요한 데이터가 포함된 문서를 개별적으로 작성한다.

개발사에서 소개하는 MongoDB

10gen의 CTO인 Eliot Horowitz가 시기 적절한 기술 팟캐스트를 통해 이 오픈 소스 문서 데이터베이스에 대해 자세히 설명한다. 지금 들어보자.
오픈 소스 문서 지향적 데이터베이스 중에서 MongoDB는 RDBMS 기능을 갖춘 NoSQL 데이터베이스라고 언급되기도 한다. 예를 들어, MongoDB에서는 미리 정의된 MapReduce 함수가 없어도 동적 쿼리가 지원된다. 또한 MongoDB에는 손쉽게 데이터 저장소에 액세스하는 기능을 제공하는 대화식 쉘이 있으며 기본적으로 지원되는 샤드(shard) 기능을 사용하면 여러 노드로 확장할 수 있다.
MongoDB의 API는 JSON 오브젝트와 JavaScript 함수의 혼합체이다. 개발자는 명령행 인수를 사용할 수 있는 쉘 프로그램이나 언어 드라이버를 통해 MongoDB와 상호 작용하여 데이터 저장소 인스턴스에 액세스할 수 있다. 그렇지만 JDBC와 같은 드라이버는 없다. 이는 ResultSet 또는 PreparedStatement를 다루지 않아도 된다는 의미이다.
MongoDB는 빠르다는 장점도 가지고 있다. 이는 주로 데이터를 쓰는 방법 즉, 데이터를 메모리에 저장한 후 나중에 백그라운드 스레드를 통해 디스크에 기록하는 방법이 속도 향상 효과를 제공하기 때문이다.

이 시리즈의 정보

처음 Java 기술이 발표된 이후로 Java를 개발하는 과정은 급속도로 변화되었다. 오픈 소스 프레임워크와 신뢰할 수 있는 임대용 전개 인프라 덕택에 Java 애플리케이션을 신속하고 저렴하게 어셈블하고 테스트하고 유지할 수 있게 되었다. 이 시리즈에서 Andrew Glover는 이러한 새로운 Java 개발 패러다임을 가능하게 하는 다양한 기술과 도구를 탐구한다.
MongoDB에 대해 설명하는 이 기사는 CouchDB에 대해 소개하는 필자의 기사(참고자료 참조)를 바탕으로 하며 다시 한번 주차 티켓 예제를 사용하여 스키마리스 데이터 저장소의 유연성을 보여 준다. MongoDB의 API 및 동적 쿼리 지원이 MongoDB의 두 가지 주요 차별 요소이므로 이러한 차별 요소에 중점을 두고 MongoDB의 쉘 및 Java 언어 드라이버의 사용법을 보여 주는 예제를 살펴본다. 기사 후반부에서는 MongoDB의 MapReduce 구현에서 제공하는 일부 정보를 사용하는 Groovy 랩퍼인 GMongo에 대해서도 소개한다. 이 기능도 이 특별한 NoSQL 옵션의 주요 특징 중 하나이다.
스키마리스로 이동해야 하는 이유
스키마리스 저장소가 모든 분야에 적합한 것은 아니기 때문에 문서 지향적 방법과 관계형 방법의 선택 기준을 이해해야 한다. 데이터를 다양한 양식으로 참조할 수 있지만 기본 모델이 동일한 분야에서는 문서의 유연성이 중요하다. 명함이 전형적인 예이다. 수많은 명함을 보면 다양한 데이터가 있다는 것을 알 수 있다. 일부 명함에는 팩스 번호나 회사 URL이 적혀 있기도 하지만 우편 주소, 두 개의 전화번호 또는 Twitter 핸들이 적혀 있는 명함도 있다. 데이터는 다양하지만 모델이나 기능은 동일하다. 즉, 명함에는 연락처 정보가 있다.
명함을 관계형 용어로 모델링할 수는 있지만 꽤 복잡하다. 관계형 데이터베이스를 보면 예를 들어, 팩스 번호를 사용하는 하나 또는 두 개의 레코드마다 팩스 열의 값이 널값인 레코드를 많이 볼 수 있다. 또한 관계형 시스템에서는 열 유형을 지정해야 하기 때문에 주소 필드 길이 등으로 인한 제약이 발생할 수 있다. (아마도 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch에 사는 사람의 주소를 저장해야 하는 경우를 생각해 본 적이 없을 것이다. 하지만 이 마을이 실제로 존재한다.)
문서 지향적 데이터 저장소로 명함을 모델링하는 작업은 매우 쉽다. 스키마를 사용하지 않는다는 것은 길이에 상관 없이 필요한 모든 데이터를 문서에 담을 수 있다는 것을 의미한다. 명함의 특성을 고려하면 다양한 특성을 지닌 문서로 모델링하는 것이 적합하다.
스키마리스 데이터 저장소는 대부분 ACID(Atomicity, Consistency, Isolation, and Durability)를 완벽하게 지원하지는 않기 때문에 안정성 및 일관성이 중요한 분야에서는 문제가 발생할 수 있다. NoSQL 방법의 지지자는 확장하기 위해 다중 노드를 도입하는 순간부터 불가피하게 발생하는 중단 시간을 고려하지 않는 경우에만 ACID가 작동한다고 주장한다. 핵심은 스키마리스 데이터 저장소가 관계형 데이터 저장소보다 쉽게 확장할 수 있으므로 문서 지향적 저장소가 웹 기반 애플리케이션에 적합하다는 것이다.
MongoDB 시작하기
MongoDB는 대상 운영 체제별 다운로드를 제공하므로 매우 쉽게 시작할 수 있다. 예를 들어, MongoDB를 Mac OS X에 설정할 경우 해당 2진 파일을 다운로드한 후 압축을 풀고 데이터 디렉토리(MongoDB가 데이터 저장소의 내용을 쓰는 디렉토리)를 작성한 다음 mongodb 명령을 사용하여 인스턴스를 시작하기만 하면 된다. (물론 데이터를 쓸 위치를 프로세스에 알려줘야 한다.)
목록 1에서는 MongoDB를 시작하면서 data/db 디렉토리에 데이터를 저장하도록 지정하고 있다. 그리고 verbose 플래그도 지정한다. v가 많을수록 더 많은 세부사항이 표시된다.

목록 1. Mac OS X용 MongoDB
iterm$ ./bin/mongod — dbpath ./data/db/ ——vvvvvvvv

MongoDB를 시작한 후에는 대화식 쉘 작업을 바로 시작할 수 있다. mongo 명령을 실행하면 목록 2와 같은 내용이 표시된다.

목록 2. MongoDB 쉘 시작하기
iterm$ ./bin/mongo
MongoDB shell version: 1.6.0
connecting to: test
>

쉘을 시작하면 초기에 "test" 데이터 저장소에 연결된다는 것을 알 수 있다. 여기에서는 이 데이터 저장소를 사용하여 문서를 작성하고 찾는 방법을 설명할 것이다. 이 작업에는 일부 JavaScript 및 JSON을 작성하는 작업도 포함되어 있다.
문서 작성 및 찾기
CouchDB와 마찬가지로 MongoDB에서도 JSON을 사용하여 문서를 작성한다. (이러한 문서는 효율성을 위해 JSON의 2진 양식인 BSON으로 저장된다.) 대화식 쉘에서 주차 티켓을 작성하기 위해 목록 3과 같은 JSON 문서를 작성할 수 있다.

목록 3. 간단한 JSON 문서
> ticket =  { officer: "Kristen Ree" , location: "Walmart parking lot", vehicle_plate: 
  "Virginia 5566",  offense: "Parked in no parking zone", date: "2010/08/15"}

Enter 키를 누르면 목록 4와 같이 형식화된 JSON 문서가 표시된다.

목록 4. MongoDB의 응답
{
  "officer" : "Kristen Ree",
  "location" : "Walmart parking lot",
  "vehicle_plate" : "Virginia 5566",
  "offense" : "Parked in no parking zone",
  "date" : "2010/08/15"
}

방금 전 주차 티켓의 JSON 표현을 작성했으며 그 이름은 "ticket"이다. 이 문서를 지속적으로 유지하려면 관계형 용어의 스키마와 유사한 콜렉션에 연관시켜야 한다. 목록 5에서는 ticket을 tickets 콜렉션에 연관시킨다.

목록 5. ticket 인스턴스 저장하기
> db.tickets.save(ticket) 

MongoDB에서는 tickets 콜렉션을 미리 작성하지 않아도 된다. 콜렉션은 처음 참조될 때 작성된다.
이제 위와 같은 방법으로 티켓을 몇 개 더 작성한다. 다음 섹션에서는 이러한 티켓을 찾아볼 것이므로 좀 더 흥미로울 것이다.
문서 찾기
콜렉션에 있는 모든 문서를 찾는 작업은 find 명령만 호출하면 되기 때문에 쉽다(목록 6 참조).

목록 6. MongoDB의 모든 문서 찾기
> db.tickets.find()
{ "_id" : ObjectId("4c7aca17dfb1ab5b3c1bdee8"), "officer" : "Kristen Ree", "location" : 
  "Walmart parking lot", "vehicle_plate" : "Virginia 5566", "offense" : 
  "Parked in no parking zone", "date" : "2010/08/15" }
{ "_id" : ObjectId("4c7aca1ddfb1ab5b3c1bdee9"), "officer" : "Kristen Ree", "location" : 
  "199 Baldwin Dr", "vehicle_plate" : "Maryland 7777", "offense" : 
  "Parked in no parking zone", "date" : "2010/08/29" }


매개변수 없이 find 명령을 실행하면 특정 콜렉션의 모든 문서가 리턴된다. 이 경우에는 tickets 콜렉션의 문서가 리턴된다.
목록 6을 보면 MongoDB가 각 문서의 ID를 작성했다는 것을 알 수 있다. 이러한 ID에는 _id 키가 지정되어 있다.
JSON 문서의 개별 키를 검색할 수 있다. 예를 들어, Walmart 주차장에서 발행한 모든 티켓을 찾으려는 경우 목록 7의 쿼리를 사용할 수 있다.

목록 7. 쿼리로 찾기
> db.tickets.find({location:"Walmart parking lot"})

JSON 문서의 사용 가능한 키를 검색할 수 있다(이 경우에는 offense, _id, date 등). 그리고 정규식을 사용하여 키 값(예: location)을 검색할 수도 있다(목록 8 참조). 이 정규식은 SQL의 LIKE 명령문과 매우 비슷하게 작동한다.

목록 8. 정규식으로 찾기
> db.tickets.find({location:/walmart/i})

정규식 명령문(이 경우에는 walmart) 뒤에 오는 i는 해당 명령문에서 대소문자를 구분하지 않는다는 것을 의미한다.
MongoDB의 Java 드라이버
MongoDB의 Java 언어 드라이버가 이전 섹션에 살펴본 대부분의 JSON 및 JavaScript 코드를 추상화하는 기능을 제공하므로 사용자는 Java API를 직접 사용하면 된다. MongoDB의 Java 드라이버를 시작하려면 해당 드라이버를 다운로드한 후 결과 .jar 파일을 클래스 경로에 추가한다(참고자료 참조).
이제 tickets 콜렉션에 다른 티켓을 작성해 보자. 이 콜렉션은 test 데이터 저장소에 저장되어 있다. Java 드라이버를 사용할 경우에는 먼저 MongoDB 인스턴스에 연결한 다음 test 데이터베이스와 tickets 콜렉션을 가져온다(목록 9 참조).

목록 9. MongoDB의 Java 드라이버 사용하기
Mongo m = new Mongo();
DB db = m.getDB("test");
DBCollection coll = db.getCollection("tickets");

Java 드라이버를 사용하여 JSON 문서를 작성하려면 BasicObject를 작성한 후 이 오브젝트에 이름과 값을 연관시킨다(목록 10 참조).

목록 10. Java 드라이버로 문서 작성하기
BasicDBObject doc = new BasicDBObject();

doc.put("officer", "Andrew Smith");
doc.put("location", "Target Shopping Center parking lot");
doc.put("vehicle_plate", "Virginia 2345");
doc.put("offense", "Double parked");
doc.put("date", "2010/08/13");

coll.insert(doc);

Java 드라이버를 사용하면 매우 쉽게 문서를 찾고 결과 커서를 반복할 수 있다(목록 11 참조).

목록 11. Java 드라이버로 문서 찾기
DBCursor cur = coll.find();
while (cur.hasNext()) {
 System.out.println(cur.next());
}

Java 개발자는 Java 드라이버를 기반으로 빌드된 Groovy의 멋진 추상화를 포함한 몇 가지 MongoDB 라이브러리를 사용할 수 있다. 다음 섹션에서는 애플리케이션을 빌드하는 과정을 통해 기본 Java 드라이버와 조금 더 많은 기능을 제공하는 Groovy 드라이버를 살펴본다. 이 멋진 애플리케이션에서는 MongoDB의 MapReduce 함수도 볼 수 있다. 이 기사에서는 이 기능을 사용하여 문서 콜렉션을 처리한다.
MongoDB를 이용한 Twitter 분석
데이터베이스에 저장되어 있기만 한 데이터는 별로 의미가 없다. 그러한 데이터를 효율적으로 활용할 수 있어야 한다. 이 기사에서는 이 애플리케이션을 사용하여 먼저 Twitter의 일부 정보를 캡처하여 MongoDB에 저장할 것이다. 그런 다음 필자를 가장 많이 리트위트한 사용자와 필자의 트위트 중 가장 많이 리트위트된 트위트를 계산할 것이다.
이 애플리케이션을 실행하려면 먼저 Twitter와 통신하여 데이터를 캡처할 수 있는 방법이 필요하다. 이를 위해 Twitter의 일부 RESTful API를 간단한 Java API로 추상화한 Twitter4J라는 멋진 라이브러리를 사용한다(참고자료 참조). 여기에서는 이 API를 사용하여 필자의 리트위트를 찾는다. 데이터를 찾은 후에는 데이터에 목록 12와 같은 JSON 문서 형식을 지정한다.

목록 12. JSON을 통해 저장된 리트위트
{ 
  "user_name" : "twitter user",
  "tweet" : "Podcast ...", 
  "tweet_id" :  9090...., 
  "date" : "08/12/2010" 
}

목록 13에서는 MongoDB의 네이티브 Java 드라이버와 Twitter4J를 간단한 드라이버 애플리케이션(이 또한 Java 코드로 작성됨)에서 함께 사용하여 데이터를 캡처하고 MongoDB에 저장한다.

목록 13. MongoDB에 Twitter 데이터 삽입하기
Mongo m = new Mongo();
DB db = m.getDB("twitter_stats");
DBCollection coll = db.getCollection("retweets");

Twitter twitter = new TwitterFactory().getInstance("<some user name>", "<some password>");
List<Status> statuses = twitter.getRetweetsOfMe();
for (Status status : statuses) { 
  ResponseList<User> users = twitter.getRetweetedBy(status.getId());
  
  for (User user : users) {
    BasicDBObject doc = new BasicDBObject();
    doc.put("user_name", user.getScreenName());
    doc.put("tweet", status.getText());
    doc.put("tweet_id", status.getId());
    doc.put("date", status.getCreatedAt());
    coll.insert(doc);
 }
}

목록 13의 "twitter_stats" 데이터베이스는 드라이버 실행 전에 없었기 때문에 요청이 발생할 때 작성되었다. 이는 "retweets" 콜렉션에도 동일하게 적용된다. 데이터베이스와 콜렉션이 작성되면 Twitter4J의 Twitter 오브젝트가 생성된 후 최근 20개의 리트위트가 리턴된다.
이제 Twitter4J에서 리턴한 Status 오브젝트의 List에는 필자의 리트위트가 있다. 각 항목에 대해 관련 데이터를 쿼리한 후 MongoDB의 BasicDBObject 인스턴스가 작성되고 관련 데이터가 이 인스턴스에 채워진다. 마지막으로 각 문서가 저장된다.
MongoDB의 MapReduce
모든 데이터를 저장했으므로 이제 데이터를 조작할 차례이다. 원하는 정보를 가져오려면 두 가지 일괄처리 조작을 수행해야 한다. 먼저 각 Twitter 사용자가 나열된 횟수의 합계를 구한다. 그런 다음 각 tweet(또는 tweet_id)가 팝업된 횟수의 합계를 구한다.
MongoDB에서는 MapReduce를 사용하여 일괄처리 데이터 조작을 수행한다. 크게 보았을 때 MapReduce 알고리즘은 문제를 두 단계로 나눠서 접근한다. 먼저 Map 함수는 대량 입력을 받아서 작은 단위로 분할한 다음 다른 프로세스에게 전달하도록 설계되었다. 그리고 분할된 데이터를 받은 프로세스에서 데이터에 대한 조작을 수행한다. Reduce 함수는 Map의 개별 응답을 하나의 최종 출력으로 작성하는 역할을 담당한다.
MongoDB의 핵심 API가 JavaScript이기 때문에 MapReduce 함수도 JavaScript로 작성되었다. Java 드라이버를 사용하기는 해도 JavaScript로 MapReduce 함수를 작성해야 한다. 물론 JavaScript를 String이나 BasicDBObject와 유사한 오브젝트로 정의할 수 있다. 여기에서는 MongoDB의 기본 드라이버를 기반으로 하는 간단한 랩퍼 라이브러리를 사용하여 작업을 단순화하고 코딩 시간을 절약한다. GMongo라는 이 랩퍼는 Groovy에서 활용할 수 있도록 Groovy로 작성되었다. 그래도 여전히 MapReduce 함수를 JavaScript로 작성해야 하지만 Groovy의 다중 행 문자열 기능을 사용하면 문자열을 이스케이프하지 않아도 되기 때문에 작업이 조금 더 쉬워진다.
JavaScript로 작성된 MapReduce 함수
필자를 가장 많이 리트위트한 사용자를 찾으려면 두 가지 작업을 수행해야 한다. 먼저 JSON 문서 구조의 user_name 특성을 키로 사용하는 map 함수를 작성해야 한다. 이 작업은 목록 14와 같이 매우 쉽다.

목록 14. JavaScript로 작성된 간단한 Map 함수
function map() {
  emit(this.user_name, 1); 
}

map 함수는 간단하다. 전달된 모든 문서의 user_name 특성을 가져온 다음 emit를 호출하며, 두 번째 매개변수는 값이다. 이 값은 기본적으로 키의 수이다. 개별 문서의 경우에는 이 값이 1이다. 앞으로 이 값을 사용하여 합계를 구하는 방법을 살펴볼 것이다.
목록 14에서는 필자가 지정한 키(user_name 특성)와 값을 사용하여 emit 함수를 호출했다. 이 함수의 컨텍스트에서 this 변수는 JSON 문서 자체를 의미한다.
다음으로 reduce 함수를 작성해야 한다(목록 15 참조). 이 함수는 적절하게 그룹화된 모든 문서를 가져와서 값의 합계를 구한다.

목록 15. JavaScript로 작성된 Reduce 함수
function reduce(key, vals) {
  var sum = 0;
  for(var i in vals) sum += vals[i];
  return sum;
}

목록 15에서 볼 수 있듯이 reduce에 전달된 keyvals 변수는 function reduce("asmith", [1,1,1,1]);와 같은 형태로 해석되며 물론 이는 user_nameasmith인 사용자가 네 개의 다른 문서에 있다는 것을 의미한다. 즉, A. Smith가 필자를 네 번 리트위트했다는 것이다.
vals 변수를 반복하여 리턴된 간단한 sum을 통해 이를 확인할 수 있다.
Groovy로 작성된 MapReduce 함수
다음으로 GMongo를 사용하는 Groovy 스크립트를 작성한 후 mapreduce 함수를 적절하게 삽입한다(목록 16 참조).

목록 16. MapReduce를 위한 Groovy 스크립트
mongo = new GMongo()
def db = mongo.getDB("twitter_stats")

def res = db.retweets.mapReduce(
    """
    function map() {
        emit(this.user_name, 1); 
    }
    """,
    """
    function reduce(key, vals) {
        var sum = 0;
        for(var i in vals) sum += vals[i];
        return sum;
    }
    """,
    "result",
    [:] 
)

def cursor = db.result.find().sort(new BasicDBObject("value":-1))
       
cursor.each{
  println "${it._id} has retweeted you ${it.value as int} times"
}

목록 16에서는 먼저 GMongo의 인스턴스를 작성하고 "twitter_stats" 데이터 저장소를 가져온다. 이 모든 작업은 기본 Java 드라이버를 사용할 때와 매우 비슷하다.
그런 다음 retweets 콜렉션에 대해 mapReduce 메소드를 호출한다. GMongo 드라이버를 사용하면 목록 13과는 달리 콜렉션을 가져오지 않고 직접 참조할 수 있다. mapReduce 메소드에서는 네 개의 매개변수를 사용한다.
처음 두 개는 JavaScript로 정의된 mapreduce 함수를 나타내는 String이다. 세 번째 매개변수는 MapReduce의 결과를 가지고 있는 오브젝트의 이름이며, 마지막 매개변수는 조작을 완료하는 데 필요한 입력 쿼리이다. 예를 들어, MapReduce 함수에 특정 JSON 문서(예를 들어, 특정 데이터 범위 내의 문서) 또는 그러한 문서 중 일부만 전달할 수 있다.
그런 다음 result 오브젝트(JSON 문서)를 쿼리하고 sort 메소드를 호출한다. 목록 16sort 메소드를 호출하려면 {value:-1}과 같은 JSON 문서가 필요하다. 이는 큰 값이 맨 위에 오는 역순으로 정렬하겠다는 의미이다. 리턴된 cursor 오브젝트는 기본적으로 반복자이다. 따라서 Groovy의 멋진 each를 이 오브젝트에 직접 사용하여 간단한 보고서를 출력할 수 있다. 이 보고서에서는 필자를 가장 많이 리트위트한 사용자부터 차례대로 보여 준다.
목록 16의 스크립트를 실행하면 목록 17과 같은 출력이 표시된다.

목록 17. MapReduce 출력
bglover has retweeted you 3 times
bobama has retweeted you 3 times
sjobs has retweeted you 2 times
...

이제 가장 많이 리트위트한 사용자를 알게 되었다. 하지만 가장 많이 리트위트된 트위트를 보고하려면 어떻게 해야 할까? 이 또한 매우 간단한 작업이다. user_name 대신 tweet 특성을 키로 사용하는 map 함수를 정의하면 된다(목록 18 참조).

목록 18. 또 하나의 Map 함수
function map() {
  emit(this.tweet, 1); 
}

하나 더 덧붙이자면 reduce 함수는 단순히 그룹화된 키의 합계만 구하는 것이므로 앞에서 사용한 함수를 그대로 사용할 수 있다.
결론
이 기사에서는 MongoDB를 빠르게 살펴보면서 수행할 수 있는 작업의 일부만을 다루었다. 그럼에도 불구하고 이 기사를 통해 높은 유연성을 제공하는 MongoDB의 스키마리스 특성을 이해했기를 바란다. 이 특성은 기사의 앞부분에서 제시했던 명함 예제와 같이 데이터 요소가 다양하면서도 일반적으로 관련되어 있는 분야에 특히 유용한다.
MongoDB와 CouchDB는 둘 다 스키마리스 유연성을 지원하기는 하지만 큰 차이점을 가지고 있다. MongoDB의 기능은 RDBMS와 유사한 형태를 지니고 있기 때문에 RDBMS 관점에서 작업하기도 쉽고 익숙하기도 하다. MongoDB를 사용하면 동적 쿼리를 실행할 수 있고 Java, Ruby, PHP 등의 네이티브 언어로 작업할 수도 있다. 그리고 강력한 MapReduce도 활용할 수 있다.
문서 지향적 데이터베이스가 모든 분야에 적합한 것은 아니다. 금융 데이터를 처리하는 분야와 같이 많은 트랜잭션이 발생하는 분야에서는 아마도 안정적인 ACID가 지원되는 기존 RDBMS가 더 적합할 것이다. 하지만 높은 처리 속도와 유연한 데이터 모델이 필요한 애플리케이션에는 MongoDB가 적합할 것이다.

참고자료
교육
제품 및 기술 얻기
  • MongoDB.org: MongoDB와 Java 언어 드라이버를 다운로드할 수 있다.
  • Download GMongo: 기본 Java 언어 드라이버 대신 사용할 수 있는 Groovy 랩퍼이다.
토론
  • My developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
필자소개
Andrew Glover Andrew Glover는 개발자이자 저자이며 또한 강사이자 기업가로 동작 지향적 개발 및 연속적인 통합, 신속한 소프트웨어 개발에 대한 열정으로 가득 차 있다. 그의 블로그에서 그에 관한 다양한 정보를 얻을 수 있다.

Java development 2.0: NoSQL

http://www.ibm.com/developerworks/kr/library/j-javadev2-8/

Java development 2.0: NoSQL

Bigtable과 Groovy의 Gaelyk을 사용한 스키마 없는 데이터 모델링
Andrew Glover, 대표, Stelligent Incorporated
요약: Bigtable과 CouchDB와 같은 NoSQL 데이터 저장소는 확장성 문제를 광범위하게 해결하기 때문에 Web 2.0 시대에 변방에서 중심으로 이동하고 있습니다. Google과 Facebook은 NoSQL을 받아들인 두 개의 대표적인 사이트이며 NoSQL은 아직 초기 단계에 있습니다. 스키마 없는 데이터 저장소는 기본적으로 전형적인 관계형 데이터베이스와 다르지만 생각보다 활용하기 쉽습니다. 특히 관계형 모델이 아니라 도메인 모델로 시작하는 경우에 더 쉽습니다.
원문 게재일:  2010 년 5 월 11 일 번역 게재일:   2010 년 6 월 08 일
난이도:  중급 영어로:  보기 PDF:  A4 and Letter (154KB | 15 pages)Get Adobe® Reader®
페이지뷰: 8584 회
의견: 0 (의견 추가)

이 시리즈의 정보

처음 Java 기술이 발표된 이후로 Java를 개발하는 과정은 급속도로 변화되었다. 오픈 소스 프레임워크와 신뢰할 수 있는 임대용 전개 인프라 덕택에 Java 애플리케이션을 신속하고 저렴하게 어셈블하고 테스트하고 유지할 수 있게 되었다. 이 시리즈에서 Andrew Glover는 이러한 새로운 Java 개발 패러다임을 가능하게 하는 다양한 기술과 도구를 탐구한다.
관계형 데이터베이스는 30년 넘게 데이터 스토리지 분야를 지배해 왔지만 높아진 스키마 없는(NoSQL) 데이터베이스의 인기는 변화가 진행 중임을 시사하고 있다. RDBMS는 전형적인 클라이언트-서버 아키텍처에서 데이터를 저장하는 데 필요한 매우 견고한 기반을 제공하지만 저렴한 비용으로 쉽게 복수의 노드로 확장하지 못한다. Facebook과 Twitter과 같이 확장성이 높은 웹 애플리케이션의 시대에 이 부분은 매우 유감스러운 단점이다.
관계형 데이터베이스에 대한 초기의 대체 데이터베이스(예: 오브젝트 지향 데이터베이스)는 아주 긴박한 문제를 해결하지 못했기 때문에 Google의 Bigtable과 Amazon의 SimpleDB와 같은 NoSQL 데이터베이스가 높은 확장성을 바라는 웹의 요구에 대한 직접적인 응답으로 생겨났다. 본질적으로 NoSQL은 Web 2.0이 발전함에 따라 웹 애플리케이션 개발자가 더 많이 부딪히게 될 심각한 문제점의 킬러 애플리케이션이 될 수 있다.
Java development 2.0에 대한 이 기사에서 필자는 관계형 데이터 모델에 익숙한 많은 개발자에게 NoSQL에 대한 기본적인 문제인 스키마 없는 데이터 모델링부터 시작한다. 앞으로 알게 되겠지만 관계형 모델이 아니라 도메인 모델로 시작하는 것이 부담없이 시작할 수 있는 비결이다. 필자의 예에서와 같이 Bigtable을 사용하는 경우에는 Google App Engine에 대한 경량 프레임워크 확장인 Gaelyk의 지원을 받을 수도 있다.
NoSQL: 새로운 사고 방식?
개발자들이 비관계형 또는 NoSQL 데이터베이스에 대해 이야기할 때 가장 먼저 제기되는 질문은 이러한 데이터베이스를 사용하려면 사고 방식을 바꿔야 하는지에 대한 것이다. 필자는 이것은 처음에 데이터 모델링에 어떻게 접근하는지에 달렸다고 생각한다. 먼저 데이터베이스 구조를 모델링하여 애플리케이션을 설계(즉, 테이블과 연관된 관계를 먼저 파익함)하는 데 익숙한 경우에는 Bigtable과 같은 스키마 없는 데이터 저장소를 사용하여 데이터 모델링을 수행하려면 사고 방식을 바꿔야 한다. 하지만 도메인 모델에서 시작하여 애플리케이션을 설계하는 경우에는 Bigtable의 스키마 없는 구조가 더 자연스럽게 느껴질 것이다.

확장을 위해 빌드됨

확장성이 큰 웹 애플리케이션의 새로운 문제점과 함께 새로운 솔루션이 제공된다. Facebook은 스토리지 요구에 대해 관계형 데이터베이스에 의존하지 않고 키/값 저장소를 사용한다(반드시 고성능 HashMap이 어야 함). 내부 솔루션(더빙된 Cassandra)은 Twitter와 Digg에서도 사용되며 최근 Apache Software Foundation에 기부되었다. Google은 비관계형 데이터 스토리지를 찾기 위해 폭발적인 증가에 필요한 또다른 웹 엔티티이다(결과는 Bigtable임).
비관계형 데이터 저장소에는 join 테이블 또는 기본 키나 외부 키에 대한 개념도 없다(두 유형의 키는 느슨한 형식으로 존재함). 따라서 관계형 모델링을 NoSQL 데이터베이스에서 데이터 모델링의 기반으로 사용하려고 하면 실패하게 될 것이다. 도메인 모델에서 시작하면 단순해진다. 실제로 필자는 도메인 모델에서 스키마 없는 구조의 유연성이 상당히 높다는 것을 알아냈다.
관계형에서 스키마 없는 데이터 모델로 이동 시 상대적인 복잡도는 접근 방식(관계형에서 시작하는지 아니면 도메인 기반 설계에서 시작하는지 등)에 따라 다르다. CouchDB나 Bigtable과 같은 데이터 저장소로 마이그레이션하면 현재로서는 적어도 Hibernate와 같은 확립된 지속성 플랫폼의 장점을 잃게 된다. 한편으로는 자체적으로 빌드할 수 있는 효과도 있다. 프로세스를 진행하는 동안 스키마 없는 데이터 저장소에 대해 자세히 살펴본다.
엔티티와 관계
스키마 없는 데이터 저장소는 먼저 오브젝트를 사용하여 도메인 모델을 설계하는 유연성을 제공한다(Grails와 같은 새로운 프레임워크에서 자동으로 활용함). 작업이 진행된 후 기본 데이터 저장소에 대한 도메인의 맵핑이 된다. Google App Engine의 경우에는 정말 쉽게 수행된다.
"Java development 2.0: Google App Engine을 위한 Gaelyk"에서 필자는 Google의 기본 데이터 저장소에 대한 작업을 활용하는 Groovy 기반 프레임워크인 Gaelyk을 소개했다. 이 기사의 많은 부분에서는 Google의 Entity 오브젝트 활용에 대해 집중적으로 다루었다. 해당 기사의 다음 예제에서는 Gaelyk에서 오브젝트 엔티티가 어떻게 작동하는지를 보여 준다.

Listing 1. 엔티티에 대한 오브젝트 지속성
def ticket = new Entity("ticket")
ticket.officer = params.officer
ticket.license = params.plate
ticket.issuseDate = offensedate
ticket.location = params.location
ticket.notes = params.notes
ticket.offense = params.offense

오브젝트를 기준으로 한 설계

데이터베이스의 설계보다 오브젝트 모델을 선호하는 패턴이 Grails와 Ruby on Rails와 같은 최신 웹 애플리케이션 프레임워크에 등장하여 오브젝트 모델의 설계를 강조하고 기본 데이터베이스 스키마 작성을 처리하고 있다.
오브젝트 지속성에 대한 이러한 접근 방식은 효과가 있지만 티켓 엔티티를 많이 사용한 경우에는 장황해지기 쉽다(예를 들어, 다양한 서블릿에서 티켓 엔티티를 작성하거나 찾은 경우). 일반적인 서블릿(또는 Groovlet)이 태스크를 처리하도록 하면 부담을 다소 덜 수 있다. 필자가 설명할 좀 더 자연스러운 옵션은 Ticket 오브젝트를 모델링하는 것이다.
레이스로 돌아가기
Gaelyk에 대한 소개에 있는 티켓 예제를 다시 실행하는 대신 필자는 최신 상태를 유지하고 이 기사에서 실행 중인 테마를 사용하고 필자가 다루는 기술에 대해 설명하는 데 필요한 애플리케이션을 빌드한다.
그림 1의 다대다 다이어그램에 표시되는 것과 같이 Race에는 다수의 Runner가 있으며 Runner는 다수의 Race에 속할 수 있다.

그림 1. Race와 Runner
Race와 Runner의 관계를 보여 주는 다대다 다이어그램
필자가 이 관계를 설계하기 위해 관계형 테이블 구조를 사용해야 한다면 최소한 세 개의 테이블(세 번째는 다대다 관계를 링크하는 join 테이블임)이 필요했을 것이다. 필자는 관계형 데이터 모델에 얽매여 있지 않다는 사실에 안도한다. 대신 필자는 Gaelyk(및 Groovy 코드)을 사용하여 이 다대다 관계를 Google App Engine을 위한 Google의 Bigtable 추상화에 맵핑한다. Gaelyk을 사용하면 EntityMap처럼 처리할 수 있다는 사실 때문에 프로세스가 매우 단순해진다.

샤드(shard)를 사용한 스케일링

샤딩(sharding)은 여러 노드에서 테이블 구조를 복제하지만 이러한 노드 사이에서 데이터를 논리적으로 나누는 파티셔닝의 한 양식이다. 예를 들어, 하나의 노드에 미국에 있는 계정과 관련된 모든 데이터가 있고 다른 노드에 유럽에 있는 모든 계정과 관련된 데이터가 있을 수 있다. 노드에 관계가 포함된 경우 (즉, 상호 샤드(shard) 결합) 샤드(shard) 문제가 발생한다. 이러한 문제는 해결하기 쉽지 않으며 많은 경우에 지원을 받지 못한다. (관계형 데이터베이스에 대한 확장성 문제와 샤딩(sharding)과 관련한 Google Max Ross에 대한 필자의 의견은 참고자료의 링크를 참조한다.)
스키마 없는 데이터 저장소의 한 가지 매력은 사전에 알고 있어야 할 내용이 없다는 것이다. 즉, 관계형 데이터베이스 스키마를 사용할 때보다 훨씬 쉽게 변경사항을 수용할 수 있다. (스키마를 변경할 수 없다는 의미가 아니다. 필자는 단지 변경사항을 더 쉽게 수용한다는 것만 말하려고 한다.) 필자는 도메인 오브젝트에 특성을 정의하지 않는다. 필자는 Groovy의 동적 특성(이를 통해 본질적으로 필자는 Google의 Entity 오브젝트에 대해 필자의 도메인 오브젝트 프록시를 작성할 수 있음)에 이러한 정의를 미룬다. 대신 필자는 오브젝트를 찾고 관계를 처리하는 방법을 파악하는 데 시간을 투자한다. 이것은 NoSQL과 스키마 없는 데이터 저장소를 활용하는 다양한 프레임워크에서 아직 빌드하지 않은 것이다.
모델 기본 클래스
필자는 Entity 오브젝트의 인스턴스를 보유하는 기본 클래스를 작성하여 시작할 것이다. 그런 다음 Groovy의 편리한 setProperty 메소드를 통해 해당 Entity 인스턴스에 추가될 동적 특성을 서브클래스가 포함할 수 있도록 할 것이다. 오브젝트에 실제로는 존재하지 않는 특성 setter에 대해 setProperty가 호출된다. (이상하게 보일 수도 있지만 걱정할 것 없다. 실제로 해 보면 이해할 수 있다.)
Listing 2에서는 필자의 예제 애플리케이션에 대한 Model 인스턴스에서의 첫 번째 시도를 보여 준다.

Listing 2. 단순 기본 모델 클래스
package com.b50.nosql

import com.google.appengine.api.datastore.DatastoreServiceFactory
import com.google.appengine.api.datastore.Entity

abstract class Model {

 def entity
 static def datastore = DatastoreServiceFactory.datastoreService

 public Model(){
  super()
 }

 public Model(params){
  this.@entity = new Entity(this.getClass().simpleName)
  params.each{ key, val ->
   this.setProperty key, val
  }
 }

 def getProperty(String name) {
  if(name.equals("id")){
   return entity.key.id
  }else{
   return entity."${name}"
  }
 }

 void setProperty(String name, value) {
  entity."${name}" = value
 }

 def save(){
  this.entity.save()
 }	
}

추상 클래스가 특성의 Map을 가져오는 생성자를 어떻게 정의하는지 유의한다. 필자는 언제나 나중에 생성자를 추가할 수 있으며 곧 추가할 것이다. 이 설정은 양식에서 매개변수가 제출되는 웹 프레임워크에 매우 편리하다. Gaelyk과 Grails는 이러한 매개변수를 params라는 오브젝트에 원활하게 랩핑한다. 생성자는 이 Map을 반복하고 각각의 키/값 쌍에 대해 setProperty 메소드를 호출한다.
setProperty 메소드를 살펴보면 키는 기본 entity의 특성 이름으로 설정되어 있고 해당 값은 entity의 값으로 설정되어 있는 것을 알 수 있다.
Groovy 트릭
앞서 언급했듯이 Groovy의 동적 특성으로 인해 필자는 getset Property 메소드를 통해 존재하지 않는 특성에 대한 메소드 호출을 캡처할 수 있다. 따라서 Listing 2에 있는 Model의 서브클래스는 자체 특성을 정의하지 않아도 된다. 이러한 서브클래스는 특성에 대한 모든 호출을 기본 entity 오브젝트에 단순히 위임한다.
Listing 2에 있는 코드에서는 짚고 넘어갈 만한 Groovy의 다른 몇 가지 고유 작업을 수행한다. 먼저 필자는 특성 앞에 @를 추가하여 특성의 액세서 메소드를 생략할 수 있다. 생성자의 entity 오브젝트 참조에 대해 이를 수행해야 한다. 그렇지 않으면 setProperty 메소드를 호출한다. 이때 setProperty를 호출하면 setProperty 메소드에 있는 entity 변수가 null이 되기 때문에 패턴이 명백하게 파괴된다.
두 번째로 생성자에서의 this.getClass().simpleName 호출은 entity의 "유형"을 설정한다. simpleName 특성은 패키지 접두어가 없는 서브클래스의 이름을 생성한다(simpleNamegetSimpleName에 대한 호출이지만 Groovy는 해당 JavaBeans 형식의 메소드 호출 없이 특성에 액세스할 수 있도록 허용하는 점에 유의).
마지막으로 id 특성(즉, 오브젝트의 키)에 대한 호출이 작성되면 getProperty 메소드는 기본 keyid를 요구한다. Google App Engine에서는 entitieskey 특성이 자동으로 생성된다.
Race 서브클래스
Race 서브클래스를 정의하는 것은 Listing 3에 보여지는 것처럼 쉽다.

Listing 3. Race 서브클래스
package com.b50.nosql

class Race extends Model {
 public Race(params){
  super(params)
 }
}

매개변수 목록(즉, 키/값 쌍이 포함된 Map)으로 서브클래스가 인스턴스화되면 해당 entity가 메모리에 작성된다. 이를 지속시키려면 save 메소드를 호출하기만 하면 된다.

Listing 4. Race 인스턴스를 작성하여 GAE의 데이터 저장소에 저장하기
import com.b50.nosql.Runner

def iparams = [:]
                              
def formatter = new SimpleDateFormat("MM/dd/yyyy")
def rdate = formatter.parse("04/17/2010")
              
iparams["name"] = "Charlottesville Marathon"
iparams["date"] = rdate
iparams["distance"] = 26.2 as double

def race = new Race(iparams)
race.save()

Groovlet인 Listing 4에서는 세 가지 특성(레이스의 이름, 날짜 및 거리)이 포함된 Map(더빙된 iparams)이 작성된다. (Groovy에서는 비어 있는 Map[:]를 통해 작성된다는 것에 유의한다.) Race의 새 인스턴스가 작성되어 save 메소드를 통해 기본 데이터 저장소에 저장된다.
그림 2와 같이 Google App Engine 콘솔을 통해 기본 데이터 저장소를 검사하여 데이터가 실제로 기본 데이터 저장소에 있는지 확인할 수 있다.

그림 2. 새로 작성된 레이스 보기
Google App Engine 콘솔에서 새로 작성된 레이스 보기
파인더 메소드에서 지속된 엔티티를 생성함
Entity를 저장했으므로 이를 검색할 수 있는 기능이 있으면 유용하다. 이에 따라 필자는 "파인더" 메소드를 추가할 수 있다. 여기서는 클래스 메소드(static)를 파인더 메소드로 하며 이름으로 Race를 찾을 수 있도록 할 것이다(즉, name 특성을 기반으로 검색함). 나중에 언제든지 다른 특성을 사용하여 다른 파인더를 추가할 수 있다.
또한 필자는 이름에 all이라는 단어가 없는 파인더는 하나의 인스턴스를 찾도록 지정하는 파인더에 대한 규칙을 채택할 것이다. findAllByName에서와 같이 all이라는 단어가 포함된 파인더는 인스턴스의 Collection 또는 List를 리턴할 수 있다. Listing 5에서는 findByName 파인더를 보여 준다.

Listing 5. 엔티티 이름을 기반으로 검색하는 단순 파인더
static def findByName(name){
 def query = new Query(Race.class.simpleName)
 query.addFilter("name", Query.FilterOperator.EQUAL, name)
 def preparedQuery = this.datastore.prepare(query)
 if(preparedQuery.countEntities() > 1){
  return new Race(preparedQuery.asList(withLimit(1))[0])
 }else{
  return new Race(preparedQuery.asSingleEntity())
 }
}

이 단순 파인더에서는 Google App Engine의 QueryPreparedQuery 유형을 사용하여 이름이 전달된 이름과 정확하게 일치하는 "Race" 유형의 엔티티를 찾는다. 둘 이상의 Race가 이 기준을 충족하면 파인더에서는 페이지 표시 제한 1(withLimit(1))에 의해 지시된 대로 목록의 첫 번째 항목을 리턴한다.
해당 findAllByName은 비슷하지만 Listing 6과 같이 원하는 수량이라는 매개변수가 추가된다.

Listing 6. 이름을 기준으로 모두 찾기
static def findAllByName(name, pagination=10){
 def query = new Query(Race.class.getSimpleName())
 query.addFilter("name", Query.FilterOperator.EQUAL, name)
 def preparedQuery = this.datastore.prepare(query)
 def entities = preparedQuery.asList(withLimit(pagination as int))
 return entities.collect { new Race(it as Entity) }
}

앞서 정의한 파인더와 같이 findAllByName은 이름을 기준으로 Race 인스턴스를 찾지만 모든 Race를 리턴한다. 하지만 Groovy의 collect 메소드는 필자가 Race 인스턴스를 작성하는 해당 루프에 들어갈 수 있도록 한다. Groovy가 어떻게 메소드 매개변수의 기본값도 허용하는지에 유의한다. 두 번째 값을 전달하지 않으면 pagination의 값은 10이 된다.

Listing 7. 작동 중인 파인더
def nrace = Race.findByName("Charlottesville Marathon")
assert nrace.distance == 26.2

def races = Race.findAllByName("Charlottesville Marathon")
assert races.class == ArrayList.class

Listing 7에 있는 파인더는 예상대로 작동한다. findByName은 하나의 인스턴스를 리턴하고 findAllByNameCollection을 리턴한다(둘 이상의 "Charlottesville Marathon"이 있다고 가정함).
Runner 오브젝트도 많이 다르지 않음
이제는 Race의 인스턴스를 쉽게 작성하고 찾을 수 있기 때문에 빠른 Runner 오브젝트를 작성할 준비가 되어 있다. 프로세스는 초기 Race 인스턴스를 작성할 때만큼 쉽다. Listing 8과 같이 Model을 확장하기만 하면 된다.

Listing 8. Runner는 매우 쉬움
package com.b50.nosql

class Runner extends Model{
 public Runner(params){
  super(params)
 }
}

Listing 8을 보면서 필자는 결승선에 거의 다 왔음을 느낀다. 여전히 러너와 레이스 사이에 링크를 작성해야 한다. 물론 러너가 둘 이상의 레이스에 참여하기를 바라기 때문에 다대다 관계로 모델링할 것이다.
스키마 없는 도메인 모델링
Bigtable 위의 Google App Engine 추상화는 오브젝트 지향적이지 않다. 즉, 관계를 있는 그대로 저장할 수는 없지만 키를 공유할 수 있다. 결국 RaceRunner 사이의 관계를 모델링하기 위해 Runner 키 목록을 Race의 각 인스턴스에 저장한다(또한 그 반대로도 저장함).
하지만 발생하는 API가 자연스럽길 바라기 때문에 키 공유 메커니즘에 약간의 논리를 추가할 것이다. Runner 키 목록에 대해 Race를 요구하지 않고 Runner 목록을 원한다. 다행히 이러한 작업은 어렵지 않다.
Listing 9에서는 Race 인스턴스에 두 개의 메소드를 추가했다. Runner 인스턴스가 addRunner 메소드에 전달되면 해당 id가 기본 entityrunners에 있는 idCollection에 추가된다. runners의 기존 collection이 있는 경우에는 새 Runner 인스턴스 키가 추가된다. 그렇지 않으면 새 Collection이 작성되고 Runner의 키(엔티티의 id 특성)가 추가된다.

Listing 9. 러너 추가 및 검색하기
def addRunner(runner){
 if(this.@entity.runners){
  this.@entity.runners << runner.id
 }else{
  this.@entity.runners = [runner.id]
 }
}

def getRunners(){
 return this.@entity.runners.collect {
  new Runner( this.getEntity(Runner.class.simpleName, it) )
 }
}

Listing 9에 있는 getRunners 메소드가 호출되면 Runner 인스턴스의 콜렉션이 id의 기본 콜렉션에서 작성된다. 따라서 Listing 10과 같이 새 메소드(getEntity)가 Model 클래스에 정의된다.

Listing 10. ID로부터 엔티티 작성하기
def getEntity(entityType, id){
 def key = KeyFactory.createKey(entityType, id)			
 return this.@datastore.get(key)
}

getEntity 메소드는 Google의 KeyFactory 클래스를 사용하여 데이터 저장소에서 개별 엔티티를 찾는 데 사용할 수 있는 기본 키를 작성한다.
마지막으로 Listing 11과 같이 엔티티 유형을 승인하는 새 생성자가 정의된다.

Listing 11. 새로 추가된 생성자
public Model(Entity entity){
 this.@entity = entity
}

Listing 9, 1011그림 1의 오브젝트 모델에서 알 수 있듯이 RunnerRace에 추가할 수 있으며 Race에서 Runner 인스턴스 목록을 가져올 수도 있다. Listing 12에서는 등식의 Runner 쪽에 비슷한 연결을 작성한다. Listing 12에서는 Runner 클래스의 새 메소드를 보여 준다.

Listing 12. 러너와 해당 레이스
def addRace(race){
 if(this.@entity.races){
  this.@entity.races << race.id
 }else{
  this.@entity.races = [race.id]
 }
}

def getRaces(){
 return this.@entity.races.collect {
  new Race( this.getEntity(Race.class.simpleName, it) )
 }
}

이 방식으로 스키마 없는 데이터 저장소를 사용하여 두 개의 도메인 오브젝트를 모델링했다.
일부 러너와 함께 레이스 끝내기
이제는 Runner 인스턴스를 작성하여 Race에 추가하기만 하면 된다. 그림 1의 오브젝트 모델이 보여 주는 것과 같이 양방향 관계를 원하는 경우에는 Listing 13과 같이 Race 인스턴스를 Runner에 추가할 수 있다.

Listing 13. 레이스가 있는 러너
def runner = new Runner([fname:"Chris", lname:"Smith", date:34])
runner.save()

race.addRunner(runner)
race.save()

runner.addRace(race)
runner.save()

Runnerrace에 추가하고 Racesave를 호출한 후 그림 3의 스크린샷과 같이 데이터 저장소가 ID 목록으로 업데이트되었다.

그림 3. 레이스에 있는 러너의 새 특성 보기
레이스에 있는 러너의 새 특성 보기
Google App Engine에서 데이터를 자세히 살펴보면 그림 4와 같이 Race 엔티티에는 이제 Runnerlist가 포함되어 있음을 알 수 있다.

그림 4. 새 러너 목록 보기
새 러너 목록 보기
마찬가지로 그림 5와 같이 Race를 새로 작성된 Runner 인스턴스에 추가하기 전에는 특성이 존재하지 않는다.

그림 5. 레이스가 없는 러너
레이스가 없는 러너
RaceRunner에 연관시킨 후 데이터 저장소는 새 race idlist를 추가한다.

그림 6. 레이스를 시작한 러너
레이스를 시작한 러너
스키마 없는 데이터 저장소의 유연성은 새로운 느낌을 준다. 특성은 요구 시 기본 저장소에 자동으로 추가된다. 개발자로서 필자는 스키마를 업데이트하거나 변경하지 않아도 되고 전개도 하지 않아도 된다.
NoSQL에 대한 찬반 논쟁
물론 스키마 없는 데이터 모델링에도 찬반 논쟁이 있다. Back to the Races 애플리케이션의 한 가지 장점은 매우 유연하다는 것이다. Runner에 새 특성(예: SSN)을 추가하려고 하는 경우 많은 작업을 수행하지 않아도 된다. 해당 특성을 생성자의 매개변수에 포함시키면 거기에 해당 매개변수가 존재한다. SSN을 사용하여 작성되지 않은 이전 인스턴스에는 무슨 일이 생기는가? 아무일도 생기지 않는다. null인 필드가 존재하게 된다.

빠른 리더

속도는 NoSQL 대 관계형 논쟁에서 중요한 요소이다. 잠재적으로 수백만 명의 사용자를 위해 데이터를 전달하는 최신 웹 사이트(Facebook의 사용자 수 4억 명을 떠올려 보자)의 경우 관계형 모델은 너무 느릴 뿐만 아니라 비용도 많이 든다. 반면에 NoSQL의 데이터 저장소는 읽기 속도에 있어서는 엄청나게 빠르다.
한편으로는 효율성을 위해 일관성 및 무결성의 저하를 감수했다. 애플리케이션의 현재 데이터 아키텍처는 제한조건을 두지 않는다. 이론적으로는 동일한 오브젝트의 인스턴스를 수에 제한없이 작성할 수 있다. Google App Engine의 키 처리 하에서는 모두 고유 키를 가지지만 그 외는 모두 동일하다. 더욱이 연계 삭제는 존재하지 않기 때문에 동일한 기법을 사용하여 일대다 관계를 모델링한 경우 상위가 제거되면 무효인 하위만 남게 될 수 있다. 물론 자체 무결성 검사를 구현할 수 있다. 하지만 이것이 핵심이다. 다른 모든 사항을 수행한 것과 마찬가지로 이 검사를 자체적으로 해야 한다.
스키마 없는 데이터 저장소를 사용하려면 규율이 필요하다. 다양한 유형의 Races(일부는 이름이 있고 일부는 이름이 없으며 일부는 date 특성이 있고 일부는 race_date 특성이 있음)를 작성하면 필자와 필자의 코드를 활용하는 모든 사용자의 발등을 찍는 것이 된다.
물론 Google App Engine과 함께 JDO 및 JPA를 사용할 수도 있다. 여러 프로젝트에 관계형 모델과 스키마 없는 모델을 둘 다 사용한 경우에는 Gaelyk의 하위 레벨 API가 가장 작업하기 쉽고 유연하다. Gaelyk을 사용하면 Bigtable과 스키마 없는 데이터 저장소에 대해 전반적으로 이해할 수 있는 장점도 있다.
결론
유행은 변하기 때문에 가끔은 이러한 유행을 무시하는 것이 낫다(옷장에 레저용 옷만 가득한 사람이 제공하는 현명한 조언이다). 하지만 NoSQL은 유행이라기 보다는 확장성 높은 웹 애플리케이션 개발의 새로운 기반이다. 하지만 NoSQL 데이터베이스는 RDBMS를 대체하지는 않고 보완한다. 무수히 많은 성공적인 도구와 프레임워크가 관계형 데이터베이스 위에서 활발하게 사용되고 있으며 RDBMS 자체는 인기가 시드는 위험에 처해 있지 않다.
결국 NoSQL 데이터베이스는 오브젝트-관계형 데이터 모델에 대한 적절한 대안으로 존재한다. NoSQL 데이터베이스는 다른 방식을 사용할 수 있으며 매우 뛰어난 구체적인 유스 케이스의 경우에는 다른 방식이 더 낫다는 것을 보여 준다. 스키마 없는 데이터베이스는 빠른 데이터 검색 및 확장성이 필요한 멀티노드 웹 애플리케이션에 가장 적합하다. 개발자가 관계형 지향 관점이 아니라 도메인 지향 관점에서 데이터 모델링에 접근하도록 교육한다는 부수적인 효과도 있다.

참고자료
교육
  • Java development 2.0: 이 developerWorks 시리즈에서 Gaelyk(2009년 12월), Google App Engine(2009년 8월) 및 CouchDB(2008년 11월)를 포함하여 Java 개발 환경을 재정의하는 기술과 도구에 대해 살펴보자.
  • "NoSQL Patterns"(Ricky Ho, Pragmatic Programming Techniques, 2009년 11월): NoSQL 데이터베이스의 개요 및 목록을 확인할 수 있고 NoSQL 데이터 저장소의 일반적인 아키텍처에 대한 자세한 설명을 볼 수 있다.
  • "Saying Yes to NoSQL; Going Steady with Cassandra"(John Quinn, Digg Blogs, 2010년 3월): 엔지니어링에 대한 Digg의 VP에서는 MySQL에서 Cassandra로 전환하는 결정에 대해 설명한다.
  • "Sharding with Max Ross"(JavaWorld 팟캐스트, 2008년 7월): Andrew Glover가 Google의 Max Ross와 함께 나눈 샤딩(sharding) 기법과 Hibernate Shards의 개발에 대한 대화를 들어보자.
  • "Is the Relational Database Doomed?"(Tony Bain, ReadWriteEnterprise, 2009년 2월): 클라우드의 내부와 외부 모두에서 비관계형 데이터베이스가 등장하면서 "광범위한 온디맨드 확장성을 원하는 경우에는 비관계형 데이터베이스가 필요하다."라는 명확한 메시지가 제시되고 있다.
  • Google App Engine for Java: Part 3: 영속성과 관계"(Richard Hightower, developerWorks, 2009년 8월): Rick Hightower가 Google App Engine의 현재 Java 기반 지속성 프레임워크의 단점과 일부 대안에 대해 설명한다.
  • "Amazon Web Services를 사용한 클라우드 컴퓨팅, Part 5: SimpleDB를 통해 클라우드의 데이터세트 처리하기"(Prabhakar Chaganti, developerWorks, 2009년 2월): Amazon SimpleDB의 개념에 대해 알아보고 Amazon SimpleDB와 상호작용하기 위한 오픈 소스 Python 라이브러리인 boto에서 제공하는 일부 기능을 살펴보자.
  • "Bigtable: A Distributed Storage System for Structured Data"(Fay Chang 등, Google, 2006년 11월): Bigtable은 매우 큰 크기(수천 개의 상용 서버에 걸쳐있는 페타바이트 크기의 데이터)까지 범위를 확장하도록 설계된 구조화된 데이터를 관리하는 데 필요한 분산 스토리지 시스템이다.
  • "The Vietnam of Computer Science"(Ted Neward, 2006년 6월): 관계형 모델에 대한 오브젝트 맵핑과 연관된 과제를 해결한다.
  • 기술 서점에서 다양한 기술 주제와 관련된 서적을 살펴보자.
  • developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
  • Gaelyk: Google App Engine을 위한 Groovy의 경량 애플리케이션 개발 프레임워크를 사용해 보자.
토론
필자소개
Andrew Glover 사진Andrew Glover는 Stelligent Incorporated의 사장이다. 회사들이 코드 품질을 일찍 그리고 자주 모니터할 수 있게 하는 효과적인 개발자 테스팅 전략과 지속적 통합 기법으로 소프트웨어 품질 문제를 해결하는 것을 돕고 있다. Andy의 저서 목록은 그의 블로그를 보라.

memcached를 적용하여 사이트 성능 향상

http://www.ibm.com/developerworks/kr/library/os-memcached/index.html

memcached를 적용하여 사이트 성능 향상

데이터베이스 및 데이터 소스에서부터 읽기 감소
Martin Brown, 자유기고가, IT 컨설턴트/프리랜서 작가
요약: 오픈 소스 memcached 도구는 디스크나 데이터베이스와 같이 상대적으로 속도가 느린 소스에서부터 정보를 로드하는 것(및 처리하는 것)을 방지하도록 자주 사용하는 정보를 저장하기 위한 캐시입니다. 이는 전용 상황에서나 기존 환경에서 여유 메모리를 다 사용하는 메소드 중 하나로 배치할 수 있습니다. memcached의 간결성에도 불구하고 이는 때때로 올바르지 않게 사용되거나 잘못된 환경 유형에서 솔루션으로 사용됩니다. memcached 사용을 최상으로 활용하는 시점에 대해 알아봅시다.
원문 게재일:  2010 년 8 월 03 일 번역 게재일:  2011 년 3 월 29 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (111KB | 18 pages)Get Adobe® Reader®
페이지뷰: 1880 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 1표)
소개
memcached는 애플리케이션 프로세스의 속도를 높이는 데 사용된다. 여기에서는 사용자의 애플리케이션과 환경 내에서 이를 배치하기 위한 모범 사례에 주목할 것이다. 이는 저장해야 하는 것과 저장하지 말아야 할 것, 데이터의 유연한 분배를 처리하는 방법 및 데이터의 memcached로 저장된 버전을 업데이트하기 위한 메소드를 규제하는 방법이 포함된다. 또한 IBM WebSphere® eXtreme Scale을 비롯한 고가용성 솔루션의 지원에 대해서도 다룰 것이다.
모든 애플리케이션, 특히 많은 수의 웹 애플리케이션은 정보에 액세스하여 클라이언트에 리턴하는 속도를 최적화해야 한다. 하지만, 동일한 정보가 자주 리턴된다. 데이터 소스(데이터베이스 또는 파일 시스템)로부터 데이터를 로드하는 것은 비효율적이며, 정보에 액세스하려 할 때마다 궁극적으로 동일한 쿼리를 실행한다면 특히 비효율적이다.
비록 많은 수의 웹 서버는 정보를 다시 보내기 위해 캐시를 사용하도록 구성할 수 있지만, 이는 대부분 애플리케이션의 동적인 특성과 함께 작동하지 않는다. 이러한 점에서 memcached가 유용할 수 있다. 이는 네이티브 언어객체를 포함하여 모든 것을 보유할 수 있는 일반화된 메모리 저장소를 제공하여, 사용자는 다양한 정보를 저장할 수 있으며 많은 애플리케이션과 환경에서부터 액세스할 수 있다.
기본
memcached는 많은 서버에서 여유 RAM을 활용하도록 설계된 오픈 소스 프로젝트로서, 자주 액세스하는 정보에 대해 메모리 캐시로서 작동한다. 핵심 요소는 캐시(cache)라는 단어의 사용이다. 즉, memcached는 아무 곳에서부터 로드할 수 있는 정보의 임시 스토리지를 메모리 내에 제공한다.
예를 들어, 일반적인 웹 기반 애플리케이션을 살펴보자. 동적으로 동작하는 웹 사이트 조차도 대개 페이지의 사용 내내 일부 컴포넌트나 정보 상수가 있다. 블로그 내에서 개별 블로그 포스트를 위한 카테고리의 목록은 페이지 보기 사이에 정기적으로 변경될 가능성이 낮다. 쿼리를 통해 데이터베이스로 이 정보를 매번 로드하는 것은, 특히 데이터가 변경되지 않을 때에는 비교적 비용이 많이 든다. 그림 1에서 블로그 내에서 캐시될 수 있는 일부 페이지 단편을 볼 수 있다.

그림 1. 일반적인 블로그 페이지의 캐시 가능한 요소
다이어그램에서는 캐시 가능한 블로그 요소 및 레이아웃을 표시한다. 즉, 맨 위의 Page Header, 왼쪽의 Current post lists, 오른쪽의 About, Post History, External Links 및 Category list 등이다.

이 구조를 블로그, 게시자 정보, 설명의 — 블로그 자체를 포스트하는 경우에도 — 다른 요소로 추론한다. 단지 홈페이지의 내용을 표시하기 위해서 발생하는 10-20개의 데이터베이스 쿼리와 포맷팅을 식별할 수 있다. 이를 매일 수 백 또는 수 천 번 이상의 페이지 보기로 반복하면, 사용자의 서버 및 애플리케이션은 페이지 내용을 표시하는 데 필요한 것보다 훨씬 더 많은 쿼리를 실행할 것이다.
memcached를 사용하면 데이터베이스에서부터 로드된 포맷된 정보를 웹 페이지에서 직접 사용이 가능한 형태로 저장할 수 있다. 그리고 정보가 데이터베이스와 다른 처리를 통해 디스크에서부터가 아니라 RAM에서부터 로드되었기 때문에, 정보로의 액세스도 거의 동시적이다.
다시 반복하면, memcached는 디스크나 데이터베이스와 같이 상대적으로 속도가 느린 소스에서부터 정보를 로드하고 처리하는 것을 방지하도록 자주 사용하는 정보를 저장하기 위한 캐시이다.
memcached로의 인터페이스는 네트워크 연결을 통해 제공된다. 이는 여러 클라이언트와 함께 단일 memcached 서버(또는 이 기사의 후반부에서 시연할 여러 서버)를 공유할 수 있다는 뜻이다. 네트워크 인터페이스는 빠르고, 성능을 향상시키기 위해 서버는 의도적으로 인증 또는 보안 통신을 지원하지 않는다. 하지만 이로 인해 배치 옵션을 제한해서는 안 된다. memcached 서버는 네트워크의 내부에 존재해야 한다. 네트워크 인터페이스가 실용적이고 memcached 인스턴스를 여러 개 배치하기 쉬워서 여러 장비에서 여분의 램을 활용할 수 있고 캐시 전체 크기를 늘릴 수 있다.
memcached를 사용하는 스토리지 메소드는 많은 언어에 사용 가능한 해시 또는 연관 배열과 유사한 단순한 키워드/값 쌍이다. 키와 값을 제공하여 정보를 memcached로 저장하고, 지정된 키로 정보를 요청하여 정보를 복구한다.
정보는 다음 중 하나의 경우가 발생하지 않는 한, 캐시에서 영구적으로 보존된다.
  1. 캐시에 할당된 메모리가 고갈된다 — 이 인스턴스에서 memcached는 LRU(최근에 가장 적게 사용된) 메소드를 사용하여 캐시에서부터 항목을 제거한다. 최근에 사용되지 않은 항목이 캐시에서부터 삭제되며, 오래된 항목부터 액세스된다.
  2. 항목이 구체적으로 삭제된다 — 언제나 항목을 캐시에서부터 삭제할 수 있다.
  3. 항목이 만료된다 — 개별 항목은 만기가 있어서, 키에 대해 저장된 정보가 너무 오래될 가능성이 높을 때에 항목을 캐시에서부터 비울 수 있다.
이러한 상황은 캐시에서의 정보를 최신 상태로 보장하는 애플리케이션 논리와 결합하여 사용할 수 있다. 이러한 내용을 염두에 두고 애플리케이션에서 memcached를 사용하는 최상의 방법에 대해 살펴보자.
memcached를 사용하는 경우
애플리케이션 성능을 개선하기 위해 memcached를 사용할 때 수정 가능한 다양한 핵심 프로세스와 단계가 있다.
정보를 로드할 때에 일반적인 시나리오는 그림 2에 표시된다.

그림 2. 표시하기 위해 정보를 로드하는 일반적인 순서
다이어그램에서는 데이터를 로드하는 것에서부터 데이터를 처리/포맷하여 데이터를 클라이언트에 전송하는 것까지의 흐름을 보여준다.

일반적인 경우에 단계는 다음과 같다.
  1. 하나 이상의 쿼리를 실행하여 데이터베이스에서부터 정보를 로드한다.
  2. 표시(또는 향후 처리)에 적합한 정보를 포맷한다.
  3. 포맷된 데이터를 사용하거나 표시한다.
memcached를 사용할 때에 캐시를 수용하기 위해 애플리케이션 논리를 약간 변경할 수 있다.
  • 캐시에서부터 정보를 로드하도록 시도한다.
  • 정보의 캐시된 버전이 존재하는 경우에 이를 사용한다.
  • 존재하지 않는 경우:
    1. 하나 이상의 쿼리를 실행하여 데이터베이스에서부터 정보를 로드한다.
    2. 표시 또는 향후 처리에 적합한 정보를 포맷한다.
    3. 정보를 캐시로 저장한다.
    4. 포맷된 데이터를 사용한다.
이는 그림 3에 요약되어 있다.

그림 3. memcached를 사용할 때 표시하기 위해 정보 로드
다이어그램에서는 요청된 데이터가 캐시에 존재하는 경우, 모든 처리 단계를 건너뛰어 시간이 절약된 것을 보여준다.

이렇게 하면 데이터의 로딩은 대체적으로 3단계 프로세스가 된다. 즉, 캐시나 데이터베이스에서부터 데이터를 로드하고 적절하게 캐시에 저장하는 것이다.
최초로 이 프로세스가 발생하면, 데이터가 정상적으로 데이터베이스나 다른 소스에서부터 로드되고 memcached로 저장된다. 다음에 정보에 액세스하면 데이터베이스에서부터 로드하는 것이 아니라 memcached에서부터 빼내어 시간과 CPU 사이클을 줄여준다.
이 등식의 다른 쪽은 memcached 내에서 저장될 수 있는 정보를 변경하는 경우, memcached 버전을 업데이트하는 동시에 백엔드 정보를 업데이트하도록 보장하는 것이다. 이는 그림 4에 표시된 일반적인 순서를 그림 5에서의 약간의 수정으로 수정한다.

그림 4. 일반적인 애플리케이션에서 데이터 업데이트 또는 저장
다이어그램에서는 데이터를 업데이트하는 것에서부터 데이터를 처리/포맷하여 업데이트된 데이터를 클라이언트에 전송하는 것까지의 흐름을 보여준다.

그림 5에서는 memcached를 사용하여 수정된 순서를 보여준다.

그림 5. memcached를 사용할 때 데이터 업데이트 또는 저장
다이어그램에서는 데이터를 업데이트하는 것에서부터 데이터를 처리/포맷하여 memcached에 저장하고 업데이트된 데이터를 클라이언트에 전송하는 것까지의 확대된 흐름을 보여준다.

예를 들면, 기본으로 블로그를 사용하여 블로그 시스템이 데이터베이스에서 카테고리의 목록을 업데이트할 때에 업데이트는 이 순서를 따라야 한다.
  1. 데이터베이스에서 카테고리 목록 업데이트
  2. 정보 포맷
  3. 정보를 memcached에 저장
  4. 정보를 클라이언트에게 리턴
memcached 내에서 스토리지 조작은 원자적이기 때문에, 클라이언트는 부분적인 데이터만 받지 않고 정보가 업데이트되어 이전 버전이나 새 버전을 받게 된다.
대부분의 애플리케이션에서 이 두 개의 조작에 대해 우려해야 한다. 사람들이 사용하는 데이터에 액세스하면 이는 캐시에 자동으로 추가되고 캐시에서 자동으로 업데이트되는 데이터로 변경된다.
키, 네임스페이스 및 값
memcached를 사용하면서 중요한 고려사항은 캐시 내에 저장한 데이터를 어떻게 조직하고 이름을 지정하는가이다. 이전 블로그 예제에서 분명한 것은 일관된 이름 지정 구조를 사용해야 한다는 것이다. 이렇게 하여 블로그 카테고리, 히스토리 및 기타 정보를 로드할 수 있고, 정보를 로드하거나(및 캐시를 업데이트하거나) 데이터를 업데이트할 때에(및 캐시를 다시 업데이트할 때) 이를 사용할 수 있다.
사용자가 사용하는 정확한 이름 지정 시스템은 특정 애플리케이션에 적합하지만, 일반적으로는 대개 고유 ID 종류를 기반으로 하는 기존 애플리케이션의 유사한 구조를 사용할 수 있다. 이는 데이터베이스에서부터 정보를 빼내거나 정보 콜렉션을 대조할 때에 발생한다.
블로그 포스트를 예제로 사용하여 카테고리의 목록을 category-list 키로 항목에 저장할 수 있다. blogpost-29와 같이 포스트 ID에 대해 하나의 포스트로 연관되는 값을 사용할 수 있는 동시에 항목에 대한 설명은 29가 블로그 포스트 ID인 blogcomments-29에 저장할 수 있다. 이 방법에서는 정보를 식별하는 다른 접두부를 사용하여 엄청나게 다양한 정보를 캐시에 저장할 수 있다.
memcached 키/값 저장소의 간결성(및 보안의 부재)이라는 것은, 동시에 동일한 memcached 서버를 사용하여 다수의 애플리케이션을 지원하려는 경우, 특정 애플리케이션에 속한 데이터를 식별하는 다른 수량사의 형태를 사용하는 것을 고려할 수 있다는 의미이다. 예를 들어, blogapp:blogpost-29와 같은 애플리케이션 접두부를 추가할 수 있다. 키는 자유 형식이기 때문에, 키 이름으로 아무 문자열이나 원하는 대로 사용할 수 있다.
값 저장에 대해서는 캐시에 저장한 정보가 애플리케이션에 적합한지 확인해야 한다. 예를 들어, 블로그 시스템을 사용하면 원시 HTML이 아니라 포스트 정보를 포맷하기 위해 블로그 애플리케이션에서 사용한 오브젝트를 저장하려 할 수 있다. 이는 동일한 기본 구조가 애플리케이션 내에서 여러 장소에 사용되는 경우 더 실용적일 수 있다.
Java™, Perl, PHP 및 기타 등등을 비롯하여 대부분의 언어 인터페이스는 memcached 내에서 스토리지에 대해 언어 오브젝트를 직렬화할 수 있다. 이를 통해 사용자는 애플리케이션 내에서 수동으로 재구성하는 것이 아니라 전체 오브젝트를 저장하고 메모리 캐시에서부터 이후에 복구할 수 있다. 많은 오브젝트 또는 이들이 사용하는 구조는 특정한 종류의 해시 또는 배열 구조를 기반으로 한다. JSP 환경과 JavaScript 환경 사이에 동일한 정보를 공유하려 할 때와 같은 교차언어 환경에서는 JSON(JavaScript Object Notation) 또는 심지어 XML과 같이 아키텍처 중립형 포맷을 사용할 수 있다.
memcached를 채우고 사용하기
오픈 소스 제품이자 기존 오픈 소스 환경 내에서 작업하도록 개발된 memcached는 광범위한 환경과 플랫폼에서 지원된다. memcached 서버와 통신하기 위한 인터페이스는 다양하여, 종종 모든 언어에 대해 여러 구현으로 나타난다. 일반적인 라이브러리와 툴킷에 대한 참고자료를 확인한다.
지원되는 제공 인터페이스와 환경을 모두 나열할 수는 없지만, 이들은 모두 memcached 프로토콜로 제공되는 기본 API를 지원한다. 이러한 설명은 간결하며, 다른 값을 사용하여 오류를 표시할 수 있는 다른 언어의 컨텍스트 내에서 다루어야 한다. 기본 함수는 다음과 같다.
  • get(key) — 지정된 키를 저장한 memcached에서부터 정보를 얻는다. 키가 존재하지 않는 경우에 오류를 리턴한다.
  • set(key, value [, expiry]) — 캐시에서 ID 키를 사용하여 지정된 값을 저장한다. 키가 이미 존재하는 경우 업데이트된다. 만료 시간은 초 단위이며, 값이 30일(30*24*60*60) 미만인 경우 상대 시간으로 처리되고, 또는 이 값 이상인 경우 절대 시간(에포크)으로 처리된다.
  • add(key, value [, expiry]) — 키가 존재하지 않는 경우 캐시에 추가하고, 또는 키가 이미 존재하는 경우 오류를 리턴한다. 이는 키가 이미 존재하는 경우 업데이트하지 않고 명시적으로 새 키를 추가하려고 하는 경우 유용할 수 있다.
  • replace(key, value [, expiry]) — 지정된 키의 값을 업데이트하고, 키가 존재하지 않는 경우에 오류를 리턴한다.
  • delete(key [, time]) — 캐시에서부터 키/값 쌍을 삭제한다. 시간을 제공하는 경우, 지정된 기간 동안 이 키로 새 값을 추가하는 것이 차단된다. 제한시간을 통해 데이터 소스에서부터 값을 항상 다시 읽도록 보장할 수 있다.
  • incr(key [, value]) — 지정된 키를 하나씩 또는 지정된 값만큼 증분한다. 수적인 값에서만 작동한다.
  • decr(key [, value]) — 지정된 키를 하나씩 또는 지정된 값만큼 감소시킨다. 수적인 값에서만 작동한다.
  • flush_all — 캐시에서 모든 현재 항목을 무효화(또는 만료)한다.
예를 들어, Perl에서 기본 연산 세트는 Listing 1에 표시된 대로 처리될 것이다.

Listing 1. Perl에서 기본 연산 세트
use Cache::Memcached;

my $cache = new Cache::Memcached {
    'servers' => [
                   'localhost:11211',
                   ],
    };

$cache->set('mykey', 'myvalue');

Listing 2에 Ruby에서의 동일한 기본 연산을 보여준다.

Listing 2. Ruby에서 기본 세트 연산
require 'memcache'
memc = MemCache::new '192.168.0.100:11211'

memc["mykey"] = "myvalue"

두 개의 예제 모두에서 동일한 기본 구조로, memcached 서버를 설정한 다음에 값을 지정하거나 설정하는 것을 볼 수 있다. Java 기술에서 이를 작동하는 것을 비롯하여 다른 인터페이스도 사용 가능하여, 사용자가 WebSphere 애플리케이션에서 memcached를 사용할 수 있다. memcached 인터페이스 클래스를 통해 Java 오브젝트를 바로 memcached로 직렬화할 수 있기 때문에, 복잡한 구조를 저장하고 로드할 수 있다. WebSphere와 같은 환경에서 배치할 때에 두 가지 사항이 매우 중요하다. 즉, 서비스의 복원성(및 memcached를 사용할 수 없는 경우에 해야 할 일)과 WebSphere eXtreme Scale과 같은 여러 환경이나 애플리케이션 서버를 사용할 때 성능을 개선하기 위해 캐시 스토리지를 증가시키는 방법이다. 다음으로 이러한 문제점을 모두 살펴볼 것이다.
복원성 및 가용성
memcached에 대한 가장 일반적인 질문 중 하나는 "캐시를 사용할 수 없을 때 어떠한 일이 발생하는가?"이다. 이전 섹션에서 명확히 한 바와 같이 캐시에서의 정보가 유일한 정보의 소스가 되어서는 안 된다. 다른 위치에서부터 캐시에 저장된 데이터를 로드할 수 있어야 한다.
현실은 애플리케이션의 성능을 저하시키는 캐시로부터 정보에 액세스할 수 없다 하더라도, 이로 인해 애플리케이션의 작동을 중지해서는 안 된다. 다음과 같이 발생 가능한 몇 가지의 시나리오가 있다.
  1. memcached 서비스가 고장나면, 애플리케이션은 원본 데이터 소스로부터의 정보를 로드하고 필요한 경우 표시하기 위해 포맷하도록 폴백(fall back)해야 한다. 또한 애플리케이션은 memcached에서의 정보를 로드하고 저장하기 위해 계속 시도해야 한다.
  2. memcached 서버가 다시 사용 가능하게 되면 애플리케이션이 자동으로 데이터를 저장하도록 시도해야 한다. 캐시된 데이터를 강제로 다시 로드해야 할 필요는 없으며, 사용자는 캐시를 정보로 로드하고 채우는 표준 액세스를 사용할 수 있다. 결과적으로 캐시는 가장 일반적으로 사용되는 데이터로 다시 채워진다.
다시 말해서, memcached는 정보의 캐시이고 유일한 소스가 아니다. memcached 서버를 손실하면 memcached 서버가 백업될 때까지 성능이 저하될 수는 있지만, 그렇다고 해서 애플리케이션을 더 이상 쓰지 못하게 되어서는 안 된다. 실제로 memcached 서버가 상대적으로 간결하고, 손상방지(crash-free)가 아니라고 해도 간결성으로 인해 오류가 더 적게 발생한다.
캐시 분배
memcached 서버는 네트워크 전체에서 키에 대해 값을 저장하는 캐시에 불과하다. 여러 시스템이 있는 경우, 모든 여유 시스템에 memcached 인스턴스를 설정하여 엄청난 양의 네트워킹된 RAM 캐시 스토리지를 제공하고 싶을 것이다.
이 아이디어를 접하게 되면, 시스템 사이에 키/값 쌍을 복사하는 특정 분배 또는 복제 메커니즘의 종류가 필요하다고 가정하려 할 것이다. 이 접근방식의 문제점은 실제로 사용 가능한 RAM 캐시를 늘리는 것이 아니라 감소시키는 것이다. 그림 6을 살펴보면 각각 memcached 인스턴스로 액세스가 있는 세 개의 애플리케이션 서버가 있음을 볼 수 있다.

그림 6. 여러 memcached 인스턴스의 잘못된 사용
다이어그램에서는 각각 1GB 캐시 공간을 양산하는 세 개의 애플리케이션 서버를 지원하는 memcached의 세 개로 분리된 1GB 인스턴스를 보여준다.

비록 각 memcached 인스턴스의 크기가 1GB(RAM 캐시의 3GB 부여)라고 하더라도 각 애플리케이션 서버가 자체 캐시만 보유하는 경우(또는 memcached 인스턴스 사이에 데이터의 복제가 있는 경우) 전체 설치는 여전히 각 인스턴스에 걸쳐서 1GB의 캐시 복제만 보유할 것이다.
memcached가 네트워크 인터페이스에 걸쳐서 정보를 제공하기 때문에 단일 클라이언트는 memcached 인스턴스에서부터 액세스 권한이 있는 데이터에 액세스할 수 있다. 데이터가 각 인스턴스에 걸쳐서 복사 또는 복제되지 않는 경우, 결과적으로는 그림 7에 표시된 대로 각 애플리케이션 서버에 사용 가능한 3GB RAM 캐시가 나타난다.

그림 7. 여러 memcached 인스턴스의 올바른 사용
다이어그램에서는 전체적으로 3GB 공유 캐시 공간을 양산하는 세 개의 애플리케이션 서버를 지원하는 memcached의 세 개로 된 대화식 1GB 인스턴스를 보여준다.

이 접근방식의 문제점은 키/값 쌍을 어느 서버에 저장하도록 선택하는 것과 값을 복구하려고 할 때에 어느 memcached 서버와 대화하는지 어떻게 결정하는가이다. 솔루션은 검색 테이블과 같은 복잡도나 memcached 서버가 이 프로세스를 처리할 것이라는 예상을 무시하는 것이다. 그 대신에 memcached 클라이언트는 단순하게 생각해야 한다.
memcached 클라이언트는 이 정보를 판별하기 위해 보유하는 것이 아니라 정보를 저장할 때에 지정한 키에서 단순한 해싱 알고리즘을 사용한다. memcached 서버 목록에서부터 정보를 얻거나 저장할 때에 memcached 클라이언트는 일관된 해싱 알고리즘을 사용하여 키에서부터 수적인 값을 유추한다. 예를 들어 설명하면, mykey 키는 23875 값으로 변환된다. 정보를 저장하는 중이거나 얻는 중이든지 여부와 상관 없이, memcached 서버에서부터 고유 ID를 로드하는 것과 동일한 키를 사용하게 된다. 따라서 이 예제에서 "mykey"는 항상 23875의 값에 해시를 얻게 될 것이다.
두 개의 서버가 있는 경우 memcached 클라이언트는 이 수적인 값에 단순한 계산(예: 계수)을 수행하여 첫 번째나 두 번째로 구성된 memcached 인스턴스에 값을 저장해야 하는지 여부를 결정한다.
값을 저장할 때에 클라이언트는 저장하기 위해 사용된 키와 서버에서부터 해시 값을 결정한다. 값을 얻으면 클라이언트는 키에서부터 동일한 해시 값을 결정하고 정보를 얻기 위해 동일한 서버를 선택한다.
모든 애플리케이션 서버에서 동일한 서버 목록(및 동일한 순서)을 사용하는 경우 모든 애플리케이션 서버는 동일한 키를 저장하거나 검색할지 묻는 경우 동일한 서버를 선택하게 된다. 동일한 1GB의 공간을 복제하는 것이 아니라 현재 3GB의 memcached 공간을 공유하고 있어서, 캐시를 더 많이 사용하도록 부여하여 더 많은 사용자를 위해 애플리케이션의 성능을 개선할 가능성이 높다.
이 프로세스에 복잡도가 있지만(서버를 사용할 수 없는 경우 어떠한 일이 발생하는가와 같이), 문서에서 자세한 정보를 제공한다(참고자료 참조).
대표적인 memcached 오용 사례
memcached는 단순한 도구인데 memcached 인스턴스를 원래 의도와 다른 부적절한 방식으로 사용하고 싶어할 수도 있다.
memcached는 데이터베이스가 아니다
memcached의 가장 일반적인 잘못된 사용은 아마도 캐시가 아니라 데이터 저장소로서 사용하는 것이다. memcached의 기본 용도는, 그렇지 않았으면 다른 소스에서부터 복구되거나 구성될 수 있는 데이터에 대한 응답 시간을 개선하는 것이다. 예로는 데이터베이스에서부터 정보를 복구하는 것이다(특히, 사용자에게 정보가 표시되기 전에 포맷하거나 조작하는 경우). memcached는 메모리에 이 정보를 저장하도록 설계되어, 데이터가 복구될 때마다 해당 태스크의 반복적인 수행을 방지한다.
애플리케이션을 실행하는 데 필요한 유일한 정보의 소스로 memcached를 사용해서는 안 된다. 즉, 데이터는 항상 다른 소스에서도 끌어올 수 있어야 한다. 또한 memcached는 키/값 저장소에 불과하다는 것을 유의하자. 데이터에 걸쳐 쿼리를 수행하거나 정보를 추출하기 위해 내용 전체에 대해 반복할 수는 없다. 이를 대규모 단위에서 사용하는 데 필요한 데이터의 오브젝트나 블록을 저장하기 위해 사용해야 한다.
데이터베이스 행 또는 파일을 캐시하면 안 된다
데이터베이스에서부터 로드되면서 데이터의 행을 저장하기 위해 memcached를 사용할 수 있다 하더라도 이는 실제로 쿼리 캐싱이며, 대부분의 데이터베이스는 자체의 쿼리 캐싱 메커니즘을 제공한다. 이와 동일한 내용을 파일시스템으로부터의 파일이나 이미지와 같은 다른 오브젝트에 대해서도 적용할 수 있다. 많은 수의 애플리케이션과 웹 서버는 이러한 유형의 작업에 대해 이미 훌륭하게 최적화된 솔루션을 보유하고 있다.
로드하고 포맷한 후에 정보의 전체 블록을 저장하기 위해 memcached를 사용하면 이로부터 더 많은 유용성과 성능 개선을 확보하게 된다. 블로그 예제를 보면, 정보를 저장하는 최상의 지점은 블로그 카테고리를 오브젝트로 포맷한 후이거나 심지어 HTML로 포맷한 이후였다. 그 다음에 memcached에서부터 컴포넌트(블로그 포스트, 카테고리 목록, 포스트 히스토리 등)를 로드하고 완성된 HTML을 다시 클라이언트에게 쓰면 블로그 페이지의 구성을 수행할 수 있다.
memcached는 안전하지 않다
최대 성능을 보장하기 위해 memcached는 인증이나 암호화 중 어떠한 형태의 보안도 제공하지 않는다. 이는 사용자의 memcached 서버로의 액세스 권한을 애플리케이션 배치 환경의 동일한 비공개 구역에 두고 처리해야 한다는 의미이다. 보안이 필수인 경우 UNIX® 소켓을 사용하고 현재 호스트 액세스하는 memcached 서버에서 애플리케이션만 허용한다.
이는 유연성과 복원성 및 네트워크에서 여러 시스템에 걸쳐 RAM 캐시를 공유하는 기능이 제거되지만, 이는 이 상황에서 사용자의 memcached 데이터를 보안하는 유일한 솔루션이다.
스스로 제한해서는 안 된다
memcached 인스턴스를 사용해서는 안 되는 사항이 있지만, 그럼에도 불구하고 memcached의 유연성을 무시해서는 안 된다. memcached가 애플리케이션과 동일한 아키텍처 상의 레벨에 있기 때문에, 이를 통합하고 연결하기에 쉽다. 그리고 memcached를 활용하기 위해 애플리케이션을 변경하는 일은 복잡하지 않다. 게다가 memcached가 캐시에 불과하기 때문에, 문제점이 발생할 때에 애플리케이션의 실행을 중지하지 않아도 된다. 올바르게 사용된다면 서버 인프라의 나머지 부분에서 로드를 낮춘다(데이터베이스와 데이터 소스에서부터 읽기 절감). 이는 하드웨어를 더 많이 요구하지 않고도 클라이언트를 더 많이 지원한다는 의미이다.
그러나 이는 단지 캐시에 불과하다는 점을 유의하자!
요약
이 기사에서는 memcached 및 이를 사용하는 최상의 방법에 대해 다루었다. 특히 정보가 저장되는 방법, 합리적인 키를 선택하는 방법 및 저장하기 위해 정보를 선택하는 방법에 대해 살펴보았다. 또한 여러 서버의 사용, memcached 인스턴스가 고장날 때 해야 하는 일과 아마 가장 중요하게도 memcached를 사용하지 않는 방법을 비롯하여 모든 memcached 사용자가 경험하는 일부 키 배치 문제에 대해서도 살펴보았다.
memcached의 성능과 유용성은 오픈 소스 애플리케이션이자 매우 간결하고 쉬운 목표로 되어 있기 때문에, 이 간결성에서부터 나온다. 정보의 광대한 RAM 저장소를 제공하고, 네트워크에서 이를 사용 가능하고 광범위한 다양한 인터페이스와 언어 등을 통해 액세스 가능하게 하여 memcached를 엄청나게 다양한 설치로 통합할 수 있다.

참고자료
교육
  • MySQL memcached 문서는 일반적인 데이터베이스 배치 환경에서 memcached를 사용하는 방법에 대해 많은 정보를 제공한다.
  • IBM solidDB product family를 확인하여 IBM의 상용 캐싱 솔루션인 solidDB®에 대해 배워보자.
  • developerWorks 팟캐스트에서 소프트웨어 개발자를 위한 흥미로운 인터뷰와 토론을 들을 수 있다.
  • developerWorks의 기술 행사 및 웹 캐스트를 통해 최신 정보를 얻을 수 있다.
  • Twitter의 developerWorks 페이지를 살펴보자.
  • IBM 오픈 소스 개발자에게 유익한 컨퍼런스, 기술 박람회, 웹 캐스트 및 기타 행사를 확인하고 참여하자.
  • developerWorks 오픈 소스 영역에서는 오픈 소스 기술을 활용하여 개발 작업을 수행하고 이러한 기술을 IBM 제품과함께 사용하는 데 도움이 되는 사용 정보, 도구 및 프로젝트 업데이트와 가장 인기 있는 기사 및 튜토리얼을 확인할 수 있다.
  • My developerWorks 커뮤니티는 매우 다양한 주제를 다루는 일반 커뮤니티로 성공적으로 운영되고 있다.
  • 무료로 제공되는 developerWorks On demand demos를 통해 IBM 및 오픈 소스 기술에 대해 배우고 제품 기능을 익히자.
제품 및 기술 얻기
토론
필자소개
Martin Brown은 8년 넘게 기술 필자로 활약해왔다. Brown은 다양한 주제를 다루는 수 많은 책을 집필했고 기사를 작성했다. Brown은 펄, 파이썬, 자바(Java™), 자바스크립트, 베이직, 파스칼, 모듈라-2, C, C++, 레볼, gawk, 셸 스크립트, 윈도우(Windows®), 솔라리스, 리눅스, BeOS, 맥 OS X을 비롯하여 웹 프로그래밍, 시스템 관리, 통합에 이르리까지 다양한 개발 언어와 플랫폼을 경험했다. Brown은 마이크로소프트(Microsoft®) SME(Subject Matter Expert)이며 ServerWatch.com, LinuxToday.com, IBM developerWorks에 주기적으로 기고한다. Brown은 또한 컴퓨터월드, 애플 블로그, 기타 사이트에 주기적으로 블로그 기사를 올린다. 연락 주소는 Brown이 운영하는 웹 사이트를 참조하기 바란다.