2011년 8월 30일 화요일

LAMP 시스템 조율, Part 3: MySQL 조율

http://www.ibm.com/developerworks/kr/library/l-tune-lamp-3.html


LAMP 시스템 조율, Part 3: MySQL 조율

서버 조율 팁으로 MySQL 서버에 날개를 달자
Sean A. Walberg, 선임 네트워크 엔지니어
요약: LAMP(Linux®, Apache, MySQL, PHP/Perl) 아키텍처를 활용하는 응용 프로그램은 끊임없이 개발되고 배포되고 있습니다. 하지만 때로 서버 관리자는 다른 사람이 작성했다는 이유만으로 응용 프로그램 자체에 대한 통제권이 거의 없습니다. 기사 셋으로 이뤄진 이번 연재물은 응용 프로그램 성능을 향상시킬 서버 환경 설정 항목을 다룹니다. 연재 마지막인 세 번째 기사에서는 최대 성능을 발휘하도록 데이터베이스 계층을 조율하는 데 초점을 맞춥니다.
원문 게재일:  2008 년 5 월 06 일
난이도:  중급 원문:  보기
페이지뷰: 1681 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 3표)
MySQL 조율에 대해
MySQL 서버를 빠르게 하기 위한 방법은 세 가지가 있는데, 효율이 낮은 쪽에서 높아지는 쪽으로 나열하면 다음과 같다.
  1. 하드웨어로 문제를 푼다.
  2. MySQL 프로세스 설정을 조율한다.
  3. 질의를 최적화한다.

DB2로 이주

MySQL에서 IBM DB2로 이주하는 명쾌하고 비용이 들지 않는 방법을 찾고 싶은가? "MySQL 또는 PostgreSQL에서 DB2 Express-C로의 마이그레이션 (영문)" 기사에서는 이주 도구를 활용해 쉽게 이전하는 방법을 보여준다. 공짜 DB2 Express-C를 내려받아 지금 바로 시도해보자.
하드웨어로 문제를 푸는 방법이 가장 먼저 떠오른다. 특히 데이터베이스가 자원을 잡아먹는 괴물이라는 사실을 감안하면 말이다. 하지만 이 해법에는 한계가 있다. 현실을 고려할 때 CPU나 디스크 속력은 두 배로, 메모리 용량은 네 배에서 여덟 배 정도만 늘일 수 있다.
두 번째로 좋은 방법은 mysqld라는 MySQL 서버 조율이다. 이 프로세스 조율은 올바른 위치에 메모리를 할당하고 어떤 부하가 걸릴지 mysqld에 알려주는 조정 기법을 의미한다. 디스크 속력을 좀 더 빠르게 만드는 대신, 필요한 디스크 접근 횟수를 줄이는 편이 유리하다. 비슷하게, MySQL 프로세스가 올바르게 동작하도록 만드는 조율은 개발자가 임시 디스크 테이블과 파일 여닫기 같은 배경 작업에 신경을 쓰는 대신 질의에 대한 서비스에 좀 더 많은 시간을 보낼 수 있음을 의미한다. mysqld 조율은 이번 기사에서 주로 다룰 내용이다.
최고로 좋은 방법은 질의 최적화다. 이는 적절한 색인을 테이블에 만들어 놓고, MySQL의 장점을 최대로 활용하는 방향으로 질의를 작성하는 조율 기법을 의미한다. 이번 기사에서 질의 조율을 다루지는 않지만(이 주제로 책을 써도 되겠다), mysqld 환경 설정을 변경해 조율이 필요한 질의를 보고하도록 만든다.
중요한 조율 순서를 제시하긴 했지만, 그렇다고 해서 적절히 조율을 마친 질의를 위해 하드웨어나 mysqld 설정을 무시하라는 말은 아니다. 느린 기계는 느린 기계일 뿐이며, 제대로 작성한 질의를 돌리더라도 부하가 걸려 실패하는 경우를 목격했는데, mysqld가 질의를 서비스하는 대신 바쁘게 움직이느라 시간을 소모하고 있었기 때문이었다.
느린 질의 기록
SQL 서버에서 자료 테이블은 디스크에 위치한다. 색인은 전체 테이블을 찾지 않고서 서버가 테이블에서 자료 열을 찾아내도록 도와준다. 전체 테이블을 찾을 때 테이블 탐색을 수행한다고 부른다. 종종 테이블에서 일부만 원하는 경우가 있는데, 전체 테이블 탐색은 디스크 I/O와 시간을 상당히 많이 소비한다. 이런 문제는 테이블 조인 과정에서 복합적으로 나타나는데, 양쪽 테이블에 들어있는 열을 하나씩 비교해야 하기 때문이다.
물론 테이블 탐색이 항상 두통거리만은 아니다. 종종 전체 테이블을 읽는 경우가 일부만 읽는 경우보다 더 효과적인 경우도 있다(이런 결정을 내리려면 질의 계획이라는 작업을 거쳐야 한다). 색인을 비효율적으로 사용하거나 전혀 색인을 사용하지 않으면 질의가 느려지며, 테이블 크기가 증가하면서 서버에 부하가 걸리면 이런 문제점은 더욱 두드러진다. 실행을 위해 주어진 시간보다 더 오래 걸리는 질의는 느린 질의라고 부른다.
mysqld 환경 설정에서 느린 질의 로그라고 적절히 이름이 붙은 느린 질의 기록을 활성화할 수 있다. 관리자는 이 로그 파일을 살펴 응용 프로그램에서 어느 곳을 추가로 조사할지 결정한다. Listing 1은 느린 질의 로그를 활성화하기 위해 my.cnf에 필요한 환경 설정을 보여준다.

Listing 1. MySQL 느린 질의 로그 활성
[mysqld]
; 느린 질의 로그를 활성화한다. 기본은 10초다.
log-slow-queries
; 5초 이상 걸리는 질의를 기록한다.
long_query_time = 5
; long_query_time보다 적게 걸릴 경우 색인을 사용하지 않는 질의를 기록한다.
; MySQL 4.1 이상 버전에만 통한다
log-queries-not-using-indexes

이와 같은 세 가지 설정을 함께 사용하면, 5초 이상 지속되는 질의나 색인을 사용하지 않는 질의를 기록한다. log-queries-not-using-indexes에 대한 경고가 하나 있다. 반드시 MySQL 4.1 이상 버전을 사용해야만 한다. 느린 질의 로그는 MySQL 자료 디렉터리에 들어 있으며, 파일 형식은 hostname-slow.log이다. 다른 이름이나 경로를 사용한다면, my.cnf에서 log-slow-queries = /new/path/to/file을 지정하자.
느린 질의 로그를 읽으려면 mysqldumpslow 명령을 내린다. 로그 파일 경로를 지정하는 방법으로 느린 질의를 발견 순서에 따라 정렬한 목록을 얻는다. 도움을 주는 기능 한 가지는 mysqldumpslow가 결과를 비교하기 앞서 사용자 정의 자료를 제거하므로 동일한 질의로 여러 번 수행해도 하나로 센다. 이는 대다수 작업에 필요한 질의를 찾아내는 데 도움을 준다.
질의 캐시
대다수 LAMP 응용 프로그램은 데이터베이스에 상당히 의존하며 동일한 질의를 여러 번 반복한다. 질의를 만들 때마다 데이터베이스는 똑같은 작업을 해야만 한다. 즉 질의를 해석해, 실행 방법을 결정하고, 디스크에서 정보를 메모리에 올리고, 클라이언트에 이를 반환한다. MySQL은 질의 캐시라는 기능을 사용해서 메모리에 질의 결과를 저장하며 필요할 때 찾아쓴다. 여러 인스턴스에서 이런 캐시는 극적으로 성능을 높힌다. 하지만 질의 캐시는 기본적으로 비활성화되어 있다는 사실을 염두에 두자.
query_cache_size = 32M를 /etc/my.conf에 추가하면 질의 캐시로 32MB를 잡는다.
질의 캐시 감시
질의 캐시를 활성화한 다음에, 효율적으로 사용하고 있는지 이해하는 과정이 중요하다. MySQL은 여러 변수를 사용해서 캐시에서 어떤 일이 벌어지는지 감시하도록 만든다. Listing 2는 캐시 상태를 보여준다.

Listing 2. 질의 캐시 통계 출력
mysql> SHOW STATUS LIKE 'qcache%';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| Qcache_free_blocks      | 5216       |
| Qcache_free_memory      | 14640664   |
| Qcache_hits             | 2581646882 |
| Qcache_inserts          | 360210964  |
| Qcache_lowmem_prunes    | 281680433  |
| Qcache_not_cached       | 79740667   |
| Qcache_queries_in_cache | 16927      |
| Qcache_total_blocks     | 47042      |
+-------------------------+------------+
8 rows in set (0.00 sec)

각 항목을 분리하면 표 1과 같다.

표 1. MySQL 질의 캐시 변수
변수 이름설명
Qcache_free_blocks 캐시에 있는 연속적인 메모리 블록 숫자. 높은 숫자는 단편화가 일어난 징표다. FLUSH QUERY CACHE는 캐시 조각을 모아 자유 블록 하나로 만든다.
Qcache_free_memory 캐시에 있는 자유 메모리
Qcache_hits 캐시에서 질의를 가져올 때마다 값이 증가한다.
Qcache_inserts 질의가 들어올 때마다 증가한다. inserts를 hits로 나누면 비적중률을, 1에서 비적중률을 빼면 적중률을 구할 수 있다. 직전 에제에서 대략 질의 중 87%를 캐시에서 가져왔다.
Qcache_lowmem_prunes 캐시를 위한 메모리가 부족해져 더 많은 질의를 위한 공간을 확보하기 위해 정리되어야 하는 횟수. 이 숫자를 계속해서 살펴보는데, 증가 추세에 있다면 단편화가 심각하거나 메모리가 부족하다는 징표다(위에서 언급한 free_blocksfree_memory를 살펴본다).
Qcache_not_cached 일반적으로 SELECT 구문이 아니기 때문에 캐시 후보에서 제외된 질의 숫자
Qcache_queries_in_cache 현재 캐시되어 있는 질의 숫자(응답 숫자 포함)
Qcache_total_blocks 캐시에 있는 블록 숫자
종종 이런 값의 변화 추이를 살펴보면 캐시를 효율적으로 사용하는지 파악하는 데 도움을 준다. FLUSH STATUS는 몇몇 카운터를 초기화하므로 서버가 동작 중에 있을 경우 도움이 된다.
모든 내용을 캐시하도록 과도하게 큰 캐시를 잡고 싶은 유혹이 든다. mysqld는 메모리 부족으로 인한 정리 작업과 같은 캐시 관리 작업도 해야 하므로, 여기에만 신경을 쓸 경우 서버가 꼼짝달싹하지 못한다. 일반적인 규칙을 설명하자면 FLUSH QUERY CACHE가 오래 걸린다면 캐시가 너무 큰 상황이다.
제약 가하기
시스템 부하가 자원 부족으로 이어지지 않도록 mysqld에 몇 가지 제약을 가해야 한다. Listing 3은 my.cnf에서 몇 가지 중요한 자원 관련 설정을 보여준다.

Listing 3. MySQL 자원 설정
set-variable=max_connections=500
set-variable=wait_timeout=10
max_connect_errors = 100

최대 접속은 첫째 행에서 다룬다. 아파치가 사용하는 MaxClients와 같이, 서비스가 가능한 접속 수만 허용한다. 지금까지 서버가 처리한 최대 접속 수를 확인하려면 SHOW STATUS LIKE 'max_used_connections' 명령을 내린다.
둘째 행은 mysqld가 10초 이상 쉬고 있는 접속을 끊어버리도록 만든다. LAMP 응용 프로그램에서 데이터베이스 접속은 일반적으로 웹 서버가 요청을 처리하는 동안에만 이뤄진다. 종종 부하가 걸린 상태에서 연결이 일시 정지된 상황에서 접속 테이블 공간을 차지하는 경우가 있다. 활성 사용자가 많거나 데이터베이스에 영속적인 접속이 이뤄지고 있다면, 이 값을 낮춰잡는 정책은 바람직하지 않다.
마지막 행은 안전 벨트다. 호스트에 서버 접속 관련 문제가 생겨 너무 많이 요청을 취소한다면, FLUSH HOSTS를 수행할 때까지 호스트는 잠겨버린다. 기본적으로 열 번 정도 실패하면 잠겨버리도록 설명한다. 이 값을 100으로 바꾸면 문제가 무엇이든 복구할 시간을 서버에 충분히 준다. 더 높은 값으로 설정하더라도 그다지 도움을 주지 않는 이유는 서버가 한 번에 100번 연결해도 실패한다면, 이후 계속 시도하더라도 연결에 성공할 가능성이 희박하기 때문이다.
버퍼와 캐시
MySQL은 100개가 넘는 조율 설정값을 지원한다. 하지만 천만다행으로 이 중에서 몇 가지만 알면 충분하다. 설정을 올바르게 하려면, SHOW STATUS 명령을 통해 상태 변수를 살펴보고, 이를 통해 mysqld가 원하는 방식으로 움직이는지 파악한다. 시스템에 존재하는 메모리 자원을 넘어서 버퍼와 캐시를 할당할 수 없기에 종종 조율 과정에서 타협이 필요하다.
MySQL 조율값은 mysqld 프로세스 전체나 개별 클라이언트 세션에 대해 설정이 가능하다.
서버 단위 설정
각 테이블은 디스크에 파일 형태로 저장되며, 테이블을 읽기 위해서는 파일을 열어야 한다. 파일 읽기 과정을 빠르게 하기 위해, mysqld는 /etc/mysqld.conf에 지정된 숫자(table_cache)만큼 열린 파일을 캐시한다. Listing 4는 열린 테이블에 대한 활동 상황을 출력하는 방법을 보여준다.

Listing 4. 열린 테이블 활동 상황 출력
mysql> SHOW STATUS LIKE 'open%tables';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables   | 5000  |
| Opened_tables | 195   |
+---------------+-------+
2 rows in set (0.00 sec)

Listing 4는 현재 테이블 5000개가 열려있으며, 테이블 195개가 열려야 했음을 보여준다. 캐시에 유효한 파일 기술자가 없기 때문에 이런 현상이 일어난다(직전에 통계를 초기화했으므로 5000개 열린 테이블 중에 단지 195개만 열렸다고 기록이 남는다). SHOW STATUS 명령을 다시 실행할 때 Opened_tables가 급격하게 올라가면, 캐시 적중률이 떨어진 상황이다. table_cache 설정값보다 Open_tables 설정값이 훨씬 낮으면, 캐시를 너무 크게 잡았다(물론 여유있게 설정하는 방식은 나쁘지 않다). 예를 들어, table_cache = 5000으로 테이블 캐시 값을 조정한다.
테이블 캐시와 마찬가지로 스레드를 위한 캐시도 있다. mysqld는 접속을 받을 때 필요한 스레드를 만든다. 바쁜 서버에서 접속이 빠르게 연결되었다 끊어지면, 초기 접속 속력을 높아기 위해 나중에 사용할 요량으로 스레드를 캐시한다.
Listing 5는 충분한 스레드가 캐시되었는지 살펴보는 방법을 보여준다.

Listing 5. 스레드 사용량 통계 보기
mysql> SHOW STATUS LIKE 'threads%';
+-------------------+--------+
| Variable_name     | Value  |
+-------------------+--------+
| Threads_cached    | 27     |
| Threads_connected | 15     |
| Threads_created   | 838610 |
| Threads_running   | 3      |
+-------------------+--------+
4 rows in set (0.00 sec)

여기서 가장 중요한 값은 Threads_createdmysqld가 새로운 스레드를 생성할 때마다 하나씩 증가한다. 연속적으로 SHOW STATUS 명령을 내릴 때, 이 숫자가 급격하게 올라가면 스레드 캐시 수치를 높여야 한다. 예를 들어, my.cnf에서 thread_cache = 40을 설정하면 된다.
키 버퍼는 MyISAM 테이블을 위한 색인 블록을 저장한다. 이상적으로 이런 블록에 대한 요청은 디스크가 아니라 메모리에서 일어나야 한다. Listing 6은 메모리와 디스크에서 얼마나 많은 블록을 읽는지 확인하는 방법을 보여준다.

Listing 6. 키 효율성 확인
mysql> show status like '%key_read%';
+-------------------+-----------+
| Variable_name     | Value     |
+-------------------+-----------+
| Key_read_requests | 163554268 |
| Key_reads         | 98247     |
+-------------------+-----------+
2 rows in set (0.00 sec)

Key_reads는 디스크에서 요청한 숫자이며, Key_read_requests는 전체 숫자다. Key_reads를 Key_read_requests로 나누면 비적중률이 나온다. Listing 6을 보면 1000개 요청 당 0.6개가 적중하지 않았다. 1000개 요청 당 1개 이상 적중하지 않는다면 키 버퍼를 늘려야 한다. 예를 들어, key_buffer = 384M를 지정하면 버퍼를 384MB로 늘인다.
임시 테이블은 GROUP BY 절과 같이 추가 처리가 필요할 때 임시로 자료를 저장할 곳으로, 좀 더 고급 질의에서 사용된다. 이상적으로 이런 테이블은 메모리에 생성하지만, 임시 테이블이 너무 커질 경우 디스크에 써야 한다. Listing 7은 임시 테이블 생성과 관련한 통계를 보여준다.

Listing 7. 임시 테이블 사용량 보기
mysql> SHOW STATUS LIKE 'created_tmp%';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 30660 |
| Created_tmp_files       | 2     |
| Created_tmp_tables      | 32912 |
+-------------------------+-------+
3 rows in set (0.00 sec)

임시 테이블을 사용하면 Created_tmp_tables가 증가한다. 디스크 기반 테이블을 사용하면 Created_tmp_disk_tables가 증가한다. 이 비율을 정확하고 빠르게 결정하지 못하는 이유는 질의에 의존하기 때문이다. 시차를 두고 Created_tmp_disk_tables를 관찰하면 생성된 디스크 테이블 비율을 알 수 있고, 설정 값이 유효한지 살펴볼 수 있다. tmp_table_sizemax_heap_table_size 둘 다 임시 테이블 최대 크기를 제어하므로, my.cnf에서 양쪽 설정을 모두 확인해야 한다.
세션 단위 설정
이어지는 설정은 세션 단위다. 이 값을 설정할 때 신경을 써야 하는 이유는 잠재적인 접속 숫자에 설정값이 곱해지므로 메모리 사용량이 늘어나기 때문이다. 코드에서 해당 세션 값을 변경하거나 my.cnf에서 모든 세션 값을 변경할 수 있다.
MySQL이 정렬 작업을 수행할 때, 디스크에서 읽는 열을 저장하기 위한 정렬 버퍼를 할당한다. 정렬할 자료 크기가 너무 크다면, 디스크에 임시 파일로 자료를 저장하고, 다시 한번 정렬해야 한다. sort_merge_passes 상태값이 높으면, 디스크 활동량이 많다는 증거다. Listing 8은 정렬 관련 상태 카운터 몇 가지를 보여준다.

Listing 8. 정렬 통계 보기
mysql> SHOW STATUS LIKE "sort%";
+-------------------+---------+
| Variable_name     | Value   |
+-------------------+---------+
| Sort_merge_passes | 1       |
| Sort_range        | 79192   |
| Sort_rows         | 2066532 |
| Sort_scan         | 44006   |
+-------------------+---------+
4 rows in set (0.00 sec)

sort_merge_passes가 높다면, sort_buffer_size 쪽에 관심을 기울여야 한다. 예를 들어, sort_buffer_size = 4M를 지정하면, 정렬 버퍼를 4MB로 늘인다.
MySQL은 또한 테이블을 읽기 위한 메모리를 할당한다. 이상적으로 보면 색인은 필요한 열에서만 읽도록 충분한 정보를 제공하지만, (자료 특성 때문이나 설계 잘못으로 인해) 읽어야 할 테이블이 많은 질의도 있기 마련이다. 이런 행동 양식을 이해하려면, (색인으로 직접 접근하는 대신) 테이블에서 다음 열을 직접 읽어야 하는 숫자와 SELECT 문 개수를 알아야 한다. 이렇게 하려면 Listing 9에서 소개하는 명령을 내린다.

Listing 9. 테이블 탐색 비율 확인
mysql> SHOW STATUS LIKE "com_select";
+---------------+--------+
| Variable_name | Value  |
+---------------+--------+
| Com_select    | 318243 |
+---------------+--------+
1 row in set (0.00 sec)

mysql> SHOW STATUS LIKE "handler_read_rnd_next";
+-----------------------+-----------+
| Variable_name         | Value     |
+-----------------------+-----------+
| Handler_read_rnd_next | 165959471 |
+-----------------------+-----------+
1 row in set (0.00 sec)

Handler_read_rnd_next / Com_select는 테이블 탐색 비율을 보여주는데, Listing 9에서는 521:1이다. 4000이 넘어가면, read_buffer_size = 4M와 같이 read_buffer_size 값이 충분히 크게 설정되어 있는지 확인한다. 이 값이 8M 이상으로 커진다면, 개발자에게 질의 조율이 필요하다고 알려주자!
세 가지 필수 도구
구체적인 설정값을 파고 들 때, SHOW STATUS 명령이 유용하긴 하지만, mysqld에서 제공하는 방대한 자료를 해석하는 과정에 도움을 주는 몇 가지 도구가 필요하다. 세 가지 필수 도구를 찾아내었는데, 참고자료 절에 정리해놓았다.
대다수 시스템 관리자는 top 명령에 익숙하다. top은 태스크가 소비하는 CPU와 메모리를 주기적으로 갱신하면서 보여준다. mytoptop을 모델로 만든 프로그램으로 현재 동작 중인 질의와 연결된 모든 클라이언트를 보여준다. mytop은 또한 키 버퍼와 질의 캐시 효율성에 대한 실시간 및 과거 자료를 제공하며, 실행 중인 질의 통계도 보여준다. 상황 파악에 유용한 도구이며, 10초 내로 서버 상태와 문제를 초래하는 모든 접속을 표시할 수 있다.
mysqlard는 MySQL 서버에 접속하는 데몬으로, 5초마다 자료를 수집해 라운드 로빈으로 동작하는 데이터베이스 뒷단에 저장한다. 웹 페이지는 테이블 캐시 사용량, 키 효율성, 접속된 클라이언트, 임시 테이블 사용량과 같은 자료를 출력한다. mytop은 서버 상태를 스냅 사진으로 찍어주며, mysqlard는 장기간에 걸친 서버 상태를 보여준다. 보너스로, mysqlard는 수집한 몇몇 정보를 활용해 서버 조율 방법을 제안한다.
SHOW STATUS 정보를 수집하는 또 다른 도구는 mysqlreport다. 이 도구가 mysqlard가 보고하는 내용보다 훨씬 자세한 보고 내역을 제공하는 이유는 서버의 모든 측면을 분석하기 때문이다. mysqlreport가 서버 조율에 뛰어난 이유는 상태 변수를 적절히 계산해 수정이 필요한 내용을 알려주기 때문이다.
요약
MySQL 조율 기본기를 설명하는 이 기사로 LAMP 컴포넌트 조율을 다루는 연재물을 마무리한다. 조율은 대부분 동작 원리를 이해하고 적절하게 동작하는지 확인하고 조정하고, 다시 평가하는 작업이다. 리눅스, 아파치, PHP, MySQL로 대표되는 각 컴포넌트마다 각자 요구 사항이 존재한다. 개별적으로 컴포넌트를 이해하고 있으면, 응용 프로그램을 느리게 만드는 병목을 제거하는 데 도움이 된다.

참고자료
교육
제품 및 기술 얻기
  • 지금부터 3년 전에 나왔음에도 불구하고 High Performance MySQL는 여전히 가치있는 책이다. 저자는 또한 MySQL에 대한 다양한 기사를 제공하는 웹 사이트를 운영한다.
  • mytop은 정확하게 그 순간 MySQL 서버에서 어떤 일이 일어나는지를 말해주며, 몇몇 핵심 통계 자료를 제공한다. 데이터베이스 문제가 발생했을 때 처음 사용하는 프로그램이다.
  • mysqlard는 MySQL 서버에서 성능 지표를 그래프로 보여주며, 조율에 대한 조언도 한다.
  • mysqlreport는 필수 도구다. 이 도구는 여러분을 대신해 SHOW STATUS 값을 분석한다.
  • phpMyAdmin에 대한 링크 없이는 MySQL 기사가 끝나지 않는다. 상태 변수에 대한 몇 가지 해석과 더불어 관리를 쉽게 만드는 기능을 제공한다.
  • IBM 평가판 소프트웨어: developerWorks에서 직접 내려 받아 다음번 리눅스 프로젝트에 활용하자.
토론
필자소개
Author photo Sean Walberg는 1994년 이래로 학계, 회사, 인터넷 서비스 제공 업체 환경을 두루 거치며 리눅스와 유닉스 분야에서 일해왔다. Walberg는 여러 해 동안 시스템 관리 서적을 집필해왔다.

LAMP 시스템 조율, Part 2: 아파치와 PHP 최적화

http://www.ibm.com/developerworks/kr/library/l-tune-lamp-2.html


LAMP 시스템 조율, Part 2: 아파치와 PHP 최적화

아파치가 느려지는 이유와 PHP 성능을 최대로 끌어내는 방법
Sean A. Walberg, 선임 네트워크 엔지니어
요약: LAMP(Linux®, Apache, MySQL, PHP/Perl) 아키텍처를 활용하는 응용 프로그램은 끊임없이 개발되고 배포되고 있습니다. 하지만 때로 서버 관리자는 다른 사람이 작성했다는 이유만으로 응용 프로그램 자체에 대한 통제권이 거의 없습니다. 기사 셋으로 이뤄진 이번 연재물은 응용 프로그램 성능을 향상시킬 서버 환경 설정 항목을 다룹니다. 첫 번째 기사는 LAMP 아키텍처, 성능 기법, 기본적인 리눅스 커널, 디스크, 파일 시스템 미조정을 다뤘습니다. 두 번째 기사에서는 아파치와 PHP 컴포넌트를 최적화하는 방법에 초점을 맞춥니다.
원문 게재일:  2008 년 4 월 29 일
난이도:  중급 원문:  보기
페이지뷰: 1977 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 2표)
리눅스, 아파치, MySQL, PHP(또는 펄)은 일정 목록부터 블로그와 전자 상거래 사이트에 이르기까지 많은 웹 응용 프로그램의 토대가 된다. LAMP 컴포넌트에 의존하는 많은 오픈 소스 패키지는 다양한 문제를 해결한다. 응용 프로그램 부하가 증가할수록, 기반 구조에서 병목 현상이 발생해 사용자 요청에 대한 반응이 느려지는 형태로 나타난다. 직전 기사에서는 리눅스 시스템 조율 방법과 LAMP 기초, 성능 측정 방법에 대한 기초를 다뤘다. 이번 기사에서는 아파치와 PHP로 대표되는 웹 서버 구성 요소에 초점을 맞춘다.
아파치 조율
아파치는 환경 설정이 자유로운 소프트웨어다. 기능도 많지만 각 기능마다 비용을 치뤄야 한다. 아파치 조율을 위해 적절한 자원 할당이 필요하며, 환경 설정을 줄여 필요한 항목만 남겨두는 지혜가 필요하다.
MPM 환경 설정
아파치는 기능을 쉽게 추가하거나 삭제할 수 있는 모듈화된 구조를 따른다. MPM(Multi-Processing Module)은 이런 모듈화된 구조를 네트워크 연결 관리와 요청 처리를 위한 아파치 핵심 기능으로 제공한다. MPM은 스레드를 사용하도록 만들고 심지어 아파치를 다른 운영체제로 이동하도록 만들어준다.
한번에 MPM 하나만 활성화되며, --with-mpm=(worker|prefork|event)를 사용해 정적으로 컴파일해야 한다.
요청당 프로세스 하나를 띄우는 전통적인 모델을 prefork라고 한다. 스레드를 적용한 새로운 모델은 worker라고 하는데, 다중 프로세스를 사용하며 부하를 줄이고 성능을 높이기 위해 다중 스레드를 사용한다. 최종적으로 event MPM은 실험적인 모듈로 다양한 작업을 수행하도록 독립된 스레드 풀을 유지한다. 현재 사용 중인 MPM을 살펴보려면 httpd -l 명령을 수행한다.
MPM 선택은 여러 가지 요인에 달려있다. 실험 상태를 벗어날 때까지 event MPM 설정을 미뤄두야 하므로, 스레드를 사용할지 스레드를 사용하지 않을지를 놓고 선택이 필요하다. 표면적으로 PHP가 사용하는 모든 라이브러리를 비롯하여 모든 기반 모듈이 스레드 안전을 보장하면 thread 방식은 fork 방식보다 그럴 듯하게 들린다. prefork는 좀더 안전한 선택이다. worker 모델을 선택할 때는 신중하게 실험해야 한다. 성능 향상은 또한 배포판과 하드웨어에 따라오는 라이브러리에 달려있다.
선택한 MPM 종류가 무엇이든 제대로 설정해야 한다. 일반적으로 MPM 설정은 아파치에게 얼마나 많은 worker가 동작할지 제어하는 방법과 스레드나 프로세스 모델을 선택하는 기준을 알려준다. prefork MPM을 위한 중요한 환경 설정 옵션을 Listing 1에 제시한다.

Listing 1. prefork MPM을 위한 환경 설정
StartServers       50
MinSpareServers   15
MaxSpareServers   30
MaxClients       225
MaxRequestsPerChild  4000

소프트웨어 직접 컴파일하기

내가 유닉스(UNIX®)를 시작했을 때, 시스템에 집어 넣는 모든 소프트웨어를 직접 컴파일해야 한다고 우겼다. 업데이트 관리는 궁극적으로 내게 맡겨졌고, 이런 과업을 쉽게 처리하도록 빌드하는 방법을 익혔다. 결국 내가 소비한 시간 대부분은 배포판을 만들기 위한 노력과 중복됨을 깨달았다. 이제 가능하다면 대부분 배포판에서 제공하는 소프트웨어를 사용하며, 반드시 필요한 경우에만 직접 패키지를 만든다.
비슷한 상황으로, 업체에서 제공하는 패키지 유지보수는 최신이자 훌륭한 코드와 함께 하는 장점을 무색하게 만든다. 종종 성능 조율과 시스템 관리 목표가 충돌한다. 상용 리눅스를 사용하거나 외부 협력사 지원에 의존한다면 업체 지원을 고려해보자.
자립하고 싶다면, 배포판으로 패키지를 빌드하는 방법과 패치 시스템으로 통합하는 방법을 배우자. 이렇게 하면 미세 조정과 더불어 소프트웨어가 일관성 있게 만들어지고 다중 시스템에서 동작함을 보장한다. 적절한 메일링 리스트와 RSS 피드 구독을 통해 소프트웨어 업데이트를 최우선 목표로 삼자.
prefork 모형에서 새로운 프로세스는 요청 단위로 생성된다. 여분의 프로세스는 들어오는 요청을 처리하기 위해 쉬고 있으며, 이는 초기 시동 대기 시간을 줄여준다. 직전에 보여준 환경 설정 항목에 따르면 웹 서버가 시동하면서 프로세스 50개를 시작하며, 쉬고 있는 서버가 10개와 20개 사이를 유지하도록 시도한다. 프로세스 최대 한계 수치는 MaxClients로 지정한다. 프로세스가 연속적인 요청을 처리할 수 있음에도 불구하고, 아파치는 접속이 4000개가 넘어간 다음에 프로세스를 죽여 메모리 누수 위험을 방지한다.
스레드 MPM 설정은 비슷하지만 사용하는 스레드와 프로세스 개수를 결정해야만 한다는 점이 다르다. 아파치 문서는 모든 매개변수와 필요한 계산 방법을 설명한다.
사용할 값을 선택하는 동안 시행 착오가 필요하다. 가장 중요한 값은 MaxClients다. 목표는 충분한 작업 프로세스나 스레드가 과도하게 서버 스왑 현상을 일으키지 않으면서 동작하는 데 있다. 처리할 수 있는 용량보다 요청이 많이 들어오면, 최소한 들어온 요청까지는 서비스를 해줘야 하며, 나머지는 기다리도록 만든다.
MaxClients가 너무 높으면 모든 클라이언트가 형편 없는 서비스를 경험하게 된다. 웹 서버가 프로세스 하나를 스왑 아웃하고 다른 프로세스를 돌려야 하기 때문이다. 설정 값을 너무 낮춰잡으면 불필요하게 서비스를 거부할지도 모른다. 높은 부하에서 동작하는 프로세스 수와 아파치 프로세스가 사용하는 메모리 점유 상황을 보여주는 결과는 이 값을 설정할 때 힌트를 준다. MaxClients가 256을 넘어갈 경우 ServerLimit를 동일한 숫자로 설정해야 한다. 이와 관련한 경고를 설명하는 MPM 문서를 주의 깊게 읽어보자.
시작하고 여분으로 남겨둘 서버 수 조율은 서버쪽 역할에 의존한다. 서버가 아파치만 돌릴 경우 Listing 1에 보여준 적당한 값을 사용할 수 있다. 서버를 완전히 활용할 수 있기 때문이다. 시스템이 데이터베이스나 다른 서버를 공유한다면 동작할 서버 여분 숫자를 줄여야 한다.
옵션 활용과 효과적인 중복 지정
아파치가 처리하는 각 요청은 웹 서버가 반드시 따라야 할 제약이나 특별한 명령을 지정하기 위한 복잡한 설정 규칙을 거쳐야 한다. 폴더 접근은 특정 폴더에 대한 IP 주소로 제약을 가하거나 사용자와 암호로 설정할 수 있다. 또한 이런 옵션은 디렉터리 목록을 제공한다면, 특정 파일 유형 처리 방식이나 출력 결과 압축 같은 특정 파일 처리도 포함한다.
이와 같은 환경 설정은 httpd.conf에 디스크 위치를 참조하도록 설정을 명세하는 <Directory>나 참조 값이 URL에서 경로를 나타내는 <Location> 같은 컨테이너 형태를 따른다. Listing 2는 Directory 컨테이너를 예로 든다.

Listing 2. 루트 디렉터리에 적용한 디렉터리 컨테이너
<Directory />
    AllowOverride None
    Options FollowSymLinks
</Directory>

Listing 2에서, Directory/Directory 태그로 둘러쌓인 환경 설정은 특정 디렉터리와 이 디렉터리 하부에 적용된다. Listing 2의 경우에는 루트 디렉터리가 된다. 여기서 AllowOverride 태그는 사용자가 다른 옵션을 중복 지정하지 못하게 만든다(나중에 설명한다). FollowSymLinks 옵션을 활성화하면, 웹 파일을 포함하는 디렉터리 외부에 파일이 존재할지라도 아파치가 요청을 처리하기 위해 심볼릭 링크를 따라가도록 만든다. 이는 웹 디렉터리에 있는 파일이 /etc/passwd를 가리키는 심볼릭 링크일지라도 웹 서버가 기꺼히 요청한 파일을 제공하리라는 사실을 의미한다. 대신 -FollowSymLinks를 사용하면, 이 기능은 비활성화되며, 동일한 요청은 클라이언트에 오류로 반환된다.
마지막 시나리오는 두 가지 사항을 고려하도록 만든다. 먼저 성능 문제다. FollowSymLinks를 비활성화하면, 아파치는 심볼릭 링크가 아님을 확인하기 위해 파일 이름에 속한 각 컴포넌트(디렉터리와 파일 자체)를 점검해야만 한다. 이는 디스크 활동량을 높이므로 부하가 추가로 발생한다. 관련 옵션인 FollowSymLinksIfOwnerMatch는 파일 소유주가 링크 소유주일 경우에 심볼릭 링크를 따라가도록 만든다. 이는 symlinks를 따라가지 못하도록 막는 경우와 비슷한 성능 저하가 일어난다. 최고 성능을 위해서는 Listing 2에 나온 옵션을 사용한다.
보안을 염두에 두는 독자에게 지금 경종을 울리겠다. 보안은 항상 기능과 위험 사이에서 절충을 벌여야 한다. 이 경우에는 속력이 우선이므로 시스템에 존재하는 파일에 대해 무조건 접근을 허용하도록 위험을 감수한다. 한가지 다행인 점은 LAMP 응용 프로그램 서버는 일반적으로 특수 목적으로 운영되며, 사용자는 잠재적으로 위험한 심볼릭 링크를 걸지 못한다는 것이다. 심볼릭 링크 점검을 활성화해야 한다면, Listing 3에서 보여주듯 특정 파일 시스템 영역에만 적용한다.

Listing 3. 사용자 디렉터리로 FollowSymLinks를 제한하기
<Directory />
   Options FollowSymLinks
</Directory>

<Directory /home/*/public_html>
   Options -FollowSymLinks
</Directory>

Listing 3에서, 사용자 홈 디렉터리에 있는 public_html 디렉터리는 자신과 하위 디렉터리에 대해 FollowSymLinks 옵션을 제거한 상태가 된다.
지금까지 살펴보았듯이, 옵션은 주 서버 환경 설정을 통해 디렉터리 단위로 설정할 수 있다. (AllowOverrides로 관리자가 허락했다면) 사용자는 .htaccess라는 파일을 디렉터리에 놓아두는 방법으로 이런 서버 환경 설정 자체를 중복 지정할 수 있다. 시스템에 사용자가 없다는 초기 설명에도 불구하고, 많은 LAMP 응용 프로그램은 이런 기능을 활용해 접근 통제와 URL 다시쓰기를 구현하므로 동작 원리를 이해하는 편이 좋겠다.
AllowOverrides 구문이 원하지 않는 모든 작업을 막아버릴지라도, 아파치는 여전히 해야 할 일이 남아있는지 확인하기 위해 .htaccess 파일을 살펴봐야 한다. 어버이 디렉터리는 자식 디렉터리에서 오는 요청을 처리하도록 지시자를 명세할 수 있는데, 이렇게 하기 위해서는 아파치가 요청 파일 앞에 나오는 디렉터리 구조를 구성하는 각 요소를 살펴봐야만 한다. 이렇게 되면 당연히 각 요청마다 디스크 활동량이 상당히 늘어난다.
어떤 중복 지정도 허용하지 않는 가장 손쉬운 방법으로 아파치가 .htaccess 파일 점검을 하지 않도록 만들면 된다. 특별한 환경 설정은 httpd.conf에 직접 지정한다. Listing 4는 .htaccess 파일을 만들어넣고 AllowOverrides에 의존하는 대신에 사용자 프로젝트 디렉터리를 암호로 보호하도록 httpd.conf에 추가한 내용을 보여준다.

Listing 4. .htaccess 환경 설정을 httpd.conf로 옮기기
<Directory /home/user/public_html/project/>
  AuthUserFile /home/user/.htpasswd
  AuthName "uber secret project"
  AuthType basic
  Require valid-user
</Directory>

환경 설정을 httpd.conf로 옮기고 AllowOverrides를 비활성화하면 디스크 활동량이 줄어든다. 사용자 프로젝트만으로는 그다지 흥미를 끌지 못할지도 모르지만, 바쁜 사이트에 적용할 때 이런 기법이 얼마나 강력한지 생각해보기 바란다.
종종 .htaccess 파일을 제거하지 못하는 경우도 있다. 예를 들어 Listing 5를 보면 특정 파일 시스템에 제약을 두는 옵션이 있을 때, 중복 지정 옵션 자체도 제약을 가할 수 있다.

Listing 5. .htaccess 점검 범위를 줄이기
<Directory />
  AllowOverrides None
</Directory>

<Directory /home/*/public_html>
  AllowOverrides AuthConfig
</Directory>

Listing 5를 구현한 다음에, 아파치는 여전히 어버이 디렉터리에서 .htaccess 파일을 찾지만, public_html 디렉터리에서 멈춘다. 나머지 파일 시스템은 기능적으로 비활성화 상태가 되기 때문이다. 예를 들어, /home/user/public_html/project/notes.html에 사상된 파일을 요청할 경우, 단지 public_html과 project 디렉터리만 탐색한다.
마지막 주의 사항으로 디렉터리 단위 환경 설정에는 순서가 있다. 아파치 조율에 대한 문서는 HostnameLookups off 지시자를 통해 DNS 탐색을 비활성으로 만들어라고 조언한다. 서버에 연결한 각 IP 주소를 역으로 도메인 이름으로 바꾸는 시도는 자원 낭비다. 하지만 호스트 이름에 기반을 둔 제약을 통해 웹 서버가 클라이언트 IP 주소를 거꾸로 찾아내고 이름 진위를 파악하기 위한 결과를 찾아내도록 만든다. 따라서 클라이언트 호스트 이름을 기반으로 하는 접근 제어는 피하되 필요할 때만 기술하도록 영역을 줄이는 방식이 바람직하다.
지속적인 접속
클라이언트가 웹 서버에 접속할 때, 동일한 TCP 연결을 통한 다중 요청을 허용하면 다중 연결에 따른 접속 지연을 줄인다. 이런 방식은 웹 페이지가 여러 이미지를 참조할 경우 유용하다. 클라이언트는 페이지를 요청해 연결 하나로 모든 이미지를 받을 수 있다. 단점으로 서버 쪽 작업 프로세서가 다음 요청으로 옮겨가기 전에 클라이언트가 닫은 세션을 기다려야 한다.
아파치는 지속적인 접속 설정을 다루는 방법인 keepalives 값을 바꾸도록 허용한다. httpd.conf에서 KeepAlive 5를 전역으로 설정해 놓으면, 연결을 강제로 끊기 전에 서버가 연결 하나에 요청 다섯 개를 처리하도록 허용한다. 이 값을 0으로 만들면 지속적인 접속 사용을 비활성화한다. 또한 KeepAliveTimeout을 전역으로 설정해 놓으면 아파치가 세션을 닫기 전에 다른 요청을 얼마나 오랫동안 기다릴지 설정한다.
지속적인 접속 처리는 만능이 아니다. keepalives를 비활성(KeepAlive 0)으로 만들어 놓으면 좋은 사이트도 있고, 활성으로 만들어 놓는 편이 상당한 효과를 발휘하는 사이트도 있다. 유일한 해법을 찾으려면 둘 다 시도해 직접 실험해보면 된다. keepalives를 활성화할 경우 KeepAliveTimeout 2를 지정해 2초 정도로 낮은 타임아웃을 사용하는 편을 권장한다. 이렇게 하면 연속으로 요청을 만들어내기를 바라는 클라이언트에 충분한 시간을 제공하면서도 작업 프로세스가 결코 오지 않을 다른 요청을 기다리느라 시간을 낭비하지도 않게 만든다.
압축
웹 서버는 클라이언트에게 자료를 반환하기 전에 결과물을 압축할 수 있다. 이렇게 하면 웹 서버 CPU 사이클을 사용해 인터넷으로 전송하는 페이지를 좀더 작게 만들 수 있다. CPU 부하를 견딜만한 서버라면 이는 페이지를 훨씬 더 빨리 내려받도록 만드는 훌륭한 방법이다. 압축 후에 페이지 크기가 1/3로 줄어드는 경우도 흔하다.
이미지는 일반적으로 이미 압축되어 있으므로 압축은 텍스트 출력으로 제한해야 한다. 아파치는 mod_deflate를 통해 압축을 지원한다. mod_deflate 활성화 자체는 쉬울지 모르겠지만, 상당히 복잡한 기능을 제공하므로 매뉴얼을 살펴서 설명을 읽어봐야 한다. 이 기사는 적절한 문서 링크(참고자료 절 참조)를 제외한 나머지 압축 환경 설정은 다루지 않는다.
PHP 조율
PHP는 응용 프로그램 코드를 돌리는 엔진이다. 사용할 계획이 있는 모듈만 설치해야 하며, 정적 파일이 아니라 (일반적으로 .php로 끝나는 파일인) 스크립트 파일에 대해서만 PHP를 사용하도록 웹 서버 환경 설정을 바꿔야 한다.
중간 코드 캐싱
PHP 스크립트를 요청할 때, PHP는 스크립트를 읽어 실행하기 위한 코드의 이진 표현인 젠드 중간 코드로 컴파일한다. 이 중간 코드는 PHP 엔진이 수행한 다음에 버린다. 중간 코드 캐시는 컴파일된 중간 코드를 저장해 다음 번에 페이지를 호출할 때 재사용한다. 이런 캐시는 시간을 상당히 절약해준다. 여러 중간 코드 캐시가 존재하는데, 나는 eAccelerator로 재미를 봤다.
eAccelerator를 설치하려면 컴퓨터에 PHP 개발 라이브러리가 필요하다. 리눅스 배포판마다 다른 위치에 파일을 두므로, eAccelerator 웹 사이트에서 직접 설치 명령을 참조하는 편이 최선이다(참고자료 절에 있는 링크를 참조하자). 또한 배포판이 이미 패키지로 묶인 중간 코드 캐시를 탑재하고 있다면, 설치만 하면 끝난다.
eAccerlerator를 어떻게 가져왔거나에 상관없이 몇 가지 살펴볼 환경 설정 옵션이 있다. 이 환경 설정 파일은 /etc/php.d/eaccelerator.ini다. eaccelerator.shm_size는 컴파일된 스크립트를 저장하는 공간인 공유 메모리 캐시 크기를 정의한다. 이 값은 메가바이트 단위다. 적절한 크기는 응용 프로그램에 따라 다르다. eAccelerator는 메모리 사용을 포함하여 캐시 상태를 보여주는 스크립트를 제공한다. 64메가바이트면(eaccelerator.shm_size="64") 적당한 시작값이다. 또한 여러분이 선택한 값을 받아들이지 않을 경우 커널의 최대 공유 메모리 크기를 조정할 필요가 있다. kernel.shmmax=67108864 항목을 /etc/sysctl.conf에 넣고 sysctl -p 명령을 내리면 설정값이 반영된다. kernel.shmmax 값은 바이트 단위다.
공유 메모리 할당을 초과하면 eAccelerator는 메모리에서 옛날 스크립트를 제거한다. 기본적으로 이런 동작은 비활성으로 남아있다. eaccelerator.shm_ttl = "60"을 지정하면 eAccelerator가 공유 메모리 부족을 감지했을 때 60초 동안 접근하지 않은 스크립트를 제거한다.
eAccelerator를 대체할 다른 대안은 Alternative PHP Cache(APC)다. 젠드 제작사는 효율성을 높이기 위한 최적화기를 포함한 상용 중간 코드 캐시를 판매한다.
php.ini
php.ini에서 PHP 환경을 설정한다. 표 1에 정리한 네 가지 중요한 설정 값은 PHP가 얼마나 시스템 자원을 소비할지 통제한다.

표 1. php.ini에서 자원 관련 설정값
설정설명권장값
max_execution_time얼마나 많은 CPU-초를 스크립트가 소비하는지를 지정30
max_input_time얼마나 오랫동안(초) 스크립트가 입력 자료를 기다릴지를 지정60
memory_limit얼마나 많은 메모리를(바이트) 죽기 전에 스크립트가 소비할지를 지정32M
output_buffering얼마나 많은 자료를(바이트) 클라이언트에게 전달하기 전에 버퍼에 저장할지를 지정4096
여기서 설명하는 값은 대부분 응용 프로그램에 따라 달라진다. 사용자에게 대규모 파일을 받아들일 요량이라면, php.ini나 코드에서 max_input_time을 증가시켜야 한다. 비슷하게 CPU나 메모리를 많이 소비하는 프로그램은 더 큰 설정 값을 지정한다. 목적은 폭주한 프로그램을 막아내는 데 있으므로 전역 설정 값을 비활성화하는 상황은 바람직하지 못하다. max_execution_time에 대한 설명을 추가하겠다. 이 설정값은 절대 시간이 아니라 프로세스의 CPU 시간을 참조한다. 따라서 I/O 작업이 많고 계산이 적은 프로그램일 경우 max_execution_time보다 더 오래 동작할지도 모른다. 이는 한 max_input_timemax_execution_time보다 큰 경우를 설명한다.
PHP로 로그 파일을 남기는 양도 조정이 가능하다. 실제 운영 환경에서는 가장 중요한 로그 파일을 제외한 나머지 로그를 끄면 디스크 쓰기를 줄인다. 문제 해결용으로 로그가 필요하면 원하는 만큼 로그 단계를 높일 수 있다. error_reporting = E_COMPILE_ERROR|E_ERROR|E_CORE_ERROR는 문제를 추적할 만큼 충분한 정보를 로그에 남기지만, 스크립트에서 나오는 수다스러운 내용은 제거한다.
요약
이번 기사는 아파치와 PHP라는 웹 서버 조율에 초점을 맞춘다. 아파치의 경우에는 .htaccess 파일 처리와 같은 웹 서버가 반드시 실행해야 하는 추가적인 점검 과정을 건너뛰도록 만든다. 또한 MPM을 조율해 작업 요청을 받아주는 일꾼 숫자와 시스템 자원 사이에 균형을 잡아줘야 한다. PHP로 할 수 있는 최선의 방법은 중간 코드 캐시 설치다. 모든 사람을 위해 스크립트가 시스템을 느리게 만들고 자원을 잡고 있지 않도록 만들기 위해 몇 가지 자원 설정에 촉각을 곤두세워야 한다.
다음에 소개할 연재 기사 마지막 회에서는 MySQL 데이터베이스 조율 기법을 살펴본다. 계속해서 기대하시라!

참고자료
교육
제품 및 기술 얻기
토론
블로그, 포럼, 포드캐스트, 새로운 developerWorks space에 있는 새로운 공동체 토픽을 통해 developerWorks 공동체에 참여한다.

LAMP 시스템 조율, Part 1: LAMP 아키텍처 이해 (한글)

http://www.ibm.com/developerworks/kr/library/l-tune-lamp-1/


LAMP 시스템 조율, Part 1: LAMP 아키텍처 이해 (한글)

LAMP 시스템 동작 원리, 성능 측정 방법, 기반 운영체제 조율 방법
Sean A. Walberg, 선임 네트워크 엔지니어
요약: LAMP(Linux®, Apache, MySQL, PHP/Perl) 아키텍처를 활용하는 응용 프로그램은 끊임없이 개발되고 배포되고 있습니다. 하지만 때로 다른 사람이 작성했다는 이유만으로 응용 프로그램 자체에 대한 통제권이 서버 관리자에게는 없습니다. 기사 셋으로 이뤄진 이번 연재물은 응용 프로그램 성능을 향상시킬 서버 환경 설정 항목을 다룹니다. 첫 번째 기사는 LAMP 아키텍처, 성능 기법, 기본적인 리눅스 커널, 디스크, 파일 시스템 미조정을 다룹니다. 이어지는 기사에서는 아파치, MySQL, PHP 컴포넌트를 조율하는 방법을 다룹니다.
원문 게재일:  2008 년 4 월 22 일
난이도:  중급 영어로:  보기
페이지뷰: 1897 회
의견: 0 (의견 추가)
1 star2 stars3 stars4 stars5 stars 평균 평가 등급 (총 3표)
리눅스, 아파치, MySQL, PHP(또는 펄)은 일정 목록부터 블로그와 전자 상거래 사이트에 이르기까지 많은 웹 응용 프로그램의 토대가 된다. 워드프레스와 플리그(Pligg)는 강력한 고성능 웹 사이트를 유지하는 공통 소프트웨어 패키지다. 이런 아키텍처는 LAMP라고 알려졌다. 거의 모든 리눅스 배포판에는 리눅스, 아파치, MySQL, PHP와 펄이 포함되어 있으므로 LAMP 소프트웨어 설치는 식은 죽먹기다.
설치가 쉽기 때문에 소프트웨어 실행까지 쉬워보일지도 모르겠지만, 이는 사실이 아니다. 궁극적으로 응용 프로그램 부하는 백엔드 서버에 포함된 설정값을 무력화하며, 결국 응용 프로그램 성능 저하가 일어난다. LAMP 설치는 지속적인 감시와 조율과 평가를 요구한다.
시스템을 조율하는 작업은 사람마다 의미가 달라진다. 이번 연재에서는 리눅스, 아파치, MySQL, PHP라는 LAMP 컴포넌트 조율에 초점을 맞춘다. 응용 프로그램 자체 조율은 또 다른 복잡한 문제다. 응용 프로그램과 백엔드 서버 사이에는 공생 관계가 있다. 잘못 조율된 서버는 최상의 응용 프로그램조차도 부하가 걸릴 경우 실패하도록 만들며, 잘못된 응용 프로그램을 앞에 놓고 서버 조율을 해봤자 굼벵이를 달팽이로 만들 뿐이다. 다행스럽게도 적절한 시스템 조율과 감시는 응용 프로그램에 존재하는 문제점을 찾아내준다.
LAMP 아키텍처
시스템 조율 과정에서 첫 단계는 동작 원리 이해다. 가장 단순한 수준에서 보면 LAMP 기반 응용 프로그램은 리눅스 호스트에서 동작하는 아파치 웹 서버 일부로 동작하는 PHP와 같은 스크립트 언어로 작성된다.
PHP 응용 프로그램은 요청받은 URL을 통해 클라이언트로부터 폼 정보를 얻으며, 세션 정보를 포착해 수행할 작업을 결정한다. 필요하다면 서버는 (역시 리눅스에서 동작하는) MySQL 데이터베이스에서 자료를 끌어 당겨 HTML 탬플릿에 맞춰 정보를 결합한 다음 클라이언트에 반환한다. 이런 과정은 사용자가 응용 프로그램을 탐색하는 과정에서 계속 반복되며, 시스템에 여러 사람이 접근할 경우 병렬로 진행된다. 자료 흐름이 일방향이 아닌 이유는 세션 자료 형태, (투표를 비롯한) 통계 수집, 댓글이나 사이트 개선 내용과 같이 사용자가 제출한 내용 형태로 데이터베이스 갱신이 일어날 수 있기 때문이다. 또한 동적 구성 요소는 물론이고 이미지, 자바스크립트 코드, CSS와 같은 정적인 구성 요소도 존재한다.

다양한 LAMP 변종

LAMP는 엄밀히 말해 리눅스, 아파치, MySQL, PHP 또는 펄로 시작했다. 하지만 리눅스에 익숙하지 않다면 마이크로소프트 윈도우(Microsoft® Windows®) 위에서 리눅스, 아파치, MySQL, PHP를 돌리는 경우도 흔하다. 다시 한번 말하지만, 발음이 안 되서 그렇지 아파치를 lighttpd와 같은 경량 웹 서버로 바꾸고도 LAMP 스타일로 시스템을 운영할 수 있다. 아니면 PostgreSQL이나 SQLite 같은 오픈 소스 데이터베이스나 IBM DB2와 같은 상용 데이터베이스나 심지어 상용이지만 공짜인 IBM® DB2® Express-C와 같은 엔진을 사용할 수도 있다.
이 기사에서 전통적인 LAMP 아키텍처에 초점을 맞추는 이유는 웹을 돌아다닐 때 가장 흔히 보는 시스템이며, 구성 요소가 모두 오픈 소스이기 때문이다.
LAMP 시스템을 통한 요청 흐름을 살펴본 다음에는, 속력이 느려지는 지점을 찾기 시작한다. 데이터베이스는 동적 정보를 만들어내므로 클라이언트는 질의에 반응하는 동안 지연을 느낀다. 웹 서버는 스크립트를 번개처럼 수행할 수 있어야 하며, 다중 병렬 요청을 처리할 수 있어야 한다. 최종적으로 기반 운영체제는 응용 프로그램을 원활하게 지원해야 한다. 네트워크를 통해 여러 서버 사이에서 파일을 공유하도록 만들 경우 잠재적인 병목이 생긴다.
성능 측정
지속적인 성능 측정은 두 가지 측면에서 도움을 준다. 첫 번째로 측정은 좋든 나쁘든 경향을 파악하는 데 도움을 준다. 간단한 예를 들자면, 웹 서버에서 CPU 사용량을 관찰하는 방법으로 부하가 걸릴 때를 알 수 있다. 비슷하게 과거에 사용했던 전체 대역폭을 관찰하고 이를 통해 미래에 일어날 대역폭을 보간법으로 추정해보면 네트워크 업그레이드가 필요할 시점을 결정할 수 있다. 이런 측정은 다른 측정과 비교해서 관찰하면 최상의 효과를 얻는다. 예를 들어, 사용자가 응용 프로그램이 느리다고 불평할 때, 디스크 용량이 가득 차 있을지도 모른다.
성능 측정을 두 번째로 활용하는 방법은 조율이 상황을 좋게 만들었는지 나쁘게 만들었는지 판단하는 것이다. 변화 직전과 직후에 측정한 결과를 비교함으로써 이런 판단이 가능하다. 물론 효과를 높이려면 한번에 항목 하나만 변경하며, 변경 효과를 결정하기 위해 적절한 척도를 사용해야 한다. 한번에 하나만 변경하는 이유는 명백하다. 결국 동시에 두 가지를 변경하면 서로 방해할 가능성이 높기 때문이다. 척도 결정 방식은 더욱 미묘하다.
응용 프로그램 사용 관점에서 영향을 미치는 내역을 관찰하기 위해 고른 척도는 아주 중요하다. 변경 목표가 데이터베이스 메모리 사용량 감소라면, 다양한 버퍼 제거는 틀림없이 도움을 주지만 응용 프로그램 성능과 질의 속력을 희생해야 한다. 그 대신 척도 중 하나가 응용 프로그램 반응 시간이라면 단순히 데이터베이스 메모리 사용량을 넘어서 다른 조율 가능성이 열리게 된다.
응용 프로그램 반응 속력은 다양한 방법으로 측정이 가능하다. 아마도 가장 손쉬운 방법은 Listing 1에 나오는 curl 명령이다.

Listing 1. 웹 사이트 반응 속력을 측정하기 위해 cURL 사용하기
$ curl -o /dev/null -s -w %{time_connect}:%{time_starttransfer}:%{time_total}\
    http://www.canada.com
0.081:0.272:0.779

Listing 1은 인기 있는 뉴스 사이트를 살펴보기 위해 사용한 curl 명령어를 보여준다. 일반적으로 HTML 코드인 결과물은 -o 매개변수로 /dev/null로 보내지며, -s는 상태 정보를 보여주지 않는다. -w 매개변수는 curl에게 표 1에서 기술한 타이머와 같은 몇몇 상태 정보를 출력하게 만든다.

표 1. curl이 사용한 타이머
타이머설명
time_connect서버로 TCP 연결이 이뤄질 때까지 걸린 시간
time_starttransfer요청 이후 웹 서버가 첫 바이트를 반환할 때까지 걸린 시간
time_total요청을 완료할 때까지 걸린 시간

각 타이머는 DNS 탐색 전인 트랜잭션 시작 시점에 상대적이다. 따라서 요청 이후 웹 서버가 요청을 처리해 자료를 되돌려주기 시작하는 데 걸린 시간은 0.272 - 0.081 = 0.191초다. 클라이언트는 서버에서 자료를 내려받는 동안 0.779 - 0.272 = 0.507초를 소비했다.
curl 자료를 관찰하며 시간 경과에 따라 변화 추이를 살펴보면 사이트가 사용자에게 반응을 얼마나 제대로 하는지 아이디어를 얻을 수 있다.
물론 웹 사이트는 단순히 페이지 하나가 아니다. 이미지, 자바스크립트 코드, CSS, 다뤄야 할 쿠키 등을 포함한다. curl은 단일 구성 요소에 대한 반응 시간을 측정하는 데 유용하지만 종종 전체 페이지를 얼마나 빨리 가져오는지 알아야 하는 경우도 있다.
파이어폭스를 위한 템퍼 데이터 확장(참고자료 절에서 링크를 살펴본다)은 웹 브라우저가 만들어내는 모든 요청과 각 요청마다 자료가 날아오는 데 걸리는 시간을 표시한다. 이 확장을 사용하라면 Tools > Tamper Data를 선택해서 Ongoing requests 윈도우를 연다. 의심스러운 페이지를 불러서 브라우저가 각각 요청한 상태와 함께 구성 요소가 날아오는 데 걸리는 시간 정보를 살펴본다. 그림 1은 developerWorks 홈 페이지를 여는 과정에서 나온 결과를 보여준다.

그림 1. developerWorks 홈페이지를 여는 과정에서 사용한 요청 사항 분해
developerWorks 홈페이지를 여는 과정에서 사용한 요청 사항 분해
각 행은 항목 별로 여는 데 걸리는 시간을 보여준다. 요청을 시작한 시각, 여는 데 걸린 시간, 크기, 결과와 같은 다양한 자료를 출력한다. Duration 열은 구성 요소 자체를 여는 데 걸린 시간을 나열하는 반면에, Total Duration 열은 하위 구성 요소까지 모두 여는 데 걸린 시간을 보여준다. 그림 1에서 main 페이지를 여는 데 516밀리초가 걸린 반면, 모든 구성 요소를 열어 전체 페이지를 출력하는 데 걸린 시간은 5101밀리초다.
Tamper Data 확장을 활용하는 또 다른 방법으로 페이지를 여는 데 걸리는 시간을 그래프로 표현한다. Ongoing reqeusts 윈도우에서 상단 아무 곳에서 오른쪽 마우스 버튼을 누르고 Graph all을 선택하자. 그림 2는 그림 1에서 얻은 자료를 시각적으로 표현한 모습이다.

그림 2. developerWorks 홈 페이지를 열기 위해 사용한 요청을 시각적으로 표현한 모습
developerWorks 홈 페이지를 열기 위해 사용한 요청을 시각적으로 표현한 모습
그림 2에서 각 요청 별 주기는 어두운 청색으로 출력했으며, 페이지가 열리는 시작 시점에 상대적으로 보여준다. 따라서 전체 페이지를 여는 과정에서 어떤 요청이 병목인지를 볼 수 있다.
페이지 열기와 사용자 경험에 초점을 맞춤에도 불구하고 디스크, 메모리, CPU, 네트워크와 같은 핵심 시스템 척도를 외면하지 않도록 주의해야 한다. 다양한 유틸리티가 이런 정보 수집을 위해 준비되어 있다. 가장 도움을 주는 도구는 sar, vmstat, iostat다. 참고자료 절을 참조해서 각 도구에 대한 세부 정보를 파악하자.
기본 시스템 미세 조율
시스템에서 아파치, MySQL, PHP 구성 요소를 조율하기 앞서 기반 리눅스 구성 요소가 제대로 동작하는지를 확인할 시간을 확보해야 한다. 필요한 서비스만 수행하도록 동작 중인 서비스 목록을 이미 줄여놓았어야 함은 말할 나위도 없다. 훌륭한 보안 실천법일 뿐만 아니라 CPU 사이클과 메모리를 절약하는 좋은 방법이다.
간단한 커널 조율
대다수 리눅스 배포판은 버퍼와 기타 TCP 매개변수를 보수적으로 정의하고 있다. 네트워크 성능 향상을 위해 좀더 많은 메모리를 할당하도록 이런 매개변수를 바꿔야 한다. 커널 매개변수는 /proc에서 값을 읽고 쓰는 방법으로 proc 인터페이스를 통해 설정한다. 다행스럽게도 sysctl 프로그램은 /etc/sysctl.conf에서 값을 읽어서 필요에 따라 /proc에 설정하는 방식으로 작업을 좀더 쉽게 만든다. Listing 2는 인터넷 서버에 사용해야 하는 좀더 공격적인 네트워크 설정을 보여준다.

Listing 2. 좀더 공격적인 네트워크 설정을 담은 /etc/sysctl.conf
# 필요할 때 TCP syncookies를 사용한다
net.ipv4.tcp_syncookies = 1
# TCP 윈도우 스케일링을 활성화한다
net.ipv4.tcp_window_scaling = 1
# TCP 최대 버퍼 크기를 늘인다
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# 리눅스 자동 조율 TCP 버퍼 제약을 늘인다
net.ipv4.tcp_rmem = 4096 87380 16777216 
net.ipv4.tcp_wmem = 4096 65536 16777216
# 가용 포트 숫자를 늘인다
net.ipv4.ip_local_port_range = 1024 65000

이미 /etc/sysctl.conf에 존재하는 내용에 이 파일 내용을 추가한다. 첫 번째 설정은 TCP SYN 쿠키를 활성화한다. 패킷에 SYN 비트를 설정하는 방식으로 새로운 TCP 연결이 클라이언트에서 들어올 때, 서버는 절반이 열린 연결을 위한 항목을 만들고 SYN-ACK 패킷으로 반응한다. 일반적인 연산 과정에서, 원격 클라이언트는 ACK 패킷으로 반응하며, 절반이 열린 연결을 완전히 열린 연결로 바꾼다. SYN flood라는 기법은 ACK 패킷을 절대로 되돌려주지 않음으로써 서버가 들어오는 연결을 처리할 공간이 없어지도록 만드는 방법이다. SYN 쿠키 기능은 이런 조건을 감지해서 큐에 공간을 보존하기 위한 우아한 방법을 사용해 문제를 해결한다(세부 설명은 참고자료 절을 참조한다). 대다수 시스템에는 이런 기능이 기본적으로 활성화되어 있지만 설정이 되어 있는지 확인하는 편이 좋겠다.
TCP 윈도우 스케일을 활성화하면 클라이언트가 높은 속력으로 자료를 내려받을 수 있다. TCP는 기본적으로 64킬로바이트까지 다중 패킷에 대해 원격지에서 ACK를 받지 않고 보내도록 허용하는데, 지연 시간이 높은 컴퓨터끼리 대화할 때는 패킷을 꽉꽉 채워야 한다. 윈도우 스케일은 윈도우 크기를 늘이도록 헤더에 사용되는 특별한 비트를 활성화한다.
다음 네 가지 환경 설정 항목은 TCP 송수신 버퍼를 늘인다. 이렇게 하면 응용 프로그램이 자료를 좀더 빨리 보낼 필요가 없어지므로 다른 요청에 대응할 수 있으며, 서버가 바쁠 때 원격 클라이언트가 자료를 송신하는 능력도 높인다.
마지막 환경 설정 항목은 사용 가능한 지역 포트 수를 늘여서, 한번에 서비스가 가능한 연결 최대 개수를 늘인다.
이런 설정 값은 다음번 컴퓨터를 재시작하거나 sysctl -p /etc/sysctl.conf를 다음에 수행하는 시점에서 반영된다.
최대 성능을 위한 디스크 설정
디스크는 LAMP 아키텍처에서 핵심적인 역할을 맡는다. 정적 파일, 템플릿, 코드는 디스크에 저장되어 있으며 자료 테이블과 색인은 데이터베이스를 구성한다. 특히 데이터베이스에 적합한 대다수 권장 조율이 디스크 접근을 피하는 쪽에 맞춰져 있는 이유는 디스크에 접근하면 상대적으로 지연 시간이 길어지기 때문이다. 따라서 디스크 하드웨어 최적화에 공을 들이는 편이 유리하다.
가장 먼저 고려할 사항은 파일 시스템에서 atime 감사 기록을 비활성화했는지 확인하는 작업이다. atime은 파일에 마지막으로 접근한 시각이며, 기반 파일 시스템은 누군가 파일에 접근할 때마다 타임스탬프를 기록해야 한다. atime은 시스템 관리자가 거의 사용하지 않으므로 이를 비활성화하면 디스크 접근 시간을 줄일 수 있다. /etc/fstab의 네 번째 열에 noatime 옵션을 추가하는 방법으로 atime 기록을 막아버린다. Listing 3은 에제 환경 설정을 보여준다.

Listing 3. noatime을 활성화하는 방법을 보여주는 예제 fstab
/dev/VolGroup00/LogVol00 /                      ext3    defaults,noatime        1 1
LABEL=/boot             /boot                   ext3    defaults,noatime        1 2
devpts                  /dev/pts                devpts  gid=5,mode=620  0 0
tmpfs                   /dev/shm                tmpfs   defaults        0 0
proc                    /proc                   proc    defaults        0 0
sysfs                   /sys                    sysfs   defaults        0 0
LABEL=SWAP-hdb2         swap                    swap    defaults        0 0
LABEL=SWAP-hda3         swap                    swap    defaults        0 0

Listing 3에서 단지 ext3 파일 시스템만 수정했는데, noatime은 디스크에 저장된 파일 시스템에만 도움이 되기 때문이다. 이런 변화를 적용하기 위해 컴퓨터를 다시 시작할 필요는 없다. 각 파일 시스템을 다시 마운트하면 끝난다. 예를 들어, 루트 파일 시스템을 다시 마운트하려면 mount / -o remount 명령을 내린다.
다양한 디스크 하드웨어 조합이 가능한데, 리눅스는 항상 디스크에 접근하는 최적화된 방법을 안정적으로 감지해내지는 못한다. hdparm 명령은 IDE 디스크 접근에 사용하는 방법을 확인하고 설정하는 데 사용한다. hdparm -t /path/to/device는 벤치마크에 활용 가능한 속력 테스트를 수행한다. 안정적인 결과를 얻으려면, 이 명령을 수행하는 동안 시스템은 놀고 있어야 한다. Listing 4는 hda에 수행한 속력 테스트 결과를 보여준다.

Listing 4. /dev/hda에 수행한 속력 테스트
# hdparm -t /dev/hda

/dev/hda:
 Timing buffered disk reads:  182 MB in  3.02 seconds =  60.31 MB/sec

테스트 결과를 보면 디스크는 초당 대략 60MB 정도 자료를 읽는다.
디스크 조율 옵션을 파고 들기 전에, 경고 한 마디 하고 넘어가겠다. 잘못된 설정 값은 파일 시스템을 망가뜨릴지도 모른다. 때로 하드웨어와 호환되지 않는 옵션을 선택하면 경고가 나오지만 때로는 그냥 넘어간다. 이런 이유로 인해, 시스템에 실제 적용하기 앞서 전반적으로 테스트를 수행해야 한다. 서버를 표준 하드웨어로 통일하면 이런 문제를 해결할 수 있다.
표 2에 자주 사용하는 몇몇 공통 옵션을 정리했다.

표 2. hdparm을 위한 공통 옵션
옵션설명
-vi사용 중인 설정 값과 지원하는 설정 값을 결정하기 위해 드라이브에 질의를 던진다.
-c(E)IDE 32비트 I/O 지원을 질의/활성화한다. hdparm -c 1 /dev/hda 명령으로 활성화한다.
-m인터럽트 모드 당 다중 섹터를 질의/설정한다. 설정값이 0보다 크면 인터럽트 당 해당 숫자만큼 섹터를 전송한다.
-d 1 -XDMA를 활성화하고 IDE 전송 모드를 설정한다. hdparm 매뉴얼 페이지를 참조해 -X 뒤에 붙는 숫자를 확인하자. 최고 빠른 모드를 사용할 계획이 아니라면 -vi 옵션으로 충분하다.

불행하게도 광 채널이나 SCSI 시스템을 위한 조율 방법은 디바이스 별로 다르다.
rc.local과 같은 시동 스크립트에 필요한 설정을 추가하기 바란다.
네트워크 파일 시스템 조율
NFS는 네트워크를 경유해 디스크 볼륨을 공유하는 수단이다. NFS는 동일한 자료 복사본을 호스트마다 유지하며 변경 내역을 모든 노드에 반영하는 과정에 도움을 준다. 기본적으로 NFS는 대용량 저장 장치로 설정되어 있지 않다.
각 클라이언트는 원격 파일 시스템을 마운트할 때 rsize=32768,wsize=32768,intr,noatime 옵션을 붙여야 하는데, 다음 사항을 염두에 두자.
  • 대규모 읽기/쓰기 블록 크기를 사용한다(상황에 따라 바뀌며, 이 경우에는 32KB다)
  • NFS는 중단 상황에서 인터럽트가 걸릴 수 있다.
  • atime은 주기적으로 갱신되지 않는다.

Listing 3에서 제시한 설정값을 /etc/fstab에 넣어둘 수 있다. automounter를 사용한다면 적절한 /etc/auto.* 파일을 살펴보자.
서버 단에서 NFS 커널 스레드가 클라이언트 요청을 모두 다룰 수 있을 정도로 충분한지 확인하는 과정이 중요하다. 기본적으로 스레드 하나만 시작하지만, 레드햇과 페도라 시스템은 8부터 시작한다. 바쁜 NFS 서버를 위해, 이 값을 32나 64와 같은 높은 값으로 시작하도록 만들자. 클라이언트 쪽 RPC(Remote Procedure Call) 통계를 보여주는 nfsstat -rc 명령으로 NFS 서비스 중단이 일어나지 않는지 클라이언트 단에서 평가할 수 있다. Listing 5는 웹 서버에 대한 클라이언트 통계를 보여준다.

Lisgint 5. NFS 클라이언트의 RPC 통계 보기
# nfsstat -rc
Client rpc stats:
calls      retrans    authrefrsh
1465903813   0          0       

둘째 열인 retrans가 0인데, 마지막 시동 후 재전송이 필요하지 않음을 보여준다. 이 숫자가 증가하면 NFS 커널 스레드를 높여야 한다. rpc.nfsd에 원하는 스레드 숫자를 넘기면 되는데, 예를 들어 rpc.nfsd 128은 스레드 128개로 시작한다. 이런 조정은 언제든지 가능하다. 다시 한번 말하지만 시스템에서 NFS를 시작하는 시동 스크립트를 바꿔야 한다.
NFS에 대한 마지막 정리로 끝을 내겠다. 가능하면 NFSv2를 피해야 하는 이유는 v3이나 v4보다 성능이 떨어지기 때문이다. 현대적인 리눅스 배포판에서는 문제가 되지 않지만 혹시 NFSv2 호출이 이뤄지는지 서버 단에서 nfsstat으로 점검할 필요가 있다.
미리 보기
이번 기사는 LAMP 기본기와 LAMP 설치를 위한 몇 가지 간단한 리눅스 조율 과정을 살펴보았다. NFS 커널 스레드를 제외하고, 이번 기사에서 다룬 매개변수를 설정한 다음 잊어버려도 좋다. 다음에 이어지는 연재 기사에서는 아파치, MySQL, PHP 조율에 초점을 맞춘다. APM 조율이 리눅스 조율과 다른 이유는 소통량이 증가하고 read/write 분포가 바뀌고 응용 프로그램이 발전함에 따라 계속해서 매개변수를 조율할 필요가 있기 때문이다.

참고자료
교육
제품 및 기술 얻기
  • 파이어폭스를 위한 Tamper Data 확장은 동적으로 HTTP 헤더를 살피고 변경하도록 만들어주며, 페이지 구성 요소를 여는 과정을 요약한 그래프를 그려준다.
  • IBM 평가판 소프트웨어: developerWorks에서 직접 내려 받아 다음번 리눅스 프로젝트에 활용하자.
토론
블로그, 포럼, 포드캐스트, 새로운 developerWorks space에 있는 새로운 공동체 토픽을 통해 developerWorks 공동체에 참여한다.

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는 개발자이자 저자이며 또한 강사이자 기업가로 동작 지향적 개발 및 연속적인 통합, 신속한 소프트웨어 개발에 대한 열정으로 가득 차 있다. 그의 블로그에서 그에 관한 다양한 정보를 얻을 수 있다.