the platform 2011

264

Upload: naver-d2

Post on 30-Jul-2015

588 views

Category:

Technology


6 download

TRANSCRIPT

Page 1: The platform 2011
Page 2: The platform 2011

The Platform 2011

Page 3: The platform 2011

이 책은 NHN㈜의 지적 재산이므로 어떠한 경우에도 NHN㈜의 공식적인 허가 없이

이 책의 일부 또는 전체를 복제, 전송, 배포하거나 변경하여 사용할 수 없습니다.

이 책은 정보 제공의 목적으로만 제공됩니다.

The Platform은 NHN 개발자들이 직접 참여하는, 개발자들을 위한 사내 월간지입

니다. 지난 1년간 The Platform 기사 중 함께 나눌 수 있는 것을 모아 책자로 제작

하였습니다.

The Platform 2011은 네이버 북스 앱으로도 보실 수 있습니다.

The Platform 2011

Page 4: The platform 2011

가장 자신있게 무엇에 대해 알고 있다고 말할 수 있는 방법은

무엇에 대한 책을 쓰는 것이라고 들었습니다.

긴 호흡으로 이야기를 풀어낼만큼 지식을 갖추어야함은 물론,

활자로 인쇄되어 많은 사람의 확인과 비판을 감수할마음의 준비가 되었다는 뜻일 터이니까요.

The Platform은 매달 대여섯편의 기사를 모아 만드는 월간 정보지입니다.

명문을 만드는 가장 큰 동기는 마감이라는 말처럼,

매달 말 대여섯 명의 기고자들은

마감을 목전에 두고 완성하지 못할 것 같았던 기사를 완성하는

기적을 경험했다고 하더군요.

그렇게 매달 기사가 모여

삼백여 페이지에 이르는 적지 않는 분량의

The Platform 2011을 만들 수 있게 되었습니다.

책을 만드는 사람의 처지에서는

독자 여러분이 책의 처음부터 끝까지 모든 단어를 읽어주기를기대합니다.

그러나 설사 모든 부분이 읽히지 않더라도

여러분의 서가에 당당히 꽂혀있기만 해도

크게 뿌듯해할 수 있을 것 같습니다.

열심히 그리고 뿌듯하게 만들었습니다.

이 책을 재미나게 읽어주시고,

소중히 간직해 주셨으면 하는 바람입니다.

감사합니다.

인사말

박원기 _ IT 서비스 사업 본부장

소중하게 간직해 주세요,

The Platform 2011

Page 5: The platform 2011

4

Page 6: The platform 2011

5

Page 7: The platform 2011

CUBRID

CUBRID

CUBRID

CUBRID

2011년 9월, NHN 전체 서비스의 30%에 적용하였습니다. 2012년 6월, NHN 전체 서비스의 50%에 도전합니다.CUBRID의 도전은 계속됩니다.

2011년 9월, 월 평균 다운로드 5,000건을 넘었습니다. 2012년 6월, 월 평균 다운로드 10,000건에 도전합니다.

CUBRID의 도전은 계속됩니다.

By NHN For Everyone

Page 8: The platform 2011

CUBRID

CUBRID

CUBRID

CUBRID

2011년 9월, NHN 전체 서비스의 30%에 적용하였습니다. 2012년 6월, NHN 전체 서비스의 50%에 도전합니다.CUBRID의 도전은 계속됩니다.

2011년 9월, 월 평균 다운로드 5,000건을 넘었습니다. 2012년 6월, 월 평균 다운로드 10,000건에 도전합니다.

CUBRID의 도전은 계속됩니다.

By NHN For Everyone

Page 9: The platform 2011

차례8 The Platform 2011

01

NHN 서비스들여다보기

CSSQuery 스마트하게 사용하기

Jindo에서 CSSQuery 사용하기 20

단일 요소 찾기 20

다중 요소 찾기 21

CSS 선택자 알아보기 22

성능 비교 24

성능 비교 결과 24

지원 범위 25

마치며 25

파이썬에 대해 26

생산성 측면에서의 파이썬 27

성능 측면에서의 파이썬 27

파이썬의 단점 28

파이썬 튜닝하기 28

파이썬과 QP 활동 31

프로토타입과 도구 개발을 위한 파이썬 33

마치며 33

웹 보안 관제 서비스 34

악성코드가 유포되는 과정 35

백신을 설치해도 소용 없다면...... 36

네이버에서는 어떻게? 36

AMIGO 37

마치며 39

메일 서비스 파이썬 적용 경험

AMIGO - 행위 기반 악성코드 탐지

Page 10: The platform 2011

차례 9The Platform 2011

CSSQuery 스마트하게 사용하기

01

NHN 서비스들여다보기

uMon의 이해

모니터링의 필요 40

uMon 시스템의 구성 40

서비스와 uMon 연동 시 고려 사항 44

마치며 45

지도 서비스는 어떻게 만드는 것일까?

지도 서비스 간단 설명 46

지도 이미지 타일 46

벡터 데이터의 구성 요소 47

지도 레이어 48

지도의 축척 49

지도의 레벨 49

레이어의 표현 범위 49

지도 이미지 타일의 구성 50

네이버 지도 서비스의 미래 52

RTCS 실시간 웹 서비스를 위한 도전

RTCS 구성 요소와 기능 54

서버 Push 방식: Long Polling vs. 스트리밍 54

브라우저별 구현 방법 56

보안 처리 59

다양한 네트워크 상황에 대한 대처 60

서버 메시지 전송 실패 시 처리는? 60

대용량 처리 60

RTCS 기반 서비스 구성하기 61

앞으로 할 일 61

Page 11: The platform 2011

차례10 The Platform 201110

XE 현황과 발전 방향

플레이넷, 네이버 ID로 모든 게임을

야구9단 아키텍처

XE 역사 62

XE 구성 요소 62

XE 사용 환경 64

XE 사용 현황 64

XE의 장점 65

XE의 단점 67

XE 발전 방향 69

맺음말 69

플레이넷? 70

시스템 소개 71

플레이넷 as a 게임 채널링 서비스 73

플레이넷 시스템 설계 기준 73

플레이넷의 도메인 구조 74

사용자 시나리오 74

회원 76

인증 76

제3자 정보제공 동의 및 게임 이용 약관 동의 77

게임 실행(게임 플레이) 79

마치며 80

야구9단 81

시스템 구성 82

게임 프로세서 83

리그 컨트롤러 84

게임 노티파이어 85

마치며 85

01

NHN 서비스들여다보기

Page 12: The platform 2011

차례 11The Platform 2011

네이버 건강 서비스, MFO를 적용한 대용량 데이터 처리 사례

건강 서비스 MFO 적용 사례 86

건강 서비스에 MFO를 적용하면서 생긴 난관들 88

마치며 90

MQ를 이용한 SNS 글쓰기 성능 개선

얼른 넣고 잘 가져가기 91

잘 넣고 얼른 가져가기 92

1 : N+1 92

하지만 이제 시작일 뿐 94

PICK/CAFE 서비스 94

참고 문헌 95

Hadoop과 MongoDB를 이용한 로그분석시스템

들어가며 96

Map-Reduce를 활용한 데이터 집계와 추출 97

MongoDB의 장점은 개발 생산성 99

집계 정보 저장소 선정: MongoDB vs. 카산드라 100

최종 Hadoop & MongoDB 기반 집계 시스템 102

마치며 103

01

NHN 서비스들여다보기

Page 13: The platform 2011

차례12 The Platform 2011

테스트 자동화 도구GUITAR

GUITAR 주요 기능 104

GUITAR 구조 105

스크립트 구조 105

한글 스크립트 105

대상과 명령 106

XPath vs 이미지 106

이미지 방식의 장점 107

이미지 방식의 단점과 해결 방안 107

그 외 기능 110

GUITAR 적용 효과 111

API 테스트 111

데이터 검수 112

맺음말 112

01

NHN 서비스들여다보기

Page 14: The platform 2011

차례 13The Platform 2011

02

NHN의 플랫폼과 도구

모바일 Push와nPush

모바일 Push 114

Push 기술 114

nPush 122

맺음말 124

ffmpeg을 이용한 iOS 동영상 플레이어

ffmpeg 소개 125

소프트웨어 디코딩 과정 126

ffmpeg의 아이폰용 컴파일 126

ffmpeg 사용 시 성능 이슈와 최적화 127

GPU를 이용한 색상 공간 변환 처리 127

ARM® NEON™ 기술을 이용한 디코딩 최적화 129

마무리 130

분산 고속 저장소nStore

탄생 배경 131

nStore는? 132

컨테이너 132

nStore 구조 133

분산 및 복구를 위한 제약 135

성능 측정 135

마치며 136

Page 15: The platform 2011

차례14 The Platform 2011

CUBRID브로커 이야기

위치 정보 플랫폼nLocation

데이터베이스 테스트 기법과Coverage4iBatis

CUBRID 클라이언트 모듈 137

CUBRID 브로커 139

브로커 사용의 단점 139

브로커 사용의 장점 141

브로커 확장 143

마치면서 144

nLocation 아키텍처 146

쿼드트리 147

nLocation v1.0 성능 148

nLocation Unit 148

nLocation v1.5 소개 149

데이터베이스 테스트 가이드 150

어떻게 검증할까 152

Coverage4iBatis 153

데이터베이스 테스트를 고민하는 단계는? 155

마치면서 156

02

NHN 서비스들여다보기

Page 16: The platform 2011

차례 15The Platform 2011

maven-precheck-plugin,배포 시 장애를 감지하는 방법

서비스 배포 후 종종 발생하곤 했던 문제들 157

maven-precheck-plugin 158

기능이 비슷한 도구 160

마무리 161

메이븐을 이용한정적 파일 배포

정적 파일 배포의 어려움 162

아파치 웹 서버 설정을 이용한 최적화 163

정적 파일 최적화 Q&A 163

BDS를 이용한 배포 165

설정 및 사용 방법 166

마치면서 166

02

NHN의 플랫폼과 도구

Page 17: The platform 2011

차례16 The Platform 2011

03

기술개발동향

JVM Internal

JDK 7

구르믈 버서난 달처럼

가상 머신 168

자바 바이트코드 169

클래스 파일 포맷 171

JVM 구조 174

The Java Virtual Machine Specification, 180

Java SE 7 Edition 180

마치며 182

들어가며 183

Project Coin 185

More New I/O APIs 195

Fork/Join Framework 202

맺음말 206

사람들은 왜 구름 위로 올라가고 싶어할까? 207

구름에도 새털구름부터 뭉게구름, 비구름 등 여러 종류가 있다 212

구름 제조 공장 215

구름에서 비가 내리면 218

마치며 219

Page 18: The platform 2011

차례 17The Platform 2011

03

기술개발동향 왜

NoSQL인가?

왜 NoSQL인가? 220

YCSB를 사용한 벤치마크 테스트 221

카산드라와 HBase 아키텍처 222

MongoDB 아키텍처 224

RDBMS 아키텍처 225

마치며 225

NoSQL가용성과 운영 안정성

카산드라의 장애 처리와 데이터 정합성 226

HBase의 장애 요소와 복구 방식 228

MongoDB의 복제 및 장애 복구 방식 229

결론 230

성능 향상을 위한 SQL 작성법

인덱스 구조 231

인덱스 스캔 232

인덱스 스캔을 이용한 질의 처리 과정 232

인덱스 사용하기 233

인덱스 활용 최적화 234

은 총알은 없다(No Silver Bullet) 237

Page 19: The platform 2011

차례18 The Platform 2011

Velocity 2011, 속도혁신 최근 동향

Git vs. Mercurial

iOS 플랫폼 소개

Velocity 2011 238

웹 성능 최적화 도구 239

웹 성능 최적화 적용 사례 240

모바일 241

자바스크립트 242

웹 성능 최적화 전망 242

참고 자료 242

DVCS로의 이주 244

Git와 Mercurial 245

Subversion과의 연계 248

결론 248

아이폰의 등장 251

브라우저 252

멀티 터치 인터페이스 253

GPS 및 다양한 센서의 지원 253

Wi-Fi 지원 253

아이폰의 서비스 플랫폼 253

iOS 5와 iOS의 진화 257

마치면서 258

03

기술개발동향

Page 20: The platform 2011

19

NHN 서비스들여다보기

CSSQuery 스마트하게 사용하기 20

메일 서비스 파이썬 적용 경험 26

AMIGO - 행위 기반 악성코드 탐지 34

uMon의이해 40

지도 서비스는 어떻게 만드는 것일까? 46

RTCS 실시간 웹서비스를 위한 도전 53

XE 현황과 발전 방향 63

플레이넷, 네이버 ID로 모든 게임을 70

야구9단 아키텍처 81

네이버 건강 서비스, MFO를 적용한 대용량 데이터 처리 사례 86

MQ를 이용한 SNS 글쓰기 성능 개선 91

Hadoop과 MongoDB를 이용한 로그 분석 시스템 96

테스트 자동화 도구 GUITAR 104

Page 21: The platform 2011

NHN 서비스 들여다보기The Platform 201120

CSSQuery란 CSS 선택자를 이용하여 DOM 객체에 접근하는 자

바스크립트 함수다. NHN 표준 자바스크립트 라이브러리인 Jindo

에서 어떻게 CSSQuery를 사용할 수 있는지 알아보도록 하자.

Jindo에서 CSSQuery 사용하기

CSSQuery는 특정 자바스크립트 라이브러리가 아닌 거의 모든 자

바스크립트 라이브러리에서 지원하는 기능이다. 예를 들면 데이터

베이스에 표준 쿼리 문법이 있고 각 데이터베이스 벤더마다 특정

쿼리 문법이 있는 것처럼 CSSQuery에도 범용적으로 사용되는 표

준 선택자(Selector)가 있고 각 자바스크립트 라이브러리에서 별도

로 지원하는 선택자가 있다. 따라서 가능하면 별도로 지원하는 선

택자보다 표준 선택자를 사용하는 것이 좋다. 일반적으로 자바스크

립트 라이브러리의 CSSQuery 선택자는 거의 같기 때문에 서비스

의 자바스크립트 라이브러리가 바뀌더라도 큰 문제가 발생하는 일

이 드물다.

이 글은 Jindo를 기준으로 작성되어 있지만 다른 라이브러리와

유사한 선택자를 사용하기 때문에 이해하는 데 큰 어려움이 없을

것이다. Jindo가 지원하는 CSSQuery는 단일 또는 다중의 요소

(Element)를 찾을 수 있다.

단일 요소 찾기

단일 요소 찾기는 특정 조건을 만족하는 첫 번째 요소를 찾을 때 사

용한다. 다음 코드는 "even" 클래스를 가진 요소 중 조건을 만족하

는 첫 번째 요소를 찾는 예제이다.

<ul>

<li>1</li>

<li class="even">2</li>

<li>3</li>

<li class="even">4</li>

</ul>

jindo.$$.getSingle(".even") -> <li class="even">2</li>

CSSQuery 스마트하게 사용하기저는 진도라는 자바스크립트 프레임워크의 개발자인 전용우입니다.

자바스크립트 개발은 요소(Element)를 변경하는 일을 많이 합니다.

개발을 하다 보면 요소를 변경하기 전에 찾는 작업을 먼저 합니다. 그런데 교육을

하다 보면 찾는 작업이 익숙하지 않아 개발을 힘들어하는 경우를 보았습니다.

그래서 요소를 쉽게 찾을 수 있는 방법을 소개하게 되었습니다.

이 글로 조금이나마 자바스크립트 개발에 도움이 되었으면 좋겠습니다.

•Ajax UI3팀 _ 전용우

Page 22: The platform 2011

CSSQuery 스마트하게 사용하기The Platform 2011 21

쿼리 문법의 조건에 맞는 요소가 많아도 조건을 만족하는 첫 번째

요소 하나만 반환한다. 속도 면에서 다중 요소를 찾는 기능보다 좋

다. 한 번에 여러 개의 요소를 찾는 경우가 아니라면 단일 요소 찾

기 기능을 사용하자.

단일 요소를 찾을 때 사용하는 함수는 다음과 같다.

jindo.$$.getSingle(sCSSSelector, vElement);

파라미터

{String} sCSSSelector, {String|Element} vElement

• sCSSSelector : 쿼리 문법을 문자열 형태로 입력한다.

• vElement : 기준이 될 요소(Element)나 해당 요소의 아이디(String)를 입

력한다. 기본 값은 document이다.

• 반환 값 : 조건에 맞는 단일 요소(Element)를 반환한다.

조건에 맞는 요소를 어디에서부터 찾을지 결정하는 기준 요소를 두

번째 파라미터에 지정한다. 다음 코드를 보자.

<ul id="first">

<li>1</li>

<li class="even">2</li>

<li>3</li>

<li class="even">4</li>

</ul>

<ul id="second">

<li>5</li>

<li class="even">6</li>

<li>7</li>

<li class="even">8</li>

</ul>

jindo.$$.getSingle(".even","first") ->

<li class="even">2</li>

jindo.$$.getSingle(".even","second") ->

<li class="even">6 </li>

첫 번째 탐색은 아이디가 "first"인 요소를 기준으로 조건에 맞는 요

소를 찾기 때문에 "even" 클래스가 있는 첫 번째 요소는 값이 2인

<li> 요소가 된다. 두 번째 탐색은 아이디가 "second"인 요소를 기

준으로 찾기 때문에 값이 6인 <li> 요소가 된다.

인터넷 익스플로러와 같은 브라우저에서는 기준 요소 지정 여부가

성능에 상당한 영향을 미친다. 따라서 가능하면 기준 요소를 지정

하도록 한다.

다중 요소 찾기

다중 요소는 쿼리 문법의 조건에 맞는 모든 요소를 찾는 경우에 사용한

다. 다음 코드는 조건에 맞는 모든 요소를 배열로 반환하는 예제이다.

<ul>

<li>1</li>

<li class="even">2</li>

<li>3</li>

<li class="even">4</li>

</ul>

jindo.$$(".even") -> [<li class="even">2</li>,

<li class ="even">4</li>]

모든 요소를 찾기 때문에 단일 요소를 찾는 것에 비해 느릴 수 있지

만 직접 구현하는 것보다 빠르다. 다중 요소를 찾는 함수의 사용법

은 단일 요소를 찾는 문법과 같으며, 설명은 다음과 같다.

jindo.$$(sCSSSelector, vElement);

• sCSSSelector : 쿼리 문법을 문자열 형태로 입력한다.

• vElement : 기준이 될 요소(Element)나 해당 요소의 아이디(String)를 입

력한다. 기본 값은 document이다.

• 반환 값 : 조건에 맞는 요소를 배열(Array) 형태로 반환한다.

단일 요소를 찾는 함수와 달리 반환 타입이 무조건 배열이다. 조건

에 맞는 요소가 하나라도 배열 형태로 반환하며, 조건을 만족하는

요소가 없으면 빈 배열을 반환한다. 다음은 기준 요소를 지정한 경

우와 지정하지 않은 경우의 차이를 나타낸다.

<ul id="first">

<li>1</li>

<li class="even">2</li>

<li>3</li>

<li class="even">4</li>

</ul>

Page 23: The platform 2011

NHN 서비스 들여다보기The Platform 201122

아이디가 'parent'인 요소를 기준으로 각 용어에 해당하는 요소는

다음과 같다.

• 손 요소는 하위에 있는 모든 요소를 말한다. 'parent'의 자손 요소는

'child1', 'child2', 'child3'이다.

• 자식 요소는 한 단계 아래의 요소를 말한다. 'parent'의 자식 요소는

'child1', 'child2'이다.

• 형제 요소는 자신과 같은 레벨의 요소를 말한다. 'parent'의 형제 요소

는 'sibling1', 'sibling2'이다.

아이디가 'child1'인 요소를 기준으로 다음 용어에 해당하는 요소는

다음과 같다.

• 부모 요소는 자신의 바로 상위 요소를 말한다. 'child1'의 부모 요소는

'parent'이다.

• 동생 요소는 자신과 같은 레벨에 있는 요소 중 순서 상 자신의 이후에

있는 요소를 말한다. 'child1'의 동생 요소는 'child2'이다.

아이디가 'child2'인 요소를 기준으로 다음 용어에 해당하는 요소는

다음과 같다.

• 형 요소는 자신과 같은 레벨에 있는 요소 중 순서 상 자신의 이전에 있는

요소를 말한다. 'child2'의 형 요소는 'child1'이다.

위에서 설명한 것처럼 자손, 자식, 형제, 부모 요소에 대한 용어를

먼저 이해해야 선택자의 설명을 이해할 수 있다.

필수 선택자

CSSQuery를 제대로 사용하려면 필수 선택자는 숙지하는 것이

좋다.

타입 선택자

타입 선택자는 태그 이름으로 요소를 찾을 때 사용한다. 선택자 형

식은 다음과 같다.

tagName

다음 코드는 타입 선택자를 사용해 <div> 요소를 찾는 예제이다.

<ul id="second">

<li>5</li>

<li class="even">6</li>

<li>7</li>

<li class="even">8</li>

</ul>

jindo.$$(".even") -> [<li class="even">2</li>,

<li class= "even">4</li>, <li class="even">6</li>,

<li class="even">8</li>]

jindo.$$(".even","first") -> [<li class="even">2</li>,

<li class="even">4</li>]

첫 번째 탐색은 기준 요소가 없기 때문에 문서 전체를 탐색하게 되

고 그 결과 총 4개의 요소를 반환한다. 이와 달리 두 번째 탐색은 기

준 요소부터 탐색하여 그 결과 총 2개의 요소를 반환한다.

지금까지 Jindo의 CSSQuery 사용법을 알아봤다. 지금은 단순히

클래스 속성을 이용한 쿼리 문법을 사용했지만 다음 절부터 좀 더

다양한 CSS 선택자에 대해 알아보도록 한다.

CSS 선택자 알아보기

Jindo의 CSS 선택자는 W3C의 CSS 선택자1를 기준으로 개발되

었다. 만약, 이 모든 선택자를 알아야 하는 것이 부담스럽다면 필수

선택자만 알아도 개발하는 데 많은 도움이 될 것이다.

선택자에 대해 알아보기 전에 간단한 용어를 설명하고 진행한다.

다음 코드를 보자.

<div id="parent">

<div id="child1">자식1</div>

<div id="child2">

자식2

<div id="child3">자손</div>

</div>

</div>

<div id="sibling1">형제1</div>

<div id="sibling2">형제2</div>

1 http://www.w3.org/TR/css3-selectors/

Page 24: The platform 2011

CSSQuery 스마트하게 사용하기The Platform 2011 23

다는 의미이다. 또한 다음 형식처럼 사용하면 중첩된 방식으로 조

건을 주어 요소를 찾을 수도 있다.

E F G

E F G H

다음 코드는 자손 선택자를 사용하여 test 아이디를 가진 요소의 자

손 중 태그가 <li>인 요소를 찾는 예제이다.

<ul id="test">

<li>1</li>

<li>2</li>

<li>3</li>

</ul>

...

jindo.$$("#test li");

자식 선택자

자식 선택자는 특정 요소의 자식 요소를 찾을 때 사용한다. 선택자

형식은 다음과 같다.

E > F

위 형식은 E 조건을 만족하는 요소의 자식 요소 중 F 조건을 만족

하는 자식 요소를 찾는다는 의미이다. 다음 코드는 자식 선택자를

사용하여 test를 부모 요소로 갖고 <li> 태그를 사용한 요소를 찾는

예제이다.

<ul id="test">

<li>1</li>

<li>2</li>

<ul id="test2">

<li>1</li>

<li>2</li>

</ul>

...

jindo.$$("#test >li");

<li>3</li>

</ul>

<li>3</li>

<div id="test"></div>

...

jindo.$$("div");

아이디 선택자

아이디 선택자는 특정 아이디를 가진 요소를 찾을 때 사용한다. 선

택자 형식은 다음과 같다.

#id

아래는 아이디 선택자를 사용해 test 아이디를 가진 요소를 찾는 예

제이다.

<div id="test"></div>

...

jindo.$$("#test");

클래스 선택자

클래스 선택자는 특정 클래스를 가진 요소를 찾을 때 사용한다. 선

택자 형식은 다음과 같다.

.className

다음 코드는 클래스 선택자를 사용해 name 클래스 속성을 가진 요

소를 찾는 예제이다.

<div id="test" class="name"></div>

...

jindo.$$(".name");

자손 선택자

자손 선택자는 특정 요소의 자손 요소를 찾을 때 사용한다. 선택자

형식은 다음과 같다.

E F

E와 F는 공백 문자(space)로 떨어져 있다. 위 형식은 E 조건을 만

족하는 요소의 자손 요소 중 F 조건을 만족하는 자손 요소를 찾는

Page 25: The platform 2011

NHN 서비스 들여다보기The Platform 201124

표준 CSSQuery를 지원하지 않는 브라우저(인터넷 익스플로러

6, 7)와 지원하는 브라우저(그 외)간에 약 10배 정도의 차이를 보

여준다. 만약 표준 CSSQuery를 지원한다 해도 개발할 때 표준

CSSQuery를 사용하지 않았다면 성능이 느릴 것이다. 그래서 표준

CSSQuery를 사용하도록 작성하는 것이 중요하다고 앞서 말했던

것이다.

표 2 성능 측정 결과 표

구분 Jindo 1.4.8 Sizzle 1.0 Sly 1.0rc2 YUI 3.2 NWMatcher 1.1.2

파이어폭스 3.6 81ms 90ms 85ms 78ms 75ms

사파리 5.0 75ms 53ms 64ms 47ms 60ms

오페라 10.10 95ms 92ms 169ms 86ms 94ms

크롬 6.0 79ms 75ms 87ms 78ms 85ms

인터넷 익스플로러 8 162ms 130ms 167ms 32ms 181ms

인터넷 익스플로러 7 598ms 1130ms 828ms 859ms 780ms

인터넷 익스플로러 6 440ms 551ms 491ms 382ms 531ms

그림 1 성능 측정 그래프

표와 그래프에서 알 수 있듯이 Jindo는 YUI와 같이 빠른 축에 속해

있다. 물론 Sly, NWMatcher와도 큰 차이가 없었다. Sizzle을 제외

하곤 대부분 비슷한 성능을 보였다.

표준 CSSQuery를 지원하지 않는 브라우저(인터넷 익스플로러

6, 7)와 지원하는 브라우저(그 외)간에 약 10배 정도의 차이를 보

고급 선택자

필수 선택자 외에도 알아두면 좋은 고급 선택자가 있다. 고급 선택

자2에 대한 내용은 Jindo API2 문서를 참고하기 바란다.

성능 비교3

CSSQuery를 지원하는 라이브러리 중 주로 사용되는 라이브러리

인 jQuery, prototype에서 제공하는 sizzle, 작은 용량의 sly, 야후

의 YUI, nwmatcher와 Jindo를 대상으로 다음과 같은 환경에서

속도와 지원 범위를 측정했다.

표 1 성능 비교 환경4

비교 대상

라이브러리 Jindo 1.4.8, Sizzle 1.0, Sly 1.0rc2, YUI3.2, NWMatcher 1.1.2

브라우저 인터넷 익스플로러 6~8, 파이어폭스 3.6, 사파리 5.0, 오페라 10.10, 크롬

6.0

OS 윈도우 7, Mac OS 10.6.4

선택자 51개3

도구 Taskspeed4

측정 페이지 W3C 선택자5

참고

dojo는 성능 측정 당시 오류가 많이 발생하여 비교 대상에서 제외했다.

성능 비교 결과5

성능은 환경에 따라 조금씩 다르기 때문에, 수치의 절대적인 차이

보다 상대적인 차이에 주목하자.

표와 그래프에서 알 수 있듯이 Jindo는 YUI와 같이 빠른 축에 속해

있다. 물론 Sly, NWMatcher와도 큰 차이가 없었다. Sizzle을 제외

하곤 대부분 비슷한 성능을 보였다.

2 http://jindo.dev.naver.com/docs/jindo/latest/ko/symbols/%24%24.html#$$

3 http://jindo.nhncorp.com/taskspeed

4 http://github.com/phiggins42/taskspeed

5 http://www.w3.org/TR/2001/CR-css3-selectors-20011113/

Page 26: The platform 2011

CSSQuery 스마트하게 사용하기The Platform 2011 25

표 4 인터넷 익스플로러 8의 선택자 지원 범위

선택자 Jindo 1.4.8 Sizzle 1.0 Sly 1.0rc2 YUI 3.2 NWMatcher 1.1.2

:selected O O O O X

:not(:selected) O O O O X

~ 8/8 4/8 8/8 4/8 0/8

:nth-last-child O X O X X

:empty O O O O X

coverage rate(%) 100%

(53/53)

90%

(48/53)

96%

(51/53)

90%

(48/53)

77%

(41/53)

다음 표는 인터넷 익스플로러 6~7 버전의 선택자 지원 범위이다.

인터넷 익스플로러 6~7 버전은 표준 CSSQuery가 아닌 직접 구현

한 CSSQuery(legacy CSSQuery)를 사용한다. 인터넷 익스플로러

6~7 버전은 사용자가 가장 많이 사용하는 브라우저로 현재 Jindo

만이 100% 지원하고 있다.

표 5 인터넷 익스플로러 6~7 버전의 선택자 지원 범위

선택자 Jindo 1.4.8 Sizzle 1.0 Sly 1.0rc2 YUI 3.2 NWMatcher 1.1.2

:selected O O X X O

:not(:selected) O O X X O

~ 25/25 20/25 13/25 23/25 1/25

:nth-last-child O X X X O

:empty O O X X O

coverage

rate(%)

100%

(53/53)

90%

(48/53)

69%

(37/53)

88%

(47/53)

98%

(52/53)

마치며

브라우저별로 자바스크립트 라이브러리의 성능 및 지원 범위에 대

해 알아보았다. 위에서 살펴본 것과 같이 자바스크립트 라이브러리

간에 속도는 큰 차이가 없었다. 반면 자바스크립트 라이브러리가

지원하는 선택자의 범위는 서로 크게는 30% 이상 차이 나는 것을

확인했다. 이 글을 통해 CSSQeury의 사용법을 익혀 개발하는 데

도움이 되길 바란다.

여준다. 만약 표준 CSSQuery를 지원한다 해도 개발할 때 표준

CSSQuery를 사용하지 않았다면 성능이 느릴 것이다. 그래서 표준

CSSQuery를 사용하도록 작성하는 것이 중요하다고 앞서 말했던

것이다.

지원 범위

현재 모든 라이브러리는 CSS 1~3 버전에 맞게 개발되어 있다. 하

지만 브라우저마다 상이한 결과를 보여줄 때가 있다. 다음 표는 몇

몇 표준 CSSQuery에 대한 브라우저의 지원 여부를 나타낸다. 표

에 제시된 바와 같이 Jindo와 Sizzle은 나열한 선택자를 100% 지원

하는 반면 그 외의 라이브러리는 1~3개 정도의 선택자를 지원하지

못하고 있다. 라이브러리가 표준 CSSQuery를 지원한다고 해도 사

실 모든 선택자를 지원하는 것은 아니다. 라이브러리가 모든 선택

자를 지원하게 하려면 해당 선택자를 직접 구현해야 하한다. 비교

한 라이브러리 중에서는 Jindo와 Sizzle이 더 많은 선택자를 지원

한다.

표 3 표준 CSSQuery 지원 범위

선택자 Jindo 1.4.8 Sizzle 1.0 Sly 1.0rc2 YUI 3.2 NWMatcher 1.1.2

:selected O O X X O

:not(:selected) O O X X O

div[class!=made_up] O O O O X

p:contains(selector) O O O X O

coverage rate(%) 100%

(53/53)

100%

(53/53)

96%

(51/53)

94%

(50/53)

98%

(52/53)

다음 표는 인터넷 익스플로러 8 브라우저의 선택자 지원 범위를 나

타낸다. Jindo와 Sly만이 100% 지원하고 있다. 인터넷 익스플로러

8은 다른 브라우저와 같이 표준 CSSQuery를 지원하지만 지원 범

위가 좁다. 그래서 다음 표와 같이 표준 CSSQuery를 지원하는 브

라우저와 다른 결과를 보여준다.

Page 27: The platform 2011

NHN 서비스 들여다보기The Platform 201126

있는 생각이 '과연 얼마나 지속되고 많이 사용될까?'일 것이다. 파

이썬이 프로그래밍 언어로서 완성도를 보인 때는 파이썬 2.0 버전

이 나타난 지난 2000년이었다. 그 이후 2008년에 더 발전한 파이썬

3.0 버전이 출시되었다. 하지만 파이썬 3.0은 파이썬 2.x 버전과 호

환되지 않는다. 파이썬 3.x 버전이 가진 주요 기능을 파이썬 2.6과

2.7 버전에 하위 적용했기 때문에 파이썬 2.7 버전 사용을 권한다.

현재 파이썬 2.x 버전은 2.7.1 버전까지 3.x 버전은 3.2 버전까지 출

시되었다. 2011년 TIOBE 사가 발표한 통계 자료에 따르면 파이썬

은 전 세계에서 6번째로 많이 사용되는 언어이다(1위는 자바, 2위

는 C).

참고

Cent OS 5.3 버전에는 파이썬 2.4 버전이 설치되어 있다. 파이썬 2.7

버전 RPM을 내려 받아 설치하면 그 이후 yum 명령이 실행되지 않는다.

파이썬 2.7.x 버전 용으로는 yum 패키지가 설치되어 있지 않아서인데 이

경우 파이썬을 설치할 때 prefix로 경로를 지정하고 해당 경로의 파이썬

실행 파일을 PATH로 설정된 실행 경로의 제일 앞으로 가도록 추가해주면

된다.

기존 파이썬은 /usr/bin/python으로 그대로 두고 자동 실행 스크립트를

작성할 때 !/usr/bin/env python으로 문제없이 새로운 버전으로 실행되

면서 yum을 그대로 사용할 수 있다.

파이썬은 여러 프로그래밍 언어의 패러다임을 빠르게 흡수하고 있

고 파이썬을 위한 훌륭한 개발 환경이 준비되어 있어 소프트웨어

업계에서 점점 더 사용자가 증가하는 추세이다.

필자가 이전에 주로 사용하던 C/C++였다. C/C++은 높은 성능을

내기에 좋지만 개발 속도를 내기에는 적합하지 않은 언어이다. 얼

마 전부터 메일서비스개발2팀은 개발 속도를 향상시키고 유지 보

수를 쉽게 하기 위해 파이썬을 사용해 보기로 했다. 국내에도 많은

파이썬 사용자가 있고 파이썬에 대한 도서, 웹 자료를 쉽게 얻을 수

있어 파이썬 자체에 대한 정보를 얻는 것은 쉽지만 ‘사내 프로세스

와 품질 기준을 어떻게 만족시킬 것인가?’라는 측면에서 접근한 자

료는 쉽게 얻기 어려울 것이다. 이런 이유로 파이썬을 사용하면서

얻은 경험을 공유하려 한다.

파이썬에 대해

수많은 언어가 나타나고 사라진다. 저마다 장점을 내세우고 곧 대

세가 될 것처럼 홍보하지만 실제 프로그래밍 환경이 쉽게 변하지

는 않는다. 그래서 프로그래밍 언어를 처음 접하게 되었을 때 들 수

메일 서비스 파이썬 적용 경험프로젝트보다는 아내와 노는 것이 더 좋은 개발자입니다.

이것저것 관심 분야가 너무 많지만 한 우물을 파려고 노력 중입니다.

한 우물을 파도록 도와주세요.

•메일서버개발팀 _ 강대명

Page 28: The platform 2011

메일 서비스 파이썬 적용 경험The Platform 2011 27

그림 1 C++와 파이썬 3 성능 비교

그림에서 알 수 있듯이 C++는 파이썬보다 통상 수십 배 정도 더 빠

르고 메모리도 적게 사용하지만 작성해야 하는 코드 분량은 파이썬

이 C++의 수분의 일이다.

이번에는 자바와 비교해 보자.2

그림 2 자바와 파이썬 3 성능 비교

역시 자바가 파이썬보다 수십 배 빠르지만 파이썬이 메모리를 덜

사용하고 작성해야 하는 코드의 양도 적다는 것을 확인할 수 있다.

2 http://shootout.alioth.debian.org/u32/benchmark.php?test=all&lang=java&lang2

=python3

생산성 측면에서의 파이썬

파이썬은 생산성이 매우 높은 언어이다. 파이썬의 생산성을 알아

보기 위해서 시범 프로젝트를 진행했는데 다음과 같은 결과가 나

왔다.

표 1 C/C++ 버전과 파이썬 버전은 생산성 비교

항목 C/C++ 버전 파이썬 버전

코드 라인 수 3130 1114

유닛 테스트 수 0 73

브랜치 커버리지 0 70.98%

유닛 테스트(Unit Test)와 브랜치 커버리지(Branch Coverage)는

C/C++ 버전의 프로젝트에 존재하지 않기 때문에 단순 비교할 수

없지만 파이썬은 프로젝트의 코드 라인 수(Line Of Code)를 줄이

고 유닛 테스트를 쉽게 지원하는 장점이 있었다. 파이썬 언어 자체

에서 기본적으로 문자열(String), 리스트(List), 튜플(Tuple), 사전

(Dictionary) 등을 지원하므로 C/C++보다 쉽게 생각을 코드로 표

현할 수 있었다. 또한 언어에 쉽게 적응할 수 있어 짧은 기간 안에

생산성을 높일 수 있었다.

메일서버개발2팀에서는 파이썬을 전혀 모르던 사람이 1~2일만에

테스트나 통계 프로그램을 모두 파이썬으로 작성했는데 이는 파이

썬의 강력함을 느끼게 해주는 사례라 할 수 있다.

성능 측면에서의 파이썬

파이썬의 가장 큰 문제는 바로 성능이다. 이 부분은 진행하는 프로

젝트에 따라 상당히 다른 결과가 도출될 수 있긴 하지만 일반적으

로 파이썬은 C/C++보다 느리다. 또한 과부하 상황에서 CPU를 C/

C++보다 많이 사용한다. 다음 그림은 g++를 컴파일러로 사용하는

C++와 파이썬 3의 성능을 비교한 자료이다.1

1 http://shootout.alioth.debian.org/u32/benchmark.php?test=all&lang=gpp&lang2

=python3

C++ GNU g++ : python 3

C+

+ u

sed

25 Apr 2011 u32 n

Time Memory Code

Time Memory Code

1000x

300x

100x

30x

10x

3x

1/3

1/10

1/30

1/100

1/300

1/1000

Java 6 -Server : python 3

Java

use

d

25 Apr 2011 u32 n

Time Memory Code

Time Memory Code

1000x

300x

100x

30x

10x

3x

1/3

1/10

1/30

1/100

1/300

1/1000

Page 29: The platform 2011

NHN 서비스 들여다보기The Platform 201128

def testgo():

for i in range(100000):

send_msg = AcpSendMessage()

send_msg.add( 'A', 'A','A','A','A','A).nl()

send_msg.add( 'A', 'A','A','A','A','A).nl()

recvmsg = AcpRecvMessage()

if __name__=='__main__':

cProfile.run('testgo()')

cProfile을 실행하는 것은 이걸로 끝이다. 나머지는 cProfile이 알

아서 다 측정한다. 그럼 이제 실행 방법보다 더 중요한 결과를 해석

하는 방법을 알아보자.

위 코드를 실행하면 다음과 같은 결과가 나온다.

6600004 function calls in 11.380 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)

1 0.000 0.000 11.380 11.380 <string>:1(<module>)

1 1.327 1.327 11.380 11.380 accountworker_profile.py:218(testgo)

100000 0.506 0.000 1.300 0.000 acpmsg.py:103(_deserialize_list)

100000 0.260 0.000 0.260 0.000 acpmsg.py:11(__init__)

200000 0.210 0.000 0.251 0.000 acpmsg.py:19(nl)

200000 1.751 0.000 4.431 0.000 acpmsg.py:29(add)

800000 1.616 0.000 2.063 0.000 acpmsg.py:44(add_record)

100000 0.504 0.000 0.504 0.000 acpmsg.py:54(_serialize_header)

100000 0.270 0.000 0.356 0.000 acpmsg.py:61(_messageend)

100000 0.711 0.000 0.925 0.000 acpmsg.py:67(_serialize_body)

100000 0.345 0.000 2.130 0.000 acpmsg.py:78(serialize)

100000 0.993 0.000 2.976 0.000 acpmsg.py:85(__init__)

300000 0.578 0.000 0.984 0.000 acpmsg.py:99(_deserialize_string)

1900000 1.050 0.000 1.050 0.000 {isinstance}

200000 0.058 0.000 0.058 0.000 {len}

1200000 0.331 0.000 0.331 0.000 {method 'append' of 'list' objects}

1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

100000 0.084 0.000 0.084 0.000 {method 'endswith' of 'str' objects}

500000 0.305 0.000 0.305 0.000 {method 'rstrip' of 'str' objects}

500000 0.475 0.000 0.475 0.000 {method 'split' of 'str' objects}

1 0.005 0.005 0.005 0.005 {range}

첫 문장은 총 6600004번의 함수 호출이 있었고 총 11.380초가 걸

렸다는 의미이다. 즉, 전체 수행 시간을 알려준다.

6600004 function calls in 11.380 seconds

세 번째 줄을 보면 ncalls, tottime, percall, cumtime, percall 등의

열이 있는데 각각의 의미는 다음과 같다.

파이썬의 단점

파이썬은 GIL(Global Interpreter Lock)이라는 치명적인 단점이

있다. 사실 파이썬의 단점이라고 적었지만 이 부분은 cPython의

단점이다. 파이썬은 파이썬 구현 언어에 따라 자바로 만든 Jython,

.NET 기반의 IronPython 등이 있는데 이 중에서 cPython은 C로

구현된 파이썬 구현체이다.

GIL 문제는 현재 cPython의 인터프리터에서 동시에 오직 하나의

스레드만 동작하도록 허용함으로써 생기는 문제이다. 즉 다중 스레

드(Multi-Thread)의 성능이 떨어진다. 그래서 cPython에서는 다

중 스레드보다 다중 프로세스(Multi-Process)를 사용하길 권장하

며 또한 다중 프로세싱(MultiProcessing)이라는 패키지를 제공해

스레드를 이용하는 것과 비슷하게 구현할 수 있도록 지원하고 있

다. 하지만 다중 프로세싱 모델이기 때문에 다중 스레드보다 복잡

한 문제가 발생한다.

파이썬 튜닝하기

cProfile

파이썬 성능을 튜닝하려면 먼저 프로파일러(Profiler)를 이용해야

만 한다. 프로파일러는 해당 모듈이나 메서드 등의 속도를 측정하

고 병목 현상이 발생하는 지점을 알려준다. 실제로 프로파일러를

실행했을 때 성능에 병목을 일으키는 부분이 예측한 것과 다른 경

우가 상당히 많다. 따라서 ‘여기가 병목 지점일 것 같아’라는 추측

이 아니라 프로파일러를 이용해 성능 측정을 한 다음 병목 부분을

찾아내려는 자세가 필요하다.

프로파일러 중 cProfile는 파이썬이 기본적으로 제공하는 프로파

일러로서 각 메서드의 사용 시간, 하위 모듈의 수행 시간을 뺀 순수

수행 시간 등을 보여주는 좋은 기능을 제공한다.

그럼 먼저 cProfile을 사용하는 방법을 알아보자. 먼저 cProfile을

사용하려면 다음과 같이 cProfile을 포함(import)해야 한다.

import cProfile

testgo라는 main 함수가 있을 때 다음과 같이 작성하면 된다.

Page 30: The platform 2011

메일 서비스 파이썬 적용 경험The Platform 2011 29

#!/usr/bin/env python

from distutils.core import setup, Extension

setup(name="pure_test",

version="1.0",

description="string compare module",

author="charsyam",

author_email="[email protected]",

url="http://www.naver.com",

ext_modules=[Extension("pure_test",

["src/pure_test.cpp"])]

그리고 디렉터리 구조는 다음과 같이 구성한다.

� Bindingtest/

setup.py

� src

pure_test.cpp

� build

이제 다음과 같은 코드를 작성하면 된다.

#include<Python.h>

#include<string.h>

#include<stdio.h>

#include<stdlib.h>

#include<locale.h>

static PyObject *ErrorObject;

static PyObject* pure_test(PyObject *self , PyObject *args)

{

char *str1;

char *str2;

int ratio = 0;

if(!PyArg_ParseTuple(args,"ss",&str1,&str2))

{

return NULL;

}

setlocale(LC_CTYPE, "ko_KR.euckr");

ratio = strcmp( str1, str2 );

return Py_BuildValue("i",ratio);

}

표 2 cProfile 항목 설명

항목 내용

ncalls 호출된 횟수

tottime 해당 함수에서 호출된 서브 함수의 수행 시간을 뺀 나머지 시

간, 즉 실제로 함수 부분만 수행한 시간

percall 서브 함수를 포함하지 않은 함수 1회 수행 시간

(tottime / ncalls)

cumtime 서브 함수를 포함한 함수 수행 시간

percall 서브 함수를 포함한 함수의1회 수행 시간(cumtime/ncalls)

filename:lineno(function) 해당 함수의 이름과 파일에서의 위치

위 표의 설명을 보면 알 수 있듯이 tottime이 긴 것을 우선적으로

확인해 보면 된다. 정말 이렇게 시간이 오래 걸리는지 불필요한

로직은 아닌지 다른 것으로 대체할 수는 없는지 등을 확인해서 수

정한 후 다시 cProfile을 실행하면 중요한 부분만 쉽게 튜닝할 수

있다.

파이썬 C Binding

파이썬은 속도를 향상시키기 위해 C Binding이라는 방법을 제공

한다. 이것은 자바의 JNI(Java Native Interface)처럼 주요 모듈을

C/C++ 코드로 작성하고 파이썬에서 이를 호출해서 사용하는 방

법이다. 이런 방법은 몇 가지가 있는데 여기서는 Pure C Binding

과 Boost-Python을 이용한 Binding을 소개한다. 다만 파이썬 C

Binding을 잘못 쓰면 파이썬 가상 머신(Virtual Machine)이 멈추

는 재미있는 경험도 해볼 수 있다.

Pure C Binding

파이썬 Binding을 사용하려면 위해서는 setup.py 파일을 만들어

야 한다. 파이썬 패키지를 설치해 봤다면 그런 파일이 있다는 것을

알 것이다. setup.py 파일은 단순한 파일로 파이썬 패키지를 빌드

하고 설치하기 위한 정보를 가지고 있다. 먼저 다음과 같이 setup.

py 파일을 만든다.

Page 31: The platform 2011

NHN 서비스 들여다보기The Platform 201130

Boost-Python Binding

Boost-Python은 앞서 설명한 Pure C Binding을 Boost에서 쉽

게 사용하기 위해서 감싼(wrapping) 것이다. Pure C Binding보다

Boost-Python Binding이 사용하기 더 쉽다. 먼저 setup.py를 만

든다.

from distutils.core import setup, Extension

module1 = Extension('boost_test',

include_dirs = ['/nvmail/external/boost/include'],

libraries = ['boost_python'],

library_dirs = ['/nvmail/external/boost/lib'],

sources = ['src/boost_test.cpp'])

setup ( name = 'boost_test',

version = '1.0',

description = 'This is a demo package',

ext_modules = [module1])

다음 코드는 Boost-Python 버전으로 작성한 예제 코드이다. 비교

해 보면 Pure C Binding 버전보다 더 직관적이고 이해하기 쉽다.

#include <boost/python.hpp>

#include <boost/python/list.hpp>

#include <string>

using namespace boost::python;

int get_int( tuple list1, list listoflist ){

return boost::python::len(list1);

}

const char *get_string( list list1, list listoflist ){

list mylist = boost::python::extract<list>(listofli

st[1]);

std::string k = boost::python::extract<std::string>(myli

st[0]);

return k.c_str();

}

BOOST_PYTHON_MODULE(boost_test)

{

def("get_string", &get_string );

def("get_int", &get_int );

}

static struct PyMethodDef get_methods[]={

{"pure_test", pure_test, METH_VARARGS},{NULL,NULL}

};

PyMODINIT_FUNC

initpure_test()

{

PyObject *m;

m = Py_InitModule("pure_test",get_methods);

ErrorObject = Py_BuildValue("s","get error");

}

위 코드를 실행하면 PyArg_ParseTuple3라는 함수를 통해 넘어오

는 파라미터를 파싱한다. 그리고 Py_BuildValue라는 함수를 통해

PyObject 타입으로 결과를 반환한다. 아래 두 메서드는 해당 모듈

이 pure_test라는 메서드를 가지고 있는 것을 알려준다.

static struct PyMethodDef get_methods[ ]={

{"pure_test", pure_test, METH_VARARGS},{NULL,NULL}

};

PyMODINIT_FUNC

initpure_test()

{

PyObject *m;

m = Py_InitModule("pure_test",get_methods);

ErrorObject = Py_BuildValue("s","get error");

}

이제 빌드하면 build 디렉터리에 pure_test.so라는 모듈 파일이 생

성된다.

python setup.py build

실제로 파이썬 모듈을 동적 라이브러리(Dynamic Library) 형태로

로드해서 사용하는 것을 알 수 있다. 그리고 사용하기 전에 해당 모

듈을 설치해야 한다.

python setup.py install

3 http://www.fnal.gov/docs/products/python/v1_5_2/ext/parseTuple.html

Page 32: The platform 2011

메일 서비스 파이썬 적용 경험The Platform 2011 31

AssertionError: False is not True

--------------------------------------------------------------------Ran 1 test in 0.000s

FAILED (failures=1)

nose

단순히 unittest만 쓰다 보면 더 쉽게 사용하고 싶어질 것이다. 테스

트마다 전부 Unit test.main()이라는 코드를 작성해야 하고 일일이

테스트를 실행해야 한다면 테스트가 늘어날수록 관리 이슈가 커지

게 된다. 이럴 때 nose라는 도구를 이용할 수 있다. 불행히도 nose4

는 Python이 기본으로 제공하지 않기 때문에 추가로 설치해야 한

다. nose는 test로 시작하는 파이썬 파일을 찾아서 자동으로 테스트

를 실행하고 이에 대한 결과를 보여준다. 실행할 때 nosetests라는

명령을 사용한다.

pylint

현재 자바나 C/C++ 프로젝트의 경우 Hudson에서 Klockwork를

이용해 정적 분석을 하는 것으로 알고 있다. 파이썬에도 pylint라는

훌륭한 정적 분석 도구가 있다. pylint는 코딩컨벤션부터 문법 오류

까지 미리 확인해준다.

파이썬과 같은 스크립트 언어는 실행 시점까지 문법 오류 등을 알

수 없기 때문에 유닛 테스트의 범위가 100%가 아닌 이상 유닛 테

스트로 잡아낼 수 없는 오류가 있을 수 있다. 하지만 pylint를 이용

하면 미리 문제가 될 부분을 확인할 수 있기 때문에 좀 더 안정적인

코드를 만들 수 있다.

pylint를 설치한 후 다음과 같이 실행하면 결과를 확인할 수 있다.

pylint 파일명

그럼 실제로 간단한 샘플을 이용해서 pylint를 실행해보자.

Sample: test.py

CONFIG_OPTION_MARK="-f"

COMMAND_OPTION_MARK="-c"

4 http://somethingaboutorange.com/mrl/projects/nose/1.0.0/

파이썬과 QP 활동

unittest

파이썬에도 자바의 JUnit이나 C++의 Google Test와 같은 기능을

제공하는 모듈이 있다. 파이썬에 기본적으로 unittest라는 클래스

가 있으며 이를 이용하면 유닛 테스트를 진행할 수 있다. C/C++로

개발할 때보다 파이썬으로 개발할 때 가장 좋았던 점은 unittest가

컴파일되지 않는 것까지 바로 잡아주는 것이었다.

파이썬에서 유닛 테스트를 하려면 unittest를 포함해야 한다.

import unittest

그리고 다음과 같이 Test.TestCase를 상속받는 Test 클래스를 만들

면 된다.

import unittest

class Test_Unit Test(unittest.TestCase):

def test_true(self):

self.assertTrue(True)

if __name__ == '__main__':

Unit Test.main()

성공과 실패 여부에 따라 각각 다음과 같은 결과를 볼 수 있다.

� 성공 결과

.--------------------------------------------------------------------Ran 1 test in 0.000s

OK

� 실패 결과

F====================================================================FAIL: test_true (__main__.Test_Unit Test)--------------------------------------------------------------------Traceback (most recent call last): File "test.py", line 5, in test_true self.assertTrue(False)

Page 33: The platform 2011

NHN 서비스 들여다보기The Platform 201132

CONFIG_OPTION_MARK="-f"

^

C: 2: Operator not preceded by a space

COMMAND_OPTION_MARK="-c"

C: 4:SimpleAccountOptionParser: Missing docstring

C: 9:SimpleAccountOptionParser.parse: Missing docstring

C: 13:SimpleAccountOptionParser.parse: Invalid name

"commandParsedCount" (should match [a-z_][a-z0-9_]{2,30}$)

C: 14:SimpleAccountOptionParser.parse: Invalid name

"configParsedCount" (should match [a-z_][a-z0-9_]{2,30}$)

C: 21:SimpleAccountOptionParser.parse: Invalid name

"commandParsedCount" (should match [a-z_][a-z0-9_]{2,30}$)

W: 13:SimpleAccountOptionParser.parse: Unused variable

'commandParsedCount'

W: 14:SimpleAccountOptionParser.parse: Unused variable

'configParsedCount'

R: 4:SimpleAccountOptionParser: Too few public methods

(1/2)

coverage

pylint를 사용하여 정적 분석을 했으면 이제 유닛 테스트를 할 때

코드 커버리지(Code Coverage)를 측정해야 “아, 이제 뭔가 좀 했

구나!”라고 할 수 있다. coverage5라는 것을 이용하면 nose와 함께

프로젝트의 코드 커버리지를 쉽게 측정할 수 있다.

코드 커버리지를 측정하는 방법은 굉장히 간단하다. nosetests 명

령으로 유닛 테스트를 진행할 수 있는 환경을 만들어 두었다면

coverage 설치 후에 다음과 같이 명령을 실행하면 코드 커버리지

를 측정할 수 있다.

nosetests --with-coverage --cover-package=[테스트할 패

키지 이름] --cover-tests --with-xunit

특정 패키지만 코드 커버리지를 측정하고 싶을 때 –cover-

package 옵션을 사용한다. 아래 예제 결과를 보면 꽤 높은 코드 커

버리지가 나오는 것을 확인할 수 있다. Missing은 현재 측정되지

않는 범위를 코드 줄 번호로 보여준다.

5 http://pypi.python.org/pypi/coverage

class SimpleAccountOptionParser:

def __init__(self, args, arg_len):

self.args = args

self.arg_len = arg_len

def parse(self):

if self.arg_len != 5:

raise Exception("Option Parse Error: Not Match

Parameter Length")

commandParsedCount = 0

configParsedCount = 0

command = ""

config_path = ""

if self.args[1] == COMMAND_OPTION_MARK:

command = self.args[2]

commandParsedCount += 1

return command, config_path

결과를 보면 convention, refactor, warning, error와 같이 4가지

로 나뉘는데 각 타입과 발생한 줄 번호 그리고 어떤 오류가 발생했

는지 확인할 수 있다. 표시된 결과를 보고 오류를 수정하면 된다.

pylint test.py

No config file found, using default configuration

************* Module test

W: 5: Found indentation with tabs instead of spaces

W: 6: Found indentation with tabs instead of spaces

W: 7: Found indentation with tabs instead of spaces

W: 9: Found indentation with tabs instead of spaces

W: 10: Found indentation with tabs instead of spaces

W: 11: Found indentation with tabs instead of spaces

W: 13: Found indentation with tabs instead of spaces

W: 14: Found indentation with tabs instead of spaces

W: 16: Found indentation with tabs instead of spaces

W: 17: Found indentation with tabs instead of spaces

W: 19: Found indentation with tabs instead of spaces

W: 20: Found indentation with tabs instead of spaces

W: 21: Found indentation with tabs instead of spaces

W: 23: Found indentation with tabs instead of spaces

C: 1: Missing docstring

C: 1: Operator not preceded by a space

Page 34: The platform 2011

메일 서비스 파이썬 적용 경험The Platform 2011 33

Hudson과 연동

위에서 소개한 도구가 Hudson과 연동되기 때문에 위 도구를 사용

한 결과를 Hudson에서 확인할 수 있다. 다음 그림은 파이썬 프로

젝트를 CI(Continuous Integration) 서버에 연동해 코드 커버리

지를 볼 때의 화면(그림 3)이다.

프로토타입과 도구 개발을 위한 파이썬

파이썬을 접하면서부터 파이썬의 강력함을 느끼게 되지만 시간이

어느 정도 지나면 그 성능에 조금 좌절하기도 한다. 그렇지만 파이

썬을 익히면서 "아 정말 좋구나"라고 느끼게 된 부분이 많았다. 그

일례로 다른 팀에 API를 제공해야 할 일이 생겨 파이썬을 이용했

고 Rest 형식의 API를 제공하는데 2시간 정도 밖에 걸리지 않았다.

그 중 1시간 반 정도는 웹 인터페이스를 제공하는 라이브러리 사용

법을 익히는 시간이었고 실제 API를 개발하고 디버깅하는 데 30분

정도밖에 걸리지 않았다. 해당 API는 하루에 약 1만 번 정도 호출

되는 것이라 성능 이슈가 거의 없었다. 즉, 파이썬은 성능이 큰 이

슈가 되지 않는 프로젝트나 프로토타입을 개발하는 데 파이썬의 최

대 장점인 '생산성'이 극대화된다고 할 수 있다.

마치며

파이썬을 소개하는 가장 좋은 단어는 "생산성"이라고 생각한다. 그

리고 필자 역시 그 단어에 파이썬이라는 언어가 꼭 들어맞는다고

생각한다. 처음 파이썬을 쓴 다른 팀원이 쉽게 테스트나 통계 도구

를 만들어내는 걸 보면 앞서 말한 성능 이슈가 없는 프로젝트 또는

프로토타입을 만드는 데에 정말 좋은 언어인 것 같다. 닭 잡는 데

소 잡는 칼을 쓰지 않듯이 이 글을 통해 파이썬을 용도에 맞게 사용

하는 계기가 되면 좋겠다.

Name Stmts Miss Cover Missing

------------------------------------------------------------

pyaccount 0 0 100%

pyaccount.accountconfiginfo 45 1 98% 56

pyaccount.accountconstants 54 0 100%

pyaccount.accountworker 339 64 81% 28, 33, 48,

55, 87, 89, 94, 118, 131-132, 164, 183-185, 203, 220, 237-

238, 281-289, 296, 326-331, 345-346, 361, 373, 383-384, 415,

419, 423-429, 446-449, 452-468

pyaccount.dbgateway 290 167 42% 9-17, 20,

23, 26-27, 30, 33-34, 37-38, 41-50, 53-62, 65-74, 114-146,

149-157, 160-163, 166-169, 172-181, 184-193, 196-201, 218-

226, 229-234, 241-248, 252-253, 267-270, 273-277, 292-302,

305-314, 339-356

pyaccount.error 8 0 100%

pyaccount.simpleoptionparser 33 0 100%

pyaccount.userinfo 249 60 76% 30-31, 37,

55-56, 90-93, 145-154, 159-160, 169-181, 193-195, 200, 204,

212, 216, 225, 229, 237-238, 243-244, 253, 262, 269-270,

297-312, 329, 336, 343

------------------------------------------------------------

TOTAL 1018 292 71%

------------------------------------------------------------

그림 3 파이썬 프로젝트에 CI 서버 연동

Page 35: The platform 2011

NHN 서비스 들여다보기The Platform 201134

그림 1 사이트케어 관련 기사1

소규모 업체의 웹 사이트는 보안에 취약한 경우가 많다. 보안 관련

전문 지식과 인력이 없거나 운영할 여력이 없는 것이 주요 이유이

다. 중소 규모의 쇼핑몰이나 학교 홈페이지는 물론 유명 언론사 홈

1 http://ahnlabgirl.cafe24.com/ahnlab/911?category=2&TSSESSIONblogahnlabco

m=46bd630b7fe419393bc6aeb26c17fba4

네이버를 통해 노출되는 외부 사이트가 악성코드에 감염되어 있다

면 네이버 사용자가 피해를 입을 수 있다. AMIGO는 악성코드를

유포하는 사이트를 탐지하여 네이버 사용자와 서비스를 보호하는

시스템이다.

웹 보안 관제 서비스

지난 2010년 8월 안철수 연구소는 '사이트케어'라는 이름으로 웹

보안 관제 서비스를 출시했다. 사이트케어는 기업/기관 웹 사이트

해킹 및 악성코드 유포, 주민번호 같은 개인정보 노출을 실시간으

로 모니터링하여 유사시 빠르게 조치할 수 있도록 경고하는 서비

스이다.

AMIGO - 행위 기반 악성코드 탐지NHN 네이버 서비스의 보안을 담당하고 있습니다.

- 웹 서비스 취약점 진단

- 침해 사고 대응 업무

- 제휴 외부 사이트에 대한 악성 코드 대응 업무

•포털서비스보안팀 _ 김정웅

Page 36: The platform 2011

AMIGO - 행위 기반 악성코드 탐지The Platform 2011 35

구글이 안전 브라우징 정보를 제공하는 까닭은 사용자를 보호하려

는 목적도 있지만, 구글 서비스의 안정성을 확보하려는 목적도 있

다. 구글 서비스를 공격하는 악성코드가 불특정 다수 사용자의 컴

퓨터에 배포될 수 있기 때문이다.

악성코드가 유포되는 과정

웹 사이트에서 어떤 파일도 다운로드하지 않았고, 백신 프로그램도

설치했기 때문에 악성코드가 자신의 컴퓨터에 설치되는 일은 없을

것이라고 생각할 수도 있다. 과연 안전할까? 악성코드가 구체적으

로 무엇이고 악성코드가 어떻게 유포되는지 알게 된다면, 쉽게 안

심할 수 없을 것이다.

악성코드란 '사용자의 의사와 이익에 반해 시스템을 파괴하거나 정

보를 유출하는 등 악의적 활동을 수행하도록 의도적으로 제작된 소

프트웨어'를 말한다.

그림 4 악성코드 종류

대표적인 악성코드 유포 경로는 인터넷에서 쉽게 구할 수 있는 각

종 무료 소프트웨어이다. 특히 파일 공유 프로그램을 통해 악성코

드가 많이 유포되는데, 유포 수법은 해당 프로그램을 설치할 때 악

성코드가 같이 설치되도록 하는 것이다. 그래도 다행인 것은 장기

간 인터넷에 노출된 소프트웨어인 경우 백신 프로그램에서 이를 탐

지하고 있을 수도 있다는 점이다. 그러나 웹 사이트에 삽입된 공격

코드에 의해서도 악성코드에 노출될 수 있으니 주의해야 한다.

페이지에서도 자주 보안 문제가 발생한다. 시스템 전문 관리 인력

이 없기 때문에 실시간 모니터링이 이루어지지 못하고 자사의 웹

사이트가 해킹됐다는 사실조차 모르는 경우도 상당하다.

이런 보안 문제는 웹 사이트를 소유한 회사만의 문제가 아니다. 이

렇게 보안이 취약한 웹 사이트를 매개로 최종 사용자의 컴퓨터에

악성코드가 배포되기 때문이다. 이런 이유로 사이트케어와 같이 해

킹당한 웹 사이트를 모니터링하여 위험을 탐지하는 보안 서비스가

필요하다.

파이어폭스나 크롬을 이용하여 웹 사이트를 방문하는 중에 아래와

같은 에러 페이지를 본 경험이 있을 것이다.

그림 2 브라우저의 사이트 차단

악성코드를 유포하는 사이트에 사용자가 방문하면 사용자 보호 차

원에서 접근을 일시적으로 차단하는 기능으로 구글의 '안전 브라우

징' 정보를 이용한다.

그림 3 구글의 위험사이트 차단

Page 37: The platform 2011

NHN 서비스 들여다보기The Platform 201136

이러한 공격코드는 대부분 운영체제나 액티브 X 컨트롤, 웹 플러

그인(플래시 등) 모듈의 취약점을 이용하며 '보안패치가 발표되

지 않은 신규 취약점을 이용하는 제로데이 공격코드(Zero Day

Exploit)'1로 쉽게 변경 가능하여 심각한 문제를 유발한다.

백신을 설치해도 소용 없다면......

컴퓨터 백신 프로그램은 사용자가 악성코드를 방어하기 위한 1차

적 수단이며 필수적으로 설치해야 할 프로그램이다. 백신의 패턴은

개인 컴퓨터를 부팅할 때마다 업데이트를 진행하고, 주기적인 점검

을 통해 악성코드 감염여부를 확인하는 것이 좋다. 대부분의 백신

에서 제공하는 실시간 탐지 기능은 반드시 활성화해야 한다.

또한 운영체제 및 애플리케이션은 반드시 업데이트해야 공격코드

에서 자신의 컴퓨터는 물론이고 중요한 개인 정보를 보호할 수 있

다. 또한 사용자가 웹 서핑을 할 때 검증되지 않는 소프트웨어를 다

운로드하지 않도록 주의해야 한다.

네이버에서는 어떻게?

보안 소프트웨어는 사용자 설치형과 웹 서비스형 두 가지로 분류할

수 있다. 많이 알려진 ‘백신’, ‘알약’, ‘사이트가드’ 등이 설치형 보안

프로그램에 속하고, 앞서 설명한 구글의 안전 브라우징이 웹 서비

스 형태이다.

네이버 또한 사용자 보호를 위해 다양한 보안 지킴이 활동을 하고

있다. 안전한 서비스를 제공하기 위해 네이버 서비스 출시 전 철저

한 ‘보안 검수/진단’ 작업을 수행하고 사용자 컴퓨터의 안전을 위해

무료로 네이버 백신을 제공하고 있으며, 악성코드를 유포하는 웹

사이트를 탐지하는 AMIGO라는 시스템을 운영한다.

AMIGO 시스템은 네이버 서비스와 연계하여 공격코드가 숨어 있

는 사이트를 사전에 탐지하고 해당 사이트가 검색 결과에 노출되지

않도록 한다. 그럼 AMIGO가 무엇인지 좀 더 자세히 알아보도록

하자.

1 제로 데이(Zero Day)는 보안 문제가 발견되었을 때부터 문제를 해결하는 보안 패치가 나오기까지의

기간 중 보안 문제가 발생한 직후라는 뜻이다.

그림 5 웹 사이트를 통한 악성코드 감염

사용자가 즐겨 찾는 웹사이트를 방문한다. 만약 사용자가 방문한

사이트가 보안에 취약하여 공격자의 악성코드가 숨어 있다면 해당

사이트의 웹 페이지를 방문한 것만으로도 공격코드가 실행되어, 악

성 프로그램이 사용자의 컴퓨터에 설치된다.

이러한 공격코드를 Exploit이라고 한다. Exploit는 자바스크립트

로 작성된 경우가 많고 보통 코드를 난독화(Code Obfuscation)하

여 읽기 어렵게 만들어 놓는다.

심지어 사용자가 해당 페이지에 방문할 때마다 코드가 동적으로 변

경되기도 한다.

그림 6 악성코드 난독화(Obfuscation)

이런 형태의 공격코드는 컴퓨터의 백신의 코드를 탐지하기 위한 패

턴화 작업을 어렵게 만들며 특히, 동적으로 자동 변경되는 코드는

백신이 거의 탐지하지 못한다.

사용자

감염

악성코드 링크

웹 서버

사이트 방문

실행(Exploit)

파일 다운로드(Exploit)

Page 38: The platform 2011

AMIGO - 행위 기반 악성코드 탐지The Platform 2011 37

악성코드가 숨어 있는 사이트를 사용자가 방문하면 악성코드를 설

치하기 위해 필연적으로 '실행(Execution)' 행위가 발생하게 된다.

이러한 행위 패턴을 탐지하기 위해 AMIGO는 사용자와 동일한 환

경의 에뮬레이터를 구성하여 웹 사이트를 접속하고 실행 행위 발생

유무를 확인하여 공격코드 존재 여부를 판단한다.

그림 8 AMIGO와 컴퓨터 백신의 차이

위 그림을 보면 웹 사이트를 통한 악성코드 감염을 나타낸 그림의

사용자가 봇(Bot)으로 변경된 것 외에 큰 차이가 없다. 봇은 입력받

은 사이트에 접속하여 악성코드 유포 행위가 있는지 분석하고 담당

자에게 알려준다.

그림 9 AMIGO 탐지 로그

AMIGO

AMIGO(intelligent Agent for Malicious and Intrusive articles

to GO away)는 악의적인 코드가 삽입되어 있는 웹 사이트를 탐지

하는 시스템이다. 네이버 서비스의 페이지 뷰는 매우 높다 사용자

백신 코드가 악성 코드를 감지하여 이를 사용자에게 경고한다고 해

도 이와 같은 일이 잦다면 사용자는 네이버에 접속하지 않으려 할

것이다. 이는 곧 네이버 서비스의 신뢰도를 떨어뜨리는 일이 된다.

AMIGO 시스템은 네이버 검색 결과에 노출되는 사이트를 모니터

링하여 악성코드 유포 여부를 판별하고 악성코드가 유포되는 사이

트를 탐지할 경우 해당 사이트를 검색 결과에서 제외한다. 이런 방

법으로 사용자의 PC가 악성코드에 감염되는 것을 사전 차단하여

서비스의 안전성과 고객의 신뢰도를 높인다.

AMIGO의 탐지 원리

AMIGO 시스템의 탐지 방식은 흔히 알고 있는 컴퓨터 백신 탐지

기법과 다르다. 컴퓨터 백신은 이미 알려진 악성코드의 패턴 방식

으로 악성코드를 탐지하지만 AMIGO는 행위 기반으로 악성코드

여부를 탐지한다.

그림 7 AMIGO와 컴퓨터 백신의 차이

사이트 방문

실행(Exploit)

파일 다운로드(Exploit)악성코드 유포

행위 탐지

웹 서버

악성코드 링크

Page 39: The platform 2011

NHN 서비스 들여다보기The Platform 201138

그림 10 2009년 3분기 악성코드 탐지 분포

그림 11 2010년 2분기 악성코드 탐지 분포

그림 12 2010년 3분기 악성코드 탐지분포

그림 9는 AMIGO 시스템의 악성코드 탐지 로그를 보여준다. 위

로그를 분석하면 사용자가 'www.B.kr' 사이트를 방문할 경우 공

격코드가 포함된 'www.A.com' 사이트의 'sky.hml', 'ko1.html'

문서를 로드한다. 이때 문서에 포함된 악성코드는 'www.C.com.

cn'에 존재하는 's.exe'를 자동으로 다운로드해서 해당 프로그램을

실행/설치하는 행위를 했다고 설명할 수 있다.

이렇듯 AMIGO는 행위 기반 탐지 기법을 사용함으로써 동적 변

경되는 공격코드도 탐지할 수 있으며 제로 데이 공격코드 또한 탐

지할 수 있다.

'백신도 탐지 못하는 제로 데이 공격코드를 탐지한다고?

와우~~!!'

AMIGO는 단순한 기술이다. 단순하지만 위와 같은 막강한 기능

을 가지고 있다. 하지만 만능은 아니다.

•운영체제/소프트웨어 구성 환경에 따라 제약이 있다.

현재 AMIGO는 윈도우 XP와 인터넷 익스플로러 6 버전에서만 지원

된다.

•접속 사이트 분석에 드는 비용이 높다.

AMGO는 사용자와 동일한 행위를 하는 봇을 이용하여 사이트를 방문

하는 형태로 동작하기 때문에 URL 1개를 분석하는데 평균 7~10초 정

도가 소요된다. 대규모 사이트를 분석하려면 비용이 많이 든다.

언급한 바와 같이 AMIGO의 공통적인 문제는 비용이다. 비용을

최소화하는 것이 곧 AMIGO를 개선하기 위해 해결해야 할 중점

과제인 것이다.

AMIGO의 현황 및 효과

AMIGO 시스템은 2009년 3월에 개발 완료되었으며, 네이버

를 통해 노출되는 외부 사이트에 적용/운영하고 있다. 다음은

AMIGO가 적용된 서비스의 2009년 3분기, 2010년 2분기, 3분기

악성코드 탐지 현황이다.

Page 40: The platform 2011

AMIGO - 행위 기반 악성코드 탐지The Platform 2011 39

그림 14 2010년 3분기 요일별 악성코드 탐지 분포

이는 평일에는 관리자의 신속한 대응으로 악성코드 노출 시간대가

줄었음을 의미한다. 즉, 사용자의 악성코드 감염이 최소화되는 방

향으로 진행되고 있는 것이다.

마치며

인터넷은 사용자에게 수많은 정보와 재미 그리고 편리함을 준다.

하지만 보안 측면을 면밀히 들여다보면 여전히 악성코드에 대한 위

험성이 존재한다. AMIGO가 모든 문제점을 해결하고 방어할 수는

없다. 하지만 사용자에게 좀 더 깨끗한 길을 터 주는 것에는 분명히

AMIGO가 도움을 주고 있다고 확신한다.

위 그래프를 통해 분기마다 탐지 비율이 증가하고 있음을 알 수 있

다. 실제 AMIGO 시스템이 운영되고 있더라도 악성코드가 근절되

지는 않는다. AMIGO 시스템은 탐지만 할 뿐이며 근본적인 해결

은 해당 사이트의 관리자의 노력에 의존할 수 밖에 없다.

하지만 악성코드가 탐지된 사이트에 대해 제재 조치를 취하고 악성

코드 유포 사이트 관리자에게 문제점 해결 및 보안강화 방안을 공

유하여 신속하게 대응함으로써 사용자가 악성코드에 노출되는 위

험성을 최소화할 수 있다.

시간대, 요일별 통계 자료를 보면 초기에는 시간대와 상관없이 산

발적이고 지속적인 탐지 형태를 보였으나 시간이 지남에 따라 지속

적인 탐지 대응 노력으로 관리자가 없는 주말에 집중되고 있음을

알 수 있다. 또한 자주 탐지되는 시간대도 오후 10시에서 새벽 2시

사이로 이동된 것을 확인할 수 있다.

그림 13 2010년 3분기 시간대별 악성코드 탐지 분포

Page 41: The platform 2011

NHN 서비스 들여다보기The Platform 201140

이 글에서는 다양한 uMon 시스템의 기능 중에서 서비스의 UGC

데이터가 어떻게 uMon 시스템에 유입되는지를 중점적으로 설명

한다.

uMon 시스템의 구성

우선 uMon 시스템의 구조를 살펴보자. uMon 시스템은 다음과

같이 구성된다.

• uMon client: 서비스의 웹 서버에 위치하여 UGC 데이터를 전송한다.

• uMon IN-BLOC: uMon client가 전송한 UGC 데이터를 uMon으로

유입한다.

•IVFS: 이미지/동영상의 섬네일을 추출한다.

•uMon Batch: 배치 작업을 처리한다.

•uMon Web: 검수자에게 검수 환경을 제공한다.

•uMon OUT-BLOC: 자동으로 검수 결과를 처리한다.

•Service Bridge: 처리 결과를 서비스로 전송한다.

‘각종 광고 글, 불법 복제 음원, 보고 싶지 않은 동영상......’

이와 같은 사용자가 원치 않는 정보로부터 네이버 서비스를 지킬

수 있는 것은 uMon이 있기 때문이다.

모니터링의 필요

아무리 잘 만든 인터넷 서비스라도, 스팸성 정보나 악성 게시글이

많다면 인터넷 사용자로부터 사랑받지 못할 것이다. 통합 모니터링

시스템(이하 uMon)의 고민은 여기서 시작되었다. 그런데 왜 통합

모니터링일까?

네이버 서비스에는 다양한 종류의 UGC(User-Generated

Contents)가 있다. 글(게시글, 댓글)이나 이미지뿐만 아니라 동영

상, 음원도 있다. 같은 게시글이지만, Q&A 형태의 게시글도 있고,

단순한 형태의 게시글도 있다. 카페에는 상품 게시판도 있다.

만약 이렇게 다양한 콘텐츠를 통합하지 않고 검수해야 한다거나 서

비스마다 별도의 모니터링 시스템을 만들어야 한다면, 시스템도 늘

어나고 모니터링 정책을 한 번에 모든 시스템에 반영하기 어려운

문제가 발생한다.

uMon의 이해 포털 서비스의 품질을 높이기 위해 불법적, 악의적 유해 콘텐츠를 제거하는

시스템을 구축하는 업무를 담당하고 있습니다.

밝고 깨끗한 인터넷 세상을 만들기 위해 최선을 다하겠습니다.

•운영시스템개발실 _ 김기정

Page 42: The platform 2011

uMon의 이해The Platform 2011 41

도의 예약 작업이 필요하지 않도록

UGC를 작성할 때 자동으로 활성

화되게 설정한다. 전송 커버리지를

보장하기 위해 사내 웹플랫폼팀에

서 개발한 NPC 모듈을 사용하고,

데이터의 무결성을 향상시키기 위

해 Jaxb 2.0을 사용하여 XML에

대한 Validation Check가 가능토

록 했다. 이러한 콘셉트를 실제 개

발에 적용한 기술은 다음과 같다.

티켓 방식의 파일 핸들링

LinkedHashMap과 세마포어를

이용하여 다중 스레드에서 파일 핸

들링할 수 있도록 구현했다. 핸들

링보드 세트(Global Set)를 미리

정해놓고, 각 스레드가 활성화될 때 다른 스레드가 사용하지 않는

파일의 핸들러를 얻어온 후 핸들링보드에 등록하는 방식으로 파일

의 중복 핸들링을 방지한다.

그림 2 Ticket 방식의 XML 핸들링 알고리즘

그림 1 uMon 시스템 구성

서비스의 UGC 데이터를 uMon으로 유입하는 과정은 크게 2단계

로 나뉜다. 모니터링 시스템에서 서비스 배포 모듈(uMon client)

을 제공하는데 이 모듈을 이용하여 서비스에서 생성된 UGC를 파

일로 만들고 이 파일을 uMon IN-BLOC으로 전송하는 단계,

IN-BLOC에서 전송된 파일을 읽어 uMon의 DBMS와 OWFS에

데이터를 저장하는 단계가 있다. 또한, IN-BLOC에는 여러 가지

자동 검수 로직이 있고, 자동 검수된 데이터는 OUT-BLOC을 통

해 각 서비스로 전송되어 검수 결과가 반영되도록 한다. 서비스의

UGC 데이터가 uMon에 유입되려면 다음 조건을 만족해야 한다.

1. 서비스에서 발생하는 UGC 데이터를 유실하지 않고 전송해야 한다.

2. uMon으로 UGC 데이터를 전송하는 작업이 서비스의 성능에 영향을

미쳐서는 안 된다.

3. 짧은 시간에 대량으로 발생하는 UGC 데이터를 서비스에 부하를 주지

않고 고속으로 전송해야 한다.

4. 작성 규약을 만들어 모든 것을 제한할 수 있어야 한다.

서비스의 UGC 데이터를 유실하지 않고 전송하기 위해 uMon

client를 사용한다. uMon client는 UGC 데이터를 파일로 작성하

여 이를 특정 디렉터리에 저장한다. 그리고 저장한 데이터를 전송

하기 위해 비동기 방식의 전송 기법을 사용하며, 대량의 데이터를

전송하기 위해 스레드를 이용한다. 또한 위에서 언급한 작업이 별

CAFE

uMonWeb

uMonBatch

uMonOUT-BLOC

uMonDataBase

uMonIN-BLOC

OWFS

IVFSCAFE

Service

uMon

Service Bridge

uMonclient

uMonclient

uMonclient

uMonclient

BLOGBLOG

KIN

KIN...

...

1 2 3 4 5 6 7 8 9Xml Files

Global Set

Semaphore(임계구간)

Ticketing(권한획득)

Thread-1

LocalSet

LocalSet

LocalSet

Thread-2

Thread-3

1 2 3 4 5 6 7

7

8

8

9

9

1 2 3

4 5 6

Page 43: The platform 2011

NHN 서비스 들여다보기The Platform 201142

Event Driven 기반 전송 알고리즘

전송 스레드의 생성 기준을 백그라운드 스케줄링 방식이 아닌 사용

자가 UGC를 생성할 때 발생하는 이벤트를 기준으로 정하기 때문

에 특정 프레임워크에 상관없이 동작한다. 이를 통해 범용성을 높

인다.

원격환경제어 알고리즘

배포 모듈의 여러 옵션(최대 전송 개수, 최대 스레드 활성화 개수,

스레드 유휴 시간, 전송 Timeout시간)이 스프링 프레임워크의 환

경(context)에서 설정되도록 하고, BLOC의 제어에 따라 실시간

으로 옵션 값을 변경할 수 있도록 구현했다.

uMon client

uMon client는 서비스(블로그, 카페, 오픈캐스트 등)에 모듈 형태

로 배포되어 사용자들이 작성하는 UGC(게시글, 덧글 등)를 uMon

IN-BLOC으로 전송한다. 이 모듈은 다음과 같은 사항을 고려하여

개발했다.

• 서비스에서 발생하는 UGC 데이터를 유실하지 않고 전송해야 한다.

• uMon과 uMon 인터페이스로 인해 발생하는 부하가 서비스 응답 속도

에 영향을 미쳐서는 안 된다.

• uMon client가 서비스 운영에 영향을 미쳐서는 안 된다.

• 짧은 시간에 대량으로 발생하는 UGC 데이터를 서비스에 부하를 주지

않고 고속으로 전송해야 한다.

• UGC 데이터를 XML 형태로 변환하여 전송하며, 작성 규약을 제한할 수

있어야 한다.

uMon client는 서비스 서버에 영향을 주지 않고 대량의 데이터를

전송해야 하기 때문에 그에 맞는 정책과 기술을 적용했다. uMon

client의 프로세스를 도식화하면 다음과 같다.

스레드 핸들링 알고리즘

다중 세마포어를 이용하여 스레드 개수, 최종 전송 시간 등을 각각

의 Static 변수에 바인딩한다. 이는 정보가 저장된 각 변수를 다중

스레드에서 접근할 때 혼선을 없앤다. 그리고 임계구간 진입 대기

시간을 최소화하여 전송 속도에 영향을 미치지 않도록 한다.

그림 3 다중 세마포어 사용

[단일 세마포어 사용 - 대기시간 연장]

Static Flag

Static Flag

Thread 개수

Thread 개수

Thread 생성시간

Thread 생성시간

Global Set

Global Set

Update!!

Update!!

Update!!

Wait...

Update!!

Wait...

S-1

S-1

S-2

S-2

S-3

S-3

Thread - 1

Thread - 1

Thread - 2

Thread - 2

Thread - 3

Thread - 3

[다중 세마포어 사용 - 대기시간 단축]

3 323123 ...

Page 44: The platform 2011

uMon의 이해The Platform 2011 43

• 큐잉 모드

uMon 시스템 점검, 데이터베이스 장애, 데이터베이스 인덱스 작업 등으

로 인해 정상적으로 서버를 운영할 수 없을 때 요청받은 인터페이스를 특

정 디렉터리에 파일로 저장하는 방식

• 일반 모드 + 큐잉 모드

일반 모드로 UGC 데이터를 저장하고 큐잉된 데이터는 성능 테스트를 위

한 실험 데이터로 활용하는 방식

그림 4 uMon client 프로세스

uMon IN-BLOC

uMon client로부터 UGC 데이터가 전송되면 uMon 시스템의 인

입 서버인 uMon IN-BLOC이 해당 파일을 받아 데이터베이스

에 데이터를 저장한다. uMon IN-BLOC에는 5가지 동작 모드가

있다.

•일반 모드

일반적인 동작 상태로 UGC 데이터를 저장하는 기본적인 방식

Service BLOC Service

조건에 맞는 파일 읽음(Max 파일 개수 설정)

성공한 파일 삭제

Valid 한 문서로 변환에러난 원인 및 FILE 저장 위치 등을 OBJECT에 WRITE

Not Well-formed

BLOC 서버로 묶음 전송 요청

Not Valied

Valied 하게 만듦

시작

User Event 발생

서비스 ActionStart

Html parsing

UnMarshalExceptionHandler

UmonInterfaceBO

한 번 전송시 100개 설정

기존 userEvent 발생 시 생긴

XML 원본 그대로 저장

UNMARSHALERRFILE

TRANSFERFILE

ClientException Object기록 추가

executeInterface

Validation

registTransfer

전송 스레드 유무

서비스 ActionEnd

wakeUpTransfer

ticketing

Loop 시작

TransferNpc

N건중/N건 성공

파일 삭제

Loop 끝

종료

종료

종료

Page 45: The platform 2011

NHN 서비스 들여다보기The Platform 201144

서비스와 uMon 연동 시 고려 사항

uMon을 서비스와 연동하려면 서비스 측면에서 다음 4가지 사항

을 고려해야 한다. 첫째, 양 시스템에서 검수 결과를 송수신하고 그

이력을 남기기 위해서는 UGC의 식별자(ID)가 유일해야 한다. 둘

째, 서비스에서 uMon의 코드 정책을 따라야 한다. uMon에는 그

룹 코드와 세부 코드가 있다. 서비스와 uMon의 코드 정책이 같아

야 동일한 코드로 UGC를 검수하고, UGC의 상태를 유지할 수 있

다. 셋째, 사용자에 의한 UGC의 상태 변경과 검수 결과를 모두 관

리하기 위하여 검수 결과 상태 필드와 사용자에 의한 상태 필드를

분리해야 한다. 넷째, 모니터링 정책은 언제든지 변경될 수 있고,

검수의 오류가 존재할 수 있기 때문에 복구가 가능하도록 플래그

(flag)를 관리해야 한다.

• 재처리 모드

큐잉 모드로 저장한 XML 데이터를 다시 처리하면서 새로 전달되는

XML 데이터도 같이 처리하는 방식

• 교육용 모드

uMon이 일반 모드로 동작할 때 XML 데이터를 교육용 데이터베이스에

도 저장하는 방식(교육용 데이터를 만들 때 데이터를 수동으로 입력해야

하는 문제를 해결하고 데이터 다양성을 보장하기 위함)

uMon IN-BLOC의 프로세스를 도식화하면 다음과 같다.

그림 5 uMon IN-BLOC 프로세스

Service(blog, cafe) BLOC

시작

시작

createMessageMuchMethod인자 1.xmlMessage인자2.client에 남은 개수인자3.묶인인자4.client 전송 file 위치

운영모드

NORM : 정상 처리모드

QUEU : 파일로만 저장

DBFL : NORM + QUEURECY : NORM + QUEU mode로인해서 쌓인 파일 처리요청

NPC전송

결과 전송

실패

성공한파일명저장

성공한 파일 리스트Access

시작

User Event 발생

배포모듈

배포모듈

성공한 파일 삭제

End

UmonInterfaceBO

NORM DBFL RECY

재처리요청

umonContentBO

umonPenaltyBO

resultMap

QUEU

서비스 DBRouting

dbWork

Loop 시작

umonMstrInsert

umonMstrIvInsert

Loop 끝

sendResult

validation

운영모드

에러리턴

FILE

Page 46: The platform 2011

uMon의 이해The Platform 2011 45

현재 uMon 시스템에서 사용되는 기술 대부분이 NHN의 기술 표

준으로 채택되어 대형 서비스(카페, 블로그, 지식iN 등)를 중심으

로 38개의 서비스가 uMon과 연동되어 운영되고 있다. 즉, 연동으

로 인한 서비스의 성능 이슈나 검수 결과의 송수신 문제는 대부분

해결된 상태이다.

마치며

다양한 형태의 서비스와 UGC가 생산되면서, 이제 불법적인 스팸

정보를 관리하는 것이 서비스의 품질을 높이는 것이며, 이는 곧 서

비스의 성패를 좌우할 정도로 중요해졌다. 즉, 불법적인 스팸 정보

를 걸러내는 기술을 최대한 자동화하고, 대응 시간을 줄여 나가는

것이 중요한 이슈가 된 것이다. 이제 서비스의 성공을 위해 콘텐츠

의 품질을 보장할 수 있는 방법을 고민해야 할 때이다.

Page 47: The platform 2011

NHN 서비스 들여다보기The Platform 201146

처음에는 지도를 확대/축소하여 보여주거나, 빠른 길을 찾아주는

수준이었던 지도 서비스는 항공 사진, ‘거리뷰’와 같이 실제 사진

정보를 지도 상에 보여주는 등 기능을 빠르게 확장하고 있다.

이 글을 통해 네이버 지도 서비스가 어떻게 만들어졌는지 설명하려

한다.

지도 이미지 타일

네이버 지도 서비스에서 사용하는 하나의 이미지 파일(이미지 타

일)의 크기는 256픽셀 X 256픽셀이다. 사용자들이 보고 있는 한 장

의 지도 화면은 이런 이미지 타일을 조합하여 하나의 지도 화면으

로 보여주는 것이다.

지도 서비스는 가보지 않은 곳, 익숙하지 않은 곳을 편하고 쉽게 알

려준다.

인터넷 지도 서비스가 지금처럼 보편화되기 전을 떠올려보자. 지도

한 장이나 지도 책을 펼쳐 원하는 정보를 찾기란 얼마나 불편했고,

또 불가능한 것이 많았던가.

웹 브라우저나 스마트폰을 이용해 편하게 볼 수 있는 인터넷 지도,

그 지도 서비스가 어떻게 만들어지고 있는지 이 기사를 통해 설명

하고자 한다.

지도 서비스 간단 설명

지도 서비스란 축척(레벨)에 맞는 여러 장의 이미지 파일을 조합/

연결하여 사용자에게 보여주는 것을 말한다. 사용자가 지도 상의

이곳 저곳을 이동하면서 확대/축소하기 때문에, 빠른 시간 안에 정

보를 보여주려면 여러 장의 그림을 이어 사용하는 것이 좋다.

지도 서비스는 어떻게 만드는 것일까?1999년 GIS(Geographic Information System) 분야에 발을 담그기 시작

해서 지금까지 12년이네요.

초기에는 PC용 애플리케이션을 개발했고 현재는 인터넷과 모바일 기반의

지도 서비스를 개발했습니다. 그동안 강산이 한 번 변하더군요.

욕심으로는 10년은 더 하고 싶은데 그때는 무슨 기반의 서비스가 될지 기

대되네요. ^^

•지도서비스개발1팀 _ 신홍철

Page 48: The platform 2011

지도 서비스는 어떻게 만드는 것일까?The Platform 2011 47

왼쪽의 네이버 지도 화면은 벡터 이미지로 제작한다. 따라서 확대

및 축소가 자유롭다. 우선 어떻게 이러한 벡터 이미지를 제작하는

지 먼저 설명을 하고자 한다.

벡터 데이터의 구성 요소

벡터 데이터의 주요 구성 요소로는 점(Point)과 선(LineString) 그

리고 면(Polygon)이다. 각 요소는 지도를 표현(Rendering)할 때

사용된다. 첫 번째로 점 데이터는 지도 상에서 주요 지점을 표현하

는 텍스트나 아이콘 심볼을 표현할 때 사용하는데 X축(위도)과 Y

축(경도) 좌표를 가지고 있다.

그림 3 점(Point)

두 번째로 선 데이터는 지도 상에서 도로 중심선이나 철도, 지하철,

행정경계선, 국가경계선, 등고선 등을 표현할 때 사용하며 각 점들

을 선으로 연결하여 하나의 연속된 선형으로 표현한다(OpenGIS

SimpleFeature에서는 LineString이란 용어를 사용).

그림 1 지도이미지 타일

다음 그림의 위쪽은 네이버 지도 화면이고, 아래쪽은 동일 지역에

대한 위성 항공 영상 사진이다.

그림 2 일반지도 (왼쪽), 위성 지도 (오른쪽)

37.35951, 127.10514

37.35816, 127.10528

X

Y

Page 49: The platform 2011

NHN 서비스 들여다보기The Platform 201148

그림 4 선(PolyLine)

마지막으로 면 데이터는 건물이나 공원, 단지, 강, 바다, 도로면 등

을 표현할 때 사용하고 데이터 형식은 선 데이터와 유사하나 점 배

열(Point Array)의 첫 번재 점과 마지막 점이 연결된 하나의 다각

형으로 정의하기도 한다.

그림 5 면(Polygon)

지도 레이어

벡터 데이터는 데이터의 표현 방식이나 분석 방식에 따라 상세하게

분류할 수 있다. 이렇게 분류된 데이터 집합을 지도 레이어라 부른

다. 다음 그림에서 볼 수 있듯이 레이어는 정의된 형태의 범례와 같

이 지도 상에 표현된다. 현재 네이버 지도 서비스에서 사용하는 레

이어는 약 500개이다.

그림 6 지도 레이어와 범례

레이어는 지도를 표현할 뿐만 아니라 데이터를 분석할 때도 사용

한다. 예를 들어 '성남시 정자동' 안에 있는 건물의 개수, 도로의 총

길이, 초등학교의 개수 혹은 특정 레이어 존재 유무를 분석할 수 있

다. 이런 레이어를 그리는 순서 또한 매우 중요하다. 아래 그림과

같이 각 레이어를 순서대로 렌더링하여 하나의 지도 화면으로 만

다.

그림 7 지도 레이어

37.35951, 127.10514

37.35876, 127.10774

37.35867, 127.10626

37.35812, 127.10925

X

Y

37.35951, 127.10514

37.35876, 127.10774

37.35867, 127.10626

37.35981, 127.10763

X

Y

경계선

단지

도로

건물

지형평면도

등고선

기준점격자

오버레이

Page 50: The platform 2011

지도 서비스는 어떻게 만드는 것일까?The Platform 2011 49

지도의 레벨

위에 언급한 축척이라는 개념은 종이 지도에서의 표현방식이고 네

이버 인터넷 지도에서는 레벨이라는 용어와 개념을 사용한다. 종이

지도에서는 인치(inch)나 센티미터(cm) 단위를 사용하여 축적을

나타내지만 우리가 사용하는 컴퓨터의 모니터는 인치나 센티 단위

를 사용하지 않고 픽셀(Pixel)이라는 단위를 사용한다. 모니터의 픽

셀은 각 제조사마다 혹은 모델마다 그 크기가 다양하여 DPI(Dots

per Inch)라는 변환 계수를 사용해서 모니터의 크기를 대략적으로

환산할 수 있다. 하지만 DPI 값은 정밀하지 않으므로 실제 거리 대

비 픽셀을 기준으로 지도의 크기를 나타낸다. 네이버 지도는 총 1

레벨에서부터 14레벨까지 총 14단계의 값을 사용하는데 그 표현

범위는 아래와 같다.

네이버 지도에서는 1:5,000 지도를 하위 레벨(확대 10~14레벨)에

서 사용하고 1:25,000 지도를 1~14 레벨에 골고루 사용한다. 참고

로 종이 지도는 종이를 늘리거나 줄여서 축척을 조절할 수 없지만

좌표 기반의 전자 지도는 모니터 상에서 확대 화면과 축소 화면을

자유롭게 표현할 수 있기 때문에 아래 표와 같이 여러 단계의 레벨

범위를 사용한다.

표 2 지도 레벨

레벨 지도 상 거리 실제 거리

14 1px 0.25m

13 1px 0.50m

12 1px 1.00m

11 1px 2.00m

10 1px 4.00m

...

1 1 pixel 2048.00 m

레이어의 표현 범위

여기까지 데이터의 구성, 레이어 그리고 데이터의 표현에 대해 알

아봤다. 모든 레이어가 각 레벨마다 표현(렌더링)된다면 지도는 어

떤 모양일까? 12~14레벨에서는 상세한 지도가 표현되겠지만 점점

축소되는 상위 레벨(1~7레벨)로 갈수록 아래 그림과 같이 먹칠 현

상이 발생할 것이다.

만약 렌더링하는 순서를 임의로 정한다면 원하는 형태의 지도 화면

을 얻을 수 없을 것이다. 예를 들어 도로, 건물, 단지 레이어 순으로

렌더링하면 어떻게 될까? 화면에 단지 레이어만 보이고 나머지 레

이어는 단지 레이어 뒤에 가려질 것이다.

지도의 축척

이번에는 지도의 축척에 대해 알아보도록 하겠다. 축척이란 “지도

에서의 거리와 지표에서의 실제 거리를 비율나타낸 것으로서 몇 천

분의 일, 몇 만분의 일로 표시한다.” 네이버 지도는 1:5000 지도와

1:2500 지도를 사용하고 있다. 하지만 축척이란 종이 지도에서 사

용하던 개념이라 네이버 지도 서비스와 같은 인터넷 지도에서는 레

벨이라는 용어를 사용한다. 레벨은 다음 절에서 설명한다.

전자 지도가 사용되기 전에는 사각형 종이에 지도를 표현했다. 사

각형 종이에는 지표면을 표현하는데 한계가 있어 아래 표와 같이

축척이라는 범위를 사용하여 한반도나 세계가 다 보이는 대축척 혹

은 특정 동 단위나 구 단위의 범위가 표현되는 소축척 지도로 만들

어 사용했다.

표 1 지도 축척

축척 지도 상 거리 실제 거리

1 : 1,000 1cm 10m

1 : 5,000 1cm 50m

1 : 10,000 1cm 100m

1 : 25,000 1cm 250m

1 : 50,000 1cm 500m

예를 들어 10cm X 10cm 크기의 사각형 종이 위에 1:1000 축척 지

도로 표현할 수 있는 실제 범위는 100 m X 100m 크기의 지형이다.

예를 들면 그린팩토리 옆에 있는 롯데캐슬 모델하우스와 주차장 부

지만한 크기의 지형을 표현할 수 있다. 1:10,000 축척으로 지도를

표현한다면 1km X 1km 크기를 표현하게 되므로 그린팩토리는

1cm보다 작게 표현될 것이다.

Page 51: The platform 2011

NHN 서비스 들여다보기The Platform 201150

각 레이어는 레벨(1~14레벨)에 따라 표시 여부(hidden/visiable)

가 결정된다. 네이버 지도 서비스는 총 500개의 레이어에 대해 각

레벨별로 표현해야 하는 범위를 설정하였다.

지도 이미지 타일의 구성

일반 지도는 각 레벨별 256픽셀 X 256픽셀 크기의 격자 형태의 지

도 이미지 타일로 만들어진다. 위에서 14레벨에서 1픽셀이 표현하

는 실제 크기는 0.25m라고 했다. 한반도의 세로의 크기가 500km

이고 가로의 크기가 400km라고 가정해 보자. 그럼 이미지 타일 한

장에 가로와 세로의 크기는 64m(256 x 0.25m)를 표현한다. 그럼

총 세로 열의 개수는 7812장(500,000 ÷ 64)이고 가로 열의 개수는

6250장(400,000 ÷ 64)으로 14레벨에서 사용되는 이미지 타일의

개수는 총 48,825,000장(7812 X 6250)이 필요하다.

그림 10 이미지 타일 구성

13레벨은 1픽셀이 표현하는 실제 크기가 0.5m이기 때문에 14레

벨보다 가로와 세로가 각각 반으로 감소하므로 가로 3125개, 세로

3906개, 총 12,206,250개의 타일이 필요하다. 이렇게 1레벨까지 계

산하게 되면 위 그림과 같이 14레벨에서 1레벨로 갈수록 이미지 타

일 개수가 4분의 1씩 줄어든다.

그림 8 표현 범위(Visible Range)

이러한 현상을 방지하기 위해 각 레이어가 표현할 수 있는 레벨 범

위를 아래 표와 같이 설정한다.

표 3 표현 범위

레벨 레이어

1~2 국가경계선, 행정경계(도), 바다, 행정 도명

3~4 국가경계선, 행정경계(도), 바다, 행정 도명, 고속도로, 하천(큰강), 국립공원

5 행정 경계(도, 시, 군, 구), 고속도로, IC, JC, 주도로, 하천(큰강), 국립공원

6~7 대표 랜드마크, 일반도로, 시명, 구명, 대교(예: 한강대교, 한남대교)

8~14 등고선, 시명, 구명, 동명, 일반도로, 소로, 지하철, 소공원, 하천(일반 하천)

그림 9 표현 범위에 따른 지도 표현 예

가로 길이 : 400,000 m

타일 개수 : 6250

세로 길이 : 400,000 m

타일 개수 : 7812

Page 52: The platform 2011

지도 서비스는 어떻게 만드는 것일까?The Platform 2011 51

세로 길이 : 400,000 m

타일 개수 : 7812

위에서 언급한 레벨별 이미

지 타일 규칙을 적용하면 실

좌표에서 이미지 타일로 이

미지 타일에서 실 좌표로 변

환할 수 있다. 예를 들어 13

레벨에서 (200, 400)번째 이

미지 타일의 왼쪽 아래 모서

리 좌표는 아래와 같은 방법

으로 구할 수 있다.

위에 정의한 값을 기준으로

상대 좌표를 계산할 수 있는

데 X축의 상대 좌표는 25,600m(200번째 X 128m)로 계산되고 Y

축 상대 좌표는 51,200m(400번째 X 128m)로 변환한다. 이와 같이

상대 좌표 X(25,600), Y(51,200)를 구할 수 있고 여기서 절대 좌표

를 구하려면 위에 언급한 중국 난징 아래 쪽 이미지 타일(0, 0)의 왼

쪽 하단에 대응된 UTM-K 절대 좌표를 참고하여 실제 좌표 값을

아래와 같이 얻는다.

X = 34,090,112 + 25,600 = 34,115,712

Y = 1,4192,896 + 51,200 = 14,244,096

실제 좌표에서 인덱스 번호를 얻으려면 역으로 계산한다.

Google 지도, bing Maps 등 전세계적인 포털 지도 서비스에서 이

와 같은 방법을 사용하고 있으며 각 나라별 포털 사이트별 기준이

되는 좌표 체계가 있어 해당하는 좌표 체계로 변환을 할 수 있다면

각 지도를 겹치게 해서 조합할 수 있다.

그림 11 각 레벨별 이미지 타일 구성

위와 같이 이미지 타일의 개념적인 구성 방식에 대해 알아봤으니

네이버 지도 이미지 타일의 실제 구성 방식에 대해 알아보자. 네이

버 지도 서비스의 좌표 기반은 UTM-K라는 직각좌표계이다. 우

리가 외부에서 많이 보던 좌표계인 위도 32.5도, 경도 127.8도와 같

은 지구 타원체의 도 단위(degree)의 좌표가 아니라 한반도 지역은

직선이라고 가정하고 만든 직각좌표계를 사용하고 있다.

네이버 지도 이미지 타일의 각 모서리 부분은 이 UTM-K 기반

에 절대 좌표에 대응되어 있으며 인덱스 타일 번호가 시작되는 곳

은 아래 그림과 같이 중국 난징 아래 부분을 기점으로 이미지 타일

인덱스 번호를 행(row)과 열(col)로 부여했다. 행, 열 번호가 (0, 0)

인 이미지 타일의 왼쪽 하단 모서리 좌표 값(x, y)은 (34,090,112,

1,4192,896)으로 규정했다.

그림 12 1레벨 이미지 타일 구성

국가

시, 군

거리

Page 53: The platform 2011

NHN 서비스 들여다보기The Platform 201152

네이버 지도 서비스의 미래

2011년 5월 지도서비스개발랩에서는 nGlobe1를 출시했다.

그림 13 nGlobe 서비스

nGlobe는 네이버 지도 서비스의 미래라고 말할 수 있다. 앞으로

더 또렷한 결과를 보여줄 수 있는 고해상도 렌더링 이미지를 사용

할 것이다. '건물 높이 정보'도 반영하여 좀 더 생생한 거리 정보를

전달할 수 있을 것이다. 이렇게 nGlobe가 서비스 품질이 좋아지면,

네이버 지도 서비스가 본격적인 글로벌 서비스가 될 수 있을 것으

로 기대한다.

1 http://lab.map.naver.com/nglobe

Page 54: The platform 2011

RTCS 실시간 웹 서비스를 위한 도전The Platform 2011 53

•실시간 인터랙션: 사용자와 실시간으로 인터랙션할 수 있다.

•대용량: 많은 사용자가 동시에 이용한다.

사내에 다양한 관련 기술이 있어 UI를 비롯한 대부분의 기능은 기

존에 개발한 것을 재사용하고, 실시간 커뮤니케이션 기능 개발에

집중하기로 했다. 이름은 RTCS(Real-Time Communication

System)라고 지었다. 그 후 두 달여를 진행하여 각 목표에 대해 아

래와 같은 결과를 얻을 수 있었다.

• 접근성: 주요 5대 브라우저인 인터넷 익스플로러, 파이어폭스, 크롬, 사

파리, 오페라를 지원하고 순수하게 HTML, CSS, 자바스크립트만으로

기능을 구현하여 별도의 플러그인 없이 동작하게 만든다.

• 실시간 인터랙션: Comet 기술을 활용하여 Long Polling, 스트리밍 방

식으로 양방향 실시간 통신을 구현한다. 서버 측의 주요 구현부는 톰캣

6.0의 Advanced IO1를 기반으로 구현한다.

• 대용량: 클라이언트와 서버 간 인터랙션 작업과 로직 작업을 분리하여 처

리하고 세션 저장소를 분리하여 RTCS 서버의 수평적 확장을 쉽게 한다.

1 http://tomcat.apache.org/tomcat-6.0-doc/aio.html

때는 바야흐로 연말연시, 남들은 놀기 바쁜 이때 뭔가 새로운 시도

를 해 보려는 이들이 있었으니...…

“웹으로 신맞고를 만들어 보면 어떨까?”

그 후 한 달여가 지난 뒤 결과물을 보았을 때 놀라움을 금할 수 없

었다. 크게 두 가지가 눈에 들어왔는데, 첫째는 HTML, CSS, 자바

스크립트만으로 화려한 UI를 구현할 수 있다는 점이었고(사운드만

어쩔 수 없이 플래시 사용), 둘째는 두 사람이 실시간으로 대전을

할 수 있다는 점이었다. 물론 두 가지 모두 기반 기술이 전혀 새로

운 것이 아니라 수년 전부터 사용해 온 기술이지만 짧은 시간에 높

은 완성도를 가진 결과물을 개발할 수 있었던 것은 대단한 일이다.

이후 여러 평가에서 좋은 반응을 얻었다. 이것을 실제 서비스에 적

용할 수 있도록 재사용 가능한 솔루션으로 발전시키려는 노력을 기

울였다. 그리고 다음과 같은 주요 목표를 결정하고 새로운 도전을

시작했다.

•접근성: 브라우저만 있으면 어디에서나 활용할 수 있다.

RTCS 실시간 웹 서비스를 위한 도전RTCS 설계 및 구현에 참여했으며, 현재 한게임 포털과 게임플랫폼서버와의

연계 및 웹 서비스 백엔드 솔루션 구축에 힘쓰고 있습니다.

지난 수년간 프로젝트에서 다양한 채널 및 메시지를 중계하는

자바 기반 서버를 구현해 왔으며, AJAX 기반 서비스를 구축해 왔습니다.

SK커뮤니케이션즈를 거쳐 현재 NHN 게임서비스개발센터에서 근무 중입니다.

RTCS 프로젝트의 클라이언트 부분을 주도했으며 UI/웹/모바일 기술과 같이

사용자가 직접 접하는 기술에 지속적으로 관심을 가져왔습니다.

•게임서비스기술지원팀 _ 이창근

•게임서비스솔루션팀 _ 김경윤

Page 55: The platform 2011

NHN 서비스 들여다보기The Platform 201154

기능 구분 구성 요소

클라이언트/서버 로직 구현 API

서버 Push 방식: Long Polling vs. 스트리밍

RTCS는 Comet의 대표적인 두 가지 방식인 Long Polling과 스트

리밍을 지원한다. Long Polling의 경우 기존의 주기적인 Polling

방식(주기적으로 요청하여 결과를 확인하는 방식) 대신 요청에 대

한 응답을 서버 이벤트 발생 시점에 받는 방식이고, 스트리밍의 경

우는 요청에 대한 응답을 완료하지 않은 상태에서 데이터를 계속

내려받는 방식이다.

주기적인 Polling 방식

주기적인 Polling 방식은 클라이언트, 서버 모두 구현

이 단순하다. 요청에 대한 서버 부담이 크지 않거나 실

시간 메시지 전달이 크게 중요하지 않은 서비스에 적합

하다. 실시간 메시지 전달의 중요성에 따라 요청 주기

를 조절할 수 있지만 요청 주기가 짧으면 자칫 서버에

무리를 줄 수 있기 때문에 주의해야 한다. 또한, 실시간

메시지 전달을 고려하여 요청 주기를 짧게 설정하더라

도 서버의 상태가 자주 변경되지 않는다면 불필요한 요

청/응답 트래픽이 많이 발생할 것이다(그림 2).

Long Polling 방식

RTCS 구성 요소와 기능

RTCS는 자바스크립트 기반의 클라이언트 모듈과 톰캣 6.0

Advanced IO 기반의 서버 모듈로 구성된다. 클라이언트와 서버

간의 메시지는 HTTP 기반의 URL 요청과 JSON 형태의 응답으

로 처리된다.

다음 그림은 RTCS의 구성 요소를 나타낸다. AJAX와 Comet을

처리하는 'Ajax Client – On-demand Servlet’, ‘Comet Client

– Comet Processor/Message Sender’와 양방향 메시지 처리를

보완하는 요소로 구성되어 있다고 볼 수 있다. 그리고 클라이언트

와 서버의 로직에서 사용할 수 있는 API를 제공하고 있다.

그림 1 RTCS 구성 요소

기능별 구성 요소를 구분하면 다음 표와 같다.

표 1 RTCS 기능별 구성 요소

기능 구분 구성 요소

AJAX 메시지 처리 Ajax Client, On-demand Servlet

Comet 메시지 처리 Comet Client, Comet Processor, Message

Sender

클라이언트 상태 확인(Alive Check) Ping Pong, Idle Checker

RTCS 메시지 처리 및 메시지 유효성 확인 Protocol, Checksum

메시지 전송 예외 처리 Failover(Browser/Web Server)

클라이언트 메시지 처리 순서 보장 Message Queue, Ajax Client

스트리밍 연결의 유효성 확인 Heartbeat

Client(Web Browser)

UI Ajax client

DisplayProcessing

Processing

Rep

osi

tio

ryDisplayup-to-date

Displayup-to-date

Display

Display

Timer task

Front Backend

Server

그림 2 주기적인 Polling 동작 구조

Page 56: The platform 2011

RTCS 실시간 웹 서비스를 위한 도전The Platform 2011 55

Client(Web Browser)

UI Ajax client

Processing

Processing

Processing

Processing

Processing

Displayup-to-date

Displayup-to-date

Displayup-to-date

Displayup-to-date

Displayup-to-date

Timer task

Front Backend

Server

스트리밍 방식

스트리밍 방식은 한 번 요청 후 응답을 완

료하지 않고 해당 응답 스트림으로 필요할

때마다 데이터를 전송하는 방식이다. 응답

마다 다시 요청해야 하는 Long Polling에

비해 효율적이며, 서버의 상태 변경이 매우

잦은 경우 유리하다. 하지만 연결을 길게

맺고 있는 경우 연결의 유효성 관리 등의

부담이 발생한다. 스트리밍 방식도 보통 특

정 시간을 주기로 연결을 재설정하도록 구

현한다. RTCS는 여기에 "최소 전송 보장

길이"를 확인하는 기능이 더 있는데, Long

Polling과 달리 요청/응답의 완결 구조가

아닌 상태에서 짧은 메시지가 바로 전달되

지 못하는 경우를 대비하기 위해서다. 자세

한 내용은 '다양한 네트워크 상황에 대한 대

처'절에서 설명한다(그림 4).

그림 3 Long Polling 동작 구조

그림 4 스트리밍 동작 구조

Long Polling 방식은 실시간 메시지 전달

이 중요하지만 서버의 상태 변경이 빈번히

발생하지는 않는 서비스에 적합하다. 주기

적인 Polling 방식에 비해 불필요한 요청/

응답 트랜잭션을 덜 유발한다. '덜 유발한

다'고 한 이유는 Long Polling 방식도 보통

서버 응답을 무한정 기다리는 게 아니라 특

정 시간이 지나면 해당 요청/응답 트랜잭

션을 완료하고 새로이 요청하는 방식으로

구현하기 때문이다. RTCS는 클라이언트

의 상태 확인(Alive Check)을 위해 ping-

pong 방식을 사용하고 있으며 주기를 설

정할 수 있다(그림 3).

그림 2 주기적인 Polling 동작 구조

Client(Web Browser)

UI Comet client

Processing

Processing

Displayup-to-date

Displayup-to-date

Timer task

Front Backend

Server

Connect

(Subscribc)

Connect

(Subscribc)

Connect

(Subscribc)

Page 57: The platform 2011

NHN 서비스 들여다보기The Platform 201156

function longPoll(url, callback) {

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function() {

if(xhr.readyState == 4) {

// 응답이 완료되면 서버로

// 재연결 요청보내기

callback(xhr.responseText);

xhr.open('GET', url, true);

xhr.send(null);

}

}

// 서버로 요청 연결하기

xhr.open('POST', url, true);

xhr.send(null);

}

스트리밍

스트리밍 방식은 클라이언트에서 요청을 보내면 서버에서 응답을

보내지 않고 계속 연결을 유지한다. 그 연결을 통해 HTTP/1.1의

Chunked 인코딩 방식을 이용하여 이벤트가 발생할 때마다 서버

에서 클라이언트로 결과를 전송한다. 연결을 맺고 있는 상태에서

계속 이벤트를 줄 수 있으므로 이벤트가 빈번하고 실시간 메시지

전달 이슈가 큰 서비스에 적합하다.

스트리밍 방식의 경우도 XHR을 이용하는 것이 가장 좋으나, 인터

넷 익스플로러와 오페라의 경우 XHR 처리 시 응답이 완료되지 않

은 상태로 전송된 데이터에 접근이 허용되지 않는다. 따라서 다른

방법을 이용했다. 브라우저별로 스트리밍 방식을 구현하기 위해 결

정한 사항은 다음과 같다.

표 2 브라우저별 스트리밍 방식 구현

적용 브라우저 구현 방식 설명

파이어폭스, 크롬, 사파리 XHR 스트리밍 XMLHttpRequest를 이용하여 스트리

밍 구현

인터넷 익스플로러 Forever Frame IFrame과 Script Flush 방식을 이용하여

스트리밍 구현(htmlfile 액티브X 컨트롤

이용)

오페라 Server-Sent Events HTML5의 Server-Sent Events를 이용

Long Polling과 스트리밍 방식의 구현

RTCS는 클라이언트를 확인하기 위해 sessionKey를 사용하는데,

RTCS 클라이언트와 서버가 연결을 초기화할 때 일종의 핸드셰이

킹(handshaking) 과정을 거친다. 이때 Long Polling 방식과 스트

리밍 방식의 경우 연결 과정이 서로 다른데, 이유는 각 방식의 차

이점 때문이다. 따라서 두 방식에 대한 Comet 서블릿(톰캣 6.0

Advanced IO Comet 프로세서의 구현체)을 별도로 구현하고

있다.

Long Polling 방식은 지원하는 5대 브라우저 모두 XMLHttp

Request(이하 XHR)로 구현 가능하지만, 스트리밍 방식은 브라우

저별로 XHR, iframe을 Forever Frame, Server-Event 방식으로

구현하고 있는데, 두 방식에 따라 Comet 서블릿을 따로 구현한 첫

번째 이유이다.

두 방식의 연결 내용은 2010 DeView RTCS 발표자료2의 3.5, 3.6

절을 참조하기 바란다. 브라우저별 스트리밍 처리는 다음 “브라우

저별 구현 방법” 절에서 확인할 수 있다.

브라우저별 구현 방법

RTCS는 5대 주요 웹 브라우저를 모두 지원하기 위해서 각 브라우

저별 특성에 맞춰 Comet을 구현했다. RTCS는 Long Polling 방

식과 스트리밍 방식을 지원하는데 각각의 경우 브라우저별로 어떻

게 구현했는지 살펴보자.

Long-Polling

Long-Polling 방식은 서버에 요청을 보내고 서버 이벤트가 발생

할 때까지 연결을 유지한다. 이 상황에서 이벤트가 발생하면 응답

을 받아서 처리하고 그 즉시 또 다른 이벤트를 받기 위해 연결을 맺

는 방식이다. 방식 자체가 일반적인 HTTP 처리 형태와 별반 다르

지 않기 때문에 브라우저별로 동일한 처리가 가능하다. 이때 서버

측에서 이벤트가 발생할 때까지 연결을 맺는 작업은 비동기 처리가

가능한 XHR을 이용하여 AJAX 방식으로 구현했다. 주요 자바스

크립트 코드는 다음과 같다.

2 http://deview.naver.com/2010/file/C2.pdf

Page 58: The platform 2011

RTCS 실시간 웹 서비스를 위한 도전The Platform 2011 57

// 서버로 요청 연결하기

xhr.send(null);

}

인터넷 익스플로러와 오페라에서는 readyState가 3일 때 보안상의

이유로 응답 결과에 접근을 허용하지 않는다. 따라서 다음 절에서

설명하는 다른 방식을 이용해야 한다.

Forever Frame 방식

XHR 스트리밍 방식을 구현할 수 없을 경우 차선책으로 예전부터

웹 기반 채팅을 위해 주로 사용되던 방식인 Forever Frame 방식을

고려할 수 있다. 이 방식은 페이지에 보이지 않는 IFrame을 생성해

서 HTTP/1.1의 Chunked 인코딩3 방식을 기반으로 Comet 서버

와 연결을 맺어둔다. 이를 이용해 서버에서 이벤트가 발생할 때마

다 Script Flush4 방식으로 클라이언트에 원하는 데이터를 전송하

는 방식이다. 다음은 동적으로 IFrame을 생성하여 Comet 서버에

연결을 맺는 자바스크립트 예제 코드이다.

function foreverFrame(url, callback) {

var iframe = body.

appendChild(

document.

createElement("iframe"));

iframe.style.display = "none";

iframe.src =

url + "?callback=parent.foreverFrame.callback";

this.callback = callback;

}

그리고 Comet 서버에서 이벤트가 발생할 때 Script Flush 방식으

로 전송되는 메시지는 다음과 같다.

<script>parent.foreverFrame.callback("the first message");</

script>

<script>parent.foreverFrame.callback("the second

message");</script>

3 http://www.w3.org/Protocols/

4 "<script>JavaScript 코드</script>" 형태를 서버에서 클라이언트로 전송하므으로 실시간으로 필요

한 코드를 클라이언트에서 실행하는 방식

XHR 스트리밍 방식

XHR 스트리밍 방식은 모든 웹 브라우저에서 지원하는 사실상의

표준인 XHR을 이용하여 서버와 장시간 연결하는 것이다. XHR

을 이용하면 응답 결과의 텍스트뿐 아니라 헤더에도 직접 접근할

수 있고 장애(fail-over)도 비교적 쉽게 처리할 수 있다.

이 방식으로 클라이언트에서 이벤트를 받는 원리는 다음과 같다.

1. XHR의 onreadystatechange 이벤트가 발생하면 readyState 변수를

확인한다.

2. readyState 변수가 3일 때, 요청 연결 후부터 현재까지 서버로부터 받

은 결과에 접근한다.

3. 결과 중 마지막 이벤트에 해당하는 텍스트를 가져온다.

readyState 변수가 3인 경우 서버에서 추가적인 응답이 발생한 것

이고, 4인 경우 서버의 응답이 완료된 것이다. 일반적인 AJAX 전

송에서 XHR을 이용할 때는 readyState가 4인 경우가 중요하지

만, XHR 스트리밍 방식에서는 이벤트 발생 시점을 처리하므로

readyState가 3인 경우가 더 중요하다.

XHR 스트리밍 방식에 대한 클라이언트 측의 주요 자바스크립트

코드는 다음과 같다.

function xhrStreaming(url, callback) {

var xhr = new XMLHttpRequest();

xhr.open('POST', url, true);

var lastSize;

// 최신 텍스트를 가져오기 위한 위치

xhr.onreadystatechange = function() {

var newTextReceived;

if(xhr.readyState > 2) {

// 최신 텍스트 가져오기

newTextReceived =

xhr.responseText.

substring(lastSize);

lastSize =

xhr.responseText.length;

callback(newTextReceived);

}

// 응답이 완료되면

//새로운 요청을 다시 만든다

xhrStreaming(url, callback);

}

Page 59: The platform 2011

NHN 서비스 들여다보기The Platform 201158

// 페이지가 이동되거나

// 창이 닫힐 때 호출함수 지정

window.onunload = foreverFrameClose;

Server-Sent Events 방식

htmlfile을 이용하여 Forever Frame 방식의 문제점을 해결한 인

터넷 익스플로러와 달리 오페라의 경우는 Forever Frame 방식

을 위한 별다른 해결책이 없었다. 그래서 다른 방식을 찾아보았

는데, HTML5에 정의된 서버 푸시(Server Push) 기술의 표준인

Server-Sent Events API를 2006년에 출시된 오페라 9버전부터 이

미 지원하고 있다는 것을 알게 되었다.6 따라서 현재 사용 중인

오페라는 거의 대부분 Server-Sent Events API를 지원한다고 볼

수 있으므로 이 방식을 적용하기로 했다. 다음 코드는 이 방식으로

Comet 서버에 연결하는 자바스크립트 예제이다.

function serverSentEvents(url, callback) {

var eventSource = document.

createElement('event-source');

handler = function(event) {

callback(event.data);

};

eventSource.addEventListener('rtcs-event',

handler, false);

eventSource.addEventSource(url);

// 오페라 9.60 이후부터 아래처럼 url을 지정하면

// handler가 2번 호출됨

//eventSource.setAttribute('src', url);

document.body.appendChild(eventSource);

}

Comet 서버에서는 이벤트가 발생할 때 다음과 같은 형태의 메시

지를 생성하면 된다. 이때 Event 항목은 여러 이벤트 종류를 구별

할 수 있는 이벤트 이름이고, data 항목은 클라이언트로 전송할 텍

스트 메시지이다.

6 http://my.opera.com/WebApplications/blog/show.dml/438711

하지만 위와 같은 방식을 인터넷 익스플로러와 오페라에서 구현해

보니 사용자 경험에 있어 실제 서비스에 적용하기 어려운 몇 가지

문제점이 발견되었다. 첫 번째는 이 방식으로 Comet 서버에 연결

하면 웹 브라우저의 상태 표시줄에 연결 중임을 계속 표시하는 것

이고, 두 번째는 서버에서 이벤트가 발생해서 클라이언트에 전달될

때마다 클릭하는 소리가 나는 것이다. 다행히 인터넷 익스플로러에

서는 htmlfile이라는 액티브X 컨트롤을 이용해서 앞의 문제점을

해결할 수 있는 방법이 공개되어 있었다.5 또한 이 방법은 2006년

에 구글 Talk 서비스에 처음 적용되어 Comet 커뮤니티에서는 이

미 대중화된 방법이었다.

다음 코드는 htmlfile을 이용하여 Forever Frame 방식을 구현한

예제이다. 앞의 자바스크립트 코드와 비교해보면 빨리 이해할 수

있다.

function foreverFrameByHtmlfile(url, callback) {

// 명시적인 가비지 콜렉터 호출을 위해

//'var transferDoc...'라고 하지 않음

transferDoc = new

ActiveXObject("htmlfile");

transferDoc.open();

transferDoc.write(

"<html><script>" +

"document.domain='" +

document.domain + "';" +

"</script></html>");

transferDoc.close();

var ifrDiv =

transferDoc.createElement("div");

transferDoc.body.appendChild(ifrDiv);

ifrDiv.innerHTML =

"<iframe src='" + url +

"'></iframe>";

transferDoc.callback = callback;

}

function foreverFrameClose() {

// transferDoc 참조를 없애고,

// 명시적으로 가비지 콜렉터를 호출함

transferDoc = null;

CollectGarbage();

}

5 http://cometdaily.com/2007/11/18/ie-activexhtmlfile-transport-part-ii/

Page 60: The platform 2011

RTCS 실시간 웹 서비스를 위한 도전The Platform 2011 59

서버로부터 전송받기 때문에 RTCS 서버와 Comet을 연결할 때

SOP를 위배하지 않는다. 또한 서비스 페이지는 다음과 같이 명시

적으로 2차 도메인까지 동일하다는 선언만 하면 양쪽으로의 코드

접근이 가능하므로 크로스 도메인 문제를 해결할 수 있다.

document.domain = "hangame.com";

보안 처리

RTCS를 개발하려는 목적이 웹 게임을 위한 기반 솔루션이다보니

보안을 중요하게 생각해야 했다. 2가지 측면에서 고려했는데, 클라

이언트 상태 변조를 막는 클라이언트 보안과 서버와 클라이언트 간

의 전송 데이터 변조를 막는 전송 보안이다.

클라이언트 보안

RTCS 기반의 웹 게임 또는 서비스에서 클라이언트 로직은 대부분

자바스크립트로 통해 구현된다. 따라서 누구나 쉽게 소스 코드를

볼 수 있고 실행 중인 로직을 변경할 수도 있다. 이를 방어하는 여

러 가지 기법을 적용하더라도 완벽한 보안은 불가능하다. 또한 보

안을 위해 여러 장치를 마련하더라도 스크립트 코드의 한계로 그

에 따른 성능 저하가 쉽게 나타나므로 적용이 쉽지 않다. 특히 사용

자의 PC 환경과 웹 브라우저 종류에 따라 그 정도가 달라지는 것도

큰 문제이다. 따라서 성능 저하가 비교적 적고 효과가 큰 다음과 같

은 방식을 적용하였다.

•자바스크립트 코드 압축 및 난독화

• 주요 로직은 모두 서버에서 처리하고 클라이언트에서는 주로 UI 처리에

집중

전송 보안

서버와 클라이언트 간의 웹 전송을 안전하게 할 수 있는 방법으로

HTTPS를 생각해볼 수 있다. 하지만 HTTPS는 충분히 신뢰할 수

있는 수단을 제공하는 대신 그만큼 비용과 성능에 문제가 있어 실

시간 데이터 전송에는 어울리지 않는다. 그래서 암호화 데이터를

직접 처리하려 했으나 이번에도 자바스크립트의 성능이 문제였

다. 즉 암호화/복호화 코드의 은닉이 사실상 불가능한 자바스크립

Event: rtcs-event

data: the first message

Event: rtcs-event

data: the second message

크로스 도메인 처리

앞에서 설명한 것과 같이 RTCS 클라이언트에서 Comet 구현을

위해 주로 이용한 것이 XHR인데, 이것은 아주 엄격한 SOP(Same

Origin Policy)7를 적용 받고 있다. 이 정책으로 보안성은 강화되

었으나 페이지 서버와 접속 서버의 도메인이 서로 다르면 골치 아

픈 문제가 발생한다.

RTCS를 실제 서비스에 적용할 때 트래픽을 분산 관리하기 위하여

웹 서버와 Comet 서버를 분리할 가능성이 매우 크다. 즉 크로스 도

메인(Cross Domain) 상태가 되는 것이다. RTCS에서는 이런 경

우를 위해 다음과 같이 IFrame을 이용하여 2차 도메인까지만 동일

하면 SOP의 제약을 피할 수 있도록 하였다.

그림 4 2차 도메인 허용시 동작 구조

위의 그림에서 페이지가 호출하는 서버와 RTCS 서버의 도메인은

서로 다르다. RTCS 클라이언트에 필요한 최소한의 HTML 페이

지 소스와 자바스크립트 코드는 숨겨진 IFrame을 이용하여 RTCS

7 http://en.wikipedia.org/wiki/Same_origin_policy

서비스 페이지

서비스 페이지

IFrame(hidden)

RTCS Client

페이지 호출 서버

(www.hangame.com)

RTCS 서버

(rtcs.hangame.com)

Page 61: The platform 2011

NHN 서비스 들여다보기The Platform 201160

서버 메시지 전송 실패 시 처리는?

Long Polling, 스트리밍 두 방식 모두 연결을 재설정하는 순간이

있다. 하나의 요청에 대한 응답을 종료하고 새로운 요청을 시작하

는 사이에 서버에서 메시지를 전송하려고 할 경우 해당 RTCS 세

션의 응답 연결이 유효하지 않기 때문이다. 이 경우 RTCS는 메시

지를 세션별 장애 처리 메시지 큐(FailOver Message Queue)에

저장해 두었다가 연결이 다시 유효해지면 전송하게 된다. 이 부분

은 RTCS 서버 구현에서 메시지 전송을 보장하는 가장 주요한 부

분이다. 아래 그림은 장애 처리 과정을 도식화한 것이다.

그림 6 서버 메시지 전송 실패 시 장애 처리 구조

대용량 처리

앞에서 설명한 구성 요소 중 RTCS 서버는 로

직에서 이용할 수 있는 API를 제공하고 있다.

제공하는 프로세서의 인터페이스를 구현하여

RTCS 서버에 로직을 구현할 수도 있다. 하지

만 이 경우 로직을 수평적으로 확장하는 것이

어려워진다. 그래서 별도의 로직 서버를 구성

하고 RTCS 서버에서는 프로세서의 인터페이

스를 구현하여 해당 로직 서버와 연계하는 커

넥터(Connector)를 구현하도록 가이드하고

있다. 기본적으로 BLOC 서버 통신을 위한 커

트 코드에서 활용 가능한 암호화 방식은 공개키 알고리즘인 RSA

방식 정도인데 테스트 결과 성능상 서비스에 적용하기 힘든 결과

가 나왔다. 결국 메시지 자체는 JSON 형태 그대로 보내고, 대신

원본 메시지의 무결성을 검증하는 일종의 Checksum 방식을 적

용하였다. 실제로 RTCS는 이 방식의 보안성을 좀 더 높이기 위해

HMAC8 알고리즘을 적용하였다.

다양한 네트워크 상황에 대한 대처

스트리밍 방식의 경우 응답 연결을 통해 계속 서버의 메시지가 전

달되는데, 이때 너무 작은 메시지가 브라우저별 최소 유효 메시지

길이보다 작거나 프록시 등의 네트워크 환경에

따라 바로 전달되지 않는 경우를 감안하여 "최소

전송 보장 길이"라는 것을 구현했다.

"최소 전송 보장 길이"는 스트리밍 방식의 핸드

셰이킹 과정에서 계산하게 되는데, 연결을 통해

sessionKey 정보를 전달하면서 더미(dummy)

메시지를 함께 전달한다. 그럼 클라이언트에서는

최소 이벤트가 발생한 시점에 받은 메시지 길이

를 계산하여 "최소 전송 보장 길이"를 정하게 되

고 이를 RTCS 서버에 전달한다. 다음 그림은 이

과정을 도식화한 것이다.

그림 5 최소 전송 보장 길이 설정 구조

8  http://en.wikipedia.org/wiki/HMAC

Page 62: The platform 2011

RTCS 실시간 웹 서비스를 위한 도전The Platform 2011 61

RTCS 기반 서비스 구성하기

다음은 RTCS 기반으로 서비스를 구성하는 경우에 대한 예시이다.

웹 서버의 경우는 기존과 마찬가지로 HTML 페이지와 정적인 리

소스를 처리하고 실시간성 요청 및 서버 메시지는 RTCS 클라이언

트와 RTCS 서버가 처리한다. 앞에서 설명한 것 같이 RTCS 서버

와 로직 서버를 분리하고 별도 세션 저장소를 두어 RTCS 세션을

관리한다(그림 8).

앞으로 할 일

현재 국내외 웹 서비스 개발의 최대 화두는 단연 HTML5일 것이

다. HTML5에는 Comet 기술을 완전히 대체할 수 있는 Native

브라우저 구현 기술이 정의되어 있는데, 그것이 바로 웹 소켓

(WebSocket)10이다. 웹 소켓은 웹 기반의 안전한 TCP 소켓을 말

하며 클라이언트와 서버 간의 양방향 통신(Full Duplex)이 가능하

다. 물론 기존 Comet 기술보다 성능도 더 좋을 것이다. 따라서 언

제가 될지는 아직 정확히 모르지만 HTML5가 대중화될 때를 대

비해서 RTCS도 웹 소켓을 적용할 수 있도록 준비해야 할 것이다.

사실 RTCS 클라이언트의 주요 API는 웹 소켓의 API 형태를 참고

하여 구현했다. 그 이유는 향후 웹 소켓이 대중화되었을 때 쉽게 웹

소켓으로 교체 또는 브라우저별로 기존 Comet 방식과 병행 적용

할 수 있도록 하기 위함이다.

(이 글은 2010년 11월에 작성된 것이다. 당시의 RTCS

에 비해 성능이 크게 향상되고 톰캣뿐만 아니라 Jetty

도 지원하는 RTCS 1.0 버전이 2011년 9월에 정식으

로 릴리스되었다. 초기 구현에서 병목이 되었던 부분

을 새롭게 설계하고 구현하여 비약적인 성능향상이 있

었다. 이 내용은 2011년 11월에 있을 KGC에서 공유

될 예정이다.)

10 http://dev.w3.org/html5/websockets/

넥터가 구현되어 있으며, 반대로 로직 서버에서 RTCS 서버에 연

결된 클라이언트에 메시지를 전송하기 위한 RTCS 서버 API 모듈

을 제공하고 있다.

그런데 아직 해결되지 않은 문제가 있었으니 바로 로직 서버에서

특정 RTCS 클라이언트에 메시지를 전송하고자 할 때 해당 클라이

언트가 어느 RTCS 서버에 연결되어 있는지 알기 힘들다는 것이었

다. 그래서 RTCS 세션 정보를 공유할 별도의 세션 서버를 도입하

게 되는데, 현재 적용 중인 서비스에는 Arcus9를 도입하였다.

그림 7 로직 서버, 세션 서버와의 연동

그림 8 RTCS 기반 서비스 구성

9 http://www.slideshare.net/sdec2011/sdec2011-arcus-nhn-memcached-

cloud-8467157

Page 63: The platform 2011

NHN 서비스 들여다보기The Platform 201162

NHN이 제로보드를 인수한 것은 2007년이다. 당시만 해도 웹 게

시판 기능만 가지고 있었으나 다양한 CMS 기능을 추가하면서 XE

로 이름을 바꿨다. 2008년 잠깐 ‘제로보드 XE’라는 명칭을 사용하

기도 했으나 현재 공식 명칭은 그냥 ‘XE’이다. 제로보드라는 이름

으로는 콘텐츠 관리 시스템으로 자리잡는 것에 제약이 있어 사용하

지 않게 되었다.1

XE 구성 요소

XE를 다른 콘텐츠 관리 시스템과 구분 짓는 가장 주요한 특징은

코어와 모듈을 분리하여 높은 확장성을 얻었다는 것이다. 예를 들

면 다른 설치형 콘텐츠 관리 시스템은 설치 파일에 블로그와 같은

모듈이 기본적으로 포함되어 있지만 XE는 코어와 모듈을 완전히

분리한 상태로 제공한다. 만약 XE를 블로그로 사용하려면 XE 코

어 외에, XE의 블로그 모듈인 Textyle을 별도로 설치해야 한다. 물

론 정해진 모듈 외에 다른 모듈도 탑재할 수 있다. 필요한 것은 무

엇이든 모듈, 위젯, 애드온 형태로 만들어 추가할 수 있다.

1 XE개발팀 또한 제로보드가 아닌 XE라고 불러주는 것을 좋아한다.

XE(eXpress Engine)는 PHP 기반의 설치형 콘텐츠 관리 시스템

(Content Management System)이다. NHN은 XE를 오픈소스

(LGPL v2)로 공개하고 있다. 오픈소스 생태계와 많은 독립 사이트

를 지원하고 있는 것이다. NHN의 입장에서는 XE를 이용하여 양

질의 웹 문서 검색 결과를 얻을 수 있다.

XE 역사

XE는 제로보드에 뿌리를 두고 있다. 제로보드는 1999년에 고영수

부장(현 NHN 소속)이 개발하기 시작한 설치형 웹 보드이다. 1999

년은 네이버가 첫 서비스를 시작한 해이기도 하다. 2009년 보안 취

약점 등을 이유로 배포가 일시 중단된 때도 있었지만, 제로보드는

약 10년간 한국 웹 생태계에 널리 뿌리내리며 사용되어 왔다.

XE 현황과 발전 방향오픈소스 XE가 사용자와 만나는 접점인 프런트 엔드 분야를 담당하고 있습

니다. 다양한 환경과 웹 접근성을 고려하여 누구나 사용할 수 있는 제품을

만들자는 꿈을 실현하고 있습니다. 꿈을 실현할 기회를 준 회사 님, 고맙습

니다.

•XE개발팀 _ 정찬명

Page 64: The platform 2011

XE 현황과 발전 방향The Platform 2011 63

모듈

XE에서 모듈은 사이트에 특정 기능을 구현하기 위해 추가로 설치

하는 프로그램이다. XE에서 제공하는 대표적인 모듈에는 게시판,

카페, 블로그, 위키 등의 모듈이 있다.

위젯

위젯은 레이아웃 또는 모듈에 삽입하는 컴포넌트이다. 주로 모듈에

서 생성한 데이터를 화면에 표시하는 역할을 한다. 예를 들어 게시

판 모듈에서 작성된 최신 글 목록을 사이트의 시작 페이지에 나타

내고 싶을 때 위젯을 사용한다. 관리자 페이지에서 생성한 사이트

탐색 구조를 내비게이션 형태로 보여주기도 한다. XE에서 제공하

는 대표적인 위젯으로는 내비게이션 위젯, 최근 게시물 위젯(콘텐

츠 위젯), 다국어 선택 위젯, 로그인 위젯 등이 있다.

위젯 스타일

위젯 스타일은 위젯의 외각을 꾸며 준다. 위젯 스킨이 위젯의 디자

인을 꾸며준다면 위젯 스타일은 위젯의 외각 부분에 대한 스킨이라

고 정의할 수 있다. 위젯 스타일을 적용하면 위젯 외각 변경 외에도

위젯 제목이나 더보기 링크 등과 같이 내용 또는 기능을 추가할 수

있다.

애드온

애드온은 모듈과 결합하여 자신의 기능을 수행하는 작은 프로그램

이다. 애드온은 모듈과 같이 스스로 기능을 수행하지만 데이터 입

력을 다루지 않는다는 점에서 모듈과 다르다. XE에서 제공하고 있

는 대표적인 애드온으로는 방문자 카운터, 자동 링크 걸기, 이미지

자동 크기 조절 애드온 등이 있다.

레이아웃

사이트에 모듈, 위젯, 애드온을 설치한 후 이들을 화면에 적절하게

배치하여 보여줘야 한다. 이렇게 화면 배치를 담당하는 것이 레이

아웃이다. 레이아웃과 추가 기능(모듈, 위젯, 애드온)은 긴밀한 관

계를 유지한다. 추가 기능이 없으면 레이아웃은 아무런 내용도 출

력할 수 없다. 반대로 추가 기능이 설치되어 있으나 레이아웃이 없

으면 각 요소의 출력 위치를 알 수 없기 때문에 화면에 내용을 표시

할 수 없다. 단, 모듈은 레이아웃 없이 출력 가능하다. 예를 들면 게

시판은 레이아웃 없이 출력할 수 있다.

그림 1 XE 구성 요소와 사용 환경

그림 2 XE 구성 요소

XE 코어

코어는 XE의 설치와 운영을 담당하는 핵심으로, XE 코어가 정식

명칭이다. 문서 모듈, 댓글 모듈, 회원 모듈, 쪽지 모듈, 페이지 모

듈, 레이아웃, 모바일 레이아웃을 기본적으로 탑재하고 있으며 코

어만으로도 정적 페이지를 사용하는 사이트를 생성할 수 있다. 게

시판, 블로그, 위키와 같은 동적인 사이트를 구성하려면 사용자가

직접 해당 모듈을 추가해야 한다. 이미 만들어진 모듈을 추가로 탑

재하는 것은 클릭 한 번이면 된다.

Page 65: The platform 2011

NHN 서비스 들여다보기The Platform 201164

XE가 아직 해외에 거의 알려지지 않았기 때문에 통계로 제공하는

수치들은 대부분 국내 사용자인 것으로 추정하고 있다. 한국에는

GnuBoard2, Kimsq3와 같은 오픈소스 제품이 경쟁 상대이나 한

국 내 콘텐츠 관리 시스템의 점유율 통계를 제공하는 곳이 없어 점

유율을 정확하게 파악하지 못하고 있다.

해외 CMS 시장 점유율

세계 시장에서는 WordPress4 , Joomla5 , Drupal6과 같은 제

품이 시장의 70% 이상을 점유하고 있다. 해외 시장에서 XE 점유율

은 0.1%도 되지 않는 상황이다.

그림 5 세계 시장 점유율(2011.05)

XE 쇼케이스

2011년 5월 기준으로 XE를 이용하여 구축된 웹 사이트

가운데 잘 알려진 사이트는 다음과 같다.

2 http://sir.co.kr/

3 http://kimsq.com/

4 http://wordpress.org/

5 http://www.joomla.org/

6 http://drupal.org/

스킨

스킨은 사이트에 설치된 프로그램의 디자인을 의미한다. HTML,

CSS, JS, XE 템플릿 문법으로 구성되어 있으며 모듈 또는 위젯의

옷이라고 할 수 있다. XE 공식 사이트의 다운로드 게시판은 프로그

램별로 다양한 스킨을 제공하고 있다. 이를 잘 활용하면 본인의 개

성이 깃든 사이트를 만들 수 있다.

XE 사용 환경

XE는 다양한 운영체제(리눅스, 윈도우)와 DBMS(MySQL,

MSSQL, CUBRID, PostgreSQL, SQLite, Firebird)를 지원하고

있으며 PHP4, PHP5 버전을 모두 지원하고 있다. 이렇게 다양한

환경을 지원하다 보니 상호운용성이 좋다고 말할 수 있지만 각 환

경별로 고려 사항이 많아 성능 개선의 걸림돌이 되기도 한다.

XE 사용 현황

XE 사이트 수

네이버 웹 문서 검색 결과의 3분의 1 정도가 제로보드 또는 XE

일 것으로 추정하고 있다. 현재 약 13만 개의 사이트가 XE로 운

영되고 있으며 65만 건 이상의 다운로드가 있었다. XE 코어가

다섯 번 다운로드되면 그 가운데 하나의 사이트는 운영이 되는

셈이다. 최근 1년간 4만 8천 개의 XE 사이트가 추가됐고 이런

추이는 당분간은 지속될 것으로 전망하고 있다.

그림 4 최근 1년간 XE 사이트 증가 추이 (2010.05~2011.04)

Page 66: The platform 2011

XE 현황과 발전 방향The Platform 2011 65

그림 7 Cubrid에서 운영 중인 XE 마켓

XE의 장점

웹 서비스가 필요로 하는 거의 모든 요구 사항을 빠르게 수렴할 수

있다. 설치부터 초기 화면 구현까지 단 한 시간이면 족하다. 기본

디자인 리소스도 준비되어 있다. 당장 무엇인가를 보여 주기 쉽다.

콘텐츠 관리 시스템의 기능적 요구 사항 충족

XE의 현재 위치를 파악하기 위하여 CMS Matrix7라는 웹 사이

트를 통해 XE와 해외 유명 오픈소스 CMS와 기능을 비교했다. 비

교 대상은 WordPress, Joomla, Drupal이었으며 CMS Matrix에

서 제시하는 116개의 기능에 대한 지원 여부를 파악했다.

표 3 해외 유명 콘텐츠 관리 시스템과 기능 비교

구분 Wordpress Joomla Drupal XE

지원 45 66 51 49

미지원 27 21 20 47

기능 무료 지원 36 29 35 12

제한적 지원 8 0 10 8

7 http://www.cmsmatrix.org/

표 1 XE를 사용하여 구축된 웹 사이트

사이트 이름 사이트 주소

쿠팡 http://coupang.com/

THOTH http://www.thoth.kr/

한국경제신문사블로그 http://blog.hankyung.com/

안드로이드펍 http://www.androidpub.com/

국민참여당 http://www.handypia.org/

농심 USA http://nongshimusa.com/

그림 6 XE 기반의 소셜 커머스 사이트 쿠팡

2011년 5월 현재 한국에서 XE 관련 모듈이나 스킨을 판매하는 웹

사이트는 다음과 같다.

표 2 XE 관련 모듈이나 스킨으로 판매하는 웹 사이트

사이트 이름 사이트 주소 설명

XE 마켓 http://www.xemarket.co.kr/ 5월 16일 오픈 베타 진행 중

PremiumXE http://www.premiumxe.com/ 5천원 ~ 15만원 사이의 스킨 판매

KSODESIGN http://www.ksodesign.com/ 2만원 ~ 5만원 사이의 스킨 판매

Page 67: The platform 2011

NHN 서비스 들여다보기The Platform 201166

•SNS 퍼나르기 기능 제공

•구매/결제

그림 9 XE로 구축된 DevCafe

기업의 그룹웨어로 XE가 사용되는 경우도 있다. 그룹웨어로 사

용되는 경우는 보안상 외부 노출이 되지 않기 때문에 어느 곳에

서 사용을 하는지 정확히 알기 어려운 측면이 있어 사례를 들기

힘들지만 NHN의 경우 DevCafe와 nPlatform과 같이 기술 정보

를 공유하는 커뮤니티 시스템이 XE로 구현되어 있다. DevCafe,

nPlatform은 다음과 같은 기능을 커스터마이징하여 사용하고

있다.

•기존 그룹웨어와 회원 정보 SSO(Single Sign-On) 연동

•조직별 또는 특정 주제별 소모임을 위한 커뮤니티 공간(Cafe) 제공

•게시판(Board) 및 위키(Wiki) 제공

•프로젝트 호스팅(Project + Issuetracker) 제공

•코어와 모듈 분리로 확장성 증대

그림 10 XE는 코어와 모듈이 분리되어 있어 원하는 형태로 확장 가능함

해외 유명 콘텐츠 관리 시스템과 비교하여 XE가 지원하지 못하는

항목이 많은 것은 사실이다. 그러나 XE가 지원하지 않는 항목이 많

은 것으로 나타나는 이유는 다른 콘텐츠 관리 시스템이 서드 파티

애드온(3rd-Party Add-on)으로 제공하는 기능을 XE가 아직 지

원하지 않기 때문이다. 서드 파티 애드온으로 제공하는 기능은 비

교적 중요도가 떨어지는 항목으로 간주하고 있으며 이를 따라잡는

것은 시간 문제일 것으로 판단하고 있다. 지원 항목은 대부분 필수

적인 기능이라고 볼 수 있으며 지원 항목만 놓고 보면 XE도 해외

유명 콘텐츠 관리 시스템과 견줄만하다고 평가할 수 있다. 한마디

로 XE도 조금만 다듬으면 해외 시장에서 경쟁력을 갖출 수 있으며

CMS가 갖추어야 할 기본적인 기능을 이미 거의 갖추고 있다는 의

미이다.

국내 시장에서 검증된 제품

그림 8 메일 주소로 로그인하도록 XE를 커스터마이징한 사례

13만 개의 웹 사이트가 이미 XE로 운영 중이다. 쿠팡, 전자신문, 한

국경제신문, 안드로이드 한국 커뮤니티, 국민참여당, 농심 USA와

같이 규모있는 웹 서비스가 XE를 선택한 것은 XE로 충분히 원하

는 기능을 커스트마이징하여 만들 수 있다는 것을 검증한 사례라고

볼 수 있다. 쿠팡에서 XE는 다음과 같은 기능을 커스터마이징하여

제공하고 있다.

•이메일 주소를 ID로 처리하여 회원 가입 절차를 간소화

•이벤트 상품 전시 및 상품 Q&A 게시판 운영

160

140

160120

100

80

60

40

20

0

Page 68: The platform 2011

XE 현황과 발전 방향The Platform 2011 67

프로젝트 카테고리 프로젝트 수

위젯 스킨 3

위젯 스타일 스킨 1

레이아웃 10

에디터 컴포넌트 2

패키지 28

기타 31

XE 모듈 프로그램과 스킨은 프로젝트를 통해서만 생산되는 것은

아니다. 프로젝트 개설 없이 개인이 창작해서 등록하는 경우도 많

기 때문에 실제로 XE 공식 웹 사이트의 자료실에 등록되어 있는 프

로그램과 스킨은 통틀어 약 750개 수준이다.

표 5 XE 공식 웹 사이트에 등록된 프로그램과 스킨 수

프로그램 스킨 모바일 스킨 데이터 이전 툴

289 438 7 12

XE의 단점

XE의 단점은 너무 많은 기능을 제공하면서 UX(User eXperience)

문제를 적절하게 풀지 못했다는 점과 개발자 문서가 풍부하지 않아

외부 개발자가 적응하기 어렵다는 점이다. XE로 뭔가 보여주려면

내공이 필요하다.

UX 완성도

그림 12 XE의 첫 인상

코어, 모듈, 위젯, 애드온, 레이아웃, 스킨과 같은 구성 요소가 각각

잘 분리되어 있기 때문에 원하는 구성 요소만 별도로 추가 개발하

거나 탑재하는 것이 가능하다. 이것은 사용자가 XE를 이용하면 얼

마든지 원하는 형태로 확장하거나 커스트마이징할 수 있다는 것을

의미한다.

서드 파티 지원

XE 개발팀은 코어를 포함하여 15개의 중요한 모듈 프로그램을 직

접 만들고 유지 보수하고 있다. 그러나 서드 파티(3rd Party) 개발

자에 의하여 생산되는 모듈과 스킨의 규모는 10배 더 방대하다. 서

드 파티 개발자가 직접 운영하고 있는 XE 관련 모듈, 위젯, 스킨 프

로젝트의 규모는 현재 약 150곳에 이르며 XE 개발팀은 이들 서드

파티 개발자에게 프로젝트 호스팅 공간을 제공하고 있다. 하나의

프로젝트에서 통상 하나 정도의 모듈을 개발하고 있기 때문에 프로

젝트 개수는 곧 모듈 개수와 정비례한다.

그림 11 XE 개발팀과 서드 파티의 프로젝트 수

표 4 XE 프로젝트 카테고리별 분포

프로젝트 카테고리 프로젝트 수

코어 3

모듈 프로그램 69

모듈 스킨 15

애드온 0

위젯 프로그램 9

XE 개발팀 프로젝트

160

140

160120

100

80

60

40

20

0

XE 써드파티 프로젝트

Page 69: The platform 2011

NHN 서비스 들여다보기The Platform 201168

문서화

서드 파티 웹 개발자와 디자이너가 XE에 갖는 가장 큰 불만은 참

고할만한 문서가 없거나 찾기 어렵다는 점이었다. 문서가 전혀 없

던 것은 아니었으나 XE 개발팀은 프로젝트 호스팅이 제공하는 위

키 공간에 XE 매뉴얼을 썼고 XE 공식 웹 사이트에서 적절하게 안

내하지 못했다. 매뉴얼은 찾기 어렵거나 제대로 업데이트되지 않았

다. 다행히도 작년에 매뉴얼 작업에 공을 들인 결과 다음과 같은 정

도의 구색을 갖추게 되었다. XE에 필요한 매뉴얼은 크게 사용자 매

뉴얼, 스킨 제작 매뉴얼, 개발자 매뉴얼로 분류할 수 있다.

표 6 XE 문서화 현황

매뉴얼 한글 영문 일문 중문

사용자 매뉴얼 O O O X

스킨 제작 매뉴얼 O O O X

개발자 매뉴얼 O O O X

아직은 PDF와 마이크로소프트 워드 형식의 매뉴얼만 제공하고 있

지만 추후, 웹 문서 버전의 매뉴얼을 제공하여 검색이 가능하도록

할 예정이다.

막강한 경쟁자

국내의 콘텐츠 관리 시스템 시장은 각각 운영되는 커뮤니티의 규

모로 미루어 짐작 하건데 XE가 다른 콘텐츠 관리 시스템 도구보

다 높은 점유율을 차지하고 있다고 추정한다.8 그러나 GnuBoard,

Kimsq와 같은 제품은 XE보다 속도가 빠른 것으로 평판이 나 있다.

XE의 경우 다양한 운영체제, DBMS, 여러 버전의 PHP를 지원한

다는 장점이 성능 측면에서 걸림돌이 되기도 한다. 상호 운용성을

추구하다 보니 성능 최적화 측면에 한계가 있다.

한편 해외 시장에서 XE가 콘텐츠 관리 시스템으로서 경쟁 구도를

갖추려면 최소한 세계 시장 점유율이 0.1% 이상은 되어야 통계 사

이트에 이름이라도 올릴 수 있다. XE는 2012년 연말까지 세계 시

장 점유율을 0.1% 이상으로 끌어올리는 것을 중장기 목표로 삼고

있다.

8 아직 한국 CMS 도구들의 점유율을 제공하는 통계가 없음.

한때 XE 개발팀에서 잠깐 일했던 기획자는 XE의 첫 인상을 이렇

게 표현했다. "마치 전투기 조종석에 앉은 기분이었다. 무엇부터 해

야 할지 몰랐고 어떤 버튼을 잘못 건드릴까 두려웠다" 이렇게 말한

지 벌써 2년이나 흘렀지만 XE의 UX는 좀처럼 좋아지지 않았다.

기획자는 넘치는 의욕을 가지고 항상 팀에 제안하지만 돌아오는 대

답은 거의 같았다. "그것은 현재 XE 구조상 어렵거나 불가능하다"

이런 대화는 현재의 XE가 설계 단계에서부터 사용자 경험을 고려

하지 못했기 때문에 발생하는 대화였던 것으로 판단한다. 기능 구

현을 먼저 해놓고 UX 문제는 나중에 해결한다는 생각은 이제 바꿔

야 한다고 생각한다. 물론 과거와 비교하면 UX를 중요한 요소라고

생각하고 있으며 이는 인적 자원의 지원으로 해결 가능하다고 본

다. 현재 XE 코어를 설치하면 관리자 초기 화면은 다음과 같은 화

면을 보여준다.

그림 13 XE 코어 1.4.5.7 버전의 관리자 초기 화면

2년 전 상황과 비교했을 때 큰 변화라고 할만한 것은 화면 중앙에

등장하는 접속자 통계와 그래프이다. 2년 전에는 저 위치에 현재

설치된 모듈과 애드온의 목록이 나열되었다. 현재 설치된 모듈과

애드온 정보가 중요하긴 하지만 관리자가 매번 확인해야 할 정보는

아닐 것이다. 다행히도 관리자 인터페이스는 올해 안에 개선할 예

정이다.

Page 70: The platform 2011

XE 현황과 발전 방향The Platform 2011 69

니다. 개선된 코어는 사용 중인 낡은 버전의 모듈과 문제없이 잘 호

환되어야 하고 속도가 빨라야 한다는 것이 사용자의 기본적인 요구

이다.

둘째, '사용자 경험'을 개선해야 한다. 많은 기능을 제공하여 복잡하

더라도, 일반적인 사용자가 직관적으로 사용할 수 있도록 만들어야

한다. 우리는 늘 선택지를 줄이는 것과 선택권을 주는 사용자 인터

페이스 사이에서 고민하고 있다.

셋째, '품질'을 측정하고 보증해야 한다. 지금까지는 딱히 품질을 측

정하지도 않았고 오류가 있는 프로그램을 배포하더라도 사용자의

피드백을 받아서 빠르게 패치를 진행하면 된다고 생각해 왔다. 그

러나 이것은 장기적으로 제품에 대한 신뢰를 잃게 되는 결과를 가

져올 것이다.

앞으로의 전망

첫째, XE 코어 1.5 버전을 글로벌 웹 사이트를 통해 해외에 배포할

것이다. XE 글로벌 웹 사이트, 포럼 모듈을 루마니아 XE 조직에서

개발하고 있다. 이에 맞춰 XE 코어 1.5 버전을 한국에서 개발하고

있고 해외 시장을 겨냥한 각종 스킨을 중국에서 개발하고 있다.

둘째, 사업 규모의 차이는 있겠지만 클라우드 서비스에 탑재될 것

으로 전망한다. 이제 XE를 이용하여 누구든 사업을 쉽게 시

작할 수 있을 것이다.

셋째, 수익을 창출하는 비즈니스 모델이 될 것이다. 현재까지

는 XE를 이용해서 수익을 거두는 개인은 있었지만 기업에

직접적인 수익을 주지는 못했다. XE의 점유율 확장과 함께

빠르고 확장성 있는 플랫폼으로서의 진가를 인정 받는다면

곧 가능할 것이다.

맺음말

XE는 과연 글로벌 시장에 성공적으로 안착하고 수익을 창출하는

비즈니스 모델로서 성공할 수 있을까? XE의 장점은 제품 자체와

이를 지원하는 주변의 환경이다. 한편 UX, 문서화, 품질에 대한 현

재 상태를 자각하고 집중적으로 보완해 나간다면 글로벌 시장 진출

과 비즈니스 모델로서의 성공 가능성은 더 높아질 것이다.

지역색

XE는 한국 사람에 의해 만들어졌고 거의 한국에만 배포가 되었기

때문에 한국 사용자가 선호하는 특징이 고스란히 녹아 있다. 예를

들어 글을 쓰는 실적에 따라 회원에게 포인트를 지급하고 그 포인

트에 따라 회원에게 레벨을 부여하는 것은 해외 커뮤니티에서는 좀

처럼 발견하기 어려운 것이지만 한국 커뮤니티에서는 쉽게 찾아볼

수 있다.

한국에서는 웹 사이트에 회원으로 가입할 때 거주지 주소를 거의

필수적으로 입력받으며 우편 번호를 검색할 수 있는 데이터베이스

도 제공한다. 그러나 해외 웹 사이트 동향은 특별한 경우가 아니면

회원의 거주지 주소를 묻지 않으며 우편번호 검색 데이터베이스도

제공하지 않는다.

한국에서는 게시판을 주로 사용하지만 해외에서는 포럼 형식을 사

용한다. 이런 지역적인 특징은 XE가 세계 시장에 진출할 때 버리거

나 절충해야 하는 여러 가지 특징 중 하나이다. 이런 지역색의 차이

를 인식해야 하는 이유는 XE의 2011년 목표가 글로벌 시장에 진출

하는 것이기 때문이다.

그림 14 한국의 게시판과 외국 포럼의 차이점(저자의 레벨 표시와 글 정렬 순서가 다름)

XE 발전 방향

2011년은 XE 세계 시장 확산의 원년이다. 클라우드 서비스로 XE

가 포함될 것이라 거의 확신하고 있다. XE의 발전 방향을 내부와

외부로 구분하고 각각 세 가지로 정리했다.

내부의 시선

첫째, 글로벌 콘텐츠 관리 시스템에 걸맞는 '안정성'을 지녀야 한다.

새로운 기능을 추가하거나 모듈을 추가하는 것이 우리의 목표가 아

Page 71: The platform 2011

NHN 서비스 들여다보기The Platform 201170

플레이넷?

아래 그림에서 보듯 플레이넷1 서비스는 네이버의 콘텐츠 자산과

한게임의 게임 채널링 노하우를 바탕으로 사용자에게 게임을 할 수

있는 환경과 게임 정보 콘텐츠를 제공하는 통합 게임 채널링 서비

스이다.

네이버에서 게임을 검색한 후 클릭하

면, 플레이넷의 해당 게임 브릿지로 이

동하고, 사용자는 브릿지를 통해 게임

에 대한 자세한 정보와 부가 콘텐츠를

제공받을 수도 있다. 입점 게임일 경우

엔 네이버 아이디로 게임도 하고, 아이

템도 구매할 수 있다.

1 http://playnet.naver.com

2010년 11월 ‘Life is a Game, PlayNet!’이란 표어로, 플레이넷 베

타 서비스를 시작했다. 이 글에서는 플레이넷의 서비스 개념과 시

스템 구성에 대해 간략히 소개하고, 플레이넷의 큰 축인 게임 채널

링 서비스에 대해 설명한다.

그림 1 플레이넷 서비스 구조

블로그

카페미투데이

Asset & Contents

통합검색

지식인

SF게임DB

공유의재생산

Service KnowhowChanneling Biz Model

TechnicalSupportOur Patner

웹진

Our Patner

퍼블리셔개발자

정보컨텐츠 게임플레이

SocialNetwork

네이버ID 연계

구매연계검색과

전문정보

"브릿지"

플레이넷, 네이버 ID로 모든 게임을서비스의 빛을 쫓는 불나방이 된 지도 벌써 10년이 훌쩍 넘었네요.

아직 내가 쫓는 그 빛을 보지 못해 여전히 미련을 버리지 못하고 주위를 맴

돌고 있습니다.

언젠가는 우리 앞에 그 무엇보다 아름답게 비칠 그 빛을 찾아......

•게임전략서비스개발팀 _ 권재욱

Page 72: The platform 2011

플레이넷, 네이버 ID로 모든 게임을The Platform 2011 71

미투데이(현재 서비스 중), 카페, 지식iN(서비스 예정) 등과 같은

서비스는 이미 API가 잘 갖춰져 있어 플레이넷 서비스에 연동할 때

특별히 문제가 될만한 기술적 이슈가 없다. 플레이넷 개발자는 연

동 서비스의 점검이나 장애가 플레이넷에 전파되지 않게 하기 위해

방어 코드를 작성하고, 연동 콘텐츠에 대해 추가적으로 내부 캐시

를 처리(OSCache)하는 등 사용자에게 최대한 안정적인 서비스를

제공하는 데 초점을 맞췄다.

플레이넷은 기본적으로 네이버 검색 데이터를 기반으로 게임 정보

를 제공하는데, 해당 정보가 서비스에 표시되는 방법은 그림 3과

같다.

시스템 소개

플레이넷 서비스는 크게 게임 정보 콘텐츠 시스템, 게임 채널링

시스템, 회원 및 인증 시스템, 빌링 시스템으로 나눌 수 있다. 이

절에서는 각 시스템에 대해 간략히 소개하고, 게임 채널링 시스템

과 회원 및 인증 시스템에 대해서는 다음 절에서 조금 더 자세하

게 다룬다.

게임 정보 콘텐츠 시스템

네이버 영화, 만화, 뮤직처럼 게임 분야의 주제형 서비스 역할을 하

는 플레이넷은 검색, 지식iN 등 네이버의 다른 서비스에서 생산 또

는 가공되는 양질의 게임 관련 콘텐츠를 한 곳에 모아 사용자에게

제공한다. 또한, 미투데이와 연계해 게임에 대한 사용자의 다양한

목소리도 함께 담아내고 있다.

그림 2 플레이넷 연동 구조

영화

검색

지식IN

블로그

카페

네이버영화

주제형서비스

음악

네이버뮤직

만화

네이버만화

게임

엔터테인먼트 주제

서비스

Season 1 • 통합 검색 게임 데이터베이스 공유• 검색 경로 연동 - 랜딩페이지 역할

• 게임별 정보 제공• 입점 게임 이용 - ID 및 구매 연동

• 게임 스마트파인더 강화

• 통합 린처

• 온라인 게임 지식IN 컨텐츠 연동

• 전문성 있는 온라인게임 UGC 연동

• Social Network 연계

Season 1

Season 1

Season 1

Season 2

Season 2

Season 2

Season 2

플레이넷 서비스

블로그 카페

지식iN

Page 73: The platform 2011

NHN 서비스 들여다보기The Platform 201172

그림 3 게임 정보의 데이터 흐름도

게임 채널링 시스템

한게임-테일즈러너, 다음-테트리스 등과 같이 외부 서비스에서 제

공하는 게임을 내부 유저도 실행할 수 있게 해주는 서비스를 게임

채널링(일반적으론 줄여서 채널링) 서비스라고 하는데, 플레이넷

에서 이용 가능한 게임도 대부분 채널링 서비스 형태이다.

한게임 아이디로 나우콤에서 퍼블리싱하는 테일즈 러너를, 다음

아이디로 한게임에서 퍼블리싱하는 테트리스를, 네이버 아이디로

넥슨에서 퍼블리싱하는 크레이지 아케이드를 즐길 수 있게 한다는

것은 단순하게 생각해 보면. 회원 정보를 넘기고, 인증해주고, 게

임을 실행해주면 된다. 하지만, 우리의 업무가 대부분 그렇듯 게임

채널링 서비스도 그 단순함의 이면에 다양한 문제가 복잡하게 얽

혀 있다.

•회원 정보는 어떻게 넘기고, 인증은 어떻게 해야 하는가?

• 사용자의 동의는 어느 시점에 어떻게 받아야 하는가? 아이핀 사용자는

어떻게 처리해야 하는가?

• 사용자가 우리가 생각하는 시나리오와 다르게 접근할 경우 어떻게 할 것

인가?

뿐만 아니라 다음과 같이 생각할 수도 있다.

시리즈 정보 관리

전문 사이트 관리

기본 정보 관리

회사정보 관리 검색 키워드 관리

SDMS

통합검색 게임정보

링크 연결

플레이 넷 서비스

CP 기사

CP 기사

CP 기사

NGC의 CP 관리 부분

+

게임 패스 TF DB

< 검색 >R&R 구분선

< 게임 >

NCR

(게임DB 신규 구축)

Page 74: The platform 2011

플레이넷, 네이버 ID로 모든 게임을The Platform 2011 73

용어 설명

퍼블리셔 (게임) 서비스를 제공하는 주체

미러링 사이트 퍼블리셔가 개발/운영하는 개별 게임 사이트

위 용어를 바탕으로 플레이넷 서비스를 설명하면, 플레이넷은 아웃

바운드 채널링을 통해 유입된(외부 IDP로부터 유입된) 회원으로

하여금 인바운드 채널링된(외부 퍼블리셔가 제공하는) 게임을 이

용할 수 있게 해주는 서비스이다.

플레이넷은 이미 네이버 서비스인데 왜 아웃바운드 채널링이냐는

의구심을 가질 수도 있다. 기술적으로는 네이버도 플레이넷에 회원

을 제공해주는 하나의 IDP로 간주한다. 이는 앞서 언급한 플레이넷

사업 방향에 대한 유연성(해외 진출 및 대형 IDP와 추가 연계)과 관

련이 있다.

플레이넷 시스템 설계 기준

플레이넷의 시스템은 100%는 아니지만 기본적으로 다음 목표를

기준으로 설계되었다.

1. 아웃바운드 채널링 연동이 쉬워야 한다.

특정 서비스가 아닌, 대규모 회원을 제공할 수 있는 어떠한 IDP와도 연

계할 수 있어야 한다.

2. 인바운드 채널링 연동이 쉬워야 한다.

어떠한 퍼블리셔와도 해당 서비스의 기존 아이디와 충돌 없이 쉽게 연

동할 수 있어야 한다.

3. 특정 IDP와 플레이넷이 연동되면, IDP와 퍼블리셔 간 추가 작업 없이,

해당 IDP 회원은 플레이넷에서 인바운드 채널링하고 있는 모든 게임을

실행할 수 있어야 한다.

4. 퍼블리셔와 플레이넷이 연동되면, 퍼블리셔와 IDP 간 추가 작업 없이,

플레이넷에서 아웃바운드 채널링하는 모든 IDP의 회원이 해당 게임을

실행할 수 있어야 한다.

위 4가지 목표는 플레이넷의 핵심(core) 기능이 되어야 하며, 가능

하면 변하지 않아야 하고, 그 외 IDP에 종속적인 부분과 느슨하게

• '사용자 입장에서는 그냥 원클릭으로 게임을 실행할 수 있으면 좋겠어.'

• ‘A 개발사 담당자는 우리 가이드를 금방 이해하고 2주만에 연동을 완료

했는데, B 개발사는 벌써 4주나 지났는데 아직 연동하지 못하고 있네. 뭐

가 문제인지 확인이 필요해.’

이 외에도 생각하면 생각할수록 고민할 것이 많다. 이런 문제를 해

결하기 위해 어떻게 했고 또 현재 어떻게 하고 있는지 '회원' 절에서

자세히 설명한다.

회원/인증 시스템

플레이넷 회원 인증 방법은 크게 두 가지로 나눌 수 있다.

•네이버와 플레이넷 사이의 회원 인증

•플레이넷과 게임 사이트 간 회원 인증

플레이넷 인증 방식이 위와 같이 두 가지로 나뉘는 이유와 인증이

어떻게 처리되는지 '인증' 절에서 자세히 설명한다.

빌링 시스템(플레이 코인)

플레이넷은 네이버 코인이 아닌 플레이 코인이라는 독립적인 빌링

시스템을 가지고 있다. 이는 기술적인 문제로 인한 것이 아니라 플

레이넷 서비스만을 위한 마케팅과 사업 방향에 유연성을 갖추기 위

한 결정이다. 플레이넷 코인은 네이버 코인이나 한게임 코인과 마

찬가지로 전사 빌링 시스템을 개발하는 부서에서 담당한다.

플레이넷 as a 게임 채널링 서비스

구체적인 설명에 앞서 먼저 아래와 같이 용어 정리를 해보자.

표 1 채널링 서비스 관련 용어 정리

용어 설명

인바운드 채널링 외부 서비스를 내부 회원이 이용할 수 있게 하는 서비스 형태

아웃바운드 채널링 내부 서비스를 외부 회원이 이용할 수 있게 하는 서비스 형태

IDP(Identity

Provider) 혹은 채널러

회원(User Pool)을 제공하는 주체

Page 75: The platform 2011

NHN 서비스 들여다보기The Platform 201174

구분 네이버 다음 네이트

미러링

(테일즈 러너)

playnet.naver.com/game/

tr

playnet.daum.net/

game/tr

playnet.nate.com/

game/tr

tr.playnetwork.co.kr

위에서 검은색 글씨의 도메인은 IDP와 플레이넷 연동을 위한 도메

인이고, 붉은색 글씨의 도메인은 플레이넷과 미러링 사이트 연동을

위한 도메인이다.

이렇게 도메인을 분리함으로써, IDP와 미러링 사이트도 플레이넷

과 연동만 하면 서로 수많은 게임과 수많은 사용자를 얻게 해 준다.

이를 통해 플레이넷 게임 채널링 시스템의 기본 목표를 달성할 수

있게 된다.

사용자 시나리오

사용자가 플레이넷에서 제공하는 게임을 하려면 아래와 같은 과정

을 거쳐야 한다.

1. 플레이넷 이용 약관 동의

플레이넷이 제공하는 회원 번호(아이디) 기반의 서비스 접근할 때 필요

하다.

2. 제3자 정보 제공 동의 및 게임 이용 약관 동의

게임을 할 때, 채널링 사이트를 이용할 때 필요하다.

3. 미러링 사이트 진입 및 게임 실행

각 단계에서는 IDP의 회원 유형, 연령 등에 따라 콘텐츠를 필터링

하거나 본인 인증, 부모 인증 등과 같이 IDP나 퍼블리셔별로 요구

되는 추가적인 인증이 이루어진다. 네이버의 경우엔 실명 인증 혹

은 만 14세 미만일 경우 부모 동의를 거친 사용자에 한해서만 플레

이넷 서비스를 이용할 수 있도록 제한하고 있는데, 좀 더 구체적인

내용은 다음 플로 차트(Flow Chart)와 플레이넷 회원 정책을 참고

한다.

결합되는 구조로 설계되어야 한다. 즉, IDP가 추가되면 해당 부분

을 쉽게 바꿀 수 있는 구조로 설계되어야 한다.

플레이넷의 도메인 구조

플레이넷은 IDP와 플레이넷, 플레이넷과 미러링 사이트 간의 도메

인을 분리하고 있다.

일반적으로 네이버나 다음 등 국내 주요 IDP는 미러링 사이트에서

도 자신의 BI(Brand Identity)의 연장선이라고 할 수 있는 IDP의

도메인을 사용하길 바란다. 하지만, IDP와 동일한 도메인을 사용할

경우, 미러링 사이트 입장에선 아래와 같이 IDP가 추가될 때마다

매번 별도의 도메인을 추가로 설정해야 한다. 현재 20여 개인 경우

도 부담되는데, 향후 수백 개의 미러링 사이트를 생각하면 이 방식

을 계속 적용하기 힘들었다.

표 2 도메인 구분 예

구분 네이버 다음 네이트

IDP 도메인 naver.com daum.net nate.com

플레이넷 playnet.naver.com playnet.daum.net playnet.nate.com

미러링(테일즈러너) tr.playnet.naver.com tr.playnet.daum.net tr.playnet.nate.com

사실 위와 같이 IDP별로 도메인을 별도로 구성하면 인증 부분은 좀

더 간단하고 부드럽게 해결할 수 있다. 게다가 동일한 도메인을 사

용하기 때문에, 소위 말하는 Same-Origin Policy(이하 SOP)2를

위배할 일도 없다. 하지만, 플레이넷은 위 구조를 따르지 않고, 아래

와 같은 형식의 도메인 구성을 따른다.

표 3 플레이넷과 미러링 사이트 연동을 위한 도메인

구분 네이버 다음 네이트

IDP 도메인 naver.com daum.net nate.com

플레이넷 playnet.naver.com playnet.daum.net playnet.nate.com

www.playnetwork.co.kr

2 http://en.wikipedia.org/wiki/Same_origin_policy

Page 76: The platform 2011

플레이넷, 네이버 ID로 모든 게임을The Platform 2011 75

그림 4 회원 인증 및 게임 실행 플로 차트

그림 5 플레이넷 실명 인증 회원 정의

구분 네이버 정의 플레이넷 회원 정의 상세실명 기관

정보 유무

이용범위

게임 유료구매

성인회원

만 19세 이상의 회원 만 19세 이상의 실명 회원 1) 만 19세 이상의 가입자는 실명인증 후 가입된 회원임 � � �

2) 19세미만의 네이버에 기 가입된 이용자가 19세가 된 경우 부모 동의 테이블이 사라져, 청소년 회원 중 부모 동의 상태로 플레이넷을 이용한 이용자가 비실명상태가 됨

"플레이넷 로그인 정책 통과"시 비실명자의 실명인증 안내

� � �

청소년회원

만 14세 이상 만 19세 미만의 회원 만 14세 이상 만 19세 미만의 실명 회원

1) 14세이상~만 19세미만의 이용자가 가입하여 게임 패스 최초 진입시 실명인증 안내(신용정보기관의 실명인증 정보 있어야 가능)

� � �

2) 14세미만의 네이버에 기가입된 이용자가 14세 이상이 되는 경우, 기존 부모동의 테이블이 승계되었고, 실제 실명기관의 실명인증 정보 있는 경우

� � �

2) 14세미만의 네이버에 기가입된 이용자가 14세 이상의 되는 경우, 기존 부모동의 테이블이 승계되었으나, 실제 실명기관의 실명인증 정보 없는 경우

X�

(전체 이용가 게임만 이용 가능)

쥬니버회원

만 14세 미만의 회원 만 14세 미만의 부모 동의를 받아 가입한 회원

부모동의를 받아 실명인증 가입된 이용자 중 실제 실명 기관의 실명인증 정보 있는 경우

� � �

부모동의를 받아 실명인증 가입된 이용자 중 실제 실명 기관의 실명인증 정보 없는 경우 X

�(전체 이용가 게임만 이용 가능)

단체회원 정부에서 발행한 공식 서류(사업자등록증, 법인등기등본, 고유번호증 중 1개 이상)를 제출하여 네이버에 가입한 개인사업자, 법인 공식단체 회원

제외 "플레이넷 로그인 정책 통과"시 단체ID 사용 불가 및 개인 ID 사용 안내

X X X

국내거주외국인

외국인 등록번호 보유 회원(내국인 회원이아니지만 외국인 등록 번호로 가입한 회원

외국인 등록번호 보유통해 실명인정된 회원

외국인 등록번호 보유시는 실명인증 자격 부여되어 가입된 회원임� � �

Social ID 보유 회원(단기 체류 외국인 여권 번호로 가입한 회원)

국내입국 심사 받은 여권번호 보유의 실명 인정된 회원

국내 입국 심사 받은 여권이면 실명인증(생년월일 포함)되어 가입된 회원임

� � �

해외거주외국인

해외 IP 접속 회원(Social ID 보유 회원이 아니지만 해외 IP에서 접속하는 회원)

제외 "플레이넷 로그인 정책 통과"시 실명인증 안내X X X

해외 IP 접속 & Social ID 보유 회원 해외거주 외국인이 여권사본 첨부 등록 후 CS관련 툴에서 실명 체크된 회원

� � �

네이버 플레이넷 퍼블리셔

Y

N

YES

NO

로그인 시도

플레이넷 웹페이지 접속

(플레이넷 메인)게임이용 시도

유효한

게임아이디체크?

I-PIN 이용자?

만19세이상?

주민번호 제한체크?

(1인 1계정)

처벌회원여부

체크

해지처리우예기간

여부체크

이용약관동의?

게임이용게임이용불가

이용제한안내

게임약관 및

개인정보보호정책동의

제3자 개인정보

제공동의

(14세미만 부모동의)

주민번호제공동의

유저만나이와

게임등급여부체크

실명인증여부체크

전체이용가게임

아님?로그인 필요?

플레이넷

웹페이지 진입

플레이넷

로그인정책통과

필요서비스단체 ID 제외

플레이넷

게임 이용불가안내

채널러&플레이넷

로그인정책(1) 통과

플레이넷

최초 이용?

약관동의 개인정보 활용동의

(플레이넷)

플레이넷 이용자

Unique User No

아이디존재?

실명인증?

정상회원?

플레이넷

웹페이지 진입

네이버

회원가입

네이버로그인

실명인증페이지

네이버

처벌아이디 제외

플레이넷 이용불가

이용 제한 안내

Page 77: The platform 2011

NHN 서비스 들여다보기The Platform 201176

플레이넷은 사용자를 회원 테이블의 IDP코드와 고객식별 아이디

로 구분하고 있으며, 고객식별 아이디는 IDP에 따라 다를 수 있다

(네이버의 경우 신용 평가 기관에서 제공하는 개인 식별 코드, 한게

임의 경우는 고객 번호).

회원 테이블은 플레이넷 회원 번호(ERD 상에서 게임패스회원번

호)를 주 키(Primary Key)로 갖는다. 해당 키를 기반으로 플레이넷

의 사용자 데이터를 저장하고, 미러링 사이트와 연동할 때도 사용

자를 구분하는 기준으로 사용한다. 플레이넷이 IDP와 커뮤니케이

션할 때 회원 테이블의 IDP코드, IDP회원ID, IDP회원번호를 사용

한다.

인증

플레이넷 회원으로 가입된 사용자에 대한 인증은 아래와 같은 단계

로 진행한다.

• IDP에서 플레이넷으로 진입할 때

• IDP에 로그인한 상태로 플레이넷에 진입할 때 해당 유저가 플레이넷

회원이면 PN_LOGIN이라는 플레이넷 로그인 쿠키를 생성한다(플레

이넷 로그인 쿠키가 이미 생성되어 있는 경우라면, 유효성만 확인한

다.) 여기서 플레이넷 로그인 쿠키는 IDP 도메인이 아닌 플레이넷과

미러링 사이트 도메인(playnetwork.co.kr) 기반의 인증 쿠키를 의미

한다.

• IDP에 로그인하지 않은 상태로 플레이넷에 진입하여 플레이넷 회원

만 접근할 수 있는 페이지에 도달하면 IDP의 로그인 페이지로 이동

(Redirect)시켜 로그인을 유도한다. 사용자가 로그인 후 해당 페이지

로 돌아오면 a단계를 거친다.

•플레이넷에서 미러링 사이트로 진입할 때

• IDP에 로그인한 상태이면, 플레이넷 로그인 쿠키(PN_LOGIN)가 반

드시 생성되어야 한다.

이 때, 먼저 해당 사용자가 플레이넷 회원인지를 확인하고 아닐 경우

엔 플레이넷 회원 등록을 진행한다(시스템 내부적으로 자동 처리됨).

• IDP에 로그인하지 않은 상태이면, 플레이넷 로그인 쿠키를 생성하지

않는다.

• 로그아웃은 플레이넷 로그아웃뿐만 아니라 IDP 로그아웃까지 포함한다.

회원

플레이넷의 회원 시스템을 구성하는 주요 테이블은 IDP, 고객, 회

원이다. 각 테이블의 속성 및 테이블간 관계는 아래 객체-관계 모

델 다이어그램(Entity Relationship Diagram, 이하 ERD)를 참고

하면 된다. ERD에서 게임 패스 테이블은 플레이넷의 초창기 프로

젝트 이름으로 현재는 일종의 코드네임으로 사용 중이다.

그림 6 플레이넷 회원 시스템 ERD

IDP가 추가될 때 IDP 테이블에 필요한 정보가 추가되고, 사용자가

플레이넷의 이용 약관에 동의할 때 해당 사용자가 속한 IDP에서 사

용자 정보를 받아 고객 및 회원 테이블에 입력한다. 플레이넷 오픈

시점에는 별도의 페이지를 통해 플레이넷 이용 약관에 대한 동의를

받았으나, 현재는 사용자의 편의성을 높이기 위해 별도의 페이지

이동 없이 시스템 내부적으로 자동 처리한다.

고객 테이블에는 동일인인 경우 IDP별로 1개의 행(row)만 입력되

고, 이후 추가되는 아이디는 회원 테이블에만 추가된다. 고객 테이

블과 회원 테이블을 분리하는 이유는 네이버나 한게임처럼 동일인

에게 하나 이상의 아이디를 허용하는 IDP의 경우 코인 한도를 체

크하거나 주민등록번호 기반의 처벌 등 회원이 아닌 고객 기준으로

사용자를 관리할 필요가 있기 때문이다. 또 특정 IDP가 자체 회원

에 대해 하나 이상의 아이디를 허용하고 플레이넷은 그 중 한 아이

디로만 이용 가능하게 해달라는 요구도 있기 때문이다.

Page 78: The platform 2011

플레이넷, 네이버 ID로 모든 게임을The Platform 2011 77

플레이넷은 다양한 미러링 사이트를 지원하기 위해 아파치 모듈을

비롯하여, ISAPI, ASP.NET 용 모듈 등 여러 버전의 쿠키 파서 모

듈과 매뉴얼을 제공하고 있다.

제3자 정보제공 동의 및 게임 이용 약관 동의

IDP의 회원이 플레이넷 서비스를 이용하기 위해 플레이넷 사용 동

의(플레이넷 회원 가입) 과정을 거쳐야 하듯, 플레이넷 회원이 미러

링 사이트를 이용하기 위해서는 해당 사이트의 이용 약관(마찬가지

로 해당 사이트 회원 가입에 준함)에 동의하는 과정을 거쳐야 한다.

이 과정에서 플레이넷의 회원 정보가 미러링 사이트로 넘어가게 되

는데, 이렇게 서로 다른 사이트 사이에 회원 정보를 교환하는 행위

가 발생할 경우 법적으로 ‘제3자 정보제공 동의’라는 또 다른 사용

자 승인을 획득해야 한다.

즉, 플레이넷 회원이 미러링 사이트를 이용하기 위해서는 앞의 유

저 시나리오 2번에 해당하는 제3자 정보 제공 동의 및 게임 이용 약

관 동의를 항상 거쳐야 한다.

현재 이 두 과정을 각각 플레이넷과 미러링 사이트의 별도 페이지

에서 처리하는데, 사용자가 게임할 때 이 부분에서 불편을 느낀다

고 판단했다. 따라서 이 두 과정을 하나로 합치는 작업을 진행하고

있다. 아마 이 글이 읽히고 있는 시점에 이미 두 과정이 합쳐져 있

을지도 모른다.

제3자 정보제공 동의 및 이용 약관 동의 과정은 게임 채널링 시스

템의 핵심으로서 기술적으로나 정책적으로 가장 복잡한 부분이기

도 하다. 기술적으로 보면 이 과정은 앱 스토어(App Store)에 등록

된 애플리케이션을 사용자가 이용하는 과정과 유사하다. 여기서 언

급하는 애플리케이션은 다운로드 기반의 스마트 폰 애플리케이션

보다는 웹 기반의 페이스북이나 트위터 애플리케이션에 가깝다. 플

레이넷을 앱 스토어로, 미러링 사이트를 애플리케이션으로 보면 무

슨 의미인지 쉽게 이해할 것이다.

아무튼 담당 개발자나 기획자조차도 뒤돌아서면 잊어버릴 만큼 복

잡한 과정을 하나하나 글로 전달하는 것은 무리이므로 다이어그램

으로 소개한다.

플레이넷과 IDP 사이의 인증은 IDP에서 제공하는 인증 모듈을 통

해 이루어지고, 이 부분은 플레이넷에서만 핸들링한다. 플레이넷과

미러링 사이트 사이의 인증도 마찬가지로 플레이넷에서 제공하는

인증 모듈(PN_LOGIN 쿠키 파서)을 통해 핸들링되는데, 기본 동

작은 아래와 같다.

• 플레이넷에서 PN_LOGIN 쿠키를 생성할 때 고유의 방식으로 암호화

한다. 미러링 사이트에서 이를 사용하려면 별도의 복호화 절차가 필요

하다.

• 플레이넷에서 미러링 사이트에 제공하는 쿠키 파서 모듈은 파싱에 필요

한 암호 키를 플레이넷 API 서버에서 받아와 복호화한다. 이 때, API 서

버는 접속 허용 IP를 관리하므로, 모듈을 구동할 호스트의 실제 IP 주소

가 접속되도록 등록해야 한다.

• 쿠키를 정상적으로 파싱하면, 모듈은 파싱한 값을 HTTP 헤더에 저장

한다.

• 미러링 사이트는 HTTP 헤더에 저장된 값을 통해 플레이넷 로그인 여부

및 유저 정보를 획득한다.

그림 7 플레이넷, IDP, 미러링 웹 서버 사이의 인증

Page 79: The platform 2011

NHN 서비스 들여다보기The Platform 201178

를 획득하는 단계로 미러링 사이트가 플레이넷 회원의 회원 정보를

획득하기 위해 사용자에게 이용 약관 동의를 받는 것과 큰 차이가

없다.

사실 플레이넷이 미러링 사이트에 사용자 정보를 전송하는 것에 대

해 많은 사용자가 우려나 반감을 가질 수 있다. 하지만, 기본적으로

잠깐 옆으로 새자면 앞서 언급한 것처럼 우리가 페이스북이나 트위

터의 애플리케이션을 사용할 때도, 미러링 사이트의 이용 약관 동

의와 상응하는 과정을 거친다. 특정 애플리케이션이 페이스북의 담

벼락이나 트위터의 타임라인(Timeline)에 글을 등록할 수 있도록

허락해 달라는 페이지가 그것이다. 기술적으로 보면 애플리케이션

이 페이스북이나 트위터의 사용자 아이디(혹은 이에 상응하는 키)

그림 8 제3자 정보 제공 동의 및 게임 약관 동의 시퀀스 다이어그램

Page 80: The platform 2011

플레이넷, 네이버 ID로 모든 게임을The Platform 2011 79

트의 게임을 바로 실행할 수 있게 된다. 즉, 네이버 아이디로 액토

즈소프트에서 퍼블리싱하는 와일드 플래닛을, KTH에서 퍼블리싱

하는 와인드 업을 즐길 수 있게 되는 것이다.

게임 실행(게임 플레이)

전통적인 게임 채널링 서비스의 경우 게임 스타트는 미러링 사이

트에서 진행된다. HTTP라는 표준 프로토콜에 기반하는 웹 인증

에 비해 게임 인증은 개별 퍼블리셔마다 고유한 인증 프로토콜을

사용하기 때문에, 플레이넷이나 한게임과 같은 채널러가 해당 프

로토콜을 건별로 구현하기보다는 웹 인증만 처리하고, 게임 인증

은 각 퍼블리셔가 기존 방식대로 자체 처리하는 게 일종의 업계의

관례이다.

플레이넷도 게임 실행은 미러링 사이트에서 진행한다. 다만, 사용

자가 좀 더 편하게 게임을 실행할 수 있도록 하는 부분에 초점을 맞

췄다. 게임이 실행되려면 게임 런처가 필요한데 웹 게임을 제외한

대부분의 게임 런처는 마이크로소프트 사의 액티브 X 컨트롤로 구

현되어 있다. 새로운 게임을 실행하려면 해당 게임의 게임 런처(액

티브 X 컨트롤)를 설치해야 한다. 플레이넷에서 사용자가 매번 다

른 게임을 실행할 때마다, 적어도 한 번씩 액티브 X 컨트롤을 설치

해야 한다면 상당히 불편할 것이다. 그래서, 플레이넷은 게임 실행

전에 자동으로 게임 런처를 설치하는 방식을 도입했다.

플레이넷은 인터넷 익스플로러에 맡겨 각 게임의 게임 런처를 설치

하지 않고 플레이넷 통합 인스톨러라는 별도의 액티브 X 컨트롤을

사용해 설치하고 있다. 물론 적어도 한 번은 플레이넷 통합 인스톨

러를 설치해야 한다는 조건이 붙지만, 이를 통해 편의성이 상당히

높아질 것으로 판단한다.

통합 인스톨러의 동작 방식은 다음과 같다.

1. 특정 게임을 연동할 때 퍼블리셔로부터 게임 런처 파일(.cab)을 전달받

아, description.xml 파일(런처 설치 정보를 기술한 XML 파일)과 함께

CDN(Contents Delivery Network)에 올려둔다.

2. 게임을 실행할 때 통합 인스톨러를 통해 해당 게임의 런처가 설치되어

있는지 확인한다.

3. 게임 런처가 설치되어 있지 않을 경우 통합 인스톨러는 1번에서 설명한

description.xml 파일을 통해 게임 런처 설치 파일을 CDN으로부터 전

송받아 클라이언트 PC에 설치한다.

주민번호나 핸드폰 번호와 같이 악용될 소지가 있는 정보를 제외

하며 앞에서 언급했듯이 암호화된 쿠키를 통해 데이터를 전송하고,

또 허용된 웹 서버에서만 복호화되기 때문에 크게 걱정할 필요가

없다. 플레이넷 쿠키를 통해 미러링 사이트로 전달되는 사용자 정

보는 아래와 같다.

고유키 설명

PNC_FLAGS 로그인 쿠키 처리 결과

이 값이 "0"일 때만 다른 변수가 정상 설정된다.

• 0: 로그인 쿠키 정상

•1: 쿠키 값 없음

•2: 쿠키 길이가 일정 길이를 초과

•3: 필요 쿠키 필드 없음

•4: 쿠키 시간 만료

•5: 보안 로그인 후, 로그인 IP가 달라짐

•-1: 체크섬 에러

•-2: 암호처리 키 로딩 실패

•-3: 레지스트리 설정이 잘못된 경우

•-4: 기타 에러

PNC_ERROR 로그인 쿠키 파싱 결과에 대한 설명

PNC_MEMBERNO 플레이넷 회원 번호

PNC_VIEWID IDP 회원 아이디

PNC_SOCIALID <미사용>

PNC_NICKNAME 사용자 별명

PNC_SEX 사용자 성별. "F" 또는 "M"으로 설정

PNC_AGE 사용자의 만 나이

PNC_EMAIL 사용자의 메일 주소

PNC_VALID 플레이넷 회원 상태코드("Y" 정상)

PNC_IDPCODE IDP 코드

PNC_CUSTNO 플레이넷 고객 번호

PNC_NAME 사용자 실명

PNC_IDPMEMBERNO IDP 회원 번호

PNC_TIMESTAMP 로그인 시각.

Unix Time 형식으로 밀리 세컨드 단위.

PNC_BIRTHDAY 생년월일(형식: YYYYMMDD)

PNC_LOGINIP 로그인한 IP 주소

제3자 정보 제공 및 이용약관 동의 과정을 통해 플레이넷에서 미러

링 사이트로 사용자 정보가 전달되면, 미러링 사이트에서는 해당

정보를 바탕으로 자신만의 사용자 계정을 생성한다. 이 단계를 모

두 끝내면, IDP에서 로그인한 사용자는 IDP 아이디로 미러링 사이

Page 81: The platform 2011

NHN 서비스 들여다보기The Platform 201180

마치며

지금까지 플레이넷의 서비스 콘셉트와 주요 시스템 그리고 그 중

게임 채널링 서비스 시스템에 대해 자세히 살펴보았다.

막상 우리가 만든 시스템을 지면을 통해 외부에 공개하려고 보니,

우리가 최초에 지향했던 플레이넷 서비스의 모습을 완성하려면 앞

으로도 해결해야 할 기술적인 숙제가 아직 많음을 느꼈다. 하지만,

비약적으로 발전한 NHN 내부 플랫폼 기술과 사내 전문가를 통해

숙제를 하나하나 해결해 나갈 수 있을 것이라 믿고 있다. 더욱이 우

리에겐 여전히 식지 않는 열정을 지닌 개발자가 있지 않은가!

2011년 여름, 플레이넷은 베타 꼬리표를 떼고 정식 서비스(플레이

넷 시즌 1)로 오픈했다. 매달 2~3개의 게임이 오픈되고, 양질의 콘

텐츠도 쌓여가고 있다. 베타 서비스를 준비하던 시점에 비해 사업/

기획은 물론 개발 인력도 하나 둘씩 늘어났다. 개발 조직장 입장에

서, 주변의 지원에 부응하기 위해 대단한 서비스를 만들어 내야 한

다는 부담감도 있지만 당장의 성공보다는 오랜 기간 동안 지속 가

능한 서비스를 개발하는 데 많은 시간을 투자할 생각이다. 이 글을

읽는 독자는 물론이고, 많은 NHN 동료가 응원해 주길 기원한다.

Page 82: The platform 2011

야구9단 아키텍처The Platform 2011 81

야구9단

이미 야구9단이 무엇인지 직접 체험해본 분이 많을 것이다. 알다시

피 야구9단은 사용자가 야구 감독과 구단주가 되는 시뮬레이션 웹

게임이다.

실제 야구 선수 정보를 바탕

으로 구단 운영과 작전 지시

는 물론 선수 육성도 할 수 있

는 게임이다. 국내에서는 최초

이고 전세계적으로도 유사 사

례를 찾기 어려운 선구적인 게

임이라고 할 수 있다. 대신 그

만큼 구현에 어려움이 많았다.

이 글을 통해 야구9단 시스템

이 어떻게 구성되어 있는지 소

개해본다.

최근 야구의 인기가 치솟고 있는 가운데 한게임에서 웹 시뮬레이션

야구 게임인 야구9단을 출시했다. 현재 한창 OBT 중인 야구9단 서

비스 컨셉과 주요 시스템 아키텍처를 소개한다.

그림 1 야구9단 게임 화면

야구9단 아키텍처한게임 게임서비스개발팀에서 야구9단 개발 업무를 맡고 있습니다.

야구9단 개발을 하면서 게임에 푹 빠져 시계를 자주 보는 버릇이 생겼습니

다. 매시 정각에 실시간 개입을 해야 하거든요. ^^

밥을 먹으면서도, 버스/지하철에서 이동 중에도~

여러분도 야구9단에 함께 빠져 보아요~

아싸~ 이승엽 뽑았어요!!!!

•게임서비스개발팀 _ 조영남

Page 83: The platform 2011

NHN 서비스 들여다보기The Platform 201182

시스템 구성

야구9단은 기능별로 크게 다음과 같이 세 요소로 구성된다.

• 게임 비즈니스 로직을 수행하는 BLOC 서버

• 실시간 정보를 사용자에게 전달하는 RTCS 서버

• 게임 서비스와 메인 UI를 담당하는 WAS(Web Application Server)

다음 그림 2는 야구9단 시스템 구성도이다.

그림 2 야구9단 시스템 구성도

리그진행배치리그구성시즌 진행 & 결과 처리경기 진행 & 결과 처리

경기 시뮬레이션 프로세싱정규리그 실시간 경기 진행분산 DB 1 : 게임프로세서 3Hashing 분산 - 키:리그번호 통합 사용자 정보

이중화 구성 – MMM

게임 데이터팀, 선수, 리그, etc한 DB당 Max 10만팀 처리DB별 폐쇄 구조(월드)

이중화 구성 – MMM분산 – 팀수로 밸런싱

네이버회원, 네이버스포츠me2day, 네이버블로그

네이버쪽지, 모바일서비스네이버코인, nBoard2,

nLucene, laputa message랭킹시스템

Arcus구단 정보사용자별 전국 랭킹 목록RTCS 사용자 접속 정보

첼린지 모드 실시간 경기 진행

PVP 실시간 경기 진행

경기결과 알림 시스템 SMS, 메일

프로야구 경기 정보 수집 Src: 스포츠투아이네이버 회원탈퇴 처리

GameResult Notifier

PVP Processor

Admin Server

Support

Challenge Processor

Game Processor

League Controller

BLOC 서버

게임 비즈니스 로직을 담당하는 5개의 BLOC 서버가 있으며 각 서

버의 설명은 다음과 같다.

• 게임 프로세서(Game Processor)

각 경기 시뮬레이션을 처리한다.

실시간 사용자 명령을 바탕으로 시뮬레이션을 처리한다.

• 리그 컨트롤러(League Controller)

사용자 또는 컴퓨터가 창단한 구단을 대상으로 리그를 구성한다.

페넌트 레이스 및 포스트 시즌에 해당되는 시즌을 진행하고 결과를 처

리한다.

경기를 진행하고 그 결과를 처리한다.

Page 84: The platform 2011

야구9단 아키텍처The Platform 2011 83

게임 프로세서

게임 시뮬레이션

과거 한국 프로야구 실제 데이터를 근거로 한 선수의 능력치를 계

산한다. 이렇게 계산한 투수와 타자의 능력치를 바탕으로 매 타석

마다 시뮬레이션한다. 야구9단에서 좋은 결과를 얻기 위해서는 작

전 관리와 선수 관리, 선수 영입을 모두 잘 해야 하는데, 이는 모두

자신이 보유한 선수 개개인의 능력치를 최대로 끌어올리기 위한 노

력이며 선수 능력치는 시뮬레이션 결과에 절대적인 영향을 미친다.

타석 결과 = 타격 시뮬레이션 + 수비ž 주루 시뮬레이션

타격 시뮬레이션

타격 시뮬레이션에 필요한 값은 투수와 타자가 가진 8종의 능력치

이다.

• 타자 능력 : 컨택, 파워, 선구안, 정신력, 체력, 수비력, 건강, 주력

• 투수 능력 : 구속, 구위, 제구력, 변화구, 체력, 수비력, 건강, 정신력

선수 각각의 능력치는 경험, 훈련, 나이, 작전, 특수 능력 적용, 아이

템 적용에 따라 변경된다.

• 챌린지 프로세서(Challenge Processor)

특정 선수로 이루어진 컴퓨터(AI)와의 경기 시뮬레이션을 처리한다.

• PVP 프로세서(PVP Processor)

정식 리그 경기가 아닌, 불특정 사용자 구단과의 경기 시뮬레이션을 처

리한다.

• 게임 노티파이어(Game Notifier)

경기 결과를 실시간으로 사용에게 웹, 메일, SMS로 알린다.

RTCS 서버

RTCS(Real Time Communication System) 서버는 실시간 정보

를 유저에게 전달하며, 다음과 같은 역할을 한다.

• 실시간 중계 및 개입 정보 전달

시뮬레이션된 경기를 재실행(replay)해주며, 사용자의 개입으로 변경

된 경기 결과를 실시간으로 클라이언트에 전달한다.

• 채팅

순수 웹으로만 구현된 채팅 시스템을 제공한다.

• Long Polling 방식 사용

RTCS가 제공하는 2가지 서버 푸시

(push) 방식인 스트리밍(Streaming)과

Long Polling 중 Long Polling 방식을

사용한다.

WAS

WAS는 게임 서비스와 메인 UI를 담당

한다.

• BLOC 서버로부터 전달받은 데이터를 바

탕으로 시뮬레이션한 경기 결과, 일정 UI

를 사용자에게 제공한다.

• 부가적인 서비스의 비즈니스 로직과 UI

처리를 담당한다.

그림 3 시뮬레이션 처리 과정

Page 85: The platform 2011

NHN 서비스 들여다보기The Platform 201184

그림 6 개입 시 클라이언트와 서버의 데이터 흐름

리그 컨트롤러

야구9단은 1주일 동안 페넌트 레이스와 포스트 시즌을 모두 진행

한다. League Controller는 페넌트 레이스 스케줄과 포스트 시즌

스케줄을 생성 관리하며, 1분에 한 번씩 배치 작업을 실행해서 신

규 사용자를 대상으로 신규 리그를 구성한다. 아래 그림처럼 1시간

에 한 경기씩 게임이 진행되도록 일정을 생성하고, 시뮬레이션 결

과를 저장한다.

그림 7 리그 일정 화면

수비/주루 시뮬레이션

타격 시뮬레이션 결과가 안타(홈런 제외), 땅볼 아웃, 뜬공 아웃이

라고 하더라도 타격의 방향, 수비수의 능력과 위치, 타자의 주력, 주

루 플레이에 따라 결과가 바뀔 수 있다.

참고

타격 시뮬레이션과 수비/주루 시뮬레이션의 자세한 구현 방법은

대외비이므로 더 설명하지 않는다.

실시간 중계와 개입

모든 경기는 작전과 선수의 능력치에 의해 미리 경기 시작 전에 시

뮬레이션되어 게임 서버의 메모리에 로딩한다. 경기가 시작되고,

사용자가 실시간 중계를 보기 위해 입장하면 RTCS 서버를 이용하

여 게임 서버에서 미리 만들어진 시뮬레이션 결과를 한 타석 단위

로 사용자(브라우저)에게 전달하고, 이를 받은 브라우저에서 경기

진행 상황을 재현하는 것이다. 즉 매 시마다 실제로 게임을 하는 것

이 아니라 미리 시뮬레이션한 결과를 재실행(replay)하는 것이라

할 수 있다. 하지만 앞에서 말했듯이 야구9단에서는 사용자가 게임

에 개입을 할 수 있다. 이 때는 경기 결과가 바뀔 수 있다.

그림 4 공격 개입

그림 5 수비 개입

한 타석에서 3초 이내로 사용자가 위 그림과 같이 개입하면, 지시

내용이 게임 서버로 전달된다. 경기 결과가 게임 서버에서 다시 시

뮬레이션된 뒤 사용자에게 전달된다. 실시간 개입에 대한 클라이언

트와 서버의 데이터 흐름은 다음 그림를 참조한다.

Page 86: The platform 2011

야구9단 아키텍처The Platform 2011 85

리그 결과에 따라서 상위 4개팀은 상위 리그로 올라가고 하위 4개

팀은 하위 리그로 떨어뜨리는 작업을 하며, 해당 리그별 일정을 생

성/관리하는 기능을 담당한다.

게임 노티파이어

야구9단은 매시간마다 경기를 진행하는데 실시간으로 경기 결과를

확인하기 어려운 사용자를 위하여 온라인으로 접속하지 않아도 경

기 결과를 알 수 있도록 Game Notifier 시스템을 제공하고 있다.

아래 그림과 같이 League Controller에서 “매 경기 결과”와 “전날

경기 요약” 정보를 뽑아서 결과 알림 시스템에 전달하며 다시 메일

과 SMS를 사용하여 사용자에게 전달한다.

그림 8 Game Notifier 동작 구조

마치며

NHN에서 처음으로 개발한 스포츠 시뮬레이션 게임이라 구현 중

어려운 부분도 있었다. 서비스 초기에는 시뮬레이션 결과가 실제

야구 게임과 다소 다를 수도 있을 것이라 생각한다. 이런 부분은 사

용자의 피드백을 통해 밸런스를 조절하면 점점 나아질 것이라고 생

각한다.

시뮬레이션 게임의 핵심인 시뮬레이션에 대한 자세한 내용을 기술

하지 못해서 아쉽지만, 이 부분을 공유할 수 없음을 이해해 주길 바

란다. 마지막으로 직접 야구9단 사이트(http://ya9.naver.com)를

방문하여 게임을 즐기면 어렵지 않게 내용을 이해할 수 있을 것이

다. 물론 재미도 있으니 즐겨보기 바란다.

Page 87: The platform 2011

NHN 서비스 들여다보기The Platform 201186

그렇다면 네이버 건강 서비스(이하 건강 서비스)에서 MFO를 어

떻게 적용했는지 어떤 문제가 있었는지 알아보기로 하자.

건강 서비스 MFO 적용 사례

위에서 언급했듯이 건강 서비스에서는 음식 화면 페이지를 기준으

로 1주일 동안 사용자가 가장 많이 찾아본 음식 정보를 1위부터 10

위까지 화면에 노출하라는 요구 사항이 있었다(그림 1).

이를 구현하기 위해 건강 서비스에 매일 저장되는 아파치 서버 웹

로그 정보를 이용하여 음식 코드를 검색해서 조회 횟수를 세는데,

사용자 PV가 많으면 많을수록 그에 비례하여 아파치 서버 웹 로그

파일 크기도 증가한다. 즉 대용량 데이터가 된다는 것이다.

로그의 접속 URL 패턴을 분석하여 음식 ID 정보만 가져온다. 예

를 들어 F01789이라는 음식 ID를 하나 찾아내면 (F01789, 1)이라

는 Map 형태로 데이터를 저장하게 된다. (F01789, 1)이 10개 나타

났다면 Reduce 과정을 거쳐 F01789=10의 형태로 파일에 출력하여

우리가 원하는 결과를 가져올 수 있게 된다. F01789의 음식이 갈비

탕이라면 사용자들이 갈비탕을 10번 호출했다고 할 수 있다(그림 2).

네이버 건강 서비스(http://health.naver.com)는 2011년 6월 오픈

한 서비스로, 정확한 종합 건강 정보를 한 곳에서 찾고 사용자의 건

강 상태를 기록하고 관리하는 기능을 제공한다. 네이버 건강 서비

스는 질병/검사, 의약품, 영양/다이어트, 운동/피트니스, 개인건강

기록부, 병원/약국 찾기 등의 메뉴로 구성되어 있다.

오픈 전 개발 단계에서 영양/다이어트 스프린트 진행 중에 가장 많

이 본 음식을 10위까지 정렬하여 노출하라는 요구 사항이 있었고,

이를 구현하기 위해 아파치 서버 웹 로그를 분석하기로 했다. 이때

서버 1대의 로그만 분석하면 신뢰도가 낮으므로, 대용량 데이터를

분산 처리하기 위해 저장시스템개발팀에 문의하여 OwFS(Owner

based File System)와 MFO(MapReduce Framework for

OwFS)를 도입했다.

OwFS는 대용량 데이터를 처리하는 Owner 기반의 분산 파일 시

스템으로, 연관성을 가진 다수의 파일을 Owner라는 저장소 단위

로 여러 대의 서버에 분산하여 저장한다. Hadoop은 NHN의 사내

시스템인 OwFS에 비해 운영 및 관리 비용이 추가되기 때문에 사

용하지 않았다. MFO는 대용량 파일 처리에 적합한 프레임워크이

며 MapReduce는 Apache Hadoop의 하위 프로젝트이다.

네이버 건강 서비스, MFO를 적용한 대용량 데이터 처리 사례얼마 전에 “학생이니?”라는 소리를 들은 바쁜 동안(미녀) 개발자입니다.

솔로 남성들 소개팅도 해 줘야 하고 고양이 집사 노릇도 해야 하고 아마

바쁘게 살아서 늙을 새가 없나 봐요. ㅋㅋㅋ

마지막으로 집필에 큰 도움주신 달마대사님께 감사 인사를 전합니다.

•책건강서비스개발팀 _ 황사영

Page 88: The platform 2011

네이버 건강 서비스, MFO를 적용한 대용량 데이터 처리 사례The Platform 2011 87

// client가 실제로 request 요청한 URL 부분

responseCode = StringUtils.

trimToEmpty(patternMatcher.group(3));

// request에 대한 responset code 부분

// 요청 URL이 음식 End Page이고,

// HTTP 응답코드가 200(OK)일 경우

if (accessUrl.indexOf(FOOD_URL_PATTERN) > -1 &&

responseCode.startsWith("200")) {

codeValue = StringUtils.substringBetween(

accessUrl, CODE_BEFORE_TEXT,

CODE_AFTER_TEXT);

// 코드값(codeValue)이 없을 경우, codeValue는

// null이 할당되므로 아래 trimToEmpty로 방어코드.

foodId.set(StringUtils.trimToEmpty(

codeValue));

context.write(foodId, one);

}

}

}

}

위 코드는 map 함수이다. 미리 정의한 아파치 서버 웹 로그의 패

턴을 비교하여 foodId, 즉 codeValue 값을 가져와서 foodId와

hadoop에서 쓰이는 LongWritable(1)의 값으로 저장한다. 이 값

이 코드에서 one에 해당한다.

다음으로 Reduce를 보겠다. Reduce는 말 그대로 map들을 정리

하고 카운팅하여 순위를 출력할 수 있게 하는 연산이다.

public void reduce(Text key, Iterable<LongWritable> values,

Context context) throws IOException, InterruptedException {

long sum = 0;

for (LongWritable val : values) {

sum += val.get();

}

countResult.set(sum);

context.write(key, countResult);

}

위 연산을 토대로 아래와 같은 Reduce 결과가 반영된 Owner 파

일을 얻게 된다.

그림 1 음식 화면 페이지 예시

그림 2 조회 순위 계산 과정

Input 단계에서는 로그 파일을 읽어 foodId만 추려서 키(Key)/값

(Value) 쌍으로 만들고, Map 단계에서는 각 foodId로 개수를 키/

값 형태로 출력한다. Shuffle 단계에서는 이를 병합하고 Reduce

단계에서 카운팅 연산을 수행하여 최종적으로 output 단계에서

F01789의 foodId를 가진 갈비탕이 3번 호출되어 가장 많이 본 음

식 1위를 차지하게 되는 것이다.

그렇다면 이제 실제 건강 서비스에서 사용하는 코드를 통해

MapReduce의 과정을 살펴보도록 하자.

protected void map(Text key, Text value, Context context)

throws IOException, InterruptedException {

Matcher patternMatcher = regex.matcher(

value.toString());

// 아파치 서버 웹 로그 패턴이 정의한 로그 패턴과

// 일치하는지 체크

if (patternMatcher.matches()) {

accessUrl = StringUtils.

trimToEmpty(patternMatcher.group(2));

...nutrition/detail.

nhn?foodid=F017

89&foodTypecode-

F01

(F01789.1)

(F01789.1)

(F01123.1)

(F01456.1)

(F01789.1)

(F01123.1)

(F01123, [1, 1])

(F01456, [1])

(F01789, [1, 1, 1])

(F01123, 2)

(F01456, [1])

(F01789, 3)

F01789 = 3

F01123 = 2

F01456 = 1

INPUT MAP SHUFFLE REDUCE OUTPUT

Page 89: The platform 2011

NHN 서비스 들여다보기The Platform 201188

표 1 AbstractRankingTasklet의 메서드

메서드명 역할

readReduceResult() Reduce Owner에 저장된 Key/Value를 정렬하기 위해 Map

에 저장한다.

descendingSortByValue() 순위를 구하기 위해 내림차순으로 정렬한다.

Init() MFO 풀의 호스트명과 등록된 서비스 코드(health)를 바탕으

로 Owfs를 초기화한다.

deleteExpiredOwner() 전날을 기준으로 일주일 전보다 오래된 Owner는 삭제하여

Map 대상에서 제외한다.

isExpiredOwner() 분석해야 하는 대상의 Owner인지 판단한다.

deleteReduceOwnerFile() Reduce 실행을 통해 생성되는 파일이 Owfs상에 이미 존재하

는지 체크하고 존재하면 삭제한다. 삭제하지 않으면 이미 파일

이 존재한다는 예외가 발생한다.

commonOwfsInputOutput

Format()

Hadoop의 mapreduce 실행 job 및 Owfs Input/Output

Format관련 공통 코드이다.

createTopRanking() 랭킹 정보 데이터베이스 입력을 위한 공통 코드이다.

AccessLogReducer에는 앞에서 보았던 Reduce task 로직이 있고,

LogFileFilter는 Map 클래스에서 이용하는 대상 OwFS Owner

조건 및 Owner 내부의 파일 조건을 필터링한다.

FoodRankingTasklet은 Map과 Reduce 및 MFO API를 이용하

여 실제 Business Logic을 구현하는 배치 실행 클래스이다.

FoodAccessLogMapper는 Hadoop의 Map 클래스 구현체로 원

하는 End Page URL 패턴을 Key/Value 형태로 읽는 클래스로서

앞에서 설명한 Map의 역할을 하는 클래스이다.

건강 서비스에 MFO를 적용하면서 생긴 난관들

새로운 것을 한 번에 완전하게 적용하긴 쉽지 않다. 건강 서비스도

예외는 아니었다.

첫 번째 난관은 Mapper & Reducer의 ClassNotFoundExce

ption이었다. Task Tracker Node에서 Map과 Reduce task가 실

행되기 위해서 Child JVM을 띄우는데 JVM 구동 시 최소 참조 단

위는 jar이므로 단순 클래스 파일(.class)로는 Map과 Reduce 클래

스에 대해서 ClassNotFoundException이 발생했다. 서버에서 실

행하면 문제가 없지만 클라이언트에서 실행하면 문제가 발생했다.

결국 Eclipse에서는 해당 jar를 자기 참조(self reference)하도록 설

정하여 문제를 해결했다. 다음 그림을 보자.

F01001=2

F01042=2

F01044=1

F01127=1

F01133=2

F01203=1

F01231=1

F01235=3

F01249=1

F01256=1

F01260=2

F01273=1

F01294=13

건강 서비스에서 가장 많이 본 음식 MFO 관련 클래스 다이어그램

은 다음과 같다.

그림 3 가장 많이 본 음식 MFO 관련 클래스 다이어그램

접속 랭킹을 구하며, A b s t r a c t R a n k i n g Ta s k l e t 은

FoodRankingTasklet 클래스에서 공통적으로 사용하는 메서드가

존재하는 슈퍼클래스이다. 각 메서드는 다음과 같은 역할을 한다.

Page 90: The platform 2011

네이버 건강 서비스, MFO를 적용한 대용량 데이터 처리 사례The Platform 2011 89

7. 이 때 TaskTracker는 health-batch.jar의 위치를 읽어서 JobTracker

에게 자신이 수행하고 있는 task들의 상태 및 장비 환경 정보(예: 사용 메

모리)를 heartbeat 메시지로 주기적으로 알린다. JobTracker는 해당

TaskTracker가 수행해야 할 task가 있을 경우 이 heartbeat의 응답으

로 task 정보를 전달한다.

8. TaskTracker는 수행할 task가 있으면 task 수행에 필요한 정보를

Owfs로부터 읽어온다. 이 때 JobTracker에서 알려준 health-batch.

jar의 위치를 찾아서 TaskTracker 노드에 복사된다. 복사 수행 시간이 걱

정될 수 있으나 실제로는 내부 정렬(sorting) 및 map, reduce 결과 전

송 과정에서 더 시간이 많이 걸린다.

9. TaskTracker는 task를 수행할 하위 JVM(child JVM) 프로세스를 실행

한다.

10. 복사된 health-batch.jar를 로드하고 하위 JVM에서 사용자의

MapReduce 프로그램이 실행된다.

그림 4 MFO 실행 프로세스

1. 사용자가 만든 MapReduce 프로그램이 실행되면 프로그램 내부의

JobClient가 job 수행을 요청받는다.

2. JobClient는 JobTracker에서 새로운 job의 ID를 받아온다. job에 대

한 InputSplit 정보 및 job 수행 환경 정보를 Owfs에 기록한다.

3. 위 과정에서 Owfs API는 health-batch.jar(건강 서비스 배치)를

Owfs에 복사한다.

4. JobClient는 JobTracker에게 job을 수행하도록 요청한다.

5. JobTracker는 job 수행을 위한 초기화 작업을 수행한다.

6. 그 후 Owfs에 저장되어 있는 InputSplit(hadoop의 인터페이스) 정보

및 job 수행 환경 정보를 읽어서 스케줄링한다. InputSplit의 역할은 현

재 OwfsOwnerInputFormat이 수행하고 있으며 Owner 내부의 파일

하나다.

7: heartbeat(returns task)

1: run job

3: copy jobresources

8: retrievejobresources

6: retrieveinput splits

2: get new job ID

4: submit jobMapReducepprogram

5: initialize job

10: run

9: launch

JobClient

•Shared FileSystem

:owfs

•Jobtracker node

:xdev037.vpd

•tasktracker node

:xdev021 ~ xdev030

JobTracker

TaskTracker

MapTaskor

ReduceTask

SharedFileSystem(e.g. HDFS)

Child

client JVM

client nodejobtracker node

Page 91: The platform 2011

NHN 서비스 들여다보기The Platform 201190

여기에서 하위 JVM의 최소 실행 단위가 jar이므로 클라이언트에

서 통합 테스트를 실행했을 때 ClassNotFoundException가 발생

한 것이다.

두 번째 난관은 Apache common StringUtils의 ClassNotFound

Exception이었다. 클라이언트에서는 메이븐의 dependency로

StringUtils를 참조할 수 있었지만 서버에서는 StringUtils을 찾

지 못했다. 따라서 OwFS에 lib 디렉터리를 만든 후에 common-

lang.jar 파일을 복사하여 MFO 구동 시에 참조될 수 있게 했다.

마치며

지금까지 건강 서비스에서 실제 MFO를 활용한 사례를 설명했다.

언제 어떻게 MFO를 사용하는 것이 효과적일까? 건강 서비스 개

발 결과로는 다음과 같은 결론을 내렸다.

웹 서버를 여러 개 사용하는 등의 분산 환경에서 대용량 데이터를

저장(OwFS)하고 그 데이터를 기반으로 의미 있는 분석이 필요하

다면 Hadoop의 MapReduce 분산 병렬 처리 프레임워크를 이용

하자.

참고 자료

Hadoop & HDFS & MapReduce

http://hadoop.apache.org/

http://hadoop.apache.org/hdfs/

http://hadoop.apache.org/mapreduce/docs/current/mapred_

tutorial.html

MFO

http://devcafe.nhncorp.com/ThePlatform/211090#1

http://devcafe.nhncorp.com/owfs_cafe

http://devcafe.nhncorp.com/batch/board_4/247852

Page 92: The platform 2011

MQ를 이용한 SNS 글쓰기 성능 개선The Platform 2011 91

얼른 넣고 잘 가져가기

PICK에서 타임라인을 구성하는 기본 데이터는 사용자의 포스트

이다. 모든 포스트를 하나의 테이블에 모아두고 본인의 메인보드를

조회하는 사용자의 팔로잉 관계에 따라 데이터를 가져와서 타임라

인을 구성한다.

초기에는 포스트 작성 시에 별다른 처리 없이 저장하고 조회 시점

에 가져와야 하는 조건에 따라 포스트를 쿼리하는 구조로 데이터를

관리했다.

SELECT

*

FROM (

SELECT iu2.interest_user_hash as user_hash

FROM interest_user iu2, user u

WHERE iu2.user_hash = #userHash#

AND iu2.interest_user_hash = u.user_hash

AND u.status = ‘R’

UNION ALL

SELECT #userHash# AS user_hash) ul,

post p

LIMIT #startRow#, #fetchSize#

네이버 재팬의 PICK은 트위터나 페이스북, 미투데이와 유사한 형

태의 SNS 서비스이다. PICK은 “메인보드”라고 하는 개인화된 타

임라인을 중심으로 본인이 팔로잉하는 사용자의 포스트와 본인이

작성한 포스트가 노출되는 서비스로, 트위터와 매우 유사한 구조를

가진다.

그리고 2010년 10월에 공통의 관심사를 가진 사용자들의 연결 고

리를 만들기 위해 CAFÉ라는 서비스를 오픈했다. 서비스의 지향점

은 “같은 관심사를 가진 사람들과 실시간으로 쉽게 만나서 소통할

수 있는 SNS”이다.

SNS에서 서비스의 중심이 되는 공간은 사용자의 타임라인(페이스

북에서는 담벼락)이다. 사용자의 행동에 따른 모든 결과는 타임라

인에 모이고, 이곳에서 다른 사용자의 코멘트나 리트윗, Like와 같

은 액션이 발생한다. 이렇게 발생한 액션은 또 누군가의 타임라인

에 기록된다. 따라서 SNS는 이 타임라인의 데이터를 어떻게 구성

하고 유지하고 보여주는가가 매우 중요한 부분이다.

MQ를 이용한 SNS 글쓰기 성능 개선네이버의 일본 서비스 개발에 참여하고 있으며 도메인 모델링과

서비스 아키텍처와 같은 그림 그리기에 관심이 많습니다.

내년 초에는 쌍둥이(온&유) 아빠가 될 예정입니다.

온유야~~!

•일본서비스개발1팀 _ 정민규

Page 93: The platform 2011

NHN 서비스 들여다보기The Platform 201192

2. 포스트 작성 시 포스트를 조회해야 하는 사용자의 드라이빙 테이블에 포

스트 아이디 추가

3. 조회 시에 드라이빙 테이블을 이용하여 포스트 아이디 추출 후 데이터

테이블에 쿼리

여기서 중요한 작업은 2번이다. 해당 포스트를 조회하는 모든 사용

자의 드라이빙 테이블에 포스트 아이디를 추가해야 하는데 팔로워

가 많은 사용자가 포스트를 작성하면 (팔로워의 수 + 1)만큼 삽입

(insert)을 실행해야 한다. 이렇게 구조를 변경하면 앞에서 봤던 복

잡한 쿼리가 다음과 같이 단순한 쿼리 2개로 분리된다.

� 인덱스를 뽑아오기 위한 쿼리

SELECT post_id

FROM mainboard_index_#tableIndex#

WHERE user_hash=#userHash#

LIMIT #startRow#, #fetchSize#

� 변경된 메인보드 쿼리. 위에서 구한 post_id 리스트가 IN 쿼리의 파라미터로

사용된다.

SELECT

*

FROM

post

WHERE

post_id IN (#postId#, #postId#, …)

LIMIT #startRow#, #fetchSize#

실제 서비스의 응답 속도에는 쿼리의 개수보다는 부하가 심한 쿼리

몇 개가 더 큰 영향을 미치기 때문에 인덱스를 정확하게 이용하고

있다면 쿼리 개수가 늘어나는 것에 큰 부담을 갖지 않아도 된다.

1 : N+1

위와 같이 구조를 개선하여 조회 시 부담은 상당히 줄어들었지만

포스트 작성 시 처리해야 하는 작업의 양은 늘어났다. 우선 팔로워

의 드라이빙 테이블에 포스트 아이디를 삽입해야 하는데 이 작업은

팔로워 수에 정비례하기 때문에 팔로워가 많은 사람이 글을 쓰면

이 부담이 상당하고 응답 시간에도 영향이 크다. 따라서 이 작업은

비동기식으로 처리하게 되었다.

위 SQL문이 사용자 타임라인을 가져오는 기존 쿼리이다.

#userHash#라고 표현된 부분이 해당 사용자의 키이다. 붉은색으

로 표시된 부분이 본인이 팔로잉하는 사용자를 제한하는 부분이다.

이 쿼리의 문제점은 사용자의 팔로워가 늘어나는 양에 비례하여 서

브쿼리의 부담이 커져서 쿼리가 느려진다는 데 있다.

또 하나의 큰 문제는 팔로잉 관계 외의 조건을 포함하는 것이 어렵

다는 것이다. 예를 들면 트위터의 멘션이나 페이스북의 좋아요와

같은 경우이다. PICK에서는 이것이 사용자 지정 투고라는 기능으

로 추가되었다. 지정 투고라는 것은 팔로워 관계와 상관 없이 특정

사용자가 다른 사용자에게 멘션을 보내는 기능이다. 위 쿼리에 이

새로운 룰을 추가하려면 서브쿼리가 한없이 복잡해진다.

잘 넣고 얼른 가져가기

그래서 포스트 관리의 개념을 바꾸기로 했다. 전환점의 키 포인트

는 “잘 넣고 얼른 가져가기”이다. 즉 포스트 작성 시의 처리를 조금

늘려서라도 데이터 조회 시의 부담을 줄이는 것이다. 실제로 포스

트 작성 대비 조회 쿼리의 비율이 압도적으로 높을 수 밖에 없는 서

비스 특성에는 더 바람직하다고 생각된다.

그림 1 사용자별 메인보드를 구성하기 위한 개념도

잘 넣고 얼른 가져가기 위한 절차는 다음과 같다.

1. 사용자별 메인보드를 조회하기 위한 드라이빙 테이블 유지

POST

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

post

real time background

Page 94: The platform 2011

MQ를 이용한 SNS 글쓰기 성능 개선The Platform 2011 93

private static final int MAX_PER_BATCH_SIZE = 420;

List<String> addToFollowers(final PostIndex postIndex) {

List<String> followers = followerRepository.

findFollowers(postIndex.getUserHash());

if (followers == null) {

return Collections.emptyList();

}

String lastTableIndex = StringUtils.EMPTY;

List<PostIndex> followersPerSameBatch = new

ArrayList<PostIndex>();

for (String interestUser : followers) {

PostIndex followerPostIndex = postIndex.followerPost

Index(interestUser);

// add partial indexes if no more same table

// index or over per batch limit size

if (lastTableIndex.equals(StringUtils.

EMPTY) == false && isSameIndexTable(lastTableIndex,

followersPerSameBatch, followerPostIndex)) {

postIndexRepository.add( followersPerSameBatch);

// clear batch list

followersPerSameBatch = new ArrayList<

PostIndex>();

}

followersPerSameBatch.add(followerPostIndex);

lastTableIndex = followerPostIndex.getTableIndex();

}

// add remained indexes

postIndexRepository.add(followersPerSameBatch);

LOGGER.info(" followers added : " + followers.

size());

return followers;

}

비동기식으로 처리하려면 메시지 큐가 반드시 필요하기 때문에 몇

가지 제품을 조사했고, kestrel과 rabbitMQ 중에서 레퍼런스가 많

고 SpringSource가 지원하는 rabbitMQ를 선택했다. 마침 서비스

도입 검토 시점에 나온 스프링 AMQP가 선택에 큰 영향을 주기도

했다. rabbitMQ나 AMQP에 대한 자세한 설명은 이 글의 주제를

벗어나므로 여기에서는 생략하겠다.

그림 2 시스템 개요

드라이빙 테이블에 포스트 아이디를 삽입하는 작업을 비동기로 처

리함으로써 포스트 작성 시의 응답 속도는 더 이상 문제가 되지 않

았지만, 실제 포스트 아이디를 넣는 작업의 효율을 올리는 일이 남

아 있다. 현재 서비스에서 팔로워가 가장 많은 사용자는 "pick스태

프"라고 하는 서비스 운영자 계정인데 팔로워가 약 11만명이다. 이

사용자로 포스트를 쓰면 11만 개의 레코드가 각 사용자의 드라이

빙 테이블에 삽입된다는 뜻이다. 물론 이 사용자는 특별한 경우이

기 때문에 따로 예외 처리를 할 수도 있지만, 나중에 실제로 이렇게

팔로워가 많은 사용자가 생길 수도 있기 때문에 예외 처리는 고려

하지 않았다.

INSERT 문의 숫자를 줄이기 위해서 사용한 것은 배치 인서트이

다. ORM으로 사용 중인 iBatis가 배치 인서트를 지원하기 때문에

큰 문제 없이 적용했다. 다만 쿼리 하나로 INSERT 문을 몇 개까

지 처리하는 것이 적당한지가 문제였고, DBA 테스트 결과 한 번에

420개 정도의 배치를 처리하는 것이 가장 효과적이어서 해당 개수

의 INSERT 문을 한 번에 처리하게 했다.

Page 95: The platform 2011

NHN 서비스 들여다보기The Platform 201194

만 어노테이션 기반으로 태스크를 실행할 수 있는 등의 기능을 제

공하여 좀 더 편하게 스레드를 사용할 수 있다.

다음은 스프링의 Async Executor를 사용하도록 ThreadPoolTask

Executor를 설정하는 실제 코드이다. ThreadPoolTaskExecutor

는 내부적으로 java.util.concurrent.ThreadPoolExecutor를 사용

한다.

<context:annotation-config />

<task:annotation-driven executor="defaultExecutor" />

<!-- ThreadPoolTaskExecutor -->

<task:executor id="defaultExecutor" pool-size="40-50" queue-

capacity="5000" />

위와 같이 설정하면 최대 50개의 스레드가 동시에 실행될 수 있고

그 이상의 태스크를 내부 메모리 큐에 넣은 후 처리한다. 따라서 이

상적인 경우에는 11만 개 INSERT 문에 대한 420개의 배치 쿼리를

순차적인 처리에 비해 50배 정도 빠르게 처리할 수 있다.

하지만 이제 시작일 뿐

이렇게 해서 사용자의 타임라인 조회에 대한 부담을 줄였지만 아직

해결해야 할 과제가 많이 남아 있다. 드라이빙 테이블은 36개로 해

싱되어 있는데 벌써 몇 천만 건의 데이터가 있다. 이 데이터를 어떻

게 유지할 것인가가 한 가지 관건이다. 또한 실제 포스트 데이터가

저장되는 post 테이블의 크기도 빠르게 증가하고 있기 때문에 결국

대용량 사용자 데이터를 어떻게 관리할 것인가라는 숙제를 이제부

터 고민해야 한다. 아마도 RDBMS와 NoSQL이 혼합되는 구조가

될 것으로 예상한다. 남은 숙제를 해결해서 다시 The Platform에

서 그 내용을 공유할 수 있길 바란다.

PICK/CAFÉ 서비스

http://pick.naver.jp

http://cafe.naver.jp

private boolean isSameIndexTable(String lastTableIndex,

List<PostIndex> followersPerSameBatch, PostIndex

followerPostIndex) {

return (followersPerSameBatch.size()

== getMaxPerBatchSize() || lastTableIndex.

equals(followerPostIndex.getTableIndex()) == false);

}

protected int getMaxPerBatchSize() {

return MAX_PER_BATCH_SIZE;

}

그런데 420개의 INSERT 문을 쿼리 하나로 처리해도 11만 개의

INSERT 문을 처리하려면 약 260번의 쿼리가 필요하고, 이를 순차

적으로 처리한다면 수 초의 시간이 걸린다. 따라서 다음과 같이 배

치 인서트를 병렬적으로 처리한다.

@Async

public void add(final List<PostIndex> followers) {

getSqlMapClientTemplate().execute(new SqlMapClientCallba

ck<PostIndex>() {

@Override

public PostIndex doInSqlMapClient(SqlMapExecutor

executor) throws SQLException {

executor.startBatch();

for (PostIndex follower : followers) {

executor.insert(NAMESPACE + "add",

follower);

}

executor.executeBatch();

return null;

}

});

}

병렬 처리를 위해 스레드를 사용해야 했고, 이를 위해 스프링의 @

Async를 이용했다. 스프링에서 제공하는 Executor 패키지는 자바

의 Concurrent 패키지를 사용하기 편하게 구성해 놓은 기능이기

때문에 기본적인 사용법은 java.util.concurrent와 동일하다. 하지

Page 96: The platform 2011

MQ를 이용한 SNS 글쓰기 성능 개선The Platform 2011 95

참고 문헌

http://www.amqp.org

http://www.rabbitmq.com

http://www.springsource.org/spring-amqp

http://static.springsource.org/spring/docs/3.0.x/spring-

framework-reference/html/scheduling.html#scheduling-

annotation-support-async

Page 97: The platform 2011

NHN 서비스 들여다보기The Platform 201196

들어가며

모니터링시스템개발팀에서는 xMon3과 같은 모니터링 시스템을

개발 및 운영하고 있다. 이 xMon이 사용하는 오라클 데이터베이

스의 스토리지 용량은 20TB이다. 이 오라클 데이터베이스에는 네

이버의 각 서비스에서 발생한 최근 6개월의 UGC(User Generated

Contents) 메타 데이터(본문이나 멀티미디어 데이터는 OwFS4 ,

동영상 인프라 등에 분산 저장)와 그에 대한 많은 양의 로그 데이터

가 저장되어 있다. 아래는 로그가 발생하는 UGC의 생성과 소멸 주

기에 대한 그림이다. 이 20TB의 데이터를 처리하기 위해 128GB

메모리의 오라클 DBMS 장비 3대를 사용해왔다.

3 NHN에서 사용하는 모니터링 시스템의 종류를 통칭하는 말. uMon, kMon, cMon, jrMon,

ocMon와 같은 모니터링 시스템이 있다.

4 NHN에서 개발한 분산파일저장소로써 Owner라는 단위로 각 저장소를 할당/분산하는 방식이

특징이다.

기하급수적으로 증가하는 서비스의 로그를 어떻게 하면 낮은 비용

으로 빠르게 처리할 수 있을지 고민했다. 모니터링시스템개발팀에

서 지난 몇 개월간 로그 분석을 위한 좋은 방법을 찾기 위해 노력해

왔다. 그 노력의 결과를 소개해보려 한다.

개발자와 데이터베이스 관리자는 업무에 대한 많은 고민을 안고 하

루하루 살아간다. 그 많은 고민 중 대표적인 것이 통계가 될 것이

다. 운영자(혹은 기획자)에게 “통계를 추출해 주세요.”라는 요청을

받았다고 가정하자. 몇 백만 건(row) 이하의 소규모 데이터이거나,

통계를 추출하는 것을 전담할 수 있는 백업 데이터베이스가 있다면

상대적으로 답을 얻기 쉬운 고민이겠지만, 용량이 크고 백업 데이

터베이스가 없는 상황이라면 결코 가볍지 않은 고민일 것이다. 모

니터링 시스템에서 ‘어떻게 하면 통계 작업을 좀 더 잘 할 수 있을

까?’하고 오랫동안 고민해왔고 현재 MongoDB1와 Hadoop2

을 사용하는 방법으로 그 고민을 해결했다. 이 글에서는 해결책을

찾는 긴 여정 동안 고민하고 테스트한 과정과 그 결과를 공유한다.

1 http://www.mongodb.org/display/DOCSKR/

2 http://hadoop.apache.org/

Hadoop과 MongoDB를 이용한 로그분석 시스템“개발자로 살아라!”라고 아무도 말하지 않았음에도

개발이 좋아 이렇게 살고 있음에도

개발 밖에 할 줄 아는 것이 없음에도

“무슨 개발 하세요?”라는 물음에 답을 못하고 있습니다.

•웹 서비스 개발실 _ 이정현

Page 98: The platform 2011

Hadoop과 MongoDB를 이용한 로그분석 시스템The Platform 2011 97

• 요구사항 1: 기존의 로그 데이터를 활용해서 사용자 단위 지표를 추출한

다.

•요구사항 1-1: 지표 추출 주기는 최소 1일 1회 이상이어야 한다.

• 요구사항 2: 추출된 사용자 단위 지표는 온라인 상에서 바로 사용할 수

있도록 한다.

• 요구사항 2-1: 기존에 사용하던 시스템에 조회 기능으로 추가되어야 한

다.

요구사항 1은 한마디로 요약하면, "통계 성격의 데이터인 사용자

단위 지표를 가능한 빠르게 추출해 주세요"이다. 기존의 통계 데이

터 추출은 개발자가 SQL을 작성한 후, 데이터베이스 관리자에게

전달하면 데이터베이스 관리자가 SQL을 검증하고 이를 수행하여

그 결과를 전달하는 방식으로 진행했다. 이런 방식으로는 "가능한

빠르게" 추출하는 것이 어렵다.

요구사항 2는 웹 브라우저에서 지표 데이터를 조회할 수 있게 데이

터를 제공해 달라는 것이다. 데이터의 양이 많지 않다면 문제가 되

지 않겠지만, 예상 데이터의 양이 무려 4TB에 달해 기존의 오라클

데이터베이스에 추가하기 어려웠다. 특히 UGC 메타 데이터 및 로

그 데이터가 지속적으로 증가하고 있어 지표 데이터까지 추가할 경

우 스토리지 부담은 계속 늘어날 수 밖에 없는 상황이었다.

Map-Reduce를 활용한 데이터 집계와 추출

요구사항에 필요한 지표 추출용 테이블은 30~60개의 컬럼으로

구성된 일반적인 로그 테이블 형태이다. 마스터 테이블과 논리적

인 관계는 있으나, 실제 관계(Relation)가 없는 단순한 스키마이

다. 따라서, 요구사항 1을 만족시키기 위한 방법은 누구나 예측할

수 있듯이 DBMS를 활용하거나 Map-Reduce 기법을 활용하는

것이다.

1차로 오라클에서 직접 SQL을 수행하여 지표 추출을 시도했다. 이

미 다양한 통계 데이터를 제공하기 위해 수십 개의 PL/SQL과 집

계 테이블이 있는 상황에서 쿼리를 추가하는 것은 성능의 문제만

없다면 별로 부담되지 않는다. 이를 위해 날짜를 잘게 쪼개 분할 정

복법(divide-and-conquer)으로 SQL을 작성하고 진행했다. 테스

트 베드를 시작하고 1주일이 지난 시점에 데이터베이스 관리자로

부터 연락이 왔다. 출근해 보니 860분 동안 SQL문이 실행되고 있

었고 CPU 사용량이 평소 30~40%였는데 60%로 증가하여 프로

그림 1 UGC 생성&소멸 주기

오라클 DBMS를 사용하는 환경은 시간이 지날수록 상황이 어려

워졌다. UGC가 기하급수적으로 늘어나고 있었다. 최근 6개월치

만 저장한다는 정책을 세워도 그 6개월에 해당하는 양이 지속적으

로 증가하기 때문에 관리가 어려웠다. 게다가 오라클 DBMS의 스

토리지 추가 비용 또한 무시할 수 없었다. 다음 표와 아래 그림은

xMon이 사용하는 스토리지 사용 현황과 스토리지 사용량 추이이

다.

표 1 xMon 스토리지 사용 현황

구분 남은 공간 사용 공간 전체 공간 사용율

xmona 4593GB 3506GB 8100GB 43.29%

xmonb 2768GB 5619GB 8387GB 66.99%

xmonc 1533GB 3018GB 4552GB 66.30%

그림 2 xMon 스토리지 사용량 추이

이런 상황을 버티며 통계 작업을 해오다가, 아래의 핵심 요구사항

두 가지로 인해 오라클 DBMS가 아닌 좀 더 나은 방법을 찾아야

했다.

Page 99: The platform 2011

NHN 서비스 들여다보기The Platform 201198

Hadoop 기반의 Map-Reduce를 검증하기 위해서는, 오라클

에 직접 접근할 수 없으므로 데이터 추출 작업이 선행되어야 한

다. 오라클에 저장된 로그 데이터를 추출하기 위해, 오픈 소스

ETL(Extract, Transform, Load) 솔루션인 펜타호(Pentaho)의

Kettle을 사용하였다. 이 테스트에서는 Hadoop의 여러 기능 중에

서 HDFS와 Map-Reduce를 이용했다.

테스트를 위해 세가지 환경을 구성했다. Hadoop의 경우 단일 노

드 환경(Standalone Operation)과 데이터 노드(DataNode) 3

개, 네임 노드(NameNode) 1개로 구성된 분산 노드 환경(Fully-

Distributed Operation)의 성능 차이를 살펴보았다. 그리고

MongoDB의 Map-Reduce와의 성능 비교를 수행했다. 참고로

테스트를 수행한 장비의 사양은 다음과 같다(본 테스트에 사용된

장비의 사양은 모두 동일하다).

•CPU: Intel(R) Xeon(R) CPU [email protected] * 2 (8 코어)

•메모리: 32GB

•디스크: 814GB (300GB * 6대, RAID 0 + 1)

•운영체제: CentOS 5.2 x86_64

위 사양의 서버로 Hadoop 단일 노드 환경은 1대, 분산 노드 환경

은 4대, MongoDB는 총 5대로 구성하였다. Hadoop의 분산 노드

환경은 네임 노드와 데이터 노드를 분리한다. 그 이유는 메인 컨트

롤러와 Reduce 작업을 하는 Job Tracker는 네임 노드에서 실행되

고, 실제 Map 작업을 수행하는 Task Tracker는 데이터 노드에서

실행되기 때문이다. MongoDB에 대한 자세한 설명은 뒤에서 하기

로 하고, Map-Reduce를 Hadoop과 유사하게 사용할 수 있도록

기능을 제공한다는 정도만 밝힌다.

Hadoop의 데이터 노드와 네임 노드 등의 클러스터 구성도는 아래

그림과 같다. 비교 대상인 MongoDB 의 샤드(Shard) 구성은 그림

6을 참고 바란다.

그림 3 Hadoop 클러스터 구성

세스를 멈췄다는 것이다. 이 얘기와 함께 더 이상 테스트를 진행하

기 어렵다고 했다. 혹시나 하는 생각에 시도한 1차 지표 추출은 무

참히 실패했다. 참고로 아래 코드는 최근 1개월동안 검수하지 않은

데이터의 잔량을 집계하는 코드로 실제로 활용하고 있는 SQL의

일부 예이다.

INSERT INTO kpi_total_xxxxxx

(dd, td,... )

SELECT /*+ index

(b CONT_MSTR_CATE_PK) */

SBUSTR(v_start_datehour, 1, 8) dd,

a.td,...

SUM(cnt) as cnt,...

FROM

(SELECT /*+ parallel_index

(cm 3) index_ss

(cm CONT_MSTR_IDX01) */

TO_CHAR(umon_in_tdt, 'YYYYMMDD') td,

COUNT(1) as cnt

FROM cont_mstr cm

WHERE ...

AND cm.umon_in_tdt >=

TO_DATE(TO_CHAR(ADD_MONTHS

(SYSDATE, -1),

'YYYYMM')||'01000000',

'YYYYMMDDHH24MISS')

GROUP BY

TO_DATE(cm.umon_in_tdt, 'YYYYMMDD'),

cm.category_id) a,

cont_mstr_cate b

WHERE

a.service_code = b.service_code ...

GROUP BY a.td;

그래서 오라클의 대안으로 검토한 것이 Map-Reduce였다. Map-

Reduce는 RBDMS의 정렬/병합(Sort/Merge) 기능과 흡사하

며, 대량의 데이터를 읽고 분석하는 OLAP(On-line Analytical

Processing) 분야의 데이터 일괄 분석에 적합하다. 흔히 Map-

Reduce하면 Hadoop을 떠올리지만, MongoDB, HBase,

CouchDB, Hypertable 등에서도 Map-Reduce 기능을 제공하고

있다. 그 중 사용자 층이 가장 두텁고 오랜 시간 검증된 Hadoop,

그리고 Document-oriented 방식으로 로그를 순차 저장하기 용이

한 MongoDB를 선정하여 테스트하고 검증하였다.

Page 100: The platform 2011

Hadoop과 MongoDB를 이용한 로그분석 시스템The Platform 2011 99

증가하는 문제가 있기 때문에 더 자세하게 고려하지 않았다. 또한

데이터 양이 증가하여 확장이 필요할 경우, 단일 노드 환경에서는

분할 및 취합에 대한 변경 작업이 필요하지만, 분산 노드 환경에서

는 데이터 노드만 추가하면 되므로 상대적으로 변경 작업이 간단하

다는 장점도 있다.

MongoDB가 Hadoop과 유사한 성능을 보인다면 MongoDB만

으로도 집계 시스템과 추출한 지표를 저장하는 시스템을 한 번에

구축할 수 있다는 기대에, MongoDB도 테스트하였다. 위의 표에

MongoDB의 Map-Reduce 테스트 결과가 포함되어 있는데, 아

쉽게도 Hadoop에 비해 성능이 많이 떨어진다. Hadoop의 분산

노드 환경에 비해서 4배 정도 느리다고 볼 수 있다. MongoDB의

버전이 업데이트되었지만 위 결과에서 크게 바뀌지는 않으리라 생

각한다.

MongoDB의 장점은 개발 생산성

작은 양의 데이터를 사용할 경우에는 여전히 MongoDB가 유용하

다. 성능에서 보여준 단점을, 개발 생산성의 장점으로 만회할 수 있

기 때문이다. 아래의 코드는 Hadoop의 자바 API를 이용한 Map-

Reduce 메서드이다.

//Map

public static class DailyIdCountMapClass extends Map-

ReduceBase implements

Mapper<LongWritable, Text, Text, IntWritable> {

public void map(LongWritable key, Text value,

OutputCollector<Text, IntWritable> output, Reporter

reporter) throws IOException {

String line = value.toString();

String[] tokens = line.split(UserIdConstants.

COLUMN_SEPARATOR);

String userKey = tokens[0];

output.collect(new Text(userKey), new

IntWritable(1));

}

}

//Reduce

public static class DailyIdCountReduceClass extends Map-

ReduceBase implements

Reducer<Text, IntWritable, Text, IntWritable> {

아래 표와 그림에서 알 수 있는 내용은 작은 양의 데이터는 단일 노

드 환경이, 많은 양의 데이터는 분산 노드 환경(N개의 데이터 노드

로 구성 가능)이 좋은 성능을 발휘한다는 것이다. 작은 양의 데이

터를 분산 노드 환경에서 수행할 경우, Map 작업으로 데이터를 각

데이터 노드에 분산 처리하여 얻는 장점보다 Reduce 작업 시 네임

노드에서 분산된 데이터를 모으는 데 설정과 네트워크 통신 시간이

필요하다는 단점이 더 부각되어 상대적으로 성능이 떨어진다. 본

테스트에서는 1천만 행의 데이터부터 분산 노드 환경의 성능이 더

좋았다. 이는 데이터의 양과 시스템 성능에 따라 다르므로 적용할

때 반드시 테스트할 필요가 있다.

표 2 Hadoop과 MongoDB Map-Reduce의 성능 비교

데이터 건 수 파일 크기

HadoopMongoDB

Shard 2Standalone Distributed

1만건 2MB 1초 23초 1초

10만건 25MB 3초 22초 12초

100만건 248MB 20초 29초 65초

1000만건 2,480MB 183초 53초 805초

장비 - 1대 4대 5대

그림 4. Hadoop vs. MongoDB Map-Reduce 성능비교

단일 노드 환경의 테스트에는 장비 1대가 투입되었고 분산 노드 환

경은 장비 4대가 투입되었기 때문에, 이에 대한 데이터 보정이 필

요한 것은 사실이다. 하지만 단일 노드 환경을 장비 4대로 구축할

경우 데이터 분할 및 취합의 과정이 별도로 필요하여 개발 공수가

Page 101: The platform 2011

NHN 서비스 들여다보기The Platform 2011100

집계 정보 저장소 선정: MongoDB vs. 카산드라

지금까지 기존 RDBMS의 Group by 연산을 Map-Reduce 프레

임워크로 대체하기 위한 테스트를 수행했다. 이제 집계 작업의 결

과를 저장, 관리, 제공할 저장소를 선정할 차례이다. 스토리지 증가

의 제약 때문에 오라클 환경은 처음부터 배제했다.

테스트 대상은 전세계적으로 널리 사용되고 사용자 층이 두터운 두

가지를 골랐다. 첫 번째 비교 대상은 페이스북에서 개발해 유명해

진 Column family 형태의 카산드라(Cassandra)이다. 한 기사5

에 따르면 페이스북은 HBase를 이용하여 일간 200억 이벤트를 처

리할 수 있는 실시간 분석 시스템을 구축하였다. HBase를 Map-

Reduce 및 저장소로 사용한 것이다. HBase를 벤치마크 대상에 넣

지 못한 것이 아쉬웠다.

두 번째 비교 대상은 Hadoop과 Map-Reduce 성능을 비교했던

MongoDB이다. MongoDB는 직관적인 Document-oriented 방

식의 데이터 모델이고, Schema-Free 데이터베이스의 장점으로 유

연한 필드 관리가 가능하다. 아울러 Full-Text 인덱싱을 지원함으

로써 모든 문서 필드에 대한 빠른 검색을 제공하며, 특정 필드로 인

덱스를 생성할 수도 있다. MongoDB는 성능 및 안정성 면에서도

좋은 평가를 받고 있다.

표 3을 보면 Cassandra의 쓰기 성능이 MongoDB에 비해 많

이 나쁜 것을 확인할 수 있다. 작성된 코드와 문서를 검토한 결

과, 두 솔루션의 철학적 차이가 성능 차이를 냈다고 볼 수 있다.

MongoDB는 Document-oriented이기 때문에, 데이터베이스의

1 행에 해당하는 단위를 단 한번의 insert 연산으로 처리한다. 반면

에 Cassandra는 Column-base이기 때문에 행이 아닌 열 단위의

insert 연산이 수행되고, 그만큼 수행 시간이 늘어날 수 밖에 없다.

테스트에서 사용한 데이터의 경우 30개의 컬럼으로 이루어졌으므

로, 단순히 비교해도 약 30배 가까이 차이날 것이다. 하지만 실제

성능 테스트 결과 300배 정도 차이가 났다.

아래 코드는 컬럼의 개수만큼 반복하여 insert 연산을 수행해야 한

다는 것을 보여준다.

5 http://highscalability.com/blog/2011/3/22/facebooks-new-realtime-analytics-system-hbase-to-process-20.html

public void reduce(Text key, Iterator<IntWritable>

values, OutputCollector<Text, IntWritable> output,

Reporter reporter) throws IOException {

int sum = 0;

while (values.hasNext()) {

sum += values.next().get();

}

output.collect(key, new IntWritable(sum));

}

}

그리고 아래는 MongoDB의 Map-Reduce 내장 명령(Comma

nd)이다.

> m = function() { emit(this.user_id, 1); }//Map

> r = function(k,vals) { return 1; }//Reduce

> res = db.wklog_user.Map-Reduce(m, r);

위 둘을 비교해 보면 MongoDB 명령이 월등히 단순하다. 코드를

보면 확인할 수 있지만, MongoDB의 명령은 스칼라(Scala)나 루

비(Ruby) 같은 함수형 언어(Functional Language)의 장점을 채

용하고 있기 때문이다. 물론 Hadoop도 파이썬 등의 스크립트 언

어를 이용하면 좀 더 효율적으로 작성할 수 있다.

Hadoop의 Map-Reduce 기능은 성능 면에서는 우위였지만, 개

발한 기능을 디버깅하기 위한 메시지가 분산된 데이터 노드에서 출

력되기 때문에 디버깅이 쉽지 않다. 또한 코드 작성 후 컴파일과 함

께 jar 아카이브를 생성하고 이를 실행하는 단계를 거쳐야 하므로,

유지 보수 작업도 번거로운 편이다. 대신 기본 개념만 잘 이해했다

면 Map-Reduce 기능을 작성하는 것은 큰 어려움이 없을 것이다.

추가로 파라미터 등의 튜닝이 필요하지만, 이 글에서는 생략한다.

정리하면, 데이터 집계 테스트는 오라클 DBMS와, Hadoop 단일

노드 환경, Hadoop 분산 노드 환경, MongoDB의 Map-Reduce

등 4가지를 비교하는 것으로 진행했다. 그 결과 상대적으로 빠른

속도와 확장 용이성으로 인해 Hadoop 분산 노드 환경을 구성하는

것으로 결론이 났다.

Page 102: The platform 2011

Hadoop과 MongoDB를 이용한 로그분석 시스템The Platform 2011 101

참고로 Cassandra 성능 테스트에 사용된 스키마 구조는 다음 그림

과 같으며, 자유롭게 열을 구성하여 하나의 사용자(user_id 단위)와

관련된 모든 집계 정보를 담고자 했다. 계층 구조로 설계하여 각 층

별로 데이터 항목을 추가하기 쉽게 했다. 스키마를 설계할 때 가변

적인 스키마 설계가 가능해 좋아 보였지만 테스트 결과 성능이 좋

지 않아 아쉽게 폐기했다(그림 5).

Cassandra에서 제공하는 분산 노드 기능을 사용하면 동일 키의 읽

기 성능이 지금보다는 좋을 수 있겠지만, n개의 노드 구성을 고려

한 테스트는 내부적인 사정으로 이번 테스트 범위에서 제외했다.

따라서 동일한 비교를 위해 MongoDB 역시 단일 노드로 구성하

여 테스트했다.

참고로 MongoDB는 저장 엔진의 오류 복구와 내구성을 위해

write-ahead 저널링을 1.7.5 버전부터 지원하고 있으며, 이 저널

링 옵션을 사용하면, 안정성이 올라가는 대신 MongoDB의 성능

측정 결과가 떨어질 것이다. 이 부분 역시 테스트 범위에서는 제외

했다. 집계 데이터의 특성상 복구가 용이하기 때문이다.

위의 테스트 결과와 별도로, MongoDB가 결합 인덱스 기능을 제

공하는 것도 선정의 중요한 이유가 되었다. 집계 데이터에 대한

여러 조회 조건을 활용하기 위해 복수의 인덱스 설정이 필요한데

MongoDB는 100만 건 정도되는 데이터에 대해 인덱스를 1초 내

외로 생성할 수 있다. 또한 관계형 데이터베이스에 익숙한 사용자

라면 기존의 SQL을 사용하는 것과 개념적으로 유사하여 좀 더 쉽

게 접근할 수 있다는 이유도 있다. 반면 Cassandra는 key-value

기반의 트리 구조에 대한 이해가 선행되어야 한다는 차이가 있다.

MongoDB와 Cassandra의 성능 테스

트에서 우리에게 필요한 일괄 쓰기 성능

과 여러 열에 대한 읽기 성능을 고려하여

MongoDB를 선정하였다. 읽기와 쓰기

성능 테스트는 단일 노드 환경으로 테스

트를 수행했지만, MongoDB의 최종 구

성은 다음 그림과 같이 서버 5대로 구성

했다.

ColumnPath columnPath1 = new ColumnPath(columnFamily);

columnPath1.setColumn(columnName1.getBytes());

client.insert("Keyspace1", key, columnPath1, columnValue1.

getBytes(),timestamp, ConsistencyLevel.ONE);

<중략>

ColumnPath columnPathN = new ColumnPath(columnFamily);

columnPathN.setColumn(columnNameN.getBytes());

client.insert("Keyspace1", key, columnPathN, columnValueN.

getBytes(), timestamp, ConsistencyLevel.ONE);

정말 열 단위의 insert 연산이 쓰기 성능에 영향을 미친 것인지 검

증하기 위해, 열의 수를 줄여 간단히 테스트했다. 키를 제외한 나

머지 열을 하나의 열로 결합하여 2개 열로 줄인 후 테스트를 수

행했다. 다음 표를 보면 Cassandra에서 10만건 쓰기 연산 성능

이 열이 30개일 때보다 2개일 때 속도가 많이 빨라졌지만 그래도

MongoDB의 쓰기 성능을 앞지르진 못했다.

표 3 MongoDB vs. Cassandra 쓰기/읽기 성능비교

테스트 구분 기준 MongoDB Cassandra(30개 열) Cassandra(2개 열)

쓰기 10만건 3.551초 1057.437초 32.980초

전체읽기 10만건 54.362초측정불가

(1시간 대기 응답 없음)임의읽기 임의 1건 탐색 1.162초

CPU 3% 20% 15%

그림 5 Cassandra 스키마

key value

Column Column value

Page 103: The platform 2011

NHN 서비스 들여다보기The Platform 2011102

최종 Hadoop & MongoDB 기반 집계 시스템

다음 그림은 시스템의 최종 구성도이다. 단일 노드 환경의

Hadoop 서버와 MongoDB로 시스템을 구성할 수도 있겠지만,

지속적으로 데이터가 증가하는 시스템의 특성상 이 기회에 확장이

용이하도록 구축했다. Oracle, ETL, Hadoop, MongoDB 순으로

데이터가 흐르는 구조이다.

그림 6 MongoDB 클러스터 구성도

위 그림과 같이 구성하면서 2가지의 시행 착오를 경험했다. 첫 번째

mongod config 서버의 개수를 arbiter 서버를 제외하고 4대로 설

정했다가 서버가 동작하지 않아 고생을 했다. 설정 문서에 "1대 또

는 3대로 구성하라"고 기술되어 있는데, 이를 놓친 것이다. 또한, 네

트워크 장애 상황에서 여러 서버에 걸쳐있는 샤드 구성이 올바르지

않아 데이터 정합성을 유지하지 못할 수 있다. 이를 해결하기 위해

반드시 별도의 arbiter 서버(그림의 cbat005)를 설정해야 한다.

그림 7 시스템 구성도

mongos(client)

Port : 30000

mongos(shar d1)

Port : 10001

mongos(configsvr)

Port : 20000

mongos(client)

Port : 30000

mongos(shar d1)

Port : 10001

mongos(client)

Port : 30000

mongos(shar d2)

Port : 10002

mongos(configsvr)

Port : 20000

mongos(client)

Port : 30000

mongos(shar d2)

Port : 10002

mongos(arbiter2)

Port : 10002

mongos(arbiter1)

Port : 10001

mongos(configsvr)

Port : 20000

cbat001.umon cbat002.umon cbat003.umon cbat004.umon cbat005.umon

DB

DB

DB

DB

ETL Name Node

(M/R)

Data Node

mongos

Shard(M)Shard(M)

Shard1

MongoDB ClusterHadoop Cluster

Shard2

ArbiterArbiter

Arbiter

Shard(M)

Shard(R)Shard(R)

Shard(R)

config

svr

config

svr

config

svr

mongos

mongos

mongos

Data Node

Transfer

App.

Load

Balancer

(L4)

Data NodeSecondary

Name Node

Page 104: The platform 2011

Hadoop과 MongoDB를 이용한 로그분석 시스템The Platform 2011 103

MongoDB의 Map-Reduce도 유용할 것이다.

NoSQL의 경우, 다중 인덱스가 필요한 구조라면 MongoDB를 선

택하고, 데이터 항목의 변경이 많고 unique access가 많은 경우라

면 Cassandra를 쓰는 것이 옳다고 생각한다. 물론 아직 HBase 등

을 테스트해보지 않았으므로, 이에 대한 추가 검토 필요하다.

1차 목표 후, 2차 목표에 대해서도 고민하고 있다. 지속적으로 늘어

나는 Oracle의 데이터를 효과적으로 처리할 수 있는 장기적인 대

책을 생각하고 있다. 마스터 데이터에 대한 결합도가 낮고, 조회 빈

도 역시 낮은 로그 성격의 데이터를 NoSQL 저장소로 이동시키는

것이다. 이 문제를 NoSQL 솔루션으로 해결하면, 상대적으로 고비

용인 Oracle 스토리지를 절감할 수 있을 것으로 기대한다.

NoSQL 솔루션은 매우 다양한 요구에 의해 발전하고 있다. 필자가

경험한 NoSQL의 가장 큰 장점은 확장성(scale-out)이 아닐까 생

각한다. 여기에 상대적으로 빠른 읽기와 쓰기 성능은 보너스이다.

대신 잃을 수 있는 것은 “개발에 필요한 우리의 시간”이다.

생생하게 NoSQL 적용 사례를 공유하고 싶었고, 우리가 고민한 여

러 가지 이야기가 도움이 되길 바라는 마음이다. 처음 써보는 글인

데다 부족한 글 솜씨가 많은 것을 전달하지 못한 것 같아 걱정이다.

앞으로 NoSQL에 대해 더 많은 자료가 공유되길 기대해 본다.

위 구성도의 동작 시나리오를 요약하면 다음과 같다.

1. 데이터베이스에서 집계해야 하는 대상 데이터를 선정

2. ETL 도구를 이용하여 데이터를 추출

3. Hadoop Cluster의 HDFS 데이터 노드에 데이터를 분산 적재

4. Hadoop Cluster의 네임 노드에서 Map-Reduce 수행

5. 전송 애플리케이션(Transfer Application)을 이용하여 집계 결과를

MongoDB에 전송 및 적재하며, 이 때 부하 분산(Load Balancing)을 위

해 L4를 둔다

6. MongoDB에 적재된 데이터는 외부 모듈에 의해 사용자에게 서비스

된다

위 항목 중 첫 번째부터 세 번째까지는 자연스러운 데이터의 흐름

이며, 네 번째에는 Map-Reduce로 집계 업무가 수행된다. 다섯 번

째에는 MongoDB가 제공하는 API를 통해 집계 결과를 저장한다.

MongoDB로 접근할 때 L4를 통해 부하가 분산되게 하는 것이 좋

다. 클라이언트를 작성할 때 서버 목록(mongos Server List)을 기

술할 수도 있다. 혹은 RequestBroker를 활용하는 것도 가능할 것

이다. 첫 번째부터 다섯 번째 과정까지 완료되면 사용자에게 데이

터를 제공할 수 있다. MongoDB에 데이터를 저장하는 프로세스

에 대해 보충 설명하면, 클라이언트 서버에서 config 서버를 통해

어느 샤드에 저장할 것인지 결정한다. 샤드의 마스터 서버에 데이

터를 저장한 후 복제 서버(Replication Server)로 데이터를 복사한

다. MongoDB는 Arbiter를 지정하여 분산 서버 중에 오류가 발생

한 서버가 있을 때 남은 서버에서 Master 서버를 선출할 수 있도록

해야 한다.

마치며

앞서 설명한 시스템을 구성하는 것으로 1차 목표를 달성하였다. 향

후 Map-Reduce나 NoSQL6 솔루션을 도입하고자 할 때 다음을

고려하길 바란다. Map-Reduce를 적용할 때 데이터 양이 적은 경

우 Hadoop 단일 노드 환경을 많은 경우는 3개 이상의 DataNode

로 구성된 분산 노드 환경을 구성하는 것이 적절하다. 단, 이미

MongoDB를 사용하고 있고, 데이터 양이 많지 않은 경우라면

6 http://nosql-database.org

http://blog.nahurst.com/visual-guide-to-nosql-systems

http://www.infoq.com/articles/nosql-in-the-enterprise

Page 105: The platform 2011

NHN 서비스 들여다보기The Platform 2011104

해 GUITAR(GUI test automation framework)라는 테스트 자동

화 시스템을 개발했다.

GUITAR 주요 기능

GUITAR는 이미지 기반의 웹 테스트 자동화 프레임워크이다.

IDE 환경에서 스크립트를 작성하고 스크립트에 필요한 이미지를

캡처할 수 있으며 실행한 테스트 결과 리포트도 제공한다.

주요 특징은 다음과 같다.

•한글 스크립트 사용

• 크로스 브라우징(인터넷 익스플로러, 파이어폭스, 사파리, 크롬, 오페

라) 지원

• 자바스크립트 오류 자동 검출(인터넷 익스플로러, 파이어폭스, 크롬)

•변수, include, 조건문, 반복문 지원

•원격 관리/실행 지원(CI 연동)

•리포트, 알림, 동영상 캡처 지원

•AutoIt 내부 명령어 지원

끊임없이 테스트 자동화 도구가 나오는 이유는, 그 필요성은 절대

적이지만 그 어느 것도 요구를 충분히 만족시키지 못하고 있기 때

문이다.

QA 부서에 있으면서 주변의 테스트 자동화 사례를 살펴보면, 자동

화 작업을 진행하다가 어느새 조용히 중단하기도 하고, 끝까지 완

료해도 오랫동안 유지보수하지 못하는 경우가 많다. 그 이유 중 하

나는 테스트 스크립트 작성/유지보수가 어렵기 때문이다. 프로젝

트 기간 동안 일정에 맞춰서 개발하고 QA를 진행하기에도 바쁜데

테스트 스크립트를 작성하고 변경되는 코드에 따라 테스트 스크립

트까지 계속 유지보수하려면 항상 인력과 시간이 부족하다.

그렇다면 지속적인 테스트 스크립트 유지보수는 왜 그렇게 어려울

까? 테스트 대상 프로젝트의 특성, 작업 환경, 투입 리소스, 잦은 기

능 변경, 작업자의 노력 등 여러 가지 이유를 들 수 있을 것이다. 그

러나 여기에서는 자동화 도구에 초점을 두었다. 기존의 테스트 자

동화 도구가 좋지 않다거나 기능이 떨어지는 것은 아니다. 다만 우

리의 현실과 다소 맞지 않는 것이 문제이다. 이 문제를 해결하기 위

테스트 자동화 도구GUITAR월요일 아침부터 "주말에 어디로 캠핑 갈까?" 궁리하면서 사는

평범한 QA입니다.

개인적으로 자동화한 것 중 가장 효과를 봤던 것을 꼽으라면

"자연휴양림 빈자리 알림" 자동화입니다. 올 여름 휴가 때 효과를 좀 봤거든요.

꼭 업무만 자동화하란 법은 없는 거죠? 그런 거죠?

•검색QA팀 _ 손민혁

Page 106: The platform 2011

테스트 자동화 도구 GUITARThe Platform 2011 105

그림 2 GUIRAT 스크립트 구조

테스트 스크립트에는 실제 수행될 스크립트 파일이 있고, 공용 스

크립트와 공용 이미지에는 테스트 스크립트 내부에서 참조하여 사

용하는 스크립트와 이미지가 있다. 테스트 스크립트의 최종 리프

(leaf) 폴더에는 실제 수행될 스크립트 폴더가 존재하며, 각 테스트

스크립트 폴더에는 해당 스크립트가 사용하는 이미지 폴더가 존재

한다. 계층 구조 형태로 공용 폴더나 자신의 하위 폴더에 위치한 스

크립트를 자유롭게 호출하여 사용할 수 있다.

한글 스크립트

다음은 테스트 자동화 도구로 가장 많이 사용하는 Selenium으로

작성한 테스트 스크립트의 예이다.

그림 3 Selenium 테스트 스크립트

GUITAR 구조

GUITAR는 스크립트를 유지보수/실행하는 메인 프로그램과 원

격 관리를 담당하는 에이전트, 이미지 모듈, 업데이트 모듈로 구성

된다.

GUITAR로 작성된 스크립트는 이미지와 텍스트로 저장되며, 인터

넷 익스플로러를 비롯한 다양한 브라우저를 대상으로 테스트를 수

행할 수 있다.

실행 결과는 HTML 형태로 제공되며, 전체적인 테스트 현황을 보

여주는 요약 리포트 화면과 상세 리포트 화면으로 나뉜다. 메일,

SMS 등으로 테스트 결과를 전송할 수도 있다.

그림 1 GUITAR 시스템 구조

스크립트 구조

GUITAR의 스크립트는 다음 그림과 같이 크게 3개의 트리로 구성

된다.

Page 107: The platform 2011

NHN 서비스 들여다보기The Platform 2011106

•수행할 명령

•명령의 대상

GUITAR는 테스트케이스의 핵심인 '명령'과 '대상'을 한국어 어순

에 따라 사용하기 때문에 쉽게 스크립트를 작성할 수 있다.

명령

명령에는 클릭, 이동, 입력 등과 같이 실제 테스트 스크립트 내에서

특정 동작을 수행하는 명령과 결과를 확인하는 등 수행한 명령을

확인하는 명령이 있다. 그리고 테스트 작업에 필요한 캡처, 대기와

같은 명령도 있다.

GUITAR는 현재(2011년 9월) 40여 개의 테스트 명령을 제공하고

있으며, 지속적으로 명령을 추가하고 개선할 예정이다.

대상

대상은 자동화 명령의 형태에 따라 다르다. 대부분의 대상은 이미

지 형태로 사용하지만 텍스트, 변수, 숫자가 대상인 명령도 있다.

그림 6 GUITAR에서 대상의 종류

XPath vs 이미지

기존의 테스트 도구는 대부분 명령 대상을 지정할 때 다음 그림과

같이 DOM 트리 기반의 XPath 형식을 사용한다.

위 스크립트가 어떤 테스트를 수행하는지 쉽게 알 수 있는가? 위

스크립트의 기능은 다음과 같다.

1. 네이버 메인 페이지에 접속한다.

2. 검색어 “GUI 테스트 자동화”를 입력하고 검색을 수행한다.

3. 검색 결과 내용을 확인한다.

그럼 아래 내용을 보자.

그림 4 GUITAR 테스트 스크립트

위 내용은 테스트케이스 문서의 일부가 아니다. 위의 Selenium 스

크립트와 같은 기능을 수행하는 GUITAR의 테스트 스크립트이다.

GUITAR를 사용하면 한글로 테스트 스크립트를 작성할 수 있으므

로 자동화 작업 담당자가 변경되거나 오랜 시간이 지난 뒤 유지보

수를 위해 스크립트를 다시 보더라도 테스트 스크립트를 이해하기

쉽다.

대상과 명령

위 예제에 사용한 실제 테스트케이스 문서를 한번 살펴보자.

그림 5 테스트케이스 문서

일반적으로 테스트케이스는 다음 두 가지를 핵심 항목으로 포함하

고 있다.

Page 108: The platform 2011

테스트 자동화 도구 GUITARThe Platform 2011 107

아래 그림과 같은 플래시 화면의 구성 요소는 Selenium으로는 테

스트할 수 없었지만 GUITAR로는 테스트할 수 있으며, 브라우저

의 모달(modal) 창이나 팝업 창도 쉽게 제어할 수 있다.

그림 9 Selenium으로는 테스트할 수 없는 영역

이미지 방식의 단점과 해결 방안

이미지 방식으로 대상을 찾으면 미세한 픽셀 차이에도 민감하게 반

응한다는 단점이 있다. 따라서 가변적인 데이터가 많은 시나리오에

서는 비효율적이다. 또한 테스트 시스템의 환경 차이(색 농도, 운영

체제, 시스템 설정)로 인해 이미지를 찾지 못하는 경우도 발생할 수

있다. 실제 GUITAR를 개발할 때 이런 문제가 발생했고, 다양한 방

법으로 해결했다.

색 농도 차이

운영체제의 색 농도 설정에 따라 화면에 표시되는 이미지 색상이

미세하게 달라질 수 있다. 따라서 이미지 검색 엔진은 미세한 색상

오차를 무시하고 검색할 수 있는 tolerance 옵션을 제공한다. 이 옵

션 값을 조절하면 미세하게 색상이 다른 이미지도 쉽게 찾아낼 수

있다. 아래 그림은 32비트 색 농도 환경에서 캡처한 버튼 이미지를

16비트 색 농도 환경에서 찾지 못할 때 tolerance 값을 증가시켜 이

미지를 찾는 예이다.

그림 7 Xpath 기반의 대상 지정

XPath를 사용하면 다양한 형태로 대상을 정확하게 지정할 수 있지

만, 화면 상의 고유한 XPath를 찾아내고 이를 검증하는 추가 작업

이 필요하다. 또한 스크립트를 디버깅하거나 분석하기 위해 XPath

값으로 원래 대상을 찾아내기도 쉽지 않다. 최근에는 이런 문제를

해결하기 위해 XPath 값을 변수로 바인딩하여 스크립트의 가독성

을 높이는 방법을 사용하지만 여전히 XPath를 찾아야 한다는 근본

적인 문제는 남아있다.

GUITAR는 대상으로 XPath가 아니라 이미지를 사용한다. 따라서

내용이 변경되어도 쉽게 확인하고 스크립트를 수정할 수 있다.

그림 8 이미지 기반의 대상 지정

이미지 방식의 장점

이미지를 대상으로 사용하면 브라우저 화면 상에 보이는 이미지 기

반으로 명령 대상을 찾기 때문에 HTML, 플래시, 액티브 X 컨트

롤 등의 화면 구현 방법과 관계없이 대상으로 지정할 수 있다.

Page 109: The platform 2011

NHN 서비스 들여다보기The Platform 2011108

글꼴 차이

운영체제에 따라 같은 글꼴도 미세하게 다르게 표현된다. 이렇게

달라진 글씨를 캡처하여 다른 운영체제에서 사용하면 이미지를 인

식하지 못할 수 있다. GUITAR는 이 문제를 해결하기 위해서 테스

트 작업 중에는 자동으로 운영체제의 클리어타입 옵션을 해제한다.

그림 12 클리어타입 설정 시와 해제 시 글꼴 표현 차이

다중 이미지 검색

브라우저의 렌더링 엔진에 따라 이미지가 미세하게 다르게 표현될

수도 있다. 이런 경우에는 브라우저별로 원본 이미지를 추가로 캡

처해야 한다. 하지만 캡처한 이미지의 이름을 각각 다르게 지정해

야 한다면 유지보수가 어려울 것이다.

GUITAR는 하나의 대상에 여러 이미지를 등록할 수 있다. 다음은

그림 10 tolerance 옵션

그러나 tolerance 값을 너무 높게 지정하면 의도하지 않은 이미지

를 찾는 경우도 생길 수 있다. 이 문제를 해결하기 위해 GUITAR

는 원본 이미지의 CRC 값과 대상 이미지의 CRC 값을 비교하는

작업을 수행한다.

그림 11 이미지 RC 추가 확인

Page 110: The platform 2011

테스트 자동화 도구 GUITARThe Platform 2011 109

위 예에서는 아래 그림과 같이 텍스트 나루를 포함하여 캡처해서

사용하면 된다.

그림 15 텍스트와 이미지를 같이 캡처

그런데 이렇게 여러 요소를 포함하여 캡처하면 브라우저의 특성에

따라 미세한 차이가 발생하여 해당 이미지를 찾지 못하는 현상이

발생할 수 있다.

그림 16 브라우저에 따른 차이

또한 나중에 거리뷰 버튼의 이미지가 변경되면 거리뷰 버튼을 포

함하는 이미지를 모두 다시 캡처해야 하는 상황이 발생한다.

이 문제를 해결하기 위해서 GUITAR는 대상을 쉼표로 구분하여

여러 개 지정하면 서로 인접한 이미지를 찾는다.

그림 17 복합 이미지 검색

대상 이미지를 두 개로 나누고 "나루, 거리뷰버튼"을 대상으로 지정

하면 나루에 가까이 위치한 거리뷰 버튼을 찾는다. 이렇게 주변 이

미지를 찾는 기능을 사용하면 다양한 조합으로 원하는 대상을 쉽게

찾을 수 있고, 유지보수에도 유리하다.

페이지에 접속할 때마다 이벤트 배너가 달라지기 때문에 "이벤트배

너"라는 이름으로 3개의 이미지를 등록하여 사용하는 예이다.

그림 13 다중 이미지 검색

테스트 중 현재 화면에 3개의 이미지 중 하나의 이미지와 일치하면

테스트는 성공으로 처리한다.

복합 이미지 검색

이미지를 대상으로 지정하면 화면에 찾는 이미지가 여러 개 표시되

어 원하는 이미지를 정확하게 지정하기 어려운 경우가 있다. 다음

과 같은 화면에서는 거리뷰 버튼이 여러 개 존재한다.

그림 14 한 화면에 같은 이미지가 여러 개 표시되는 예

Page 111: The platform 2011

NHN 서비스 들여다보기The Platform 2011110

그림 19 자바스크립트 오류 검출 화면

CI 등 외부 시스템 연동

GUITAR는 스크립트별로 테스트 실행 URL과 결과 보기 URL을

제공하므로 빌드 배포 시스템 등의 외부 시스템과 연동하여 배포

후 자동으로 테스트가 실행되도록 설정할 수 있다. 자동으로 테스

트가 실행되도록 설정하려면 빌드 배포 스크립트에서 배포 작업 뒤

에 URL 접속 명령을 추가한다.

NHN에서는 BDS라는 빌드 배포 시스템을 개발하여 사용하고 있

다. BDS에서 배포 서버의 PostScript에 테스트 실행 URL에 접속

하는 명령을 CURL과 같은 명령어로 추가하면 배포할 때 테스트

스크립트가 자동으로 실행된다. 현재 네이버 지도 서비스는 TEST,

STAGING, REAL 빌드를 배포할 때마다 각 단계에 맞는 테스트

스크립트를 실행하도록 설정되어 있다.

그림 20 BDS의 PostScript를 통한 테스트 실행

GUITAR는 테스트 결과 페이지의 URL을 제공하므로 CI 서버와

같은 외부 시스템에 결과 리포트 위치의 링크만 연결하면 쉽게 연

동할 수 있다.

그 외 기능

크로스 브라우징

GUITAR는 이미지를 대상으로 인식하기 때문에 다양한 브라우저

에서 테스트를 할 수 있다. 현재는 인터넷 익스플로러, 파이어폭스,

사파리, 크롬, 오페라를 지원한다.

그림 18 GUITAR가 지원하는 브라우저

변수, 조건문, 반복문 지원

GUITAR는 외부 스크립트를 참조하여 실행하는 기능과 변수, 조

건문, 반복문 등 스크립트 진행 흐름에 필요한 기능을 지원한다. 또

한 AutoIt 내부 명령도 사용할 수 있어서 다양한 예외 케이스를 처

리할 수 있다.

자바스크립트 오류 자동 검출

GUITAR는 테스트 중에 발생하는 자바스크립트 오류를 자동으로

감지하여 알려 주므로, 따로 스크립트를 검증하지 않아도 된다. 인

터넷 익스플로러, 파이어폭스, 크롬에서 테스트할 때 이 기능을 사

용할 수 있다.

Page 112: The platform 2011

테스트 자동화 도구 GUITARThe Platform 2011 111

지도 서스테이닝 통합 테스트케이스 400여 개를 자동화하는 데 1

명이 하루 3~4시간 동안 스크립트를 작성하여 1개월 정도 걸렸다.

테스트케이스 작성 기간 이후에는 일정 시간 테스트 스크립트 안정

화 기간을 거쳐 실제 서비스의 배포 프로세스에 적용하여 운영하고

있다.

이터레이션 테스트 자동화

지도 서스테이닝 자동화 이후에 반복점진개발 방식으로 진행된 "지

도 도보 길찾기" 프로젝트에도 GUITAR를 적용했다. GUITAR의

테스트 스크립트는 빠르게 작성할 수 있기 때문에 UI 산출물이 나

오기 시작하는 후반 이터레이션(iteration)부터 QA 과정 중에 일

부 자동화 작업을 같이 진행할 수 있었다. 이렇게 이터레이션 기간

중에 작성된 스크립트는 프로젝트 출시 후 회귀 테스트에서도 사용

하고 있다.

그림 23 도보 길찾기 프로젝트 이터레이션별 자동화 적용

API 테스트

조건에 따라서 서비스 API 검증 테스트에도 GUITAR를 활용할 수

있다. 최근 네이버 지도 서비스에서 "대중교통 길찾기 엔진"을 수정

했다. 지금까지는 엔진이 변경되면 QA와 테스트 엔지니어가 수동

으로 길찾기를 실행하여 그 결과를 일일이 비교해서 검증했다. 그

러나 이번에는 GUITAR를 이용하여 자동으로 길찾기 작업을 반

복하고 길찾기 결과 화면을 캡처하여 이전 내용과 비교한 리포트를

생성하도록 했다. QA와 TE는 최종 리포트 결과 화면을 보고 이전

버전과 차이가 발생하는 부분만 확인하면 되므로 검수에 들어가는

비용을 절약할 수 있었다.

그림 21 Hudson 연동 화면

GUITAR 적용 효과

GUITAR는 일반적인 회귀 테스트(regression test)에서부터 API

테스트까지 다양한 부분에 활용할 수 있다.

회귀 테스트 자동화

네이버 지도 서비스에 파일럿으로 개발 중인 GUITAR를 적용해

보았다. 자동화의 목표는 지도 서스테이닝 통합 테스트케이스를 크

로스 브라우징 기준으로 자동화하는 것이었다. 지도는 서비스 특

성상 플래시, AJAX 등으로 구현된 부분이 많아 Selenium으로는

30% 정도밖에 테스트할 수 없었다(서스테이닝 기준). 이러한 상태

에서 GUITAR를 적용하자 자동화할 수 있는 영역이 96%로 늘어

났다.

그림 22 지도 서스테이닝 GUITAR 적용 결과

Page 113: The platform 2011

NHN 서비스 들여다보기The Platform 2011112

그림 24 대중교통 길찾기 결과 비교 리포트

데이터 검수

콘텐츠 형태의 서비스나 검색 형태의 서비스는 검색어나 조건을 변

경하면서 반복적으로 데이터를 검수해야 할 때가 많다. 이런 경우

에 GUITAR의 '테이블 변수' 기능을 사용하면 쉽게 반복 테스트를

수행할 수 있다. GUITAR는 테스트를 수행하면서 자동으로 자바

스크립트의 오류도 검출해 준다. 또한 테스트 대상 브라우저를 쉽

게 추가할 수 있으므로 크로스 브라우징 테스트도 편리하게 수행할

수 있다.

맺음말

시간은 없고 빨리 테스트를 자동화해야 할 때, 제대로 된 크로스 브

라우징 테스트가 필요할 때, 팝업창이나 플래시 때문에 테스트가

막막할 때 GUITAR를 사용해 보길 바란다.

Page 114: The platform 2011

테스트 자동화 도구 GUITAR 113The Platform 2011

NHN의 플랫폼과 도구

모바일 Push와 nPush 114

ffmpeg을 이용한 iOS 동영상 플레이어 125

분산 고속 저장소 nStore 131

CUBRID, 브로커 이야기 137

위치정보 플랫폼 nLocation 145

데이터베이스테스트 기법과 Coverage4iBatis 150

maven-precheck-plugin, 배포시장애를 감지하는 방법 157

메이븐을 이용한 정적 파일 배포 162

Page 115: The platform 2011

NHN의 플랫폼과 도구114 The Platform 2011

수 있다. 모바일 Push 방식이 기존에 제공되던 TV나 라디오에 사

용된 Push 기술과 다른 점이 있다면 전송할 범위를 미리 지정할 수

있다는 것이다.

모바일 Push는 유저가 해당 서비스에 접속하지 않아도 정보를 이

용할 수 있도록 모바일 장비로 콘텐츠를 전송하는 서비스이다. 정

보 전달을 위해 주기적으로 서버에 접속하는 Pull 방식으로도 Push

기술을 구현할 수 있으나 그러기 위해서는 먼저 장비의 배터리 성

능과 통신 비용 문제를 해결해야 한다. 요즘에는 스마트폰 운영체

제별로 전용 Push 플랫폼(APNS, C2DM)을 제공한다. 최근 국내

이동통신사인 SKT는 T스토어에 등록되는 앱을 위한 Push 플랫폼

인 AOM(Always On Management)을 제공하고 있다.

APNS

APNS(Apple Push Notification Service)4는 애플 사에서 모바

4 http://developer.apple.com/library/ios/#documentation/NetworkingIn-ternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePush

Service.html

모바일 Push

대표적인 모바일 Push1 서비스는 SMS, MMS이다. 이 SMS와

MMS의 단점은 발송할 때마다 비용을 지불해야 한다는 것이고,

스마트폰에 설치한 특정 앱을 대상으로 메시지를 발송할 수 없

다는 것이다. 하지만 스마트폰은 이동통신사를 통하지 않는 오픈

Push 플랫폼2을 제공한다. 애플의 APNS, 구글의 C2DM, SKT의

AOM 등이 바로 그것이다.

이 글에서는 먼저 Push 서비스에 대해 설명하고 NHN에서 개발한

스마트폰 Push 통합 서비스인 nPush(NHN mobile Push)를 설명

한다.

Push 기술

Push는 유저가 원하든 원치 않든 정보를 전달하는 기술이다. 유저

가 원하는 정보를 직접 찾는 Pull3 기법과는 상반된 개념이라 할

1 http://en.wikipedia.org/wiki/Push_technology

2 http://www.openmobilealliance.org/technical/release_program/push_v2_3.aspx

3 http://en.wikipedia.org/wiki/Pull_technology

모바일 Push와 nPush라인, 네이버톡, 미투데이와 같은 NHN 모바일 앱을 위한 통합 푸시 클라우드

(nPush) 개발을 담당하고 있습니다.

현재, 안드로이드용 NHN 자체 푸시 플랫폼인 NNI(NHN Notification Infra)로

적은 비용으로 더 많은 사용자에게 빠르고 안정적인 푸시 알림 서비스를

제공하기 위해 노력하고 있습니다.

•메시지클라우드개발팀 _ 이희종

Page 116: The platform 2011

모바일 Push와 nPush 115The Platform 2011

APNS 메시지는 Device Token과 Payload로 구성된다. Device

Token은 애플 장비의 소재를 파악하기 위한 정보로 전화번호와 유

사한 정보이다. Payload는 특정 애플리케이션으로 전달할 JSON

형태의 데이터로 최대 크기는 256바이트이다. 메시지를 전달받은

APNS 서버는 해당 메시지를 애플 장비로 전송하고, 해당 장비는

전송받은 메시지를 팝업 형태로 표시한다. 팝업 버튼 선택에 따라

팝업 닫기 또는 해당 애플리케이션을 실행한다(그림 2).

APNS 메시지 규격 중 Command 필드에는 기본 메시지 규격

(Simple Format)인지 개선된 메시지 규격(Enhanced Format)인

지를 알려주는 정보가 저장된다. Token Length 필드와 Payload

Length 필드는 빅 엔디안(Big Endian) 방식으로

메시지 필드의 길이 정보가 저장된다(그림 3).

Identifier 필드는 Push 메시지를 식별하기 위한

임의의 값이며, 오류가 발생할 경우 APNS는 이 값

을 반환한다. Expiry 필드는 Push 메시지의 유효

시간 정보를 나타낸다. 다음 그림은 오류 응답 메시지 규격을 나타

낸다(그림 4).

일 서비스를 위해 만든 Push 서비스 플랫폼이다. 2009년 6월 17일

iOS 3.0에 포함되어 정식으로 출시되었다. 서드 파티(3rd-party)

응용 서버로부터 전달된 메시지가 항상 열려있는 IP 연결을 통해

아이폰, 아이팟, 아이패드 등의 기기로 전달된다. iOS 3.0 환경에서

는 백그라운드 프로세스를 지원하지 않기 때문에, Polling 방식을

통하여 메시지를 획득할 수 없다. 이런 이유로 APNS는 iPhone 애

플리케이션을 개발할 때 매우 유용한 플랫폼이라 할 수 있다.5 또

한, APNS에서는 전달하는 Push 메시지는 한쪽 방향으로, 순서대

로 메시지를 전달된다.

그림 1 APNS 메시지 흐름도

그림 2 APNS Push 메시지 규격 - Simple Format

그림 3 APNS Push 개선된 메시지 규격 - Enhanced Format

5 iOS 4.X 버전대에서는 백그라운드 처리를 지원한다.

그림 4 APNS 오류 응답 메시지 규격

Page 117: The platform 2011

NHN의 플랫폼과 도구116 The Platform 2011

Device Token을 전달받은 애플리케이션 서버는 APNS 서버와 보

안 연결(Security Connection)할 때 자신을 인증하기 위해 필요한

SSL 인증서(애플리케이션 ID를 이용하여 생성)를 준비해야 한다.

그림 6 애플리케이션 서버와 APNS 서버간의 보안 연결

애플리케이션 서버가 Push로 전달해야 할 메시지가 있다면 암호화

된 채널(TLS: Transport Layer Security, 구 SSL)을 통해 APNS

서버로 메시지를 전달한다.

그림 7 애플리케이션 서버에서 장비로 Push 메시지 발송

위 그림에서 보듯이 Command 필드에 값이 표시되는데, 이 값을

통해 오류 내용을 파악할 수 있다. Command 필드 값에 대한 설명

은 다음 표와 같다.

표 1 APNS 메시지 Command 필드

상태코드 설명

0 오류 없음

1 오류 처리 중

2 Device Token 없음

3 Topic 없음

4 Payload 없음

5 Token 크기 오류

6 Topic 크기 오류

7 Payload 크기 오류

8 Token 오류

255 알 수 없음.

APNS Push 메시지 규격에 대해 알았으니 이제 APNS Push 동작

방식에 대해 알아보자. 우선 APNS를 사용하기 위해 애플 장비에

설치된 애플리케이션은 APNS 서버로부터 Device Token을 발급

받는다. 그리고 발급받은 Device Token을 다시 애플리케이션 서

버로 전달한다.

그림 5 Device Token 생성 및 Device Token 애플리케이션 서버로 전달

Page 118: The platform 2011

모바일 Push와 nPush 117The Platform 2011

애플리케이션 서버가 C2DM 서버에 접근하기 위해서는 구글의

ClientLogin을 이용하여 발송자 인증 정보(Sender_Auth_Token)

를 획득해야 한다. 애플리케이션 서버와 C2DM 서버 사이의 모든

통신은 HTTPS를 통해 정보를 주고 받는다.

그림 10 발송자 인증 정보 획득 방법

구글의 ClientLogin에 사용되는 정보는 다음과 같다.

• HOSTED_OR_GOOGLE : 구글 서버와 연동하는 서버에서 사용할 문

자열 정보

• EmailOfSender : 구글 가입 시 사용한 메일 주소와 암호

• ac2dm : Google C2DM 서비스 이름

C2DM을 이용하려면 장비에 설치된 애플리케이션을 구글

C2DM 서버에 등록하여 장비 고유 번호(RegistrationId)를 발급

받아야 한다. 이후 해당 고유 번호를 애플리케이션 서버에 전달하

여 Push 메시지를 전송할 때 이용한다.

그림 11 Push 서비스 등록 및 장비 고유 번호 전달

특정 장비에서 이미 삭제된 애플리케이션으로 Push 메시지를 전달

하려고 시도할 경우 해당 장비는 Push 메시지 수신을 거부한다. 이

때 APNS는 해당 애플리케이션 서버가 연결한 피드백(feedback)

인터페이스를 통하여 삭제된 애플리케이션이 할당받았던 Device

Token과 시간 정보를 전달한다.

그림 8 APNS 피드백 서비스로 전달되는 데이터 규격

C2DM

C2DM(Cloud to Device Messaging)6은 구글의 Android 2.2

버전(Froyo)부터 사용할 수 있었으며, 구글 서비스를 기반으로

Android 마켓을 이용하는 모든 장비에 메시지를 전송할 수 있다.

또한, Android 운영체제가 설치된 스마트폰에서 유저가 구글 계

정으로 로그인해야만 이용이 가능하며, 서드 파티 서버가 간단한

메시지를 Android 애플리케이션으로 전달하는 것을 허용한다.

C2DM 서비스는 대용량의 콘텐츠를 전송하도록 설계되어 있지 않

다. 대신 애플리케이션에 새로운 데이터가 있음을 알려주고, 애플리

케이션이 해당 서버에 접속해서 데이터를 전송받도록 하고 있다.

그림 9 C2DM 메시지 흐름도

6 http://code.google.com/intl/ko-KR/android/c2dm/

Google ServersClientLogin

Service Provider /Third-Party Server

MobileDevices

Google ServersC2DM

Service Provider /Third-Party Server

Google

CONNSERVER

C2D MSGFRONTEND

APPSERVER

WAKEUP!

APP

Page 119: The platform 2011

NHN의 플랫폼과 도구118 The Platform 2011

그림 14 미사용 장비 처리

AOM

애플리케이션 서버와 연결을 유지하면서 Push 알림을 받아야 하

는 애플리케이션이 증가함에 따라 과다한 트래픽을 유발하고, 배터

리가 빨리 소모되는 문제점이 발생했다. 최근 SKT에서는 이런 문

제점을 해소하기 위해 AOM(Always On Management)이라는

Push 플랫폼을 개발했다. 애플의 APNS, 구글의 C2DM과 같이

SKT의 AOM은 하나의 데몬 프로세서(Daemon Process)가 Push

서버와 연결을 유지하고 Push 메시지를 대표로 받아서 해당 애플

리케이션에 전달한다. AOM은 크게 AOM 서버와 AOM 클라이

언트로 구성되며 2011년 6월부터 미투데이 애플리케이션에 최초

로 적용하여 서비스되고 있다.

그림 15 AOM 구성도

애플리케이션 서버가 전송할 메시지(최대 1024바이트)를 HTTPS

를 통해 C2DM 서버로 전달한다. C2DM 서버는 메시지를 지정된

장비에 전송(Routing)하고, 해당 장비에 설치된 애플리케이션은

브로드캐스트 인텐트(Intent)를 통해 실행되어(Wake-up) 메시지

를 처리한다.

그림 12 C2DM Push 메시지 전송

사용자가 더 이상 Push 서비스를 받고 싶지 않을 때 Push 서비스

를 취소할 수 있다.

그림 13 Push 서비스 등록 취소

C2DM은 애플의 APNS와 다르게 피드백 인터페이스를 별도로 제

공하지 않는다. 다만, 등록 해제된 장비로 메시지를 전송할 경우 등

록이 해제되었다는 결과를 전달한다. 따라서 등록 해제된 장비로

메시지 전송을 시도하여 실패할 경우 애플리케이션 서버는 그 결과

를 토대로 해당 장비 고유 번호를 관리 목록에서 삭제한다.

Google ServersC2DM

Service Provider /Third-Party Server

MobileDevices

Google ServersC2DM

MobileDevices

Service Provider /Third-Party Server

Push ServersC2DM

Page 120: The platform 2011

모바일 Push와 nPush 119The Platform 2011

발송을 요청한 Token이 삭제되었거나 발급되지 않은 경우 해당

Token을 피드백 메시지로 처리해야 한다.

그림 20 발급되지 않은 Token에 대한 피드백 메시지 규격

그림 21 삭제된 Token에 대한 피드백 메시지 규격

AOM Push 메시지 규격에 대해 알았으니 이제 동작 방법에 대해

알아보자. AOM Push 서비스를 사용하기 위해서는 T스토어의 상

품 등록/관리 페이지에서 Push 서비스 사용을 확인하고, 인증서를

발급 받아야 한다. 인증서는 서드 파티 애플리케이션 서버가 AOM

서버에 TLS로 접속할 때 사용되며, AOM 서버는 해당 인증서로

애플리케이션 서버를 인증한다. 발급된 인증서와 개인 키는 하나의

서비스에 대해서만 유효하다. AOM을 이용하는 서비스 제공자는

키가 외부로 유출되지 않도록 해야 한다. 인증서 사용 기간은 365

일이며, 만료일이 가까워지면 인증서를 갱신하여 사용해야 한다.

갱신되기 전 인증서는 갱신 시점부터 24시간 동안 유효하므로 서

비스 제공자는 갱신된 인증서를 24시간 안에 애플리케이션 서버에

설치해야 한다.

애플리케이션 서버는 AOM 서버(AOMS)에 접속하기 위해

AOM 서버를 할당하는 서버(AOMS Assignment Server, AAS)

접속 정보를 요청한 후 응답 받은 IP 주소와 포트에 연결을 시도한

다. 연결을 설정한 후 TLS 연결 과정을 통해 인증서를 교환하며, 이

때 SP(Service Provider) 연동 서버는 애플리케이션 서버의 인증서

를 확인하고 보안 채널을 확립한다.

AOM 메시지는 Keep-Alive, Push Request, 공지사항, 피드백 메

시지 4종류로 구분하고 있다. AOM 서버는 서드 파티 애플리케이

션 서버와의 연결을 유지하기 위해 Keep-Alive 요청/응답 메시지

를 주고 받는다. 응답 시 전달되는 메시지의 Keep-Alive Time 필

드를 이용하여 다음과 같이 Keep-Alive 요청을 수행해야 한다.

그림 16 Keep-Alive 요청 메시지 규격

그림 17 Keep-Alive 응답 메시지 규격

Push 요청 메시지는 30바이트의 Token과 256KB 미만의 Payload

필드로 구성된다.

그림 18 Push 요청 메시지 규격

Token 필드에는 애플리케이션을 식별하는 데이터가 입력되며

Payload Len. 필드에는 Payload 필드의 길이, Payload 필드에는

애플리케이션 서버와 애플리케이션 사이에 정의된 메시지가 입력

된다. Payload 필드의 최대 크기는 512바이트다.

공지사항을 보낼 때 사용하는 메시지 규격은 일반 메시지를 보낼

때 사용하는 메시지 규격과 다르다. 다음 그림을 확인해 보자.

그림 19 공지사항 메시지 규격

Page 121: The platform 2011

NHN의 플랫폼과 도구120 The Platform 2011

그림 24 AOM Push 서비스 등록 및 애플리케이션 서버로 Token 전달

AOM 서버는 애플리케이션 서버로부터 Push 메시지를 수신하면

기존에 설정된 채널(Always-on)을 통해 알림 메시지를 전달한다.

그림 25 메시지 전송

Payload는 애플리케이션 서버로부터 장비의 애플리케이션까지 전

달되는 정보이다. 또한 공지사항 메시지를 AOM 서버에 전송하면

AOM 서버는 대상 애플리케이션이 설치되어 있는 모든 장비에 공

지사항 메시지를 전달한다.

애플리케이션 서버로부터 Push 메시지를 수신한 클라이언트에

애플리케이션이 설치되어 있지않으면 애플리케이션 설치 여부를

AOM 서버로 전송한다. AOM 서버는 Push 메시지를 수신한 세

션으로 애플리케이션 서버에 피드백 메시지를 전송한다. 애플리케

이션 서버는 피드백 메시지를 수신한 후 해당 Token에 대해 Push

메시지를 보내지 않는다.

그림 22 애플리케이션 서버와 SKT AOM 서버간의 보안 연결

애플리케이션 서버는 AOM 서버와 보안 채널을 확립한 후 주기

적으로 Keep-Alive 메시지를 주고 받는다. 애플리케이션 서버는

AOM 서버에 Keep-Alive 요청 메시지를 전송하고, Keep-Alive

응답 메시지를 수신한다. Keep-Alive 메시지에 포함된 Expires 값

에 따라 다음에 전송할 Keep-Alive 주기를 관리한다.

그림 23 애플리케이션 서버와 AOM 서버간의 연결 유지(Keep-Alive)

AOM은 pushToken이라는 단말 고유 번호를 이용해서 메시지를

전달한다. AOM 서버로부터 pushToken 정보를 획득하고, 이를

애플리케이션 서버에 등록한다.

Service Provider/Application Server

Service Provider/Application Server

Service Provider/Application Server

SKT AAS Server

SKT AOM Server

SKT AOM Server

SKT AOM Server

SKT AOM Server

Service Provider /Third-Party Server

MobileDevices

MobileDevices

Page 122: The platform 2011

모바일 Push와 nPush 121The Platform 2011

MobileDevices

그림 26 피드백(Feedback) 응답 처리

NNI

NHN에서는 Android 모바일 애플리케이션의 Push 서비스를 위

해 C2DM을 이용하고 있었다. 그런데, 모바일 서비스 운영 과정에

서 Push 서비스 품질에 영향을 줄 수 있는 C2DM의 몇 가지 이슈

(서비스별 일 단위 총 메시지 발송 건수 제한, 장비당 하루 1,000건

제한, 비주기적인 메시지 수신 지연 등)를 발견했다.

이 문제를 해결하기 위해 NHN에서는 장비 하나당 하나의 연결

을 통해 여러 NHN 애플리케이션이 Push 서비스를 받을 수 있는

NNI(NHN Notification Infra) 플랫폼을 개발하였다. 이 절에서

는 NNI 서버에 대한 개략적인 내용을 기술한다.

그림 27 NNI 메시지 흐름도

NNI는 장비 고유 번호(targetId)를 NNI 서버에 요청하지 않고 장

비에서 직접 생성한다.

그림 28 NNI Push 서비스 등록 및 애플리케이션 서버로의 Token 전달

NNI 서버도 다른 Push 서비스와 같이 각 장비에 설치된 NNI 클

라이언트와 Push 세션을 유지한다. 이 Push 세션을 통해 애플리케

이션 서버로부터 전달된 메시지를 각 장비의 애플리케이션에 즉시

전달할 수 있다.

그림 29 메시지 전송

NNI 서버는 NNI 클라이언트 상태 확인을 위해 주기적으로 Ping

메시지를 전송한다. NNI 서버는 NNI 클라이언트로 REQ_CRS_

HEALCHECK 요청을 전달한

후 NNI 클라이언트에서 RES_

CRS_HEALCHECK 응답을 받

는다. 2회 연속으로 RES_SRC_

HEALCHECK 응답이 없는 경우

해당 클라이언트와의 연결을 종료한다.

Service Provider /Third-Party Server

Service Provider /Third-Party Server

Service Provider/Application Server

Service Provider/Application Server

SKT AOM Server

MobileDevices

MobileDevices

MobileDevices

NHN NNI Server

Page 123: The platform 2011

NHN의 플랫폼과 도구122 The Platform 2011

그림 31 nPush 메시지 흐름도

nPush 지원 기능

nPush는 Push 유형에 따라 서로 다른 장비 고유 번호를 등록 및 관

리한다. 장비 고유 번호는 사용자 애플리케이션으로 메시지를 전송

할 때 사용된다.

그림 32 Push 유형별 장비 고유 번호 등록

애플리케이션 서버는 Push 메시지를 전송할 때 nPush 서버의

sendMessage 인터페이스 사용한다. sendMessage 인터페이스는

서비스 ID, 장비 고유 번호, 메시지를 파라미터로 받는 방식을 지원

한다.

그림 30 NNI 서버와 클라이언트 1간의 연결 유지

nPush

nPush(NHN mobile Push)는 NHN 서비스에서 사용하는 통

합 모바일 Push 서비스이다. nPush를 이용하면 위에서 설명한 다

양한 모바일 Push 플랫폼에 대한 상세한 규격을 몰라도 NHN의

모바일 애플리케이션을 사용하는 스마트폰으로 메시지를 전송할

수 있다. NHN의 웹 플랫폼인 BLOC에서 동작하며 프로토콜로

HTTP/JSON과 NPC를 선택할 수 있다.

현재 nPush 서버는 APNS, C2DM, AOM, NNI에 대한 발송

기능을 제공하고 있다. 2010년 6월에 네이버톡에 사용할 APNS

Push 플랫폼 개발을 시작으로 같은 해 9월에 C2DM Push 플래

폼 개발과 동시에 APNS와 C2DM 플랫폼의 통합 인터페이스 개

발 프로젝트를 진행했다. 또한, 2011년 상반기에 NNI Push 플랫

폼 및 SKT AOM Push 플랫폼 연동 기능을 추가하여, 현재 nPush

는 APNS, C2DM, AOM, NNI 플랫폼에 대한 통합 인터페이스

를 제공하고 있다.

NNI Server NNI Client

MobileDevices

push ServersAPNS/C2DM/AOM

Service Provider /NPush Server

Page 124: The platform 2011

모바일 Push와 nPush 123The Platform 2011

서버 구성

nPush 서버는 서비스별 Push 메시지 발송을 지원한다. 서비스

마다 사용하는 Push 유형이 다르고 Push 유형별로 설정 정보

(APNS/AOM 인증서 및 C2DM 발송계정정보 등)에 차이가 있

다. 이에 nPush 서버는 PAM(Push Agent Manager)을 통해 각

서비스별로 서로 다른 Agent를 생성하고 관리한다. PAM은 주기

적으로 각 서비스의 Push 유형에 따라 설정 정보를 조회하여 상태

가 변경(인증서 갱신 등)된 경우에 해당 PAM을 갱신한다. nPush

서버는 NHN 사내 플랫폼인 BLOC 기반으로 구현되었다.

그림 35 서비스별 Push 메시지 발송

nPush 서버는 서비스마다 Push 유형별로 큐(Queue)와

PooledConnection로 단위로 구성되는 PAM을 구성한다. 큐와

PooledConnection으로 처리하면서 수신 및 발송 성능이 APNS

은 2배, C2DM의 경우 최대 10배까지 향상되었다. 각 서비스 및

Push 유형 사이의 간섭이 없어 장애가 발생하더라도 타 서비스와

타 유형에 영향을 주지 않는다.

그림 33 nPush 메시지 전송

각 서비스는 nPush의 피드백 인터페이스를 통해 Push 서비스 사

용하지 않는 사용자를 주기적으로 획득한다. 정보를 획득한 후 해

당 사용자에 대해 Push 메시지를 전송하지 않도록 해야 한다. 특히

APNS, AOM 플랫폼은 Push 서비스 해제 사용자에게 계속해서

메시지를 전달할 경우 해당 애플리케이션 서버의 접근을 통제할 수

있기 때문에 주의해야 한다.

그림 34 nPush 피드백 처리

Service Provider /Application Server

MobileDevices

push ServersAPNS/C2DM/AOM

Service Provider /NPush Server

NPush Server

Push ServersC2DM

Page 125: The platform 2011

NHN의 플랫폼과 도구124 The Platform 2011

그림 36 Push 유형별 PAM 구성

맺음말

현재 NHN의 다양한 모바일 애플리케이션이 nPush를 이용

하여 스마튼폰 운영체제마다 갖는 Push 플랫폼에 관계 없이

Push 서비스를 제공하고 있다. 또한, 향후 마이크로소프트의

MPNS(Microsoft Push Notification Service)와 같이 새로운

Push 플랫폼이 추가되더라도 이 때문에 추가적인 보수 비용이 들

지 않도록 nPush를 업데이트할 것이며, Push 서비스를 안정적으

로 제공할 것이다. nPush가 앞으로도 Push 서비스를 개발하는 데

많은 도움이 되었으면 한다.

Page 126: The platform 2011

ffmpeg을 이용한 iOS 동영상 플레이어 125The Platform 2011

ffmpeg 소개

ffmpeg

오픈소스 프로젝트인 ffmpeg(http://www.ffmpeg.org/)은 동

영상 파일을 저장하거나 변환하기 위해 사용하는 크로스 플랫폼

(Cross-Platform) 솔루션이다. 자체 개발한 iOS 동영상 플레이

어에서는 동영상 파일에서 비트스트림(bitstream) 형태의 압축

된 데이터를 읽어 들이고, 읽어 들인 비트스트림을 재생할 수 있

도록 디코딩(decoding)하는 기능이 필요했기 때문에 ffmpeg의

libavformat과 libavcodec을 사용했다.

libavformat

libavformat 라이브러리는 AVI, WMV, MP4 등 다양한 포맷의

동영상 파일에서 비디오와 오디오의 비트스트림을 읽어 낸다.

모바일에서 동영상을 시청하는 사용자가 늘고 있다. 단순히 동영상

파일을 재생하는 용도라면 기기에 기본적으로 내장된 플레이어를

사용하는 것으로 충분하겠지만, 애플리케이션의 성격에 따라 기본

내장 플레이어로는 부족할 때가 있다. 예를 들면 지적재산권 보호

를 위해 DRM이나 워터마크가 적용된 동영상 콘텐츠를 재생하거

나, 파일이 아닌 스트리밍 프로토콜로 동영상을 재생할 때이다. 그

래서 미디어플랫폼개발랩에서는 다양한 목적을 만족시킬 수 있는

모바일 동영상 플레이어를 개발하고 있다.

가장 먼저 개발한 것은 아이폰에서 동작하는 iOS용 플레이어이다.

iOS 기본 내장 플레이어의 부족한 기능성을 보완하고 향후 서비스

의 요구사항을 충족할 수 있는 iOS 동영상 플레이어를 자체 개발하

였는데, 이 기사에서 자체 동영상 플레이어 제작 노하우를 공유하

려고 한다.

오픈소스인 ffmpeg을 어떻게 iOS 동영상 플레이어에 적용했는지

간단히 설명하고, ffmpeg을 사용하면서 발생할 수 있는 성능 이슈

와 성능을 최적화하기 위해 사용했던 방법을 설명하겠다.

ffmpeg을 이용한 iOS 동영상 플레이어고객들이 언제, 어디서든 멀티미디어 콘텐츠를 자유롭게 볼 수 있는

그 날까지 열심히 달리고 있는 12년 차 개발자입니다.

•미디어플랫폼개발랩 _ 김성호

Page 127: The platform 2011

NHN의 플랫폼과 도구126 The Platform 2011

여 아이폰에서 실행되는 바이너리를 생성한다. 크로스 컴파일을 위

해서는 configure 시 크로스 컴파일용 컴파일러와 링커에 대한 정

보, 대상 시스템의 아키텍처 정보를 설정해 주어야 한다.

Mac에서 아이폰용 실행 파일을 만들기 위한 컴파일러 정보와 링

커 정보, 라이브러리 정보를 제공하기 위한 ffmpeg의 configure

명령 인수 설정은 다음과 같다(출처: https://github.com/yuvi/

gas-preprocessor).

아이폰 3GS 및 아이팟 터치 3세대용 ffmpeg 설정

./configure --enable-cross-compile --arch=arm --target-

os=darwin --cc='/Developer/Platforms/iPhoneOS.platform/

Developer/usr/bin/gcc -arch armv7' --sysroot=/Developer/

Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS3.0.sdk

--cpu=cortex-a8 --enable-pic

그 밖의 아이폰/아이팟용 ffmpeg 설정

./configure --enable-cross-compile --arch=arm --target-

os=darwin --cc='/Developer/Platforms/iPhoneOS.platform/

Developer/usr/bin/gcc -arch armv6' --sysroot=/Developer/

Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS3.0.sdk

--cpu=arm1176jzf-s

위 설정에서 아이폰 SDK 버전은 실제로 사용하는 버전으로 교체

해야 한다. 3.0 미만 버전은 지원하지 않으며, 정상적인 동작을 보

장하지 않는다.

아이폰/아이팟의 모든 버전을 지원하려면 각 아키텍처에 대한 빌

드를 만들고 lipo 명령어로 각 라이브러리를 합치는 것을 권장한

다. 예를 들면 다음과 같이 armv6와 armv7용 라이브러리를 각각

만든다.

lipo -create -arch armv6 armv6/libavcodec/libavcodec.a -arch

armv7 armv7/libavcodec/libavcodec.a -output universal/

libavcodec.a

libavcodec

libavcodec 라이브러리는 코덱(codec)으로 압축된 비트스트림을

디코딩한다. 코덱의 종류에 따라 libavcodec에 포함된 디코딩 함수

가 비트스트림을 재생할 수 있는 영상 픽셀(pixel) 정보와 소리 정

보로 재구성한다. libavcodec은 다양한 코덱을 지원하며, ffmpeg

을 컴파일할 때 configure 명령에 다음과 같은 옵션을 추가하여 컴

파일 시에 어떤 코덱을 포함할 것인지 설정할 수 있다.

--disable-encoder=NAME disable encoder NAME

--enable-encoder=NAME enable encoder NAME

--disable-encoders disable all encoders

--disable-decoder=NAME disable decoder NAME

--enable-decoder=NAME enable decoder NAME

--disable-decoders disable all decoders

소프트웨어 디코딩 과정

자체 개발한 iOS 동영상 플레이어에서는 ffmpeg의 libavformat

과 libavcodec을 이용하여 다음과 같은 과정을 거쳐 영상 데이터를

추출하도록 구현했다.

그림 1 소프트웨어 디코딩 과정

ffmpeg의 아이폰용 컴파일

아이폰 앱은 보통 Mac/Xcode 환경에서 개발한다. 아이폰에서 직

접 소스코드를 컴파일하는 것이 아니라 Mac에서 크로스 컴파일하

Page 128: The platform 2011

ffmpeg을 이용한 iOS 동영상 플레이어 127The Platform 2011

RGB 데이터를 OpenGL ES에서 그대로 화면에 출력하는 형태였

다. 즉, 영상을 출력하는 일련의 모든 작업이 CPU에서 이루어지고

있었다.

그림 2 개선 전 영상 데이터 흐름도

개선 전 프로파일링 결과

다음 그림과 같이 Xcode의 Instruments를 이용하여 측정된 CPU

점유율(MP4 형식, H.264/AAC 코덱, 해상도 320x240 기준)은 약

68.6%였다.

그림 3 개선 전 CPU 점유율

그리고 XCode에서 armv6와 armv7용 빌드를 만들 수 있는지 확

인한다. 여러 버전이 아니라 한 버전만 지원하려면 lipo로 라이브러

리를 합칠 필요가 없다(사실 armv6는 너무 느리다).

위 내용에서는 아이폰 3GS와 아이팟 터치 3세대만 언급하고 있으

나, 아이폰 4에서 사용하는 A4 칩이 지원하는 instruction set이

arm7으로 아이폰 3GS와 동일하므로 아이폰 4의 경우에도 아이폰

3GS의 컴파일 옵션을 사용한다.

ffmpeg 사용 시 성능 이슈와 최적화

초기에 개발된 플레이어에서는 영상 해상도가 높으면 비디오가 오

디오에 비해 느려지는 현상이 있었다. 그 원인을 파악하기 위해서

함수별 CPU 점유율을 확인하는 프로파일링을 수행했다. Xcode에

서는 프로파일링 도구로 Instruments를 제공한다. Instruments를

통해 CPU의 사용률 정보를 추적하여 점유율이 높은 함수부터 최

적화 작업을 진행했다. 프로파일링을 통해 파악한 성능 최적화 포

인트와 처리 방법은 다음과 같다.

• 디코딩된 영상의 색상 공간(color space)을 변환하는 과정(YUV 데

이터 -> RGB 데이터): OpenGL ES의 프래그먼트 셰이더(Fragment

Shader)에서 색상 공간을 변환하도록 수정하여 GPU 처리 방식으로

개선

• 파일로부터 읽어 들인 트랜스코딩(transcoding) 데이터를 디코딩하는

과정: ARM의 NEON 기술이 적용되지 않았을 경우 이를 적용하도록

ffmpeg 라이브러리 재컴파일

GPU를 이용한 색상 공간 변환 처리

개선 전 영상 데이터 흐름

앞에서 언급한 바와 같이, 개발한 플레이어에서는 ffmpeg의

libavformat과 libavcodec을 이용하여 디코딩된 영상 데이터를 얻

는다. 여기서 얻어진 영상 데이터는 보통 YUV 데이터이기 때문에

이를 iOS 기기의 화면에 표시하려면 먼저 RGB 데이터로 색상 공

간을 변환해야 한다.

초기에 개발된 플레이어는 다음 그림과 같이 ffmpeg의 libswscale

에서 제공하는 함수를 이용하여 색상 공간 변환을 수행하고 변환된

Page 129: The platform 2011

NHN의 플랫폼과 도구128 The Platform 2011

변환된 RGB 데이터는 OpenGL ES에 의해 화면으로 출력된다. 다

음과 같이 OpenGL ES 프래그먼트 셰이더에서 RGB 이미지가 저

장된 텍스처(texture)를 그대로 화면에 출력하는 방식이었다.

uniform sampler2D sampler0;

varying highp vec2 _texcoord;

void main()

{

gl_FragColor = texture2D(sampler0, _texcoord);

}

즉, sws_scale 함수를 호출해서 색상 공간 변환을 모두 처리한 후

RGB 이미지로 만드는 CPU는 바쁘고, 단순히 RGB 이미지를 화

면에 출력하기만 하는 GPU는 한가한 상황이었다. 그래서 CPU의

부담을 줄이고 GPU에게 일을 주기 위해서, GPU가 YUV를 RGB

로 바꾸는 색상 공간 변환을 맡도록 수정했다.

그림 6 개선 후 동영상 파일에서 RGB 이미지를 얻는 과정

sws_scale 함수 호출을 삭제한 후 디코딩된 YUV 데이터를 색

상 컴포넌트별로 텍스처를 3장으로 나누어 프래그먼트 셰이더에

게 전달하고, 프래그먼트 셰이더에서 YUV 데이터를 RGB 데이

터로 바꿔서 화면에 출력하도록 구현했다. YUV 데이터를 RGB

데이터로 바꾸는 과정과 같은 동일한 데이터 타입에 대한 반복

작업은 GPU에게 아주 적합한 작업이기 때문이다. 다음과 같이

다음 그림은 영상 재생 시 함수별 CPU 점유율을 보여주는 화면

이다.

그림 4 개선 전 함수별 CPU 점유율

YUV 데이터를 RGB 데이터로 변환하는 함수인 yuv2rgb_c_16 함

수의 호출 빈도가 가장 높다. 즉, YUV 데이터를 RGB 데이터로 변

환하는 작업이 전체 약 68.6%의 CPU 점유율 중에서 많은 부분을

차지하고 있음을 알 수 있다. 이제 CPU 점유율을 높이는 색상 공간

변환 작업을 어떻게 개선했는지 확인해보자.

CPU 대신 GPU에서 색상 공간 변환하기

개선 전의 플레이어는 다음과 같은 순서로 동영상 파일에서 RGB

이미지를 얻었다. av_read_frame 함수를 호출하여 파일에서 데이

터를 읽어 들이고 avcodec_decode_video2 함수를 호출하여 디코

딩된 YUV 데이터를 얻은 다음, sws_scale 함수를 호출하여 특정

크기의 RGB 데이터로 변환했다.

그림 5 개선 전 동영상 파일에서 RGB 이미지를 얻는 과정

Page 130: The platform 2011

ffmpeg을 이용한 iOS 동영상 플레이어 129The Platform 2011

개선 후 프로파일링 결과

개선 후 측정한 CPU 점유율(MP4 포맷, H.264/AAC 코덱,

320x240 해상도 기준)은 다음 그림처럼 51%였다. 기존 68.6%에

비해 성능이 약 25% 향상되었다.

그림 8 개선 후 CPU 점유율

다음과 같이 개선 후의 함수별 CPU 점유율을 살펴보면 YUV 데이

터를 RGB 데이터로 변환하는 부분은 모두 사라지고, CPU 점유율

대부분이 avcodec_decode_video2 함수 내부 과정으로 옮겨졌음

을 알 수 있다. 이제 CPU는 디코딩 작업만 열심히 하면 된다.

그림 9 개선 후 함수별 CPU 점유율

ARM® NEON™ 기술을 이용한 디코딩 최적화

ARM의 NEON 기술

ARM의 NEON은 멀티미디어 포맷을 효율적으로 처리하기 위한

일반 목적의 SIMD(Single Instruction Multiple Data) 엔진이다.

NEON은 Cortex-A 시리즈 프로세스를 위해 128bit SIMD 구조

를 확장한 것으로, 멀티미디어 어플리케이션을 위해 유연하고 강력

한 가속을 지원하도록 디자인되었다. NEON을 이용하여 비디오

의 인코딩/디코딩, 2D/3D 그래픽 처리, 게임, 오디오와 음성 처리,

GLSL(OpenGL Shading Language)를 이용하면 GPU에서 수행

할 작업을 구현할 수 있다.

uniform sampler2D sampler0; // Y Texture Sampler

uniform sampler2D sampler1; // U Texture Sampler

uniform sampler2D sampler2; // V Texture Sampler

varying highp vec2 _texcoord;

void main()

{

highp float y = texture2D(sampler0, _texcoord).r;

highp float u = texture2D(sampler1, _texcoord).r;

highp float v = texture2D(sampler2, _texcoord).r;

y = 1.1643 * (y - 0.0625);

u = u - 0.5;

v = v - 0.5;

highp float r = y + 1.5958 * v;

highp float g = y - 0.39173 * u - 0.81290 * v;

highp float b = y + 2.017 * u;

gl_FragColor = highp vec4(r, g, b, 1.0);

}

개선 후 영상 데이터 흐름

개선한 플레이어는 다음과 같은 데이터 흐름을 따른다. YUV 데이

터를 RGB 데이터로 변환하는 과정을 GPU에서 처리하면서 CPU

의 부담이 줄어들어, 높은 해상도의 영상 재생 시 비디오가 오디오

에 비해 느려지는 현상이 크게 개선되었다.

그림 7 개선 후 영상데이터 흐름도

Page 131: The platform 2011

NHN의 플랫폼과 도구130 The Platform 2011

표 1 개선 적용에 따른 CPU 점유율 비교

ARM® NEON™ 적용 GPU 색상 공간 변환 적용 CPU 점유율

O O 51%

O X 68.6% (CPU를 약 34% 더 사용)

X O 65.8% (CPU를 약 29% 더 사용)

X X 83.7% (CPU를 약 64% 더 사용)

마무리

ffmpeg을 이용하여 개발한 iOS 동영상 플레이어 사례를 통해 iOS

에서 ffmpeg을 사용할 때 고려해야 할 성능 최적화에 대해 살펴보

았다. 언젠가 모바일 기기의 성능이 일반 PC 정도 수준에 이른다면

이런 고민이 필요 없게 될지도 모른다. 하지만 현재와 같이 한계가

있는 모바일 기기에서 최적의 성능을 이끌어 내려면 모바일 환경을

반드시 이해해야 한다. 이 글에 담은 내용이 모바일 환경의 멀티미

디어 처리를 이해하는 데 조금이나마 도움이 되었으면 하는 바람이

다.

이미지 처리, 전화 등을 ARMv5의 3배, ARMv6의 약 2배 이상 성

능으로 수행할 수 있다고 한다.

아이폰, 아이패드, 아이팟 터치 등의 iOS 기기는 ARM 계열 CPU

를 사용하고 있다. 그러므로 ffmpeg을 iOS용으로 컴파일할 때

ARM의 NEON 명령어를 이용하도록 컴파일하면 디코딩 과정을

최적화하여 성능을 향상시킬 수 있다.

NEON 적용에 따른 성능 비교

ffmpeg은 NEON을 지원하며 기본적으로 NEON을 사용하도록

컴파일된다. 다음은 성능을 비교하기 위해 일부러 NEON 지원 옵

션을 끄고(--disable-neon 옵션) 성능을 측정한 결과이다.

GPU 색상 공간 변환을 적용하고 NEON을 적용하지 않은 경우:

CPU 점유율: 65.8%

GPU 색상 공간 변환과 NEON을 모두 적용하지 않은 경우: CPU 점

유율: 83.7%

다음은 성능 측정 결과를 종합하여 정리한 표이다(MP4 포맷,

H.264/AAC 코덱, 해상도 320x240 기준). NEON 기술을 적용한

상태에서 GPU 색상 공간 변환까지 적용하면 성능을 크게 향상시

킬 수 있음을 확인할 수 있다.

Page 132: The platform 2011

분산 고속 저장소 nStore 131The Platform 2011

그런데 커뮤니케이션 캐스트가 단순하게 모든 사용자 이벤트를

각각 사용자 데이터베이스에 알림이라는 레코드 형태로 저장한

다고 가정하자. 예를 들어 회원이 600만명이 넘는 중고나라 카

페에 글이 하나 등록되면 600만개의 알림이 발생한다. 이를 대

형 카페와 블로그를 대상으로 어림잡아 계산해 보면 최소 400만

TPS(Transactions Per Second) 성능의 알림 저장 시스템이 필요

하다는 결론에 도달한다.

이와 비슷한 서비스를 RDBMS로 지원하려면 어떻게 해야 할까?

사용자나 알림 아이디 같은 특정 값을 기준으로 샤드(shard)를 구

성하고 각 샤드에 맞는 알림을 데이터베이스에 저장할 수 있도록

개발해야 한다. 데이터 증가에 비례하여 더 나은 성능이 요구될 것

이므로 이때마다 분산 규칙을 바꾸고 데이터베이스를 재구성해야

하며 서비스도 업데이트해야 하는 작업을 끊임없이 반복해야 할 것

이다.

최근 NoSQL에 대한 개발자의 반응이 뜨겁다. 이는 개발자들 내에

서 RDBMS 외의 다른 저장소에 대한 요구사항이 발생하고 있다고

해석할 수 있다. 이와 같은 개발자의 요구에 부응하여 NHN에서는

새로운 분산 고속 저장소인 nStore라는 플랫폼을 준비하고 있다.

nStore는 NoSQL에서 강조하는 고가용성(High availability)과

수평적 확장성(Horizontal scalability)을 유지하면서 테이블과

SQL을 지원한다. 즉, NoSQL과 RDBMS 양 제품의 장점을 모두

취했다고 볼 수 있다. 어울리지 않을 수도 있는 양 제품의 속성을

지원하기 위한 기술적인 어려움이 있을 수 있다. 하지만, nStore를

통해 사용자가 받을 수 있는 혜택은 그만한 가치가 있다고 생각한다.

탄생 배경

nStore의 탄생은 커뮤니케이션 캐스트의 저장 구조 개선을 고민하

면서 시작되었다. 커뮤니케이션 캐스트 서비스(현재 미니 네이버

me 서비스)는 네이버에서 발생하는 모든 사용자 이벤트가 알림 형

태로 저장되고 이를 사용자에게 보여준다.

분산 고속 저장소 nStore메일 서명을 바꿨습니다. “Anyway the wind blows”에서 얼마 전 개봉한

영화 Invictus에서 인용되었던 “I am the Master of my Fate.

I am the Captain of my Soul”로요.

어디서 바람이 불던 각자 자신이 운명의 마스터이고 영혼의 선장입니다(더 이상

남 탓은 그만~ ㅎㅎ).

•저장시스템개발팀 _ 김효

Page 133: The platform 2011

NHN의 플랫폼과 도구132 The Platform 2011

nStore는 다음과 같은 2가지 특징을 지닌다.

첫째, 해싱(hashing)과 메타데이터 관리 서버(MetaData Server)

의 복제 맵 관리 방식(replica map)을 혼합하여 특정 단위

(Container Group)로 분산한다.

둘째, RDBMS 사용자가 어려움 없이 nStore를 사용할 수 있도록

데이터 모델은 tabular 모델과 비슷하게 인터페이스는 SQL과 비

슷한 쿼리 문(query language)을 제공한다. 또한 OwFS(Owner-

based File System)1를 통해 오랜 시간 개발과 운영으로 노하우

가 쌓인 개별 공간 할당 방식을 사용한다. 타 NoSQL 제품과 특징

을 간단히 비교하면 다음과 같다.

표 1 제품 간 특징 비교

제품 데이터모델 분산단위 분산방식 복제

GFS GFS Chunk MDS Master-slave

Big table Multi-dimensional map Row Index GFS에 의존

Cassandra Multi-dimensional map Row Hash/index Optional

MongoDB Document(JSON) Document Index Master-slave

CouchDB Document(BSON) Document Index Master-slave

Dynamo Blob Blob hash Sloppy quorum

nStore Tabular Container group Hash/MDS N-replicas

기본적인 동작을 설명하면 클라이언트는 HTTP 또는 표준(native)

API를 호출하여 nStore를 구성하는 임의의 서버에 쿼리(query)를

전달하고, nStore 서버는 분산된 환경에서 데이터를 저장하고 있는

서버에 데이터를 요청하여 그 결과를 전달하는 방식이다. nStore를

구성하는 모든 서버는 사용자 요청을 받는 동시에 데이터를 저장하

는 노드 역할을 수행한다.

컨테이너

nStore를 이해하려면 nStore의 가장 작은 분산 단위이자 사용자

개별 공간인 컨테이너(container)를 알아야 한다. 컨테이너는 사용

1 NHN에서 개발한 분산파일저장소로서 Owner라는 단위로 각 저장소를 할당/분산하는 방식이

특징이다.

비슷한 상황은 SNS 요소를 도입한 기타 서비스에서도 쉽게 발생할

수 있다. 10만 명이 넘는 미투데이 친구(이하 미친)를 확보하고 있

는 인기 연예인의 포스트를 어떻게 미친에게 전달할까? 미친이 십

만, 백만 명으로 늘어난다면 어떻게 해당 데이터를 효과적으로 저

장할 수 있을까?

RDBMS에서 대량의 데이터를 신속하게 처리하려면 높은 비용이

요구된다. 바로 이 지점(즉, 높은 데이터 처리 비용)에서 RDBMS

의 단점을 보완하고자 nStore를 개발했다.

nStore는?

nStore의 지향점을 한마디로 표현하면, 고속의 분산 저장소라고 할

수 있다. 그렇다면 ‘고속 분산 저장소’란 무엇인가? 먼저 각 단어의

의미를 확인해 보자.

• 고속: 비록 memcached 시스템보다는 느리지만 기존의 RDBMS보다

최소한 같거나 더 나은 성능을 냄

• 분산: 데이터와 부하(load)를 모두 분산시키고 이를 통해 높은 수준의

확장성과 가용성을 얻음

• 저장소: 용량 제한이 없고 영구적인 저장 공간을 제공

그림 1 nStore 동작 구조

Page 134: The platform 2011

분산 고속 저장소 nStore 133The Platform 2011

그림 3 구성 예

예를 들어 앞에서 언급했던 커뮤니케이션 캐스트의 단순화된 모델

을 생각해보자. 알림 메시지를 저장하는 SN_MSG 테이블, 어떤 항

목의 알림을 수신할지 그 여부를 정보를 저장한 SN_CONF 테이

블을 사용자별로 컨테이너로 구성하여 해당 사용자에게 할당한다.

새로운 알림은 SN_MSG 테이블에 쌓고, 사용자가 수신 여부를 변

경하면 이를 SN_CONF에 반영한다. 그리고 전체 서비스의 카테

고리 정보를 SN_SVC 테이블에 저장하는데 이는 사용자별로 설정

된 정보가 아니므로 전역 테이블로 구성한다. 그러면 수천만 개의

컨테이너와 하나의 전역 테이블로 구성된 커뮤니케이션 캐스트 알

림 처리용 nStore가 생성되는 것이다.

nStore 구조

그림 4 nStore 구조

자에게 할당되는 논리적인 공간으로 개별 사용자는 자신의 컨테이

너에 있는 테이블에 데이터를 읽고 쓸 수 있다. 하지만 컨테이너마

다 각각의 테이블 스키마(Table Schema)를 가질 수는 없고 모든

컨테이너는 동일한 테이블 스키마를 갖는다. 예를 들어, A라는 테

이블을 컨테이너에 추가하면 추가된 시점부터 다른 모든 컨테이너

에도 테이블 A가 추가된다. 다른 NoSQL 제품과 달리 컨테이너 안

에서 테이블 사이의 조인(join) 연산도 지원한다. 그러므로, 하나의

컨테이너 입장에서 보면 단순화된 RDBMS의 성격을 가진다고 할

수 있다.

그림 2 컨테이너 서버

nStore는 전역 테이블(Global Table) 개념을 지원한다. 컨테이너

X와 컨테이너 Y는 서로 데이터를 참조할 수 없는데, nStore의 모

든 컨테이너가 공통으로 참조해야 할 데이터가 있다면 이를 전역

테이블로 구성할 수 있다. 전역 테이블은 모든 컨테이너가 공통으

로 참조할 수 있는 테이블로 컨테이너의 지역 테이블(Local Table)

과 조인 연산도 할 수 있다. 전역 테이블은 설정이나 메타데이터 성

격의 데이터를 저장하는 데 적합하다.

Page 135: The platform 2011

NHN의 플랫폼과 도구134 The Platform 2011

이다. 그러므로 모든 컨테이너 서버가 정상인 상황에서 관리 노드

에 이상이 발생해도 사용자 요청은 정상적으로 처리되어야 한다.

애플리케이션

애플리케이션(클라이언트)은 nStore에서 제공하는 C와 자바의

API를 통해 직접 요청을 보내거나, HTTP 요청을 이용할 수 있

다. 애플리케이션으로 전달되는 결과 데이터의 포맷(Format)은

JSON2을 지원한다. 클라이언트 인터페이스와 데이터 포맷은 향

후 요구 사항에 맞춰 좀 더 다양하게 지원할 예정이다.

nSQL

nSQL은 nStore에서 지원하는 쿼리와 데이터의 명세(Query/

Data spec)이다. 우선 nSQL에서 지원하는 데이터 타입은 다음과

같다.

표 2 nSQL 데이터 타입

데이터 타입 범위 설명

integer 64비트 정수(2의 보수)

double 64비트 실수

boolean true 또는 false

string UTF-8로 인코딩된 문자열

timestamp 1970년 1월 1일부터 시작된 밀리 초 단위 시간

id 128비트 정수(UUID 포맷)

nSQL = (dml | ddl) [“;”] ;

ddl = create | alter | drop ;

dml = insert | delete | update | select ;

nSQL은 SQL과 매우 유사하다. 하지만 주의해야 할 부분이 있는

데 DML(Data Manipulation Language) 쿼리를 실행할 때는 반

드시 컨테이너 키를 같이 전달하여야 하고(전역 테이블이 대상인

경우 제외), DDL(Data Definition Language) 쿼리를 실행할 때

는 지역 테이블과 전역 테이블을 명시해야 한다는 것이다.

2 http://json.org/json-ko.html

nStore는 물리적으로 애플리케이션(클라이언트), 컨테이너 서버,

관리 노드(Management node)로 구성된다

컨테이너 서버

여러 개의 컨테이너가 모인 하나의 물리적인 서버 단위를 컨테이너

서버라고 부른다. nStore의 분산 저장소는 물리적으로 N개의 컨테

이너 서버로 구성된다. 좀 더 세분화하면 컨테이너 서버의 구성 요

소는 다음과 같다.

• HTTP 요청을 처리하는 HTTP 서버

• API 요청을 처리하는 RPC 통신

• 복제를 관장하는 Coordinator

• 쿼리를 수행하는 Executor

• 실제 데이터를 저장하는 스토리지 레이어(Storage Layer)

용도에 맞는 다양한 형태의 스토리지 엔진(Storage Engine)을 컨

테이너 서버의 스토리지 플러그인(plug-in)으로 사용하는 것을 지

향하고 있다. 현재 nStore 1.0 버전은 CUBRID 데이터베이스를 기

본 엔진으로 사용한다.

관리 노드

관리 노드(Management Node)는 nStore 관리용 인스턴스이다.

관리 노드의 용도는 다음과 같다.

• 컨테이너 서버의 동작 확인(health check)

• 컨테이너 서버의 상태 변경

• 컨테이너 서버의 구성 관리

• 컨테이너의 분산 맵(map) 관리

• 컨테이너 복구

• 데이터 재조정(Data Rebalancing)

위의 기능을 한마디로 표현하면 분산 맵 관리와 장애 관리라고 할

수 있다. 한 가지 대전제 조건은 만약 관리 노드에 장애가 발생해도

정상 동작(Normal Operation)에 영향을 주지 않아야 한다는 점

Page 136: The platform 2011

분산 고속 저장소 nStore 135The Platform 2011

표 3 Insert 연산 테스트 결과

Insert 연산의 성능 테스트 결과는 특정 메모리 청크(chunk) 단위

로 레코드를 순차적으로 쓰는 Cassandra가 가장 좋은 성능을 보였

다. 또한 HBASE도 유사한 방식으로 쓰기를 수행하므로 데이터베

이스의 자료 구조를 사용하는 MongoDB, nStore에 비해 나은 성

능을 보인다. 하지만, 이러한 쓰기 성능의 이점은 조직화되지 못한

데이터의 특성 때문에 Read 연산을 수행할 때 성능 저하를 가져

온다.

표 4 Read 연산 테스트 결과

Read 연산의 성능 테스트 결과는 하부 저장소로 데이터베이

스를 사용하는 nStore가 좋은 성능을 보이고 쓰기에 최적화한

Cassandra의 경우 좋지 않은 결과를 보였다. MongoDB는 주 메

모리 내에서 좋은 성능을 보이나 위와 같이 대용량의 데이터를 다

루는 경우 성능이 떨어졌다.

분산 및 복구를 위한 제약

데이터 복제본의 분산 및 복구를 위해 컨테이너에서 수행한 작업을

시간순(time stamp)으로 정렬한다. 이렇게 정렬한 작업을 기반으

로 데이터를 효과적으로 분산, 복구하기 위해 해당 작업에 다음의

2가지 법칙이 적용됐다고 가정하였다(트위터에서 사용하는 분산

프레임워크인 Gizzard와 유사).

• 교환 법칙(Commutative): 수행 순서가 바뀌어도 결과는 같다.

• 멱등 법칙(Idempotent): 같은 작업을 여러 번 수행해도 결과는 같다.

이러한 속성은 nStore를 대단히 유연하게 만들어주는데, 필연적으

로 다음과 같은 제약 사항을 동반한다.

• 모든 테이블은 반드시 주 키(Primary Key)를 가진다

• 내용을 참조하여 연산한 다음 데이터를 업데이트하는 작업이 실행될

때 같은 행(row)에서 충돌이 발생하면 정합성이 깨질 수 있다(예: A

= A + 1).

• 업데이트 작업을 수행하려면 주 키를 조건으로 전체 행을 모두 업데이

트해야 한다(Key-Value와 유사). 부분 업데이트와 같은 작업을 할 경우

다른 행에 충돌이 발생하면 정합성이 깨질 수 있다.

성능 측정

nStore의 성능을 측정하기 위해 타 NoSQL 솔루션과 비교해 보았

다. 측정 환경은 다음과 같이 설정했다.

• 테스트 장비: 3대 (Nehalem 6 Core x 2 CPU , 16GB Memory,

320GB x 6 HDD RAID0)

• 테스트 도구: YCSB(Yahoo Cluster Serving Benchmark)

• 워크로드

- Insert: 50MB 레코드 삽입

- Read 연산: Zipian 분포로 읽기 수행

- Read/Update 연산: 50:50 비율로 수행

- Read/Insert 연산: 50:50 비율로 수행

• 비교 대상 NoSQL 솔루션: HBASE, Cassandra, MongoDB,

nStore

25000

20000

15000

10000

5000

0

Hbase Cassandra MongoDB nStore

Insert

10000

8000

6000

4000

2000

0

Read

Hbase Cassandra MongoDB nStore

Page 137: The platform 2011

NHN의 플랫폼과 도구136 The Platform 2011

마치며

지금까지 분산 고속 저장소인 nStore에 대해 간략히 소개했다.

nStore의 지향점을 다시 한번 얘기하면 ‘데이터는 우리가 알아서

분산, 복제, 부하 분산, 복구할 테니 걱정 말고 그냥 쓰세요'이다. 많

은 시간에 걸친 연구와 고민, 동료와의 논의 및 논쟁을 통해 도달한

결론은 NoSQL로 대표되는 최근의 분산 저장 시스템은 그만큼 다

양한 워크로드와 다양한 선택의 이슈를 가지고 있다는 점이다. 이

런 환경에서 nStore가 하나의 축을 이루는 시스템으로 자리 잡고

나아가 더욱 획기적인 서비스를 지원하는 기반 플랫폼이 되길 기대

해 본다.

표 5 Read/Update 연산 테스트 결과

Update 연산의 경우 HBASE와 Cassandra는 데이터베이스의 전

통적인 Update 연산이 아닌 Insert 연산과 유사하게 동작하므로

Insert 연산과 비슷한 성능을 보인다.

표 6 Read/Insert 테스트 결과

Read/Insert 연산은 nStore를 포함하여 전체적으로 유사한 성능을

보이고, MongoDB는 모든 테스트 항목에서 좋은 않은 결과를 보

였다. 본 성능 측정을 통해서 하부 저장소로 데이터베이스를 사용

하는 nStore의 성능 특성을 파악하였고, nStore의 분산 레이어 동

작도 검증하였다.

6000

5000

4000

3000

2000

1000

0

Read/Update

Hbase Cassandra MongoDB nStore

6000

5000

4000

3000

2000

1000

0

Read/Insert

Hbase Cassandra MongoDB nStore

Page 138: The platform 2011

CUBRID 브로커 이야기 137The Platform 2011

libcubridcs.so이다. libcubridcs.so를 사용하는 네이티브 애플리

케이션을 개발하면 다음과 같은 형태가 될 것이다.

그림 1 libcubridcs.so를 사용하는 네이티브 애플리케이션 구조

위와 같은 네이티브 애플리케이션에서 SELECT 문을 실행시켰을

때 클라이언트 모듈과 서버 프로세스 동작의 흐름을 간략히 표현하

면 다음과 같다.

CUBRID는 JDBC 드라이버-브로커-데이터베이스 서버의 3계층

(3-tier) 구조를 사용한다. CUBRID를 처음 접하는 사람들이 많이

궁금해 하는 것 중 하나가 3계층 구조를 사용하는 이유이다. ‘연결

URL에 브로커 포트를 입력해야 한다는데, 브로커는 무엇을 하는

지’와 같은 질문을 종종 받곤 한다.

이 기사에서는 브로커가 왜 필요한지, 어떤 기능을 수행하는지,

브로커 사용의 장단점은 무엇인지 등을 간략히 이야기해 보고자

한다.

CUBRID 클라이언트 모듈

DBMS는 일반적으로 데이터의 집합체인 데이터베이스, 데이터베

이스의 데이터를 조작하는 서버, 서버에 요청을 보내는 클라이언트

로 구성된다. CUBRID의 경우, 데이터베이스 서버 역할을 수행하

는 프로세스는 cub_server이고 cub_server 프로세스에 요청을 보

낼 수 있는 유일한 클라이언트 모듈은 C로 작성한 라이브러리인

CUBRID브로커 이야기두 아이의 아빠입니다. 우리 아이들에게 넘겨줘야 할 것은

깨끗한 자연, 문화유산, 잘 만든 소프트웨어, ......

•DBMS개발랩 _ 강철규

libcubridcs.so

a.out cub_server

database

Page 139: The platform 2011

NHN의 플랫폼과 도구138 The Platform 2011

또한 잠금(lock) 캐시도 필요하다. 예와 같은 질의를 실행할 때 foo

테이블 조회가 끝날 때까지 테이블의 스키마는 변경되지 않아야 한

다. 테이블을 조회할 동안 테이블이 변경되지 않게 하려면 잠금을

획득해야 한다. CUBRID의 경우 테이블을 조회하는 동안 테이블

에 대한 의도 공유 잠금(intention shared lock, IS lock)을 획득한

다. 이 동안에는 해당 테이블의 데이터를 입력하거나 조회할 수는

있지만, 테이블 스키마를 변경하거나 테이블 전체를 스캔하여 변경

하는 등의 작업은 잠금이 해제된 후에나 실행할 수 있다. 클라이언

트 모듈은 조회하려는 테이블에 대해서 의도 공유 잠금을 획득해야

하는데, 필요할 때마다 잠금을 요청하거나 잠금 상태 확인을 요청

하면 성능을 저하시킬 수 있으므로 잠금을 클라이언트에 캐시한다.

마지막으로 클라이언트 모듈은 질의를 최적화하고 질의 수행 계획

을 생성한다. 사용할 수 있는 인덱스가 존재하는지, 조인 질의는 어

떤 조인 방법을 사용할지, ORDER BY 구문이 있다면 정렬 과정

을 생략할 수 있는지 등을 판단하여 최적화된 질의 수행 방법을 서

버가 실행할 수 있는 형태로 변환하여 서버에 요청한다.

위 과정을 수행하는 클라이언트 라이브러리는 C 인터페이스로 제

공한다. 그러면 자바 또는 PHP로 애플리케이션을 작성할 때에는

어떻게 사용할 수 있을까? 2계층 구조를 사용한다면 다음과 같은

두 가지 방법이 있을 것이다.

• 클라이언트 모듈을 자바, PHP 등에서 호출할 수 있는 형태로 제공한다.

이때 가장 큰 문제점은 JDBC 또는 PHP 드라이버 안에서 앞에서 설명한

많은 질의 처리 단계를 수행하기 때문에 어플리케이션 서버의 CPU, 메

모리를 많이 사용한다는 것이다.

• 애플리케이션의 요청을 받는 부분과 질의 처리에 필요한 부분을 분리하

여 질의 처리 과정은 서버에서 모두 수행하도록 구조를 변경한다.

이는 현재 CUBRID 구조를 상당히 변경해야 하는 부담도 있지만, 서

버에서 모든 과정을 처리하는 것이 정말 유리한지부터 고려해야 한다.

DBMS 서버는 데이터베이스 관리, 디스크에 저장된 데이터를 메모리

구조체로 변환하고 관리하기 위한 버퍼 관리, 동시성 제어, 장애 복구를

위한 관리 등 DBMS 기능의 대부분을 수행한다. 따라서 클라이언트보

다 많은 일을 처리해야 하고 시스템 리소스도 많이 사용한다. 대부분 시

스템의 한계 상황은 DBMS 서버가 처리할 수 있는 최대치 때문인 경우

가 많다. 서버가 처리해야 하는 일의 일부를 클라이언트가 처리할 수 있

다면 서버 시스템의 리소스 활용 측면에서 유리하다.

위 두 가지 방법의 대안으로 3계층 구조를 사용하면 어떨까? 현재

DBMS 서버와 클라이언트 모듈이 동작하는 구조는 그대로 두고

그림 2 네이티브 애플리케이션의 질의 처리 과정

위 그림에서 보는 바와 같이 CUBRID의 클라이언트 모듈은 애플

리케이션의 요청을 서버로 전달하는 작업을 포함하여 다양한 업무

를 처리한다. 클라이언트 모듈은 질의를 수행하기 위한 질의 수행

계획을 직접 생성하여 서버에 전달하고, 서버는 클라이언트 모듈이

전달한 질의 수행 계획에 따라 데이터를 조회/조작한다.

클라이언트 모듈에서 질의 수행 계획을 생성하기 위해서는 많은 정

보가 필요하다. 예를 들어 ‘SELECT * from foo where id = 1’이

라는 질의가 수행된다고 가정하자.

먼저 클라이언트는 질의가 SQL 문법에 맞는지 검사한다. 클라이

언트 모듈에 파서가 존재하여 SQL 문을 최소 단위의 토큰으로 자

르고 구문을 분석한다.

구문 검사를 완료한 후에는 질의에 의미상 오류가 없는지 검사한

다. 예를 들면, foo라는 테이블이 존재하는지, 현재 사용자에게

SELECT 권한이 있는지, id라는 컬럼이 foo 테이블에 존재하는

지, id 컬럼값을 숫자형 값과 비교할 수 있는지 등을 검사해야 한다.

foo가 뷰라면 뷰 변환을 수행한 이후에 동일한 검사를 수행하고,

조인 질의나 조회 조건이 많아질수록 해당 구문에 대한 오류 검사

도 많아진다. 의미상 오류를 검사하기 위해 필요한 스키마 정보를

필요할 때마다 서버에 요청하는 것은 비효율적이므로, 스키마에 대

한 정보는 클라이언트에 캐시한다.

a.out

parsing

optimization

plan generation

search

object lock

copy to resultset

Execute query

Fetch cursor

libcubirdcs.so

Send plan

Return cursor

cub_server

Page 140: The platform 2011

CUBRID 브로커 이야기 139The Platform 2011

애플리케이션은 cub_broker에 연결을 요청한다. cub_broker는

서비스 가능한 cub_cas를 선택하고 애플리케이션 연결을 전달하

여 드라이버와 cub_cas가 연결되도록 한다. cub_cas는 드라이버

가 요청하는 데이터베이스 서버에 접속하기 위해 cub_master에

연결을 요청한다. cub_master는 cub_cas의 연결을 cub_server에

전달하여 cub_cas와 cub_server가 연결되도록 한다. cub_cas와

cub_server가 연결된 이 후에는 드라이버에서 연결을 끊어도 cub_

cas와 cub_server의 연결은 유지된다.

그럼 이와 같은 3계층 구조의 장단점은 어떤 것이 있을지 살펴보도

록 하겠다.

브로커 사용의 단점

브로커를 사용할 때 가장 두드러지는 단점은 구조가 복잡해 보인

다는 것이다. 엄밀하게 보면 데이터베이스 서버와 브로커는 별개

의 컴포넌트로 동작한다. 데이터베이스 서버 입장에서 cub_cas

는 하나의 클라이언트이고, 브로커 입장에서 데이터베이스 서버는

드라이버 요청에 따라 선택적으로 연결해야 할 대상이다. 따라서

CUBRID를 설정할 때 데이터베이스 서버를 따로 설정해야 한다.

데이터베이스 서버 및 클라이언트 라이브러리 동작은 cubrid.conf

파일로 설정하고, 브로커 동작은 cubrid.conf 파일로 설정한다.

그림 4 데이터베이스 서버와 브로커의 설정

자바, PHP 등의 요청을 처리할 수 있는 중간 레이어를 두어서 애플

리케이션이 사용하는 드라이버는 상대적으로 단순한 작업만 처리

하고, 중간 레이어와 DBMS 서버가 요청을 처리하는 형태를 생각

해 볼 수 있다. 이때 드라이버와 DBMS 서버의 중간 레이어 역할을

수행하는 것이 바로 CUBRID 브로커이다.

CUBRID 브로커

브로커는 cub_broker와 cub_cas 두 종류의 프로세스로 구성된다.

cub_cas는 드라이버의 질의 요청에 대해 실제 질의를 처리하는 프

로세스로, 데이터베이스 서버의 클라이언트로서 질의 수행 계획을

만드는 작업을 한다. 드라이버에서 생성한 연결 중 실제 요청을 처

리하는 연결은 cub_cas에 대응된다. 드라이버 연결과 cub_cas 대

응에 대한 자세한 내용은 뒤에서 다룬다.

cub_broker는 드라이버가 새로운 연결을 요청하면 어떤 cub_cas

를 할당할지 결정하고 cub_cas 프로세스를 관리한다. 하나의 cub_

broker는 여러 개의 cub_cas를 관리하며, 사용자 설정에 따라 여

러 개의 cub_broker를 운영할 수 있다.

다음 그림은 JDBC와 같이 브로커를 사용하는 애플리케이션이 연

결을 요청했을 때 프로세스 간의 연결 과정을 나타낸다.

그림 3 연결 요청 시 프로세스 간 연결 과정

libcubridcs.so

cub_cas cub_server

cub_mastercub_broker

database

Page 141: The platform 2011

NHN의 플랫폼과 도구140 The Platform 2011

그림 5 브로커 장애 시 절체예를 들어, 애플리케이션에서 사용할 연결 수를 설정했는데 연결이

부족하여 늘려야 한다고 가정하자. 애플리케이션에서 필요한 연결

요청을 받을 cub_cas가 구동되도록 브로커 설정을 변경하고, 늘어

난 cub_cas가 cub_server에 접속할 수 있도록 cub_server 설정도

변경해야 한다. 이와 같이 양쪽 모두 설정을 변경해야 한다는 점이

복잡하고 어려워 보이지만, 브로커와 데이터베이스 서버를 독립적

으로 운영하면 DBMS를 유연하게 구성할 수 있다는 장점이 있다.

두 번째 단점은 처리 단계가 늘어나기 때문에 성능이 떨어질

수 있다는 것이다. 성능은 응답 시간(response time)과 처리량

(thoughput)으로 측정할 수 있는데, 처리 단계가 늘어나면 응답 시

간도 길어진다. 질의 하나를 수행할 때 응답 시간의 차이는 수 ms

정도지만 JDBC를 이용해서 단일 스레드로 질의를 반복 수행하는

작업을 수행한다면 큰 차이가 생길 수 있다. 응답 시간이 길어지는

가장 큰 이유는 통신 횟수 증가인데, 단일 클라이언트가 아닌 멀티

클라이언트 환경에서는 통신 횟수 증가에 따른 시간 증가가 어느

정도 상쇄된다. 또한 서버 부하가 극단적으로 심한 환경에서는 일

반적으로 서버가 수행하는 기능의 일부를 클라이언트에서 수행하

여 서버의 리소스를 확보할 수 있다. 서버의 리소스를 추가로 확보

할 수 있다는 것은 서버의 처리량을 늘릴 수 있다는 것을 의미한다.

즉, 처리량은 크게 영향을 받지 않으며 오히려 처리량을 더 늘릴 수

도 있다. 요즘은 일반적으로 연결 풀링(connection pooling)과 명

령문 풀링(statement pooling)을 사용하는데, 연결/명령문 풀링을

사용하면 드라이버에서 보내는 요청 자체가 줄기 때문에 2계층 구

조든 3계층 구조든 성능을 크게 개선할 수 있다.

세 번째 단점은 브로커라는 중간 레이어가 있기 때문에 장애 포인

트가 늘어난다는 것이다. CUBRID는 장애를 대비하기 위해 HA

기능을 제공하며 브로커 이중화도 지원한다. 애플리케이션에서

CUBRID에 접속하는 연결 URL은 서버 주소, 포트 번호, 데이터

베이스 이름, 데이터베이스 사용자 이름, 데이터베이스 사용자 비

밀번호로 구성되는데, 해당 IP와 포트로 접속이 실패했을 때 연결

을 시도할 접속 정보를 추가로 설정할 수 있다. 즉, 브로커를 이중

화하여 하나의 브로커에 장애가 발생하면 다른 브로커를 통하여 연

결하도록 설정하여 장애에 대비할 수 있다. 다음 그림은 브로커 장

애 또는 데이터베이스 서버 장애 시 서비스를 지속적으로 유지하는

과정을 나타낸다.

Fail-over�

Page 142: The platform 2011

CUBRID 브로커 이야기 141The Platform 2011

하나만 있으면 어떻게 될까? 두 개의 연결을 C1, C2라 하고 C1이

cub_cas와 대응되어 있다고 가정하자. 이때의 흐름은 다음 그림과

같다.

그림 7 하나의 cub_cas가 두 개의 연결에 대응될 때의 흐름

C2가 연결을 요청하면 cub_broker는 사용 가능한 cub_cas가 있

는지 확인한다. 유일한 cub_cas는 C1에 할당되어 있으므로 cub_

broker는 C1을 해제하고 C2를 cub_cas에 대응시켜야 한다. cub_

broker는 C1의 트랜잭션이 종료된 후 다음 질의를 시작하기 이전

시점에 C1을 해제할 수 있다.

즉, 애플리케이션에서 요청한 연결의 개수보다 cub_cas가 적으면

cub_cas를 공유할 수 있다. 드라이버 입장에서는 트랜잭션 종료 후

자신의 연결이 해제될 수 있다는 것을 인지하고, 연결이 해제되면

다시 연결을 시도한다. 연결 풀링을 사용하고 연결이 풀에서 대기

상태일 때 서버가 재시작되면 앞의 상황과 동일한 것으로 인식하

여 연결을 복구하므로 애플리케이션은 연결의 유효성을 별도로 검

사하지 않아도 된다. 자주 사용하는 연결이 하나의 cub_cas를 두고

경쟁하는 것은 성능을 저하시키므로 적절한 개수의 cub_cas를 구

동하도록 설정해야 한다. 그러나 연결 풀에 생성한 연결 대부분이

잠들어 있다면 연결을 공유하는 것이 효과적일 수 있다.

그림 6 DBMS 장애 시 절체

브로커 사용의 장점

그렇다면 브로커를 사용할 때의 장점을 살펴보자. 첫 번째 장점은

연결을 처리하는 cub_cas를 공유할 수 있다는 점이다. 애플리케이

션이 생성한 연결 중 실제 질의를 처리하는 연결은 cub_cas와 1:1

로 대응된다. 즉 동시에 생성된 연결 개수만큼 cub_cas가 동작해야

한다. 그런데 애플리케이션이 생성한 연결은 2개인데 cub_cas는

Fail-over�

a.out

Execute query

Commit

Connection close

Connection close

Execute query

exception

reconnect

Execute query

Commit

Commit

Execute query

connect

libcubirdcs.so cub_server

Page 143: The platform 2011

NHN의 플랫폼과 도구142 The Platform 2011

String connection_url = "jdbc:cubrid:localhost:53300:testdb:

public::?";

Connection con = DriverManager.getConnection(connection_url,

null, null);

String query = "select * from foo where id = ?";

PreparedStatement pstmt = con.prepareStatement(query);

pstmt.setInt(1, 1);

ResultSet rs = pstmt.executeQuery();

while (rs.next()) {

// fetch result

}

rs.close();

pstmt.close();

con.close();

03/21 18:51:36.467 (0) CLIENT IP 127.0.0.1

03/21 18:51:36.473 (0) connect db cgkdb user public url jdbc

:cubrid:localhost:53300:cgkdb:public::?

03/21 18:51:36.476 (1) prepare 0 select * from foo where id

= ?

03/21 18:51:36.477 (1) prepare srv_h_id 1

03/21 18:51:36.491 (1) execute srv_h_id 1 select * from foo

where id = ?

03/21 18:51:36.491 (1) bind 1 : INT 1

03/21 18:51:36.529 (1) execute 0 tuple 1 time 0.055

03/21 18:51:36.529 (0) auto_commit

03/21 18:51:36.529 (0) auto_commit 0

03/21 18:51:36.529 (0) *** elapsed time 0.052

03/21 18:51:36.537 (0) con_close

03/21 18:51:36.537 (0) disconnect

세 번째 장점은 다양한 브로커 모드를 활용할 수 있다는 것이다.

CUBRID는 HA 기능을 제공한다. 데이터베이스 서버는 active-

standby 구조이며, active 서버에 유입된 데이터는 standby 서버

로 복제되고 active 서버 장애 시 standby 서버로 자동 절체되어

서비스가 지속된다. 데이터의 입력, 삭제, 수정은 active 서버로 유

입되고, active 서버와 standby 서버에서 데이터를 조회할 수 있다.

애플리케이션은 용도에 따라 active 서버에 접근하거나 standby

서버에 접근하도록 모드를 선택할 수 있다. 브로커는 다음과 같은

모드를 제공한다.

예를 들어, 100개의 웹 서버에서 애플리케이션이 실행되고 있고,

각 웹 서버는 연결 풀에 10개의 연결을 가지고 있다고 가정하자. 이

경우 1000개의 연결이 필요하지만, 연결 대부분이 연결 풀에서 대

기 상태라면 데이터베이스 서버는 연결에 대한 부담만 갖는다. 이

런 상황에서 cub_cas를 통해 연결을 공유하면 효율적으로 연결을

관리할 수 있다. 이 점을 활용한 것이 CAS for Oracle/MySQL이

다. 이는 뒤에서 자세히 설명하겠다.

두 번째 장점은 모니터링과 로깅이다. JDBC 등 애플리케이션에서

요청한 모든 질의는 브로커를 통하므로 브로커 상태를 통해 처리

과정의 일부를 파악할 수 있다. 브로커 상태를 모니터링하는 유틸

리티를 이용하여 상태 정보를 확인할 수 있으며, cub_cas 상태 정

보의 의미는 다음과 같다.

• IDLE: 드라이버와 연결되지 않은 상태.

• CLIENT WAIT: 애플리케이션 연결의 트랜잭션이 진행 중이며, 애

플리케이션이 요청을 보내기를 기다리는 상태. 예를 들어 INSERT,

UPDATE 두 개의 질의를 수행한 후 커밋해야 하는데, INSERT 수행이

완료된 후 UPDATE 질의가 요청되기 이전 시점까지는 CLIENT WAIT

상태이다. 이 상태가 지속되면 애플리케이션이 요청을 보내지 못하고

있다는 의미이다.

• CLOSE WAIT: 트랜잭션이 완료된 상태에서 애플리케이션이 다음 질

의를 시작하지 않은 상태. cub_broker가 새로운 연결을 요청받았을 때

IDLE 상태의 cub_cas가 없으면 CLOSE WAIT 상태인 cub_cas의 연

결을 해제하고 새로운 연결을 할당한다.

• BUSY: 데이터베이스 서버로 질의 수행을 요청한 상태. 모니터링 유틸

리티를 이용하면 수행 중인 질의문을 확인할 수 있다.

이 밖에도 브로커가 처리하고 있는 QPS, 오류 개수, 슬로우 쿼리

(slow query) 개수 등의 정보를 알 수 있다.

연결 관련 정보 및 질의 수행에 대한 정보 등 브로커가 수행하는 작

업의 정보는 로그 파일에 기록된다. 다음은 JDBC를 이용하는 간단

한 애플리케이션을 수행했을 때 생성되는 로그의 예이다. 질의 수

행 시각, 바인딩된 값, 수행 시간, 결과 튜플 개수 등에 대한 정보를

얻을 수 있다.

Page 144: The platform 2011

CUBRID 브로커 이야기 143The Platform 2011

브로커 확장

브로커 구조를 활용하여 확장할 수 있는 형태로 다음과 같이 CAS

for Oracle/MySQL, 데이터베이스 sharding, 로드 분산 등을 생

각해 볼 수 있다.

CAS for Oracle/MySQL

CAS for Oracle/MySQL은 cub_cas가 CUBRID에 연결하는 대

신 ORACLE과 MySQL에 연결하여 드라이버의 요청을 처리하는

것으로, CUBRID 드라이버와 브로커를 사용하면서 DBMS로는

Oracle 또는 MySQL을 사용하는 구조이다. 브로커를 사용할 때의

장점을 Oracle/MySQL에서도 활용할 수 있다. 다음은 CAS for

Oracle/MySQL의 구조를 간략하게 나타낸 것이다.

그림 9 CAS for Oracle/MySQL 구조

예를 들어 하나의 웹 서버에서 특정 용도의 연결 풀에 2개의 연결

이 만들어져 있고 이 연결은 대부분 연결 풀에서 대기 상태이며, 이

런 웹 서버 10개가 운영 중인 상태를 가정하자. 이 경우 20개의 연

결이 만들어지지만, 사용 빈도가 높지 않으면 연결은 대부분 잠들

어 있을 것이다. 관리용 연결 풀을 따로 만드는 등 용도별로 연결

풀을 따로 만든다면 이렇게 잠들어 있는 연결은 더욱 많아질 것이

다. 서비스 규모가 커져서 웹 서버 수가 늘어나면 잠들어 있는 연결

수도 비례하여 늘어난다. 이때 브로커가 애플리케이션의 연결을 공

유할 수 있으면 연결 수가 증가하는 것을 막을 수 있게 된다. 웹 서

그림 8 서버 장애 시 브로커 모드에 따른 절체

• read-write: 데이터를 읽고 쓸 수 있는 모드이며 active 서버에 접속

한다.

• read-only: 데이터를 읽기만 하는 모드이며, standby 서버에 접속할

수 있으면 standby 서버에 접속하고 standby 서버에 장애가 발생하면

active 서버에 접속한다. active/standby 서버의 구성이 변경되어도

브로커가 모드에 따라 자동으로 active/standby 서버에 연결하므로 애

플리케이션은 이를 고려하지 않아도 된다.

• slave-only: standby 서버에만 접속하며, standby 서버에 장애가 발

생하면 에러를 반환한다. 이 모드는 standby 서버가 있으면 접속하고

active 서버만 있으면 요청을 처리하지 않아도 되는 경우에 사용한다.

예를 들어 데이터를 확인하기 위해 standby 서버에만 접속해야 하는

경우, 주기적으로 데이터를 수집하지만 단기간 데이터를 수집하지 못해

도 괜찮은 경우 등에는 서비스에 영향을 주지 않기 위해서 slave-only

모드를 사용할 수 있다. standby 서버의 장애가 길어져서 active 서버

에 접속해야 한다면 애플리케이션의 설정 변경 없이 브로커의 모드만

변경하면 된다.

Fail-over�

JDBC Connection

JDBC Connection pool

Page 145: The platform 2011

NHN의 플랫폼과 도구144 The Platform 2011

복제 서버의 개수를 늘릴 때에도 브로커 설정만 변경하면 자동으로

부하가 고르게 분산될 것이다.

데이터베이스 sharding과 로드 분산의 공통점은 질의가 처리되는

시점에 수행할 대상 데이터베이스를 선택해야 한다는 것인데, 3계

층 구조는 이런 형태의 요청에 훨씬 유연하게 대응할 수 있다. 데이

터베이스 sharding과 로드 분산은 현재까지는(2011년 8월) 지원하

지 않는다.

마치면서

지금까지 CUBRID의 브로커에 대해 간략히 살펴보았다. 3계층 구

조는 단점도 있지만, 서버 환경이 복잡해질수록 연결에 대한 유연

성을 확보한다는 측면에서는 매우 유용하다. 구조적인 장점을 살려

독립적인 미들웨어로서 계속 발전하길 기대한다.

버에 설정한 연결 총합이 아닌 실제 DBMS 사용 부하에 맞게 cub_

cas가 구동되도록 설정하면 DBMS 서버 부담을 줄일 수 있다.

데이터베이스 sharding

데이터베이스 규모가 커지면서 장비 하나로 관리할 수 있는 규모

이상의 데이터를 관리해야 하는 경우가 발생할 수 있다. 이때 동일

한 스키마를 가진 여러 개의 데이터베이스에 데이터를 분할해야 한

다. 일반적으로 애플리케이션에서 처리할 때에는 sharding key를

선정하고, 키 값으로 shard를 결정하는 룰을 정하여 데이터베이스

선택한 후 질의를 요청한다. 예를 들어 카페 데이터베이스의 경우

카페 ID가 shard 키가 되고, 해시 함수를 통해 shard를 결정할 수

있다.

브로커에서 이런 sharding 방법을 지원하면, 애플리케이션에서는

질의를 요청할 때 sharding 키값을 함께 전달하고 브로커는 전달

받은 sharding 키를 이용하여 shard를 결정한 후 선택된 shard에

연결하는 방법을 적용할 수 있다. 브로커가 sharding을 지원하면,

shard를 결정하는 로직을 애플리케이션에서 구현할 필요가 없다.

그리고 shard가 늘어나도 브로커만 설정하면 추가된 브로커를 활

용할 수 있으므로 애플리케이션에는 영향이 없다는 장점이 있다.

로드 분산

데이터베이스에 읽기 부하가 커졌을 때 대처 방법 중 하나는 복제

서버를 늘려서 읽기 요청을 분산하는 것이다. CUBRID도 HA 기

능을 이용하여 1:N의 active standby를 지원하며, 읽기 분산을 위

해 별도의 레플리카 서버를 추가로 둘 수 있다. 단순하게 생각하면

슬레이브 서버가 n개 있다고 생각하면 된다. 브로커가 로드를 분산

하면 하나의 브로커가 여러 개의 복제 서버에 대해 부하를 판단하

면서 동적으로 조정할 수 있다.

예를 들어 10개의 연결이 있고, 그 중 5개는 복제 서버 1로 연결되

고 나머지 5개는 복제 서버 2로 연결된다고 가정하자. 각각 5개의

연결이 생성되는 시점에는 각 연결이 얼마나 사용될지 예측할 수

없는데, 연결 풀이 FIFO(First In First Out) 방식으로 동작한다

면 각 연결의 처리량 편차가 커질 수 있으며 각 복제 서버의 처리량

도 큰 차이가 발생할 수 있다. 이때 동적으로 처리량을 판단하여 연

결을 바꿀 수 있다면 각 서버의 처리량을 비슷하게 유지할 수 있고,

Page 146: The platform 2011

위치 정보 플랫폼 nLocation 145The Platform 2011

v1.0과 v1.5의 차이는 영구 저장소 지원 여부이다. nLocation에서

사용하는 자료형을 Unit이라고 부르는데, v1.0에서는 Unit 정보를

메인 메모리에서만 관리하지만 v1.5에서는 필요하다면 영구 저장

소에도 저장할 수 있다.

그림 2 nLocation v1.5에서 추가로 구현된 부분

nLocation은 LBS(Location Based Service)를 위한 플랫폼이다.

웹플랫폼개발랩에서 개발한 nLocation의 핵심 기능은 ‘반경 검색’

이다. 2011년 상반기에 v1.0을 릴리스했고 하반기에 v1.5를 릴리스

할 계획이다.

nLocation v1.0의 주요 기능은 ‘특정 사용자의 위치로부터 반경

M 미터에 있는 N 명의 사용자를 거리순으로 조회하는 것’이다.

그림 1 지정한 반경 안의 사용자를 거리순으로 조회

위치 정보 플랫폼nLocation오라!

벽에는 꿀이 흘러 벽에 혀를 가져다 대고 있으면 배가 고프지 않고,

수도꼭지를 틀면 맥주가 쏟아져 나와 목마르지 않고,

밤새워 노래를 부르고 춤을 추워도 피곤할 리 없는 그곳,

사랑과 꿈이 있는 기선랜드로 오라!

•웹플랫폼개발랩 _ 송기선

위도

경도

nLocation v1.0 구현 영역 nLocation v1.5 구현 영역

Unit

PointUnit PolygonUnit

ImmobilePointUnitMobilePointUnit

User Venue Event

LineStringUnit

Page 147: The platform 2011

NHN의 플랫폼과 도구146 The Platform 2011

다음 그림은 nLocation 클러스터의 아키텍처이다.

그림 4 nLocation 클러스터

BLOC은 NHN에서 개발하여 사용하고 있는 자바 서버 프레임워

크로, 쉽게 RPC 서버를 제작할 수 있게 한다. NIMM 역시 NHN

에서 개발하여 사용하고 있는 메시징 플랫폼이다. 별도의 주소 체

계를 가지고 있으며, 서버-애플리케이션 간에 유니캐스트, 애니캐

스트, 멀티캐스트 송수신을 지원한다.

nLocation 클러스터 안의 nLocation들은 서로 레플리카 관계이

며 멀티 마스터(multi master) 모델을 사용한다. NIMM의 멀티캐

스트를 이용해 클러스터 안의 모든 nLocation에 Unit 정보 갱신

이 동기화되므로, 어느 하나에서 데이터가 갱신되면 클러스터 안의

다른 nLocation도 같이 갱신된다. 특별한 장애가 없다면 클러스터

안의 모든 nLocation은 같은 정보를 갖고 있기 때문에, NIMM의

애니캐스트를 이용하거나 L4를 이용하여 Nearest Neighbor 검색

을 할 때 어느 nLocation에 Unit 정보 조회를 요청해도 결과는 같

을 것이다.

이렇게 클러스터를 구성하면 내결함성(fault tolerance)을 보장할

수는 있지만 확장성은 저하된다. 따라서 nLocation은 ‘계층적으로

확장할 수 있는 구조’를 채택했다. 다음 그림은 미투데이(me2day)

를 예로 들어 계층 구조를 나타낸 것으로, 실제 적용 사례는 아니

다.

사용자는 자유롭게 이동하지만, 항상 자신의 위치를 알리고 싶어하

는 것은 아니다. 예를 들어 사용자가 어떤 베뉴(venue) 또는 위치에

서 LBS 애플리케이션에 체크인하거나 포스팅하면 해당 사용자가

그 시각에는 그곳에 있었다는 것을 알 수 있다. 하지만 곧바로 다른

곳으로 이동했을 수도 있다. 물론 LBS 애플리케이션이 항상 사용

자의 위치를 매우 짧은 간격으로 nLocation에 전달할 수도 있지만

사용자가 원하는 일은 아닐 것이다. 그래서 nLocation에서는 사용

자가 베뉴에서 체크인하면 일정 시간 동안은 해당 베뉴에 있다고

간주한다. 이 '일정 시간(timeout)'은 LBS 애플리케이션이 서비스

특성에 따라 지정할 수 있다.

앞에서 언급했듯이 v1.0에는 영구 저장 기능이 없다. 그러나 사용

자의 위치가 갱신되지 않고 일정 시간이 지나면 더 이상 해당 위치

에 존재하지 않는다고 판단하므로 사용자 위치 반경 검색 기능에는

문제가 없다.

v1.5에서는 영구 지정 기능을 활용하여 사용자의 위치뿐만 아니라

음식점 같은 지역 정보, 쿠폰 등 반경 정보를 이용한 다양한 기능을

지원할 수 있다.

nLocation 아키텍처

nLocation을 사용하는 서비스 부서에서는 nLocation을 관리 운

영할 필요가 없다. PaaS(Platform as a Service) 같은 방식이라

고 생각하면 쉽다. nLocation은 일종의 RPC 서버로, NHN 표준

RPC 프로토콜을 사용하여 API를 호출한다.

그림 3 nLocation 배치

NHN표준 RPC프로토콜

User

User

User LBS nLocation

L4

nLocation Cluster

NIMM

nLocation

BLOC BLOC

nLocation

Page 148: The platform 2011

위치 정보 플랫폼 nLocation 147The Platform 2011

nLocation에서 다루는 데이터 구조에 적합했기 때문이다. 물론

nLocation이 업데이트되면서 점(사용자 위치)이 아닌 선(도로 등),

다각형(좀 더 정밀한 행정 구역)을 처리할 필요가 생긴다면 다른 공

간 인덱스를 사용하게 될 수도 있다.

쿼드트리는 논리적 공간을 4분할하면서 트리를 키워 나가는 자료

구조이다. 예를 들어 한 노드에 정보를 두 개까지 저장할 수 있다고

가정해 보자. 한 노드에 정보를 세 개 저장해야 하는 상황이 생기면

다음 그림처럼 트리가 분할된다.

그림 6 쿼드트리 분할 과정

그 결과 트리 구조는 다음 그림처럼 각 노드는 자식 노드가 없거나,

자식 노드가 있다면 4개인 형태로 확장된다.

그림 7 쿼드트리의 예

그림 5 nLocation 클러스터 계층 구조의 예

사용자 클러스터(user cluster)에는 현재 로그인한 사용자들의 위

치 정보가 있다. 포스트 클러스터(post cluster)에는 사용자들이 작

성한 글의 위치 정보가 있다. 그리고 미투데이 클러스터(me2day

cluster)에는 사용자 위치 정보나 글의 위치 정보 외의 다른 미투

데이 정보가 있다. 이와 같은 방식으로 데이터를 분할하면(data

partitioning) 확장성을 높일 수 있다.

만약 미투데이 클러스터의 자식 클러스터로 foo라는 클러스터가

생긴다면 미투데이 클러스터에 있는 me2day.foo 정보는 foo 클러

스터로 복제하고, 완전히 복제한 후에는 미투데이 클러스터 노드

자체에는 me2day.foo 정보가 없게 한다.

위 그림에서는 LBS 도메인이 다르면 다른 클러스터로 구성하고 있

지만, 실제로는 한 클러스터에 여러 LBS 도메인을 담을 수 있다. 사

용자가 적으면 한 클러스터에 여러 도메인을 담고, 사용자가 많으

면 도메일별로 클러스터를 구성할 수 있다.

쿼드트리

nLocation은 일종의 공간 데이터베이스(spatial database)이다.

물론 기존의 공간 RDBMS와는 기능과 구조가 많이 다르다. 범용

적인 GIS 처리를 위한 공간 RDBMS와 달리 nLocation은 인터넷

LBS에 맞게 제작했기 때문이다.

RDBMS에서 사용하는 B 트리 계열은 1차원 정보만 인덱싱할 수

있다. 위치 정보는 최소 2차원 이상의 정보가 필요하기 때문에, 공

간 정보를 다룰 수 있는 공간 인덱스(spatial index)가 필요하다.

nLocation에서는 공간 인덱스로 쿼드트리(Quadtree)를 사용한

다. 쿼드트리 외에도 kD 트리, R 트리, LSH(Locality Sensitive

Hashing) 등을 검토했다. 그 중에서 쿼드트리를 선택한 이유는

Root Cluster

Hangame Cluster

user Cluster

me2day Cluster Blog Cluster

post Cluster

신규 추가

분할

Page 149: The platform 2011

NHN의 플랫폼과 도구148 The Platform 2011

호출한 API에 따라 다르지만 3만 TPS 이상의 성능을 보이고 있다.

만약 타임아웃이 5분이라면 nLocatoin 한 대가 최대 900만 명(5 x

60 x 30000)의 동시 사용자를 처리할 수 있다는 의미이다.

nLocation 서버를 늘려도 locate 성능이 크게 좋아지지 않는 것

은 네트워크 대역폭 때문이다. nLocation 서버를 동기화하기 위

해 locate API가 호출될 때마다 클러스터에 동일한 메시지가 전달

된다. 따라서 nLocation 자체의 처리 능력이 뛰어나면 네트워크가

병목 현상의 원인이 될 수 있다. 반면 NN 검색은 일정 정도까지는

서버 확장의 효과가 나타나는데, 이는 NN 검색이 애니캐스트 메

시징을 사용하기 때문이다. 따라서 대역폭이 부족하지 않다면 한

클러스터 안에서도 확장성이 보장된다. 하지만 앞에서 설명했듯이

nLocation 클러스터는 내결함성을 위한 것이지 확장성을 위한 것

은 아니다. 확장성을 위해서는 nLocation 클러스터를 트리 구조로

구성해야 한다.

nLocation Unit

nLocation에서 사용하는 자료형인 Unit은 Key-Value 구조이다.

Key는 ‘serviceCode/serviceID’로 이루어진 문자열이고, value는

arbitrary type이다.

그림 8 Unit의 구조

이러한 Unit 구조를 사용하면 다음 그림과 같이 2차원 평면 상의

다양한 대상(사용자, 베뉴, 좌표)을 나타낼 수 있다.

쿼드트리는 상대적으로 구현하기 쉽고, 삭제와 삽입이 빈번하게 발

생할 때 다른 공간 인덱스에 비해 성능이 좋다. 또한 멀티스레드 프

로그래밍에 유리하다.

반면 쿼드트리는 균형(balancing) 문제가 약점인데, 각 노드가 관

리하는 Unit의 개수를 적절히 크게 하면 트리 레벨을 줄일 수 있으

므로 크게 문제되진 않는다. 다만 각 노드가 관리하는 Unit이 일정

개수일 때까지는 순차 검색(sequential search)이 다른 방식에 비

해 빠르다가 일정 개수를 넘으면 더 느려지게 되므로 적절한 개수

를 설정해야 한다.

실제 인구 분포(강남역, 홍대 등 상대적으로 좁은 지역에 모바일 기

기 사용자 집중)를 고려하여 테스트했을 때 우려할 만큼의 균형 문

제는 없었으며, 성능에도 문제가 발생하지 않았다.

nLocation v1.0 성능

nLocation v1.0의 성능을 알아보기 위해서 1 Gbit LAN,

NimmMsgServer 2대 환경에서 nLocation 서버를 늘려가면서 다

양한 API를 호출하여 TPS(transactions per second)를 측정했다.

‘locate only’는 사용자의 위치를 알리기만 한 것이다. NN 검색

(Nearest Neighbors Search)은 1km 내의 사용자를 조회한 것이

다. LBS 특성에 따라 다르지만 일반적으로 locate 대 NN 비율이

N:1로 나타날 것이다.

Key (String type) Value (arbitrarv)

'serviceCode/serviceID'

PK: 유일성 보장 위치정보, 중복 가능

Mandatory Optional

double x;

double y;

...

Page 150: The platform 2011

위치 정보 플랫폼 nLocation 149The Platform 2011

그림 9 Unit 구조로 표현할 수 있는 대상

nLocation v1.5 소개

nLocation v1.5는 하반기에 출시할 계획이며, 영구 저장소를 지원

하고 LBS를 위한 본격적인 기능을 제공한다. 그 결과 특정 지역에

서 작성된 글을 조회하거나, 반경 M 미터 안의 식당을 평점순으로

조회하거나, 특정 베뉴에 어떤 사용자들이 있는지 조회하는 기능을

제공할 수 있게 된다.

nLocation v1.5에 추가된 기능을 정리하면 다음과 같다.

• 영구 저장소 지원: Operation Log와 Unit 정보를 저장할 수 있다.

Unit 정보를 유실하지 않고 지속적으로 관리할 수 있으며 복제가 용이

해진다.

• Unit Importer: 베뉴 정보는 지속적으로 변경되므로, 베뉴 정보가

nLocation을 사용하는 LBS에 매일 반영되도록 한다.

• 조건 검색 지원: Unit의 value를 검색할 수 있다. 예를 들면 내가 가입한

카페의 회원을 일정 반경 안에서 검색하거나, 일정 횟수 이상 포스팅된

베뉴를 일정 반경 안에서 검색할 수 있게 된다.

• 거리 외의 정렬 기준 지원: 거리순 정렬(오름차순)뿐만 아니라, value의

특정 필드값을 기준으로 오름/내림차순 정렬을 지원한다.

• 검색 결과 페이징: 검색 결과가 많을 때 일정 개수 단위로 조회할 수 있다.

• 지역 정보: 지역 정보 반경 검색 기능을 제공하며, 사용자가 자신의 위치

한 베뉴에서 체크인할 수 있게 한다. 지역 정보를 관리하는 별도의 API

도 제공한다.

nLocation v1.5부터는 본격적으로 LBS를 위한 플랫폼으로서의

면모를 갖추었다고 할 수 있다. NHN에서 훌륭한 LBS를 만들 수

있는 기반이 될 수 있도록 많은 노력을 들이고 있다.

Page 151: The platform 2011

NHN의 플랫폼과 도구150 The Platform 2011

데이터베이스 테스트 가이드

테스트 데이터베이스와 스키마, 데이터는 어떻게 준비하고 유지할

까?

SQL을 검증하기 위해서는 테스트 코드에서 사용할 ‘테스트 데이

터베이스’와 ‘테스트 데이터’를 준비해야 한다. 이 과정은 ‘데이터베

이스 설치’와 ‘스키마 생성’, ‘데이터 입력’으로 나눌 수 있다.

일반적으로는 빌드 자동화와 관계 없이 수동으로 빌드 전에 테스트

데이터베이스를 설치하여 사용한다. 그런데 HyperSQL과 같은 임

베디드 데이터베이스를 사용하면 애플리케이션 빌드 중 JVM이 구

동될 때마다 데이터베이스를 생성하도록 설정할 수 있다. 이 방법

은 ANSI SQL만을 사용하는 특수한 상황에서만 사용할 수 있다.

그러나 다른 개발자의 작업에 영향을 미치지 않고 마음대로 테스트

데이터를 변경할 수 있다는 이점이 있다.

NHN 개발팀에서는 보통 개발용, QA용, 운영용 데이터베이스를

각각 구성한다. 그리고 일부 개발팀은 데이터베이스를 더 세분화하

여 구성하기도 하는데, Scott W Ambler는 그의 저서 “Refactoring

DATABASE”에서 이를 샌드박스 개념으로 설명했다.

자바 코드는 테스트 코드로 검증하기 쉽지만, SQL 호출까지 연

결된 모듈은 테스트를 자동화하기 어려워서 답답했던 경험이 있

을 것이다. 또한 테스트하기 쉽도록 데이터베이스를 구성하고

DAO(Data Access Object) 코드를 작성하고 싶지만 어떤 방식을

적용해야 할지 고민해 본 경험도 있을 것이다.

이렇게 데이터베이스를 사용할 때 어려운 점을 해결하는 방법을 찾

기 위해 포털개발본부에 SQL TF를 만들었다. SQL TF의 활동 결

과 산출물은 다음과 같다.

• 통합 데이터베이스 관리 시스템: SQL 검수, 스키마 관리 요청 등의 프로

세스를 지원하는 사내 시스템

• 데이터베이스 테스트 가이드

• SQL의 코딩 컨벤션 가이드

• Coverage4iBatis: iBatis의 XML 파일의 테스트 커버리지를 측정하는

도구

이 글에서는 데이터베이스 테스트 가이드 문서에서 다루고 있는 사

안들과 Coverage4iBatis를 소개하고자 한다.

데이터베이스 테스트 기법과Coverage4iBatis생산성혁신랩에서 네이버의 서비스를 개발하는 팀을 지원하는 일을 합니다.

더 편하고 재미있게 개발하는 데 도움을 주는 기법, 라이브러리, 도구를

전파하려고 노력 중입니다.

NHN에서 애자일 소프트웨어 개발을 전파하고 사람들을 멘토링합니다.

방법론, 프로세스, 도구는 결국 사람들이 편하게 일할 수 있도록 도와주는 것, 그

이상 그 이하도 아니라고 생각합니다.

•생산성혁신랩 _ 정상혁

•생산성혁신랩 _ 황상철

Page 152: The platform 2011

데이터베이스 테스트 기법과 Coverage4iBatis 151The Platform 2011

대형 시스템에서 동시에 여러 개의 개선

프로젝트를 진행할 때 하나의 테스트 데

이터베이스만 사용하면 한 프로젝트의

스키마 변경이 다른 프로젝트에 영향을

미칠 수 있다. 한 프로젝트에서 컬럼을

변경하고 그에 대한 소스를 수정하면 그

소스가 다른 프로젝트의 브랜치에 합쳐

지기 전까지는 테스트 빌드가 깨질 수 있

다. 통합 빌드와 개발자 빌드가 같은 데

이터베이스를 사용하면 테스트 실행 전

쟁(test run war)으로 빌드가 불규칙적

으로 깨지는 현상이 발생하기도 한다. 예

를 들면 통합 테스트에서 쿼리를 수행하

여 테이블에 잠금(lock)이 설정되었는

데, 마침 그때 개발자 PC나 커밋 빌드에

서 같은 테이블을 조회하는 테스트를 실

행하여 타임아웃이 발생할 수도 있다.

여러 개의 개발용 데이터베이스를 사용

하는 경우에 데이터베이스 간 스키마 일

치 작업을 자동화할 때에도 Maven의 C5 migration plugin과 같

은 도구를 활용할 수 있다. 카페서비스개발팀에서는 Maven의 C5

migration plugin을 커스터마이징해서 통합 빌드용 데이터베이스

와 테스트용 데이터베이스 등의 스키마를 일치시키는 작업을 자동

으로 하고 있다. 이 방식을 사용함으로써 스키마 수정이 버전 관리

되어 언제 누가 변경했는지 추적할 수 있으며, 물리적 데이터베이

스에 반복적으로 스키마 변경을 반영하는 작업이 줄어들었다.

스키마의 생성과 관리 방안이 마련되었으므로 이제 테스트 데이터

를 입력하고 유지할 방안이 필요하다. 테스트 데이터를 빌드 스크

립트에서 입력할지 테스트 코드에서 입력할지, 그리고 테스트가 끝

난 후에는 어떻게 그 전의 상태로 데이터베이스를 되돌릴지 정해야

한다.

테스트 데이터를 입력할 때는 Maven의 SQL plugin을 포함한 여

러 도구를 사용한다. 각 플러그인이 지원하는 기능을 정리하면 다

음 표와 같다..

그림 1 데이터베이스 샌드박스 구성

NHN에서는 2009년 위키북 프로젝트라는 사내 프로젝트에서

PC마다 데이터베이스를 설치해서 개발자별로 데이터베이스 샌

드박스를 구축했다. 이 프로젝트에서는 Maven의 C5 migration

plugin을 이용하여 스키마 생성과 변경을 Maven 빌드 과정에 포

함시켰다. 이 플러그인은 SQL 파일을 실행한 이력을 남겨서 반복

적으로 데이터베이스를 초기화하지 않고도 여러 데이터베이스의

스키마를 일치시켜 준다.

Ruby 개발 환경에서는 개발자 PC마다 데이터베이스를 설치하고

스키마를 일치시켜주는 프랙티스가 권장되고 이를 지원하는 프레

임워크가 많다. 미투데이서비스개발팀에서는 Ruby의 빌드 도구인

Rake를 이용해서 한 개발자가 의도한 스키마 변경이 다른 개발자

데이터베이스에도 반영되도록 하고 있다.

하나의 애플리케이션이 여러 개의 데이터베이스와 엮여 있으면 개

발자별로 데이터베이스를 설치하기가 쉽지 않을 수 있다. 개발자마

다 하나의 데이터베이스를 구축하지는 못하더라도, 여러 용도의 테

스트 데이터베이스를 따로 구축하는 것만으로도 여러 목적의 테스

트를 독립적으로 수행할 때 유용하다.

Highly IterativeDevelopment

Project-LevelTesting

System andAcceptance

Testing

Operations andSupport

DevelopmentSandbox Production

DemoSandbox

pre-Production

Test/QASandbox

ProjectIntegrationSandbox

BrokenBulld

BugReports

Software ProblemReports(SPRs)

ControlledDeployment

FrequentDeployment

Highly-Controlled

Deployment

Page 153: The platform 2011

NHN의 플랫폼과 도구152 The Platform 2011

로 메서드를 추출하는 하는 방식이다. 다음은 INSERT 문을 검증

하는 코드의 예제이다.

@Test

public void bookShoudBeInserted() {

// given

String isbn13 = "9788991428003";

Book input = createBook(isbn13);

// when

int affectedRow = dao.insert(input);

// then

assertThat(affectedRow, is(1));

Book selected = selectByIsbn13(isbn13);

assertBookEquals(input, selected);

}

SELECT 문을 검증할 때 given 절에서 데이터 입력이 있다

면 Spring JDBC의 SimpleJdbcInsert, SimpleJdbcTemplate

등이 유용하며, 기존 DAO의 INSERT를 활용하기도 한다.

SimpleJdbcInsert를 활용한 예제는 다음과 같다. 이 클래스에서는

INSERT 문을 자동으로 만들어서 데이터베이스에 입력한다.

private void insert(Book input) {

SimpleJdbcInsert insertAction = new

SimpleJdbcInsert(dataSource).withTableName("book");

insertAction.execute(new BeanPropertySqlParameterSource(

input));

}

SQL 유형에 따라 검증 전략도 달라진다. INSERT 문이나

UPDATE 문은 name parameter가 많아서 오타에 취약하다. 그래

서 되도록 많은 필드를 assert 조건에 포함시켜야 실수를 빨리 발견

할 수 있다.

가장 검증하기 어려운 부분은 동적 쿼리다. iBatis의 동적 쿼리 표

현식은 XML로 if나 for loop와 같은 조건 분기를 프로그래밍한다.

다음은 iBatis 동적 쿼리를 사용하는 예이다.

표 1 플러그인별 지원 기능

외부 파일에

데이터 정의

자바 코드

데이터 정의

빌드 과정

단독 실행

테스트 케이스

실행

Maven SQL plugin O X O X

Ant SQL Task O X O X

DBUnit O X O O

C5 migration plugin O X O O

Spring JDBC O O X O

Spring test O O X O

외부 파일에 정의된 데이터를 입력하는 방식은 많은 데이터를 한꺼

번에 입력하기에는 편리하다. 하지만 만약 특정 데이터의 검증 조

건이 테스트 코드 안에 들어간다면, 데이터를 선언한 곳과 데이터

를 검증하는 곳의 거리가 멀어진다는 단점이 있다.

테스트 후에 데이터를 원래 상태로 되돌리기 위해서는 트랜잭션 롤

백을 가장 많이 사용한다. 테스트에 Spring의 트랜잭션이 적용된

코드를 사용했다면 추가로 코딩할 필요가 거의 없다는 장점이 있

다. 그리고 트랜잭션 격리 수준이 read commited 이상이면, 같은

공용 개발 장비를 여러 곳에서 공유해도 동시성으로 인해 서로 다

른 테스트가 영향받는 일이 발생하지 않는다. 그러나 UI 테스트 같

이 웹 컨테이너 밖에서 호출하는 테스트에는 사용할 수 없으며, 개

발자별 데이터베이스 샌드박스가 없는 경우에는 격리 수준에 따라

테스트 실행 전쟁(test run war)이 발생하기도 한다. 트랜잭션 롤

백이 어려운 상황에서는 테스트 실행 후에 삽입된 데이터를 지우는

쿼리를 수행해야 한다.

어떻게 검증할까

이제 테스트 코드를 작성할 차례이다. BDD(Behavior Driven

Development)에서 권장하는 given, when, then 형식과 유사하게

테스트 준비 부분, 실행 부분, 검증 부분을 구분하면 깔끔한 테스트

코드를 작성하는 데 도움이 된다. 이때 준비 부분과 검증 부분은 논

리적인 대칭성이 있으면 좋다. 즉 given 절에 선언된 객체가 then

절에서 검증에 쓰이거나, given 절에서 객체 생성에 메서드가 추출

된 부분이 있다면 then 절에서 검증하는 부분도 같은 논리적 단위

Page 154: The platform 2011

데이터베이스 테스트 기법과 Coverage4iBatis 153The Platform 2011

그림 2 Coverage4iBatis 동작 과정

Q3. 로그 측정을 위한 준비란 구체적으로 무엇인가?

SQL 실행 로그를 남기기 위해서 SQLMap 파일에 들어있는 쿼리

문에 특정 id 값을 설정하는 것이다. 이 작업을 Coverage4iBatis

에서는 Instrumenting이라고 부르는데 Instrumenting이 끝나면

SQLMap.xml 파일은 다음과 같이 변경된다.

<select id="count" resultClass="int">

/* CoverageID : 1 */

SELECT count(*) FROM name

</select>

Q4. 그럼 테스트는 어떤 식으로 진행하나?

Maven을 이용하여 기존 테스트케이스를 실행하며, 테스트가 끝

나면 커버리지 측정을 위한 로그 파일(coverage4ibatis.log)이 생

성된다. 생성된 로그 파일에는 다음과 같이 CoverageID가 남는데,

이 값을 이용해 커버리지를 측정한다.

<isEqual property="allWorkStatusSelected"

compareValue="false">

<isNotNull property="workStatusCodeList">

<iterate prepend=" AND pwd.work_stat_cd

property="workStatusCodeList " open="(" close=")"

conjunction=",">#workStatusCodeList[]#

</iterate>

</isNotNull>

</isEqual>

이렇게 iBatis XML로 선언된 조건 분기는 Java 같은 프로그

래밍 언어와 달리 커버리지를 측정할 수 없기 때문에 모든 로

직을 테스트했는지 확인하기 어렵다. 그래서 SQL TF에서는

Coverage4iBatis라는 도구를 만들었다.

Coverage4iBatis

Coverage4iBatis는 iBatis를 사용할 때 작성하는 SQL 파일

(xxxSQLMap.xml)의 코드 커버리지를 측정하는 도구이다.

XML 파일 내 SQL 문이 얼마나 실행되었는지를 라인 커버리지로

보여주며, 동적 쿼리에 대해서는 동적으로 분기되는 부분이 실행되

었는지 여부까지 보여준다. Coverage4iBatis가 어떤 도구인지 10

문 10답의 형식으로 정리했다.

Q1. Coverage4iBatis란 무엇인가?

Coverage4iBatis는 iBatis를 사용해 SQL을 처리하는 애플리케

이션에서 SQL 커버리지를 측정하는 도구이다. 생산성혁신랩에서

Java를 이용해 2011년 2월에 개발했다.

Q2. 어떤 방식으로 동작하나?

다른 커버리지 측정 도구와 유사하게 실행 로그를 분석하여

커버리지를 측정한다. 인터페이스로 Maven을 지원하는데

Coverage4iBatis Maven 플러그인이 하는 일은 크게 다음과 같다.

• 로그 측정을 위한 준비(Instrumenting)

• 테스트(Testing)

• 보고서 생성(Reporting)

Page 155: The platform 2011

NHN의 플랫폼과 도구154 The Platform 2011

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/

log4j/'>

<appender name="coverage4ibatis" class="org.apache.

log4j.FileAppender">

<param name="Append" value="false" />

<param name="File" value="target/sql.log" />

<layout class="org.apache.log4j.PatternLayout">

<param name="ConversionPattern" value="%m%n" />

</layout>

</appender>

<logger name="java.sql.PreparedStatement"

additivity="false">

<level value="DEBUG" />

<appender-ref ref="console" />

</logger>

<logger name="java.sql.Connection" additivity="false">

<level value="DEBUG" />

<appender-ref ref="coverage4ibatis" />

</logger>

</log4j:configuration>

Created connection 24962279.

{conn-100000} Connection

{conn-100000} Preparing Statement: /* CoverageID : 100

*/ CREATE TABLE name (id INT NOT NULL PRIMARY KEY, name

VARCHAR(50))

Returned connection 24962279 to pool.

Q5. 보고서는 어떤 형태인가?

커버리지 리포트는 Cobertura 형식을 지원한다. 허드슨에

Cobertura 플러그인을 설치하면 트렌드와 각 SQLMap 코드 라

인별로 커버된 영역을 확인할 수 있다. 현재 유효한 것은 라인 커버

리지뿐이다. 보고서는 다음 그림과 같으며 맨 앞에 숫자는 호출된

횟수를 의미한다(그림 3).

Q6. Coverage4iBatis를 사용하려면 어떤 준비가 필요한가?

SQL 문이 실행되었는지 확인하기 위해 로그를 남겨야 한다. iBatis

는 내부적으로 log4j 로거를 사용하기 때문에 아래와 같이 로거를

추가하면 특정 쿼리 ID를 갖는 SQL문이 실행되었는지 확인할 수

있다. 코드 5와 같이 설정하면 target/sql.log 파일에 로그가 남는다.

그림 3 Coverage4iBatis 수행 결과 보고서

Page 156: The platform 2011

데이터베이스 테스트 기법과 Coverage4iBatis 155The Platform 2011

Q9. 툴에 버그는 없나?

치명적인 버그는 아직 발견되지 않았으며 일부 적용하면서 발견한

버그는 현재 수정하고 있다.

Q10. Coverage4iBatis를 당장 사용하고 싶은데 어떻게 사용할

수 있나?

NHN 사내에서는 관련 리소스를 모두 공개하고 있다. 추후에 충

분히 안정되어 외부 개발자들에게 유용할 것이라고 생각되면 외부

공개를 검토할 예정이다.

데이터베이스 테스트를 고민하는 단계는?

지금까지 데이터베이스 테스트에 유용한 도구와 방법에 대해서 이

야기했지만, 이런 도구와 방법을 사용한다고 해도 데이터베이스와

SQL 테스트는 여전히 어렵고 번거로운 일이다. 데이터베이스는

애플리케이션 외부의 구성 요소이기 때문에 쉽게 해결할 수 없는

근본적인 어려움이 있다.

테스트하기 어렵다면, 이는 구조 개선이 필요하다는 냄새(smell)이

다. 그렇다면 데이터베이스를 최대한 사용하지 않는 것이 좋은 구

조일까? 저장소가 없으면 애플리케이션의 기능을 제대로 사용할

수도 없으니 말도 안 되는 이야기이다. 데이터베이스를 사용하면서

도 테스트하기 쉬운 구조를 만들려면, 데이터베이스가 저장소의 역

할에만 충실하고, 검증해야 할 핵심 로직은 애플리케이션에 존재하

게 해야 한다. 즉, 테스트해야 할 로직은 SQL보다는 Java 같은 프

로그램 코드에 존재해야 한다. Domain Driven Design이나 Ports

and Adapters 패턴에서도 비즈니스 로직을 애플리케이션 내부에

녹이는 구조를 강조한다. 데이터베이스는 아무리 가까워도 외부 자

원이다.

아래 SQL 문을 예로 들어보자.

SELECT

CASE edit_status

WHEN 'E' THEN user_score * 3

WHEN 'R' THEN user_score * 2

ELSE 0

END AS prioty

FROM notes

WHERE note_id = 3

Maven POM 파일에 coverage4ibatis를 설정한다. 다음과 같이

별도의 profile로 plugin 선언을 지정하는 것을 권장한다.

<profile>

<id>dev</id>

<build>

<plugins>

<plugin>

<groupId>com.nhncorp.coverage4ibatis</groupId>

<artifactId>maven-coverage4ibatis-plugin</

artifactId>

<version>0.1-SNAPSHOT</version>

<configuration>

<includes>

<include>com/nhncorp/cms/</include>

</includes>

<excludes>

<exclude>book/dao/LegacyBook.xml</exclude>

<exclude>book/dao/LegacyAuthor.xml</

exclude>

</excludes>

</configuration>

</plugin>

</plugins>

</build>

</profile>

Q7. 허드슨에서 사용할 수 없나?

허드슨 빌드 스텝에 아래와 같이 coverage4ibatis 플러그인을 프로

파일과 같이 설정하면 된다.

mvn coverage4ibatis:instrumentation test

coverage4ibatis:report -Pdev

Q8. 현재 어떤 서비스에서 사용하고 있는가?

현재 파일럿으로 적용된 서비스는 메인서비스개발팀의 프로젝트와

BDS 관리 시스템, 금융 서비스의 일부 프로젝트다. 이 밖에도 블로

그와 카페 서비스 일부에 적용할 예정이다.

Page 157: The platform 2011

NHN의 플랫폼과 도구156 The Platform 2011

마치면서

NHN의 생산성혁신랩에서는 테스트하기 쉬운 소프트웨어를 만드

는 기법을 계속 발굴하고 있다. NHN 내의 데이터베이스 테스트

사례 발굴이나 Coverage4iBatis 개발도 그런 활동의 일환이다. 특

정 기법이나 하나의 도구가 극적으로 테스트를 쉽게 해줄 것이라고

기대하지는 않는다. 다만 다양한 현장의 시도와 경험을 공유하다

보면, 이를 발판 삼아 새로운 기법을 시도해 볼 사람들도 많아질 것

이다. 앞으로 사내외의 다양한 실무 사례들이 더 많이 공유되길 바

란다.

위 SQL 문은 edit_status와 user_score라는 컬럼 2개로 prioty라

는 속성을 계산하는 코드이다. 위에서 edit_status값이 ‘E’일 때와

‘R’일 때를 검증하려면 그런 테스트 데이터를 INSERT 문으로 삽

입해야 한다. 그리고 ‘E’나 ‘R’같은 코드값이 무엇을 의미하는지는

주석 외에는 표현하기 힘들다.

위 기능을 Java로 작성했다면 메서드 파라미터로 바로 원하는 값을

넘겨서 검증하고, 코드값도 의미 있는 이름을 붙여서 상수나 enum

으로 선언했을 것이다. 처음에는 SQL 문 안에 로직을 넣는 방식이

편할지라도 갈수록 테스트와 코드 추적에 짐이 된다.

데이터베이스 테스트의 궁극적인 해법은 데이터베이스에서 테스

트할 것이 되도록 없게 하는 것이다. 최대한 SQL에서 하는 일을 줄

이고 테스트하기 쉬운 SQL을 만들어야 한다. 가능한 한 많은 일은

프로그래밍 언어로 수행하고 SQL은 최대한 단순하게 만든다. 여

러 테이블을 조인하는 쿼리를 단순한 쿼리 여러 개로 바꾸면 코드

가독성이나 재활용성이 훨씬 좋아지기도 한다. Coverage4iBatis가

있지만 동적 쿼리도 가능한 한 적게 사용하는 것이 좋다. Java 코

드에서 로직 분기를 하는 편이 더 테스트하기 쉬우며 동적 쿼리는

SQL 문을 검수할 때에도 어려움이 있다.

물론 성능은 고려해야 한다. WHERE 절에서 조회 대상을 제한하

는 부분까지 Java 코드의 if 문으로 구현하면 성능 손해가 너무 크

다. 조인하지 않으면 SQL 실행 횟수가 너무 많아지는 경우도 있다.

그런 경우를 제외해도 문자열 더하기, 계산식, DECODE나 CASE

WHEN 문 등 쉽게 Java 코드로 옮길 수 있는 부분은 많다.

단순한 SQL을 만들려고 노력하는 것은 자원 활용 측면에서도 의

미가 있다. 최근에는 웹 애플리케이션도 RDBMS뿐만 아니라 캐

시, NoSQL 저장소, 검색 인덱스 등 다양한 저장소를 활용하지

만, 가장 많이 접근하는 저장소는 RDBMS이고 최종 데이터는

RDBMS에 저장하기 때문에 RDBMS를 우선하여 자료 구조를 설

계해야 한다. 그러나 RDBMS는 확장하는 데 비용이 많이 들기 때

문에 다른 저장소를 함께 활용하여 효율적으로 사용해야 한다. 예

를 들어, 여러 테이블을 조인한 쿼리의 결과를 캐시 저장소에 저장

한다면, 조인한 테이블 중 하나라도 데이터가 바뀌었을 때 그 캐시

는 만료시켜야 한다. 쿼리를 단순하게 사용하면 캐시를 적용하기에

도 유리하다. 그리고 앞에서 언급한 CASE WHEN 같은 로직으로

인한 부하는 사소하지만, SQL에서 이와 같은 로직을 제거하면 그

사소한 부하라도 없앨 수 있다.

Page 158: The platform 2011

maven-precheck-plugin, 배포 시 장애를 감지하는 방법 157The Platform 2011

사례 4. 서비스에 새로운 페이지가 추가로 개발되었다. 운영 서버

배포 후 추가된 페이지가 문제 없이 동작하는 것을 확인했는데, 자

주 사용하지 않던 기존 페이지에서 500 에러가 발생했다. 알고 보

니 에러가 발생한 기존 페이지가 새로 추가된 페이지와 동일한

WebWork Package 이름을 사용하고 있어서 발생한 문제였다.

위 사례들의 공통점은 운영 서버에 배포한 후에야 문제가 발견되었

다는 것, 그리고 운영 서버에 배포하기 전에는 쉽게 인지할 수 없는

문제들이었다는 것이다. 이런 일은 꽤 빈번하게 발생하며, 문제를

알고 있는 개발자가 사전에 확인하는 것을 제외하면 마땅한 재발

방지 대책 또한 없다.

운영 서버에 배포하기 전에 문제를 미리 알 수는 없을까?

문제를 미리 알 수는 없더라도 이러한 문제에 대한 검사를 기계적

으로 자동화하여, 문제가 있다면 배포되지 않도록 할 수는 없을까?

여기에서 소개하는 maven-precheck-plugin은 바로 그 고민의

결과물이다.

maven-precheck-plugin은 배포 시 장애 요인을 사전 탐지해 운

영 시에 장애가 발생하지 않도록 도움을 주는 메이븐(Maven) 플

러그인이며, 현재 오픈소스(GPLv3)로도 공개하고 있다.

서비스 배포 후 종종 발생하곤 했던 문제들

사례 1. Staging QA(운영 배포 전 운영 환경과 동일한 환경에서 진

행하는 QA)까지 마치고 운영 서버에 배포했는데, 이미지가 표시되

지 않는다는 고객 문의가 접수되었다. 서둘러 롤백하고 코드를 살

펴보니 사내 개발 서버를 참조하는 이미지 파일이 있었다.

사례 2. 테스트하기 위해 잠깐 개발 DB(혹은 API)를 보도록 바꿔

놓았는데, 수정되지 않은 채로 운영 서버에 배포되고 말았다. 배포

하고 한참 후에 데이터가 이상한 것을 발견하고 롤백했다.

사례 3. 커스텀 태그를 복사해서 사용했는데, 복사한 커스텀 태그의

선언부도 같이 가져오는 것을 잊어버리는 바람에 정상적으로 동작

하지 않고 그대로 페이지 소스에 노출되고 말았다.

maven-precheck-plugin,배포 시 장애를 감지하는 방법2008년에 NHN에 입사하였으며, 현재 금융서비스개발팀에서

네이버 금융 서비스를 개발하고 있습니다.

평소 뚝딱뚝딱하면서 무언가를 만드는 게 취미이며,

요새는 node.js에 관심이 있습니다.

•금융서비스개발팀 _ 남재현

Page 159: The platform 2011

NHN의 플랫폼과 도구158 The Platform 2011

이미지 파일이나 CSS, JS 파일이 svn 서버나 개발 서버를 참조하고

있지는 않은지, deprecate된 DB 서버나 API를 참조하고 있지는

않은지 확인하고 싶을 때 위의 기능을 사용할 수 있다.

WebWork 설정 파일 Validation 체크

WebWork의 설정 파일에서 동일한 Package 설정 내에 action 이

름이 중복되면, 서버 구동 시에는 오류가 발생하지 않지만 실제로

해당 action을 호출했을 때 런타임 예외(runtime exception)가

발생한다. 이 경우는 QA나 개발자가 사전에 발견하기 어렵지 않

다. 문제는 Package 이름(Java의 Package가 아닌 WebWork의

Package Tag) 중복이다.

Package 이름이 중복되면 런타임 예외조차 발생하지 않는다.

WebWork는 Package 이름이 중복되었을 경우 나중에 선언된

Package 설정만 유효하게 읽어 들인다. 만약 신규 페이지를 중복

된 Package 이름으로 선언했는데 그 설정이 기존 페이지보다 뒤에

존재한다면, 새로 추가된 페이지는 잘 동작하지만 기존에 있던 페

이지는 접근할 수 없게 된다. 아래의 예를 들어 설명해 보겠다.

<Package name="com.naver.xxx" namespace="/legacy">

<action name="legacyPage" Class="..." />

</Package>

<Package name="com.naver.xxx" namespace="/new">

<action name="newPage" Class="..." />

</Package>

이 경우 /new/newPage.nhn에는 접근할 수 있지만, /legacy/

legacyPage.nhn에 접근하면 존재하지 않는 액션이라는 예외가 발

생한다. 물론 회귀테스트 때 발견할 수도 있지만, 평소에는 잘 호출

하지 않는 페이지이거나 외부 연동에만 사용하는 API인 경우에는

발견하기 쉽지 않다.

maven-precheck-plugin의 webwork goal은 위의 상황을 미리

알 수 있게 해준다.

maven-precheck-plugin

maven-precheck-plugin은 이름으로도 알 수 있듯이 메이븐

빌드에서 사용할 수 있는 플러그인이며, 문제가 발견되었을 때

BuildFailureException을 발생시켜 빌드 실패를 유도한다.

이 플러그인은 다음 네 가지 기능을 제공한다.

금칙어 체크

소스 파일에 정규표현식과 대응되는 문자열이 존재하는지 체크한다.

존재하면 해당 파일명, 라인, 문자열을 출력하고 빌드가 실패한다.

다음은 web 디렉터리 안의 모든 파일에 "http://svn", "http://

local", "http://dev"라는 문자열이 포함되었는지 점검하는 설정이다.

<build>

<plugin>

<groupId>org.openwebtop.maven.plugins</groupId>

<artifactId>maven-precheck-plugin</artifactId>

<executions>

<execution>

<id>prohibittext</id>

<phase>compile</phase>

<goals>

<goal>prohibittext</goal>

</goals>

</execution>

</executions>

<configuration>

<prohibitTexts>

<prohibitText>

<basedir>web</basedir>

<includes>

<include>**/**</include>

</includes>

<prohibitTextPatterns>

<prohibitTextPattern><![CDATA[([^/])*http://

(svn|local|dev|test)\.(.*)]]></prohibitTextPattern>

</prohibitTextPatterns>

</prohibitText>

</prohibitTexts>

</configuration>

</plugin>

</build>

Page 160: The platform 2011

maven-precheck-plugin, 배포 시 장애를 감지하는 방법 159The Platform 2011

<build>

<plugins>

<plugin>

<groupId>org.openwebtop.maven.plugins</groupId>

<artifactId>maven-precheck-plugin</artifactId>

<executions>

<execution>

<id>customtag</id>

<phase>validate</phase>

<goals>

<goal>customtag</goal>

</goals>

</execution>

</executions>

<configuration>

<customTag>

<basedir>web</basedir>

<includes>

<include>**/*.jsp</include>

</includes>

</customTag>

</configuration>

</plugin>

</plugins>

</build>

build phase별 설정 파일 상호 체크

대부분의 서비스에서는 build phase별 배포를 위해 phase별로 디

렉터리나 filter 파일을 사용하고 있을 것이다. BDS 기준으로 예

를 들면 build phase를 dev, test, real로 나눠 볼 수 있는데, 이 경

우 각각 참조하는 DB 서버나 API 또는 에러 문자열이 서로 다르다.

만약 어떤 이유에서든 real build phase가 dev, test에서 사용하는

DB 서버나 API를 참조하도록 설정된 채 운영 서버에 배포된다면

상황은 걷잡을 수 없는 장애로 치닫게 된다.

운영 서버에서 개발 DB나 API에 접근할 수 없도록 ACL이 잘 정

비된 서비스라면 staging 단계에서 미리 오류를 발견하고 수정할

수 있고 이미 배포한 뒤에라도 장애를 빨리 발견할 수 있지만, 그렇

지 않다면 발견할 때까지 걸리는 시간이 길어져 상황이 더 심각해

진다. 이런 경우 maven-precheck-plugin의 environment goal

을 사용하면 dev, test의 리소스를 real에서 참조하고 있는지 미리

체크할 수 있다.

<build>

<plugins>

<plugin>

<groupId>org.openwebtop.maven.plugins</groupId>

<artifactId>maven-precheck-plugin</artifactId>

<executions>

<execution>

<id>webwork</id>

<phase>compile</phase>

<goals>

<goal>webwork</goal>

</goals>

</execution>

</executions>

<configuration>

<webworkConfiguration>

<basedir>src/main/resources/common</basedir>

<includes>

<include>xwork*.xml</include>

</include>

</webworkConfiguration>

</configuration>

</plugin>

</plugins>

</build>

단, WebWork 설정 파일에서 다른 설정 파일을 포함시키는 태그

인 import는 maven-precheck-plugin이 인식할 수 없으니 위 설

정의 include 요소에 설정 파일을 수동으로 추가해야 한다.

JSP에서 선언되지 않은 채로 사용된 커스텀 태그 체크

커스텀 태그를 선언하지 않고 사용하면 쉽게 발견할 수 없다. 이런

실수는 주로 코드를 복사해서 그대로 사용하면서 커스텀 태그의 선

언부는 복사하지 않아서 종종 발생한다. 선언되지 않은 커스텀 태

그의 경우에는 페이지에 접근했을 때 런타임 예외조차 발생하지 않

기 때문에 개발자가 실수를 발견하기 어렵다. 잘 보이는 부분이 이

상해지면 쉽게 발견할 수 있지만, JSTL의 if 문이나 choose 문 깊

숙한 곳에 존재한다면 발견하지 못하고 놓치는 경우가 대부분이다.

maven-precheck-plugin의 customtag goal을 사용하면 위와 같

은 상황을 예방할 수 있다. 설정 방법은 다음와 같다.

Page 161: The platform 2011

NHN의 플랫폼과 도구160 The Platform 2011

</configuration>

</plugin>

</plugins>

</build>

checkTarget은 검사 대상 파일(예: real build phase의 설정 파일)

을 설정하는 부분이고, checkSource는 checkTarget과 배타적인

관계에 있는 파일(예: dev/test build phase의 설정 파일)을 설정하

는 부분이다. 이 두 설정에 해당되는 파일에서 extractRegExp의

정규표현식을 통해 추출된 문자열을 비교하여 checkSource에 존

재하는 문자열이 checkTarget에서 발견되면 빌드 실패를 유도한

다. dev/test/real에서 참조하는 리소스가 확실하게 구분되어 있다

면 거의 100% 사전에 예방할 수 있다.

기능이 비슷한 도구

maven-precheck-plugin의 금칙어 체크 기능과 유사한 기능이

있는 도구가 있어서 같이 소개한다. 바로 허드슨 플러그인인 Find

Text Hudson Plugin이다(http://wiki.hudson-ci.org/display/

HUDSON/Text-finder+Plugin).

이 도구는 허드슨을 사용해야 한다는 점과 검출할 금칙어의 정

규표현식을 한 줄로 써야 한다는 제약 사항을 제외하면 maven-

precheck-plugin과 유사한 기능을 제공한다. 따라서 메이븐을 적

용하지 않은 서비스에서는 이 허드슨 플러그인을 활용하면 좋을 것

이다.

메이븐이 적용된 프로젝트에서는 maven-precheck-plugin을 사

용하면 허드슨뿐만 아니라 BDS의 빌드 단계에서도 사용할 수 있

다. 이 경우 BDS에서 배포 자체를 실행할 수 없는 상태가 되므로

문제가 있는 코드의 운영 서버 배포를 기계적으로 막을 수 있다.

다음은 src/main/resources의 local, dev, test 디렉터리의 설정 파

일에서 사용된 IP를 real 디렉터리 설정 파일이 참조하고 있는지 확

인하는 설정의 예이다.

<build>

<plugins>

<plugin>

<groupId>org.openwebtop.maven.plugins</groupId>

<artifactId>maven-precheck-plugin</artifactId>

<executions>

<execution>

<id>environment</id>

<phase>validate</phase>

<goals>

<goal>prohibittext</goal>

</goals>

</execution>

</executions>

<configuration>

<environments>

<environment>

<description>CheckIPAddress</description>

<extractRegExp><![CDATA[\b(?:\d{1,3}\.)

{3}\d{1,3}\b]]></extractRegExp>

<checkTarget>

<basedir>src/main/resources/real</basedir>

<includes>

<include>**/**</include>

</includes>

<excludes>

<exclude>**/.svn/**</exclude>

</excludes>

</checkTarget>

<checkSource>

<basedir>src/main/resources</basedir>

<includes>

<include>local/**/**</include>

<include>test/**/**</include>

<include>dev/**/**</include>

</includes>

<excludes>

<exclude>**/.svn/**</exclude>

</excludes>

</checkSource>

</environment>

</environments>

Page 162: The platform 2011

maven-precheck-plugin, 배포 시 장애를 감지하는 방법 161The Platform 2011

마무리

maven-precheck-plugin이 위의 실수들을 미리 방지하긴 하지만

모든 실수를 찾아내는 마법의 도구는 아니다. 다만 위 기능을 충분

히 활용한다면 완전 무결한 장애 탐지까지는 아니더라도 장애로 연

결될 수 있는 상황의 상당수는 예방할 수 있을 것이다.

이 플러그인은 현재 http://code.google.com/p/maven-

precheck-plugin/에서 오픈소스(GPLv3)로 개발하고 있으며,

Maven central repository에서 배포하고 있으므로 NHN의 사내

프로젝트가 아니더라도 적용할 수 있다.

Page 163: The platform 2011

NHN의 플랫폼과 도구162 The Platform 2011

그림 1 배포 전 서버와 배포 후 서버

예를 들어 웹 애플리케이션을 배포하는 중에 사용자가 웹 페이지에

접속하여 HTML 파일은 A 서버(배포 후)에서 가져오고 CSS 파일

은 B 서버(배포 전)에서 가져오면 새로운 HTML 파일과 기존 CSS

파일을 사용하게 된다. 그러면 페이지가 제대로 보이지 않거나 오

동작이 발생할 수 있다.

이런 문제를 방지하려면 새로 배포하는 파일의 이름을 변경하여 기

웹 애플리케이션을 잘 개발하는 것 못지 않게 잘 배포하는 것도 중

요하다. 어떻게 배포하느냐에 따라 장애를 줄일 수도 있으며, 사용

자가 성능 향상을 체감하게 할 수도 있다. 여기에서는 지식쇼핑 서

비스에서 사용하는 정적 파일 배포 방법을 소개한다.

정적 파일 배포의 어려움

CSS 파일이나 자바스크립트 파일과 같은 정적 파일은 브라우저에

캐시되도록 하면 사용자 입장에서는 속도 향상을 체감할 수 있고

서버는 리소스 부담을 줄일 수 있다. 그런데 정적 파일의 내용이 변

경되면 브라우저가 캐시된 기존 파일을 사용하지 않고 새로운 파

일을 내려받도록 해야 한다. 가장 많이 사용하는 방법은 common.

css?t=20110405처럼 정적 파일의 이름에 쿼리스트링 형태로 타임

스탬프를 추가하는 것이다. 이 타임스탬프 값으로는 빌드 시각이나

웹 애플리케이션 서버의 시작 시각, 또는 저장소의 리비전 번호를

사용할 수 있다. 그런데 이때 다음과 같은 문제가 발생할 수 있다.

메이븐을 이용한정적 파일 배포"아는 것이 힘이다(scientia est potentia)" - 프란시스 베이컨.

익숙한 것에 머무르지 않고, 더 나은 것에 대한 동경으로,

무지(無知)의 두려움으로부터 헤어나기 위해,

이제 겨우 한 발자국씩 떼고 있습니다.

•쇼핑서비스개발팀 _ 오영은

A 서버(배포 후) B 서버(배포 전))

commons.css

page.nhn

commons.css

page.nhn

브라우저

Page 164: The platform 2011

메이븐을 이용한 정적 파일 배포 163The Platform 2011

# deflate 모듈 추가

LoadModule deflate_module modules/mod_deflate.so

# 압축 레벨

DeflateCompressionLevel 1

# MIME type에 따른 압축 설정

AddOutputFilterByType DEFLATE text/html text/plain text/css

text/xml text/javascript

# MIME type 추가

AddType text/javascript .js

AddType text/css .css

그리고 브라우저가 정적 파일을 캐시할 수 있도록 expire 모듈도

추가한다.

# expires 모듈 추가

LoadModule expires_module modules/mod_expires.so

ExpiresActive On

# 기본 expires 설정을 1년으로

ExpiresDefault "access plus 1 years"

정적 파일 최적화 Q&A

다음과 같이 자바스크립트 파일을 저장할 위치를 /web/js라고 가

정하고 웹 페이지에서 사용할 main.js 파일과 detail.js 파일을 /

web/js 디렉터리 아래에 둔다고 가정하자. 버전 관리를 위해서 각

자바스크립트 파일에는 버전을 표시한다(예: main_v1.js, detail_

v1.js).

그림 2 디렉터리 구조

존 HTML 파일은 기존 CSS 파일을 참조하고 새로운 HTML 파

일은 새로운 CSS 파일을 참조하도록 해야 한다. 그리고 웹 서버 장

비를 여러 대 사용할 때에는 일부 서버에만 CSS 파일이나 자바스

크립트 파일이 배포되는 경우를 막기 위해서 정적 파일을 미리 배

포하기도 한다.

위 방법을 사용하면 정적 파일을 배포할 때 발생하는 대부분의 문

제점을 예방할 수 있지만, 웹 페이지에서 참조하는 정적 파일의 개

수가 많으면 성능을 저하시킬 수 있다. 파일을 캐시하지 않은 경우

에는 파일을 매번 내려받아야 하고, 캐시한 경우에는 매번 서버에

접속해서 "304 Not Modified"를 수신하기 때문에 DNS resolve

비용과 소켓 연결 비용이 발생한다. 파일을 합쳐서 파일의 개수를

줄여도 불필요한 공백 문자나 긴 변수 이름 등으로 인해 많은 데이

터를 전송해야 한다. 특히 웹 서버에 정적 파일을 배포하면 HTTP

요청 메시지에 포함되는 쿠키 역시 전송되므로 요청 메시지의 크기

도 커진다.

모든 개발 과정에서 작성하는 정적 파일을 마치 한 명이 작성한 것

처럼 체계적으로 관리할 수 있다면 좋겠지만 관리 비용 때문에 오

히려 비효율적일 수도 있다. 따라서 배포 과정을 최대한 자동화하

는 것을 지향하며, 자동적이고 효율적인 배포를 위해 다음과 같은

규칙을 지켜야 한다.

• 타임스탬프 값을 쿼리스트링에 추가한다.

• CSS 파일과 자바스크립트 파일의 버전을 관리한다.

• 파일의 개수를 줄인다.

• 파일 내용을 최적화(minify)한다.

• CSS 파일과 자바스크립트 파일을 cookie free domain으로 제공한다.

아파치 웹 서버 설정을 이용한 최적화

아파치 웹 서버의 deflate 모듈을 사용해서 정적 파일을 압축하여

전송한다. 아파치 설정 파일에 다음과 같이 deflate 모듈 설정을 추

가한다.

Page 165: The platform 2011

NHN의 플랫폼과 도구164 The Platform 2011

개발 중에는 웹 애플리케이션개발 중에는 devOnlyUrl을 사용하고

개발 완료 후에는 가장 버전이 높은 main.js 파일을 사용하도록 설

정한다. 물론, 개발 중 여부에 대한 정보는 설정할 수 있어야 한다.

Q4. 쿠키를 사용하지 않는 도메인에서 CSS 파일이나 자바스크립

트 파일을 제공할 때 문제는 없는가?

자바스크립트 파일 내에서 사용자 쿠키 값을 이용하는 경우, 해당

자바스크립트 파일은 웹 서버 내의 경로에 존재해야 한다. 이를 위

해 커스텀 태그에서 추가로 needCookie와 같은 설정이 필요하다.

<link:js prefix="/js/cookie.js" needCookie="true" />

지금까지 정적 파일 버전 관리와 함께 개발 중인 파일에 대한 기술

방법에 대해서 이야기했다. 이제 파일의 개수를 줄이기 위해 main.

js 파일과 detail.js 파일을 묶어 하나의 core.js 파일을 생성하는 방

법을 알아보자. 설정 파일은 다음과 같이 작성한다.

<?xml version="1.0" encoding="UTF-8"?>

<compression>

<file target=”/js/core.js”>

<include>/js/main</include>

<include>/js/detail</include>

</file>

</compression>

Q5. 생성된 파일은 어떻게 버전을 관리하나?

main_v1.js 파일과 detail_v1.js 파일을 묶어 core_v1.js 파일을 생

성했다면, main_v1.js 파일과 detail_v1.js 파일에 변경 사항이 없

으면 core_v1.js라는 이름을 유지해야 한다. main_v1.js 파일이나

detail_v1.js 파일의 내용이 변경되거나 파일의 버전이 변경되면

core_v1.js 파일도 core_v2.js 파일로 변경해야 한다. 이를 위해서

는 생성할 파일을 구성하고 있는 각 파일의 이름, 크기, checksum

등의 정보를 관리해야 한다.

Q1. 각 웹 페이지에서 버전과 타임스탬프를 기술하지 않고 어떻게

자바스크립트 파일을 추가하는가?

웹 애플리케이션을 로드할 때 /web/js를 포함한 하위 디렉터리 파

일들을 스캔하여, 가장 버전이 높은 파일의 목록을 생성한 뒤, 다음

과 같이 커스텀 태그를 이용하여 파일의 경로를 표시한다.

<link:js prefix="/js/main.js"/>

다음은 실제로 HTML에 출력되는 내용이다.

<script type="text/javascript" src="/js/main_

v1.js?t=2011040512"></script>

만약 하위 호환이 되지 않는 새로운 내용을 추가하거나 변경하려면

버전을 올린다(예: main_v1.js � main_v2.js).

Q2. 이렇게 하면, 배포 시에 문제는 없나?

배포 후 웹 서버에서는 새로운 웹 페이지가 새로운 자바스크립트

파일(main_v2.js)에 연결되고, 배포 전 웹 서버에서는 기존 웹 페

이지가 기존 자바스크립트 파일(main_v1.js)에 연결된다. 정적 파

일이 미리 배포되었다면, 모든 웹 서버에는 main_v1.js 파일과

main_v2.js 파일이 존재하므로, 배포 중이라 하더라도 문제가 발생

하지 않는다.

Q3. 웹 페이지 개발 기간에는 정적 파일 역시 개발 버전(외부

URL)을 사용하게 되는데 이런 경우는 어떻게 처리하는가?

다음과 같이 커스텀 태그를 수정하여 개발 용도의 URL을 기술

한다.

<link:js prefix="/js/main.js" devOnlyUrl="http://svn.

nhndesign.com/service/main_20110404.js" />

Page 166: The platform 2011

메이븐을 이용한 정적 파일 배포 165The Platform 2011

BDS를 이용한 배포

NHN에서는 자체적으로 개발한 BDS라는 빌드 배포 시스템을 사

용하여 웹 애플리케이션을 배포한다. 다음 그림은 기존 배포 방식

을 간략하게 나타낸 것이다.

그림 3 BDS를 이용한 배포

다음 그림은 기존 배포 방식에 CSS 파일과 자바스크립트 파일 최

적화 과정을 추가한 것이다.

그림 4 CSS, 자바스크립트 최적화를 위한 배포

1. 압축 파일 생성: 메이븐을 이용하여 빌드할 때, compression.xml 파일

을 읽어 압축 파일을 생성한다.

2. 버전 파일 생성: compression.xml 파일에 기술된 정보를 이용하

여 버전 파일을 생성한 뒤, 버전 파일 저장소(pom.xml에 기술된

repository)에 저장한다.

3. CSS, 자바스크립트 파일 복사: BDS의 PostScript 기능을 이용하여

CSS 파일과 자바스크립트 파일을 CDN 또는 cookie free domain 서

버에 복사한다.

version=1

encoding=UTF-8

file./js/main_v1.js=945,UTF-

8,ac3fecacd0a6ecf74a0c711180582a8c

file./js/detail_v1.js=22572,UTF-8,ee2565f0938bfd1baa21c73afa

633c81

이제 합친 파일을 최적화(minify)할 차례이다. 최적화와 관련

된 라이브러리는 많지만 여기에서는 가장 많이 사용하는 YUI

Compressor를 사용한다.

Q6. 파일 생성이나 최적화는 런타임에 수행할 것인가, 빌드 타임

에 수행할 것인가?

정적 파일을 CDN 또는 별도의 서버에서 제공하려면 빌드 타임

에 수행하는 것을 권장한다. 이를 위해서 메이븐 플러그인 형태로

파일 생성 및 최적화 기능을 구현했다. 다음은 pom.xml 파일의

예이다.

<build>

<plugins>

<plugin>

<groupId>com.naver.shopping.maven</groupId>

<artifactId>maven-compress-plugin</artifactId>

<version>1.0.12</version>

<configuration>

<webDirectory>web</webDirectory>

<fileEncoding>UTF-8</fileEncoding>

<configLocation>web/WEB-INF/compression.xml</

configLocation>

<repository>http://xxx.shopping.naver.net/static/</

repository>

</configuration>

</plugin>

</plugins>

</build>

Page 167: The platform 2011

NHN의 플랫폼과 도구166 The Platform 2011

<script type="text/javascript" src="http://static.shopping.

naver.net/f/js/ lib_v1.js"></script>

<script type="text/javascript" src="/js/ac_v1.js"></script>

<script type="text/javascript" src="http://static.shopping.

naver.net/f/js/ notexists_v2.js?t=2011040512"></script>

/js/notexists.js 파일은 compression.xml 파일에 정의되어 있지

않은 파일이다. 이런 경우에는 /js 디렉터리 아래 notexists로 시작

하는 가장 높은 버전의 파일의 이름과 타임스탬프 값을 생성하여

반환한다.

마치면서

지금까지 간략하게나마 지식쇼핑 서비스에서 사용하는 정적 파일

배포 방법을 설명했다. 이런 방식으로 정적 파일을 관리하는 것은

크게 새로운 방법은 아니다. 그러나 사내에서 담당자끼리만 공유하

던 내용을 이렇게 지면에 실은 것을 계기로 많은 사람들이 나은 방

법을 모색하는 기회가 되었으면 한다.

설정 및 사용 방법

1. compression.xml 파일을 생성하여 web/WEB-INF 디렉터리에 저장

한다.

2. Lucy를 사용하는 웹 프로젝트는 다음과 같이 Lucy 플러그인을 설정

한다.

<lucy:plug-in name="compress" class="com.naver.shopping.

common.compress.web.CompressPlugIn">

<lucy:param name="configLocation" value="/WEB-INF/

compression.xml"/>

<lucy:param name="devOnly" value="false"/>

<lucy:param name="baseUrl" value="http://static.shopping.

naver.net/f"/>

</lucy:plug-in>

Spring MVC 기반의 웹 프로젝트는 다음과 같이 Bean을 등록한

다.

<bean class="com.naver.shopping.common.compress.web.

CompressBean">

<property name="configLocation" value="/WEB-INF/compression.

xml"/>

<property name="devOnly" value="false"/>

<property name="baseUrl" value="http://static.shopping.

naver.net/f"/>

</bean>

3. pom.xml 파일에 메이븐 플러그인 설정을 추가한다.

4. jsp 파일에 다음과 같이 기술한다.

<link:js prefix="/js/lib.js"/>

<link:js prefix="/js/ac.js" needCookie="true"/>

<link:js prefix="/js/notexists.js"/>

위와 같이 기술한 내용은 다음과 같이 변환되어, 정적 파일 중 쿠키

가 필요한 자바스크립트 파일은 웹 서버에서 서비스하고, 그렇지

않은 CSS, 자바스크립트 파일은 cookie free domain 또는 CDN

에서 서비스한다.

Page 168: The platform 2011

메이븐을 이용한 정적 파일 배포 167The Platform 2011

기술개발동향

JVM Internal 168

JDK 7 183

구르믈 버서난 달처럼 207

왜 NoSQL인가? 220

NoSQL 가용성과 운영 안정성 226

성능향상을 위한 SQL 작성법 231

Velocity 2011, 속도혁신 최근 동향 238

Git vs. Mercurial 244

iOS 플랫폼 소개 251

Page 169: The platform 2011

기술개발 동향168 The Platform 2011

JVM의 특징은 다음과 같다.

• 스택 기반의 가상 머신: 대표적인 컴퓨터 아키텍처인 인텔 x86 아키텍

처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는

데 비해 JVM은 스택 기반으로 동작한다.

• 심볼릭 레퍼런스: 기본 자료형(primitive data type)을 제외한 모든 타

입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가

아니라 심볼릭 레퍼런스를 통해 참조한다.

• 가비지 컬렉션(garbage collection): 클래스 인스턴스는 사용자 코드에

의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.

• 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장: C/C++ 등의 전

통적인 언어는 플랫폼에 따라 int 형의 크기가 변한다. JVM은 기본 자

료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한

다.

• 네트워크 바이트 오더(network byte order): 자바 클래스 파일은 네트

워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔

디안이나, RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플

랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야 하므로 네트

워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용

한다. 네트워크 바이트 오더는 빅 엔디안이다.

자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드

가 JRE 위에서 동작한다는 사실을 잘 알고 있다. 이 JRE에서 가장

중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java

Virtual Machine)이다. JRE는 자바 API와 JVM으로 구성되며,

JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)

를 통해 읽어 들여서 자바 API와 함께 실행하는 것이다.

가상 머신

가상 머신(virtual machine)이란 여러 가지로 정의할 수 있지만,

프로그램을 실행하기 위해 물리적 머신(즉, 컴퓨터)와 유사한 머

신을 소프트웨어로 구현한 것을 말한다고 할 수 있다. 지금은 비록

빛이 바랜 목표이긴 하나 자바는 원래 WORA(Write Once Run

Anywhere)를 구현하기 위해 물리적인 머신과 별개의 가상 머신을

기반으로 동작하도록 설계되었다. 그래서 자바 바이트코드를 실행

하고자 하는 모든 하드웨어에 JVM을 동작시킴으로써 자바 실행

코드를 변경하지 않고도 모든 종류의 하드웨어에서 동작되게 한 것

이다.

JVM Internal영화 <코드명 J>의 원작 소설 <Johnny Mnemonic>이 발표된 것은 1981년입니다.

그때는 대량의 데이터를 전달하려면 사람의 두뇌를 써야 할 것이라고

저자인 윌리엄 깁슨은 생각했죠.

30년이 지난 지금 사람의 머리는 두뇌대신 손톱보다 작고

훨씬 대용량인 메모리 카드를 만들어 냈습니다.

IT 기술이 이렇게 빠르게 발전하고 그만큼 데이터양도 늘어나고 있지만,

메모리 카드와 비교도 안 되게 좁은 저의 두뇌에는

아직도 채워야 할 내용이 많은 것 같습니다.

그래서 언제나 무언가를 채우기 위해 돌아다니고 두리번거립니다.

•메시징플랫폼개발팀 _ 박세훈

Page 170: The platform 2011

JVM Internal 169The Platform 2011

업데이트된 라이브러리 소스코드와 원래 소스코드는 다음과 같다.

// UserAdmin.java - 업데이트된 소스코드

public User addUser(String userName) {

User user = new User(userName);

User prevUser = userMap.put(userName, user);

return prevUser;

}

// UserAdmin.java - 원래 소스코드

public void addUser(String userName) {

User user = new User(userName);

userMap.put(userName, user);

}

즉, 반환값이 없던 addUser() 메서드가 User 클래스 인스턴스

를 반환하는 메서드로 변경되었다. 그러나, 애플리케이션 코드는

addUser() 메서드의 반환값을 사용하지 않으므로 변경하지 않았

다. 보기에는 com.nhn.user.UserAdmin.addUser() 메서드는 여

전히 존재하는 것 같은데 왜 NoSuchMethodError가 발생할까?

원인

애플리케이션 코드를 새로운 라이브러리로 다시 컴파일하지 않았

기 때문이다. 즉, 애플리케이션 코드는 우리가 보기에는 반환값과

무관하게 메서드를 호출하고 있는 것 같지만, 실제로 컴파일된 클

래스 파일은 반환값까지 지정된 메서드를 지칭한다.

이는 다음 오류 메시지를 살펴보면 확실히 알 수 있다.

java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.

addUser(Ljava/lang/String;)V

NoSuchMethodError는 "com .nhn .user .UserAdmin .

addUser(Ljava/lang/String;)V"라는 메서드를 찾지 못해서 발생

했다. 여기서 관심을 가질 부분은 "Ljava/lang/String;"과 마지막

의 "V"이다. 자바 바이트코드의 표현에서 "L<classname>;"은 클래

스 인스턴스이다. 즉, addUser() 메서드는 java/lang/String 객체

자바는 썬 마이크로시스템스가 개발했지만, JVM 명세(The Java

Virtual Machine Specification)를 따르기만 하면 어떤 벤더든

JVM을 개발하여 제공할 수 있다. 따라서 대표적인 오라클 핫스팟

JVM 외에도 IBM JVM을 비롯한 다양한 JVM이 존재한다. 안드

로이드 스마트폰에 기본 탑재된 Dalvik VM은 JVM이긴 하지만

JVM 명세를 따르지는 않는다. 스택 머신인 다른 JVM과는 달리

Dalvik VM은 레지스터 머신이며, 따라서 독자적인 툴을 이용해

자바 바이트코드를 Dalvik VM용의 레지스터 기반 명령어 코드로

변환한다.

자바 바이트코드

WORA를 구현하기 위해 JVM은 사용자 언어인 자바와 기계어 사

이의 중간 언어인 자바 바이트코드를 사용한다. 이 자바 바이트코

드가 자바 코드를 배포하는 가장 작은 단위이다.

자바 바이트코드에 대해 설명하기 전에 한 가지 사례를 살펴 보자.

이 사례는 개발 과정에서 실제로 발생한 것을 변경, 요약한 것이다.

현상

원래 잘 동작하던 애플리케이션이 라이브러리 업데이트 이후로 다

음과 같은 오류를 내고 동작하지 않는다.

Exception in thread "main" java.lang.NoSuchMethodError: com.

nhn.user.UserAdmin.addUser(Ljava/lang/String;)V

at com.nhn.service.UserService.add(UserService.java:14)

at com.nhn.service.UserService.main(UserService.java:19)

애플리케이션 코드는 변경하지 않았으며 다음과 같다.

// UserService.java

public void add(String userName) {

admin.addUser(userName);

}

Page 171: The platform 2011

기술개발 동향170 The Platform 2011

public void add(java.lang.String);

Code:

0: aload_0

1: getfield #15; //Field admin:Lcom/nhn/user/

UserAdmin;

4: aload_1

5: invokevirtual #23; //Method com/nhn/user/

UserAdmin.addUser:(Ljava/lang/String;)V

8: return

이 결과물에서 addUser() 메서드를 호출하는 부분은 4번째 줄

인 "5: invokevirtual #23;"이다. 이는 23번 인덱스에 해당하는 메

서드를 호출하라는 의미이며, 23번 인덱스의 메서드를 javap 프

로그램이 친절하게 주석으로 달아주었다. invokevirtual은 자

바 바이트코드에서 메서드를 호출하는 가장 기본적인 명령어

의 OpCode(operation code)이다. 참고로, 자바 바이트코드

에서 메서드를 호출하는 명령어 OpCode는 invokeinterface,

invokespecial, invokestatic, invokevirtual의 4가지가 있으며 각

각의 의미는 다음과 같다.

• invokeinterface: 인터페이스 메서드 호출

• invokespecial: 생성자, private 메서드, 슈퍼 클래스의 메서드 호출

• invokestatic: static 메서드 호출

• invokevirtual: 인스턴스 메서드 호출

자바 바이트코드의 명령어는 OpCode와 피연산자(Operand)로

분리할 수 있으며, invokevirtual과 같은 OpCode는 2바이트의 피

연산자를 필요로 한다.

위의 애플리케이션 코드를 업데이트된 라이브러리로 다시 컴파일

하여 다시 역어셈블하면 다음 결과를 얻을 수 있다.

public void add(java.lang.String);

Code:

0: aload_0

1: getfield #15; //Field admin:Lcom/nhn/user/

UserAdmin;

4: aload_1

하나를 파라미터로 받는 메서드이다. 이 사례의 라이브러리에서는

파라미터가 변경되지 않았으므로 이 파라미터는 정상이다. 위 메시

지 마지막의 "V"는 메서드의 반환값을 나타낸다. 자바 바이트코드

표현에서 "V"는 반환값이 없음을 의미한다. 즉, 위 오류 메시지는

java.lang.String 객체 1개를 파라미터로 받고 반환값은 없는 com.

nhn.user.UserAdmin.addUser라는 메서드를 찾지 못했다는 의미

이다.

애플리케이션 코드는 이전 라이브러리로 컴파일되었으므로, "V"

를 반환하는 메서드를 호출하도록 class 파일에 기록되어 있지만,

새로 변경된 라이브러리에서 "V"를 반환하는 메서드는 없어지고,

"Lcom/nhn/user/User;"를 반환하는 메서드가 추가되었기 때문에

NoSuchMethodError가 발생한 것이다.

참고

오류 자체는 새로운 라이브러리를 다시 컴파일하지 않았기 때문에 발생

한 것이지만, 이 사례에서는 라이브러리 제공자의 잘못이 크다고 할 수

있다. public으로 공개된 메서드의 반환값이 없다가 User 클래스 인스턴

스를 반환하도록 변경된 것이므로, 이것은 명백한 메서드 시그너처 변경

이다. 즉, 라이브러리의 하위 호환성이 깨진 것이므로 라이브러리 제공자

는 메서드가 변경되었다는 것을 반드시 사용자에게 알렸어야 한다.

다시 자바 바이트코드 자체 이야기로 되돌아 오자. JVM을 이야기

할 때에는 자바 바이트코드를 빼놓을 수 없다. JVM은 자바 바이트

코드를 실행하는 실행기이다. 자바 컴파일러는 C/C++ 등의 컴파

일러처럼 고수준 언어를 기계어, 즉 직접적인 CPU 명령으로 변환

하는 것이 아니라, 개발자가 이해하는 자바 언어를 JVM이 이해하

는 자바 바이트코드로 번역한다. 따라서 자바 바이트코드는 플랫

폼 의존적인 코드가 없기 때문에 JVM(정확하게 말하자면 같은 프

로파일의 JRE)이 설치된 장비라면 CPU나 운영체제가 다르더라도

실행할 수 있고(윈도우 PC에서 개발하여 컴파일한 클래스 파일을

리눅스 장비에서도 별다른 변경 없이 실행한다), 컴파일 결과물의

크기가 소스코드의 크기와 크게 다르지 않으므로 네트워크로 전송

하여 실행하기가 쉽다.

클래스 파일 자체는 바이너리 파일이므로 사람이 이해하기 쉽

지 않다. 이 점을 보완하기 위해 JVM 벤더들은 javap라는 역어

셈블러(disassembler)를 제공한다. javap를 이용한 결과물을 흔

히 자바 어셈블리라고 부른다. 앞의 사례에서 애플리케이션 코드

UserService.add() 메서드를 javap -c 옵션으로 역어셈블한 결과

물은 다음과 같다.

Page 172: The platform 2011

JVM Internal 171The Platform 2011

자바 바이트코드 타입 설명

J long long integer

L<classname>; reference an instance of class <classname>

S short signed short

Z boolean true or false

[ reference one array dimension

다음 표는 자바 바이트코드 표현의 예이다.

표 2 자바 바이트코드 표현 예

자바 코드 자바 바이트코드 표현

double d[][][]; [[[D

Object mymethod(int I, double d, Thread t) (IDLjava/lang/Thread;)Ljava/lang/Object;

더 자세한 내용은 "The Java Virtual Machine Specification,

Second Edition"의 "4.3 Descriptors" 절을 참고한다. 또한, 다양

한 자바 바이트코드의 명령어들은 "The Java Virtual Machine

Specification, Second Edition"의 "6. The Java Virtual Machine

Instruction Set" 장을 참고한다.

클래스 파일 포맷

자바의 클래스 파일 포맷을 설명하기 전에 다시 자바 웹 애플리케

이션에서 흔한 사례를 하나 짚고 가자.

현상

Tomcat에서 JSP를 작성하여 실행하니 실행되지 않고 다음과 같은

오류가 발생한다.

Servlet.service() for servlet jsp threw exception org.

apache.jasper.JasperException: Unable to compile class for

JSP Generated servlet error:

The code of method _jspService(HttpServletRequest,

HttpServletResponse) is exceeding the 65535 bytes limit"

5: invokevirtual #23; //Method com/nhn/user/

UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;

8: pop

9: return

23번에 해당하는 메서드가 “Lcom/nhn/user/User;”를 반환하는

메서드로 변환된 것을 확인할 수 있다.

위의 역어셈블 결과물에서 코드 앞의 숫자는 무엇을 의미할까? 바

로 바이트 번호이다. JVM이 실행하는 코드를 굳이 자바 “바이

트”코드라고 하는 이유가 바로 이것일 것이다. 즉, 위의 aload_0,

getfield, invokevirtual 같은 바이트코드 명령어 OpCode들은 1

바이트의 바이트 번호로 표현된다. aload_0 = 0x2a, getfield =

0xb4, invokevirtual = 0xb6 등이다. 따라서, 자바 바이트코드 명

령어 OpCode는 최대 256개라는 점을 알 수 있다.

aload_0, aload_1과 같은 OpCode는 피연산자가 필요 없다. 따라

서 aload_0 바로 다음 바이트가 다음 명령어의 OpCode가 된다.

그러나 getfield, invokevirtual은 2바이트의 피연산자가 필요하

다. 따라서 첫 번째 바이트에 있는 getfield의 다음 명령어는 2바이

트를 건너뛴 네 번째 바이트에 기록된다. 위의 바이트코드를 Hex

Editor로 보면 다음과 같다.

2a b4 00 0f 2b b6 00 17 57 b1

자바 바이트코드에서 클래스 인스턴스는 “L<className>;”, void

는 “V”로 표시되는 것처럼 다른 타입들도 고유의 표현이 있다. 이

표현을 정리하면 다음과 같다.

표 1 자바 바이트코드에서의 타입 표현

자바 바이트코드 타입 설명

B byte signed byte

C char Unicode character

D double double-precision floating-point value

F float single-precision floating-point value

I int integer

Page 173: The platform 2011

기술개발 동향172 The Platform 2011

cp_info constant_pool[constant_pool_count-1];

u2 access_flags;

u2 this_class;

u2 super_class;

u2 interfaces_count;

u2 interfaces[interfaces_count];

u2 fields_count;

field_info fields[fields_count];

u2 methods_count;

method_info methods[methods_count];

u2 attributes_count;

attribute_info attributes[attributes_count];

}

위 내용은 “The Java Virtual Machine Specification, Second

Edition”의 “4.1. The ClassFile Structure”에서 가져온 것이다.

앞에서 역어셈블한 UserService.class 파일의 첫 16바이트를 Hex

Editor로 살펴보면 다음과 같다.

ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b

이 값과 함께 클래스 파일 포맷을 간단히 살펴 보자

• magic: 클래스 파일의 첫 4바이트는 magic number이다. 이는 자바

클래스 파일을 구별하기 위해 미리 지정해 둔 값을 의미하며, 위의 Hex

Editor 값에서 볼 수 있듯이 항상 0xCAFEBABE이다. 즉, 어떤 파일의

첫 4바이트가 0xCAFEBABE라면 이는 자바 클래스 파일이라고 일단

추측할 수 있는 것이다. "자바"라는 이름과 연관된 나름대로 위트 있는

magic number이다.

• minor_version, major_version: 다음 4바이트는 클래스 버전을 나타

낸다. UserService.class 파일은 0x00000032이므로, 클래스 버전은

50.0이다. JDK 1.6로 컴파일한 클래스 파일의 버전은 50.0, JDK 1.5로

컴파일한 클래스 파일의 버전은 49.0이다. JVM은 자신의 버전보다 하

위 버전에서 컴파일된 클래스 파일에 대해서는 하위 호환성을 유지해야

한다. 반면에 하위 버전의 JVM에서 상위 버전의 클래스 파일을 실행하

면 java.lang.UnsupportedClassVersionError가 발생한다.

• constant_pool_count, constant_pool[]: 버전 다음으로는 클래스

파일의 상수 풀(constant pool) 정보를 기술한다. JVM 메모리 구조에

서 설명한 런타임 상수 풀 영역에 들어갈 정보가 바로 여기에 있다. JVM

은 클래스 파일을 로드하면서 이 constant_pool 정보를 메서드 영역의

원인

위와 같은 오류 메시지는 웹 애플리케이션 서버별로 조금씩 다르지

만, 65535바이트 제한 때문이라는 메시지는 같다. 이 65535바이트

제한은 한 메서드의 크기가 65535바이트를 넘을 수 없다는 JVM

명세 자체의 제한이다.

JVM 명세 자체의 제한이라고만 설명하면 아쉬우니, 이 65535바이

트 제한이 왜 생겼는지 좀 더 깊게 들어가 보자.

자바 바이트코드에서 일반적으로 사용하는 branch/jump 명령은

"goto"와 "jsr" 두 가지이다.

goto [branchbyte1] [branchbyte2]

jsr [branchbyte1] [branchbyte2]

이들은 모두 2바이트의 signed branch offset을 피연산자로 받으

므로 최대 65535번 인덱스까지 이동할 수 있다. 그러나, 자바 바

이트코드는 좀 더 여유 있는 branch를 지원하기 위해 4바이트의

signed branch offset을 받는 “goto_w”와 “jsr_w”를 이미 준비하

고 있다.

goto_w [branchbyte1] [branchbyte2] [branchbyte3]

[branchbyte4]

jsr_w [branchbyte1] [branchbyte2] [branchbyte3]

[branchbyte4]

즉, 이들을 이용하면 65535 이상의 인덱스로도 branch 가능하므

로, 자바 메서드의 65535바이트 제한을 넘을 수 있을 것 같다. 그러

나, 자바 클래스 파일 포맷의 다른 여러 제한 때문에 여전히 자바

메서드는 65535바이트를 넘을 수 없다. 다른 제한을 알아보기 위해

클래스 파일 포맷을 간단히 설명하겠다.

자바 클래스 파일의 큰 골격은 다음과 같다.

ClassFile {

u4 magic;

u2 minor_version;

u2 major_version;

u2 constant_pool_count;

Page 174: The platform 2011

JVM Internal 173The Platform 2011

{

// … 중략 - 메서드 정보 …

public void add(java.lang.String);

Code:

Stack=2, Locals=2, Args_size=2

0: aload_0

1: getfield #15; //Field admin:Lcom/nhn/user/

UserAdmin;

4: aload_1

5: invokevirtual #23; //Method com/nhn/user/

UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;

8: pop

9: return

LineNumberTable:

line 14: 0

line 15: 9

LocalVariableTable:

Start Length Slot Name Signature

0 10 0 this Lcom/nhn/service/

UserService;

0 10 1 userName Ljava/lang/String;

// … 후략 - 다른 메서드 정보 …

}

지면 관계상 일부만 발췌하였으나, 실제로 전체 출력을 살펴보면

상수 풀이 얼마나 다양한 정보를 담고 있는지, 각 메서드는 어떤 내

용들을 담고 있는지 확인할 수 있다.

메서드 크기 65535바이트 제한은 여기서 method_info 구조체

의 내용과 관련이 있다. method_info 구조체에는 위의 “javap

-verbose” 출력에서 보다시피 Code, LineNumberTable,

LocalVariableTable attribute가 있다. LineNumberTable의

길이에 해당하는 값, LocalVariableTable의 길이에 해당하는

값, Code attribute에 포함된 exception_table의 길이에 해당하

는 값은 모두 2바이트로 고정되어 있다. 따라서 메서드의 크기는

LineNumberTable, LocalVariableTable, exception_table의 길

이를 넘지 못하기 때문에 65535바이트로 제한되는 것이다.

많은 사람들이 이 메서드 크기 제한에 불만을 갖고 있으며, JVM

명세에서도 ‘앞으로 확장될 수 있을 것이다’라고 기술하고 있다. 그

런타임 상수 풀에 넣는다. UserService.class 파일의 constant_pool_

count는 0x0028이므로 constant_pool에 40-1, 즉 39개의 인덱스를

가진 것을 알 수 있다.

• access_flags: 주로 클래스의 modifier 정보, 즉 public, final,

abstract나 인터페이스 여부를 나타내는 플래그이다.

• this_class, super_class: 각각 this, super에 해당하는 클래스들에 대

한 constant_pool 내의 인덱스이다.

• interfaces_count, interfaces[]: 클래스가 구현한 인터페이스의 개수

와 각 인터페이스에 대한 constant_pool 내의 인덱스이다.

• fields_count, fields[]: 클래스의 필드 개수와 필드 정보이다. 필드 정

보에는 필드 이름, 타입 정보, modifier, constant_pool에서의 인덱스

등이 포함된다.

• methods_count, methods[]: 클래스의 메서드 개수와 메서드 정

보이다. 메서드 정보는 메서드 이름, 파라미터 타입과 개수, 반환 타입,

modifier, constant_pool에서의 인덱스와 함께 메서드 자체의 실행

코드, 예외 정보 등의 내용도 포함한다.

• attributes_count, attributes[]: attribute_info 구조체는 다양한 속

성을 갖고 있다. field_info나 method_info에서도 attribute_info를

사용한다.

javap 프로그램은 이 클래스 파일 포맷도 사용자가 읽을 수 있는

형태로 간략하게 보여준다. "javap -verbose" 옵션을 사용하여

UserService.class를 분석하면 다음과 같은 내용이 출력된다.

Compiled from "UserService.java"

public class com.nhn.service.UserService extends java.lang.

Object

SourceFile: "UserService.java"

minor version: 0

major version: 50

Constant pool:

const #1 = class #2; // com/nhn/service/

UserService

const #2 = Asciz com/nhn/service/UserService;

const #3 = class #4; // java/lang/Object

const #4 = Asciz java/lang/Object;

const #5 = Asciz admin;

const #6 = Asciz Lcom/nhn/user/UserAdmin;;

// … 중략 - constant pool 내용 계속 …

Page 175: The platform 2011

기술개발 동향174 The Platform 2011

그림 1 자바 코드 수행 과정

클래스 로더

자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처

음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다.

이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 자바 클

래스 로더의 특징은 다음과 같다.

• 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생

성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap

Class Loader)이다.

• 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구

조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여

상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요

청받은 클래스 로더가 클래스를 로드한다.

• 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래

스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를

찾을 수 없다.

• 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수

는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스

로더를 생성하는 방법을 사용할 수 있다.

러나 아직까지 뚜렷한 개선의 움직임은 없다. 클래스 파일의 내용

을 거의 그대로 메서드 영역에 로드하는 JVM 명세 특성상, 하위

호환성을 유지하면서 메서드 크기 제한을 늘리도록 구현하기는 상

당히 어려울 것이다.

자바 컴파일러의 오류 때문에 잘못된 클래스 파일이 생성되면 어떻

게 될까? 혹은 네트워크 전송이나 파일 복사 과정 중의 오류로 클

래스 파일이 깨질 수도 있을 것이다. 이런 경우에 대비하기 위해 자

바 클래스 로더는 매우 까다로운 검증 과정을 거친다. JVM 명세는

이 과정을 매우 자세하게 서술하고 있다.

참고

JVM이 클래스 파일 검증 과정을 제대로 수행하는지는 어떻게 검증할

까? 다양한 JVM 벤더가 만든 다양한 JVM이 JVM 명세를 만족하고 있

는지 어떻게 검증할까? 이 검증을 위해서 오라클은 TCK(Technology

Compatibility Kit) 라는 테스트 툴을 제공한다. 다양한 방법으로 잘

못 작성된 수많은 클래스 파일을 비롯하여 수만 개의 테스트를 수행하여

JVM 명세를 검증하는 이 TCK를 통과해야 JVM이라고 이름 붙일 수 있게

강제하고 있다.

이는 JVM 명세 뿐만 아니라 새로운 자바 기술 명세를 제안하고 제

공하는 JCP(Java Community Process; http://jcp.org )에서도

마찬가지로, 제안하는 JSR(Java Specification Request)에 대해

명세 문서, RI (Reference Implementation), TCK가 작성 완료되

어야 JSR이 마무리된다. JSR로 제안된 새로운 자바 기술을 이용하

려는 사용자는 RI 제공자에게 구현체를 라이선스받거나, 직접 구

현하여 TCK를 통과한 후에 해당 기술을 사용할 수 있다.

JVM 구조

자바로 작성한 코드는 다음 그림과 같은 과정을 통해 수행된다

(그림 1).

클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런

타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진

(Execution Engine)이 자바 바이트코드를 실행한다.

Java Source(.java 파일)

Java Byte Code(.class 파일)

Class Loader

Runtime Data Areas

ExecutionEngine

Java Compiler

Java Virtual Machine

Page 176: The platform 2011

JVM Internal 175The Platform 2011

• 시스템 클래스 로더(System Class Loader): 부트스트랩 클래스 로더와

익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드하는 것이라

한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고

할 수 있다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한

다.

• 사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션

사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.

웹 애플리케이션 서버(WAS)와 같은 프레임워크는 웹 애플리케이

션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게

하기 위해 사용자 정의 클래스 로더를 사용한다. 즉, 클래스 로더의

위임 모델을 통해 애플리케이션의 독립성을 보장하는 것이다. 이와

같은 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태

의 계층 구조를 사용하고 있다.

클래스 로더가 아직 로드되지 않은 클래스를 찾으면, 다음 그림과

같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.

그림 3 클래스 로드 단계

각 단계를 간단히 설명하면 다음과 같다.

• 로드: 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.

• 검증(Verifying): 읽어 들인 클래스가 자바 언어 명세(Java Language

Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사

한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과

정으로서 가장 복잡하고 시간이 많이 걸린다. JVM TCK의 테스트 케이

스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검

증 오류를 발생시키는지 테스트하는 부분이다.

각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스

(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래

스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully

Qualified Class Name)을 기준으로 클래스를 찾는다. 비록

FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더

가 로드한 클래스이면 다른 클래스로 간주된다.

다음 그림은 클래스 로더 위임 모델을 표현한 것이다.

그림 2 클래스 로더 위임 모델

클래스 로더가 클래스 로드를 요청받으면, 클래스 로더 캐시, 상위

클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다.

즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으

면 상위 클래스 로더를 거슬러 올라가며 확인한다. 부트스트랩 클

래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스

템에서 해당 클래스를 찾는다.

• 부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들

을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가

아니라 네이티브 코드로 구현되어 있다.

• 익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외

한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로

드하게 된다.

BootstrapClass Loader

ExtensionClass Loader

SystemClass Loader

User-DefinedClass Loader

User-DefinedClass Loader

User-DefinedClass Loader

Linking

Loading Verifying

Preparing

Resolving Initializing

Page 177: The platform 2011

기술개발 동향176 The Platform 2011

은 스레드마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method

Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가

공유해서 사용한다.

• PC 레지스터: PC(Program Counter) 레지스터는 각 스레드마다 하나

씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중

인 JVM 명령의 주소를 갖는다.

• JVM 스택: JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작

될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는

스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제

거하는(pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의

메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표

현한다.

그림 5 JVM 스택 구성

- 스택 프레임: JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레

임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되

면 스택 프레임이 제거된다. 각 스택 프레임은 지역 변수 배열(Local

Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메

서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역

변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택

프레임의 크기도 메서드에 따라 크기가 고정된다.

- 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드

가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달

된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역

변수들이 저장된다.

• 준비(Preparing): 클래스가 필요로 하는 메모리를 할당하고, 클래스에

서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비

한다.

• 분석(Resolving): 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉

트 레퍼런스로 변경한다.

• 초기화: 클래스 변수들을 적절한 값으로 초기화한다. 즉, static

initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.

JVM 명세는 이들 작업들에 대해 명시하고 있으나 작업에 따라서

수행 시점은 유연하게 적용할 수 있도록 하고 있다.

런타임 데이터 영역

그림 4 런타임 데이터 영역 구성

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실

행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의

영역으로 나눌 수 있다. 이중 PC 레지스터(PC Register), JVM 스

택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)

Class LoaderClass Loader

PC Register

JVM Stack

Native Method Stack

Runtime Constant Pool

Heap

Runtime Data Areas

Method Area

Thread

JVM Stack per Thread

Local Variable Array

Operand Stack

Refernce to ConstantPool

Stack Frame

Page 178: The platform 2011

JVM Internal 177The Platform 2011

이렇게 역어셈블된 코드와 우리가 가끔 접할 수 있는 x86 아키텍처

의 어셈블리 코드를 비교해 보면, 비록 OpCode [피연산자]라는 형

식은 유사해 보이지만, 피연산자에 레지스터 이름이나 메모리 주소

나 오프셋을 쓰지 않는다는 차이점을 확인할 수 있다. 앞에서 설명

했듯이, JVM은 스택을 사용하므로 레지스터를 사용하는 x86 아키

텍처와는 달리 레지스터를 사용하지 않으며, 또한 자체적으로 메모

리를 관리하므로 실제 메모리 주소 대신 15, 23 같은 인덱스 번호를

사용하는 것이다. 이 15, 23은 현재 클래스(여기서는 UserService

클래스)가 가지는 상수 풀의 인덱스이다. 즉, JVM은 각 클래스마

다 상수 풀을 생성하여, 실제 대상의 레퍼런스를 보관하고 있다.

위의 역어셈블된 코드의 각 줄을 간단히 해석하면 다음과 같다.

• aload_0: 지역 변수 배열의 0번 인덱스 내용을 피연산자 스택에 추가

한다. 지역 변수 배열의 0번 인덱스는 언제나 this, 즉 현재 클래스 인스

턴스에 대한 레퍼런스이다.

• getfield #15: 현재 클래스 상수 풀에서 15번 인덱스 내용을 피연산자

스택에 추가한다. 즉, UserAdmin admin 필드가 추가된다. admin 필

드는 클래스 인스턴스이므로 레퍼런스가 추가된다.

• aload_1: 지역 변수 배열의 1번 인덱스 내용을 피연산자 스택에 추가

한다. 지역 변수 배열의 1번 인덱스부터는 메서드 파라미터이다. 즉,

add()를 호출하면서 전달한 String userName의 레퍼런스가 추가된

다.

• invokevirtual #23: 현재 클래스 상수 풀에서 23번 인덱스 내용에 해

당하는 메서드를 호출한다. 이때 앞서 getfield로 추가한 레퍼런스와

aload_1로 추가한 파라미터를 모두 꺼내서 호출하는 메서드에 전달한

다. 메서드 호출이 완료되면 그 반환값을 피연산자 스택에 추가한다.

• pop: invokevirtual로 호출한 결과 반환값이 피연산자 스택에 들어가

있으므로 스택에서 꺼낸다. 예전 라이브러리로 컴파일한 코드에서는 이

부분이 없음을 확인할 수 있다. 즉, 예전 라이브러리에서는 반환값이 없

으므로 반환값을 스택에서 꺼낼 필요가 없었다.

• return: 메서드를 완료한다.

이해하기 쉽도록 이를 그림으로 그려보면 다음과 같다.

- 피연산자 스택: 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스

택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결

과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필

요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴

파일 시에 결정된다.

• 네이티브 메서드 스택: 자바 외의 언어로 작성된 네이티브 코드를 위한

스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등

의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택

이 생성된다.

• 메서드 영역: 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM

이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이

스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의

바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태

로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히

Permanent Area, 혹은 Permanent Generation(PermGen)이라

고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항

이다.

• 런타임 상수 풀: 클래스 파일 포맷에서 constant_pool 테이블에 해당

하는 영역이다. 메서드 영역에 포함되는 영역이긴 하지만, JVM 동작에

서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따

로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메

서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤

메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드

나 필드의 실제 메모리상 주소를 찾아서 참조한다.

• 힙: 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다.

JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 힙 구성 방식이

나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

다시 앞에서 살펴본 역어셈블된 바이트코드로 되돌아 가보자.

public void add(java.lang.String);

Code:

0: aload_0

1: getfield #15; //Field admin:Lcom/nhn/user/

UserAdmin;

4: aload_1

5: invokevirtual #23; //Method com/nhn/user/

UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;

8: pop

9: return

Page 179: The platform 2011

기술개발 동향178 The Platform 2011

실행 엔진

클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이

트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 자바 바이트코

드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어을 하나

씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜

리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하

나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음,

다음 OpCode를 수행하는 식으로 동작한다.

그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 것이 아니

라 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이

와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있

는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.

• 인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하

나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대

신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기

하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드

라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.

• JIT(Just-In-Time) 컴파일러: 인터프리터의 단점을 보완하기 위해 도입

된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시

점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에

는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접

실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅

하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴

파일된 코드는 계속 빠르게 수행되게 된다.

JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리

팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라

면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서,

JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼

마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일

을 수행한다.

그림 6 런타임 데이터 영역에 로드된 자바 바이트코드의 예

참고로 이 메서드에서는 지역 변수 배열이 변경되지 않았으므로 위

그림은 피연산자 스택 변경만 표시했다. 그러나 대부분의 경우 지

역 변수 배열도 함께 변경된다. 지역 변수 배열과 피연산자 스택 간

의 데이터 전달은 수많은 load(aload, iload 등) 및 store(asotre,

istore 등) 명령어로 이뤄진다.

이 그림에서는 런타임 상수 풀과 JVM 스택의 내용만 간략하게 확

인했다. 실제 동작에서는 각각의 클래스 인스턴스들이 힙에 할당되

고, User, UserAdmin, UserService, String 등의 클래스 정보가 메

서드 영역에 보관될 것이다.

UserService.admin field

UserAdmin.addUser() method

UserService.class

Constant Pool

Local Variable Array

Operand Stack

aload_0

this

this

String userName

User prevUser

String userName

UserAdmin admin field

UserAdmin admin fieldaload_1

invokevirtual #23

pop

getfield #15

Page 180: The platform 2011

JVM Internal 179The Platform 2011

내고 다시 인터프리터 모드로 동작한다. 핫스팟 VM은 서버 VM

과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사

용한다.

그림 9 핫스팟 클라이언트 VM과 서버 VM

클라이언트 VM과 서버 VM은 각각 오라클 핫스팟 VM을 실행할

때 입력하는 -client, -server 옵션으로 실행된다. 클라이언트 VM

과 서버 VM은 동일한 런타임을 사용하지만, 위 그림과 같이 다

른 JIT 컴파일러를 사용한다. 서버 VM에서 사용하는 Advanced

Dynamic Optimizing Compiler가 더 복잡하고 다양한 성능 최적

화 기법을 사용하고 있다.

IBM JVM은 JIT 컴파일러뿐만 아니라 IBM JDK 6부터

AOT(Ahead-Of-Time) 컴파일러라는 기능을 도입했다. 이는 한

번 컴파일된 네이티브 코드를 여러 JVM이 공유 캐시를 통해 공

유해서 사용하는 것을 의미한다. 즉, AOT 컴파일러를 통해 이미

컴파일된 코드는 다른 JVM에서도 컴파일하지 않고 사용할 수 있

게 하는 것이다. 또한, 아예 AOT 컴파일러를 이용하여 JXE(Java

EXecutable)라는 파일 포맷으로 프리컴파일(pre-compile)된 코

드를 작성하여 빠르게 실행하는 방법도 제공하고 있다.

자바 성능 개선의 많은 부분은 바로 이 실행 엔진을 개선하여 이뤄

지고 있다. JIT 컴파일러는 물론 다양한 최적화 기법을 도입하여

JVM의 성능은 계속해서 향상되고 있다. 초창기 JVM과 지금의

JVM은 이 실행 엔진에서 큰 차이가 있다.

오라클 핫스팟 VM은 1.3부터 핫스팟 컴파일러를 내장하기 시작하

였고, Android Dalvik VM은 Android 2.2부터 JIT 컴파일러를

도입하였다.

그림 7 자바 컴파일러와 JIT 컴파일러

실행 엔진이 어떻게 동작하는지는 JVM 명세에 규정되지 않았다.

따라서 JVM 벤더들은 다양한 기법으로 실행 엔진을 향상시키고

다양한 방식의 JIT 컴파일러를 도입하고 있다.

대부분의 JIT 컴파일러는 다음 그림과 같은 형태로 동작한다.

그림 8 JIT 컴파일러

J IT 컴파일러는 바이트코드를 일단 중간 단계의 표현인

IR(Intermediate Representation)로 변환하여 최적화를 수행하고

그 다음에 네이티브 코드를 생성한다.

오라클 핫스팟 VM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러

를 사용한다. 핫스팟이라 불리는 이유는 내부적으로 프로파일링을

통해 가장 컴파일이 필요한 부분, 즉 '핫스팟'을 찾아낸 다음, 이 핫

스팟을 네이티브 코드로 컴파일하기 때문이다. 핫스팟 VM은 한번

컴파일된 바이트코드라도 해당 메서드가 더 이상 자주 불리지 않는

다면, 즉 핫스팟이 아니게 된다면 캐시에서 네이티브 코드를 덜어

Bytecode

Native Code

Java Source Code

Java Compiler

JIT Compiler

Bytecode IR (Intermediate Representation)Generator

Optimizer

Code Generator Profiler

Runtime

JVM JIT Compiler

Hotspot Client VM Hotspot Server VM

Simple CompilerAdvanced Dynamic

Optimizing Compiler

RuntimeInterpreter, Memory,

Threads

RuntimeInterpreter, Memory,

Threads

Page 181: The platform 2011

기술개발 동향180 The Platform 2011

자바 SE 7의 자바 컴파일러에 의해 생성된 클래스 파일의 버전은

51.0이다. 자바 SE 6는 50.0이다. 클래스 파일 포맷도 상당 부분 바

뀌었으므로, 51.0 버전의 클래스 파일은 자바 SE 6 JVM에서 실행

할 수 없다.

이렇게 다양한 변화에도 불구하고 앞에서 언급했던 자바 메서드의

65535 바이트 제한은 여전히 풀리지 않았다. JVM 클래스 파일 포

맷을 획기적으로 변경하지 않는 한, 앞으로도 풀리기는 어려울 것

으로 보인다.

참고로, 오라클 자바 SE 7 VM은 새로운 가비지 컬렉션인 G1을 지

원하지만, 이는 오라클 JVM 만의 특징으로, JVM 자체는 가비지

컬렉션 방식에 아무런 제한을 두지 않는다. 따라서 JVM 명세에서

는 이를 기술하지 않는다.

String in switch Statements

자바 SE 7은 여러 가지 다양한 문법과 특성을 추가했다. 그러나,

이와 같은 자바 SE 7의 언어 자체적인 변화에 비해, JVM의 변화

는 그리 많지 않다. 그렇다면, 자바 SE 7의 새로운 특성들은 어떻

게 구현할까? 자바 SE 7의 추가 기능 중 하나인 String in switch

Statements(switch() 구문에 String을 비교 대상으로 넣는 기능)를

역어셈블하여 어떻게 구현하고 있는지 살펴보자.

예시로 다음과 같은 코드를 작성했다.

// SwitchTest

public class SwitchTest {

public int doSwitch(String str) {

switch (str) {

case "abc": return 1;

case "123": return 2;

default: return 0;

}

}

}

자바 SE 7의 새로운 기능이므로 자바 SE 6 혹은 그 이하 버전의 자

바 컴파일러에서는 컴파일되지 않는다. 자바 SE 7의 javac를 이용

하여 컴파일한다. 컴파일한 결과를 javap –c로 출력하여 살펴보면

다음과 같다.

참고

바이트코드와 같은 중간언어를 도입하고, VM이 바이트코드를 실행하며,

JIT 컴파일러 등으로 성능을 향상시키는 기법은 자바뿐만 아니라 중간언

어를 도입한 다른 언어들에서도 흔히 사용된다. 마이크로소프트의 .Net

은 CLR(Common Language Runtime)이라는 VM이 CIL(Common

Intermediate Language)이라는 일종의 바이트코드를 실행한다. CLR

은 JIT 컴파일러뿐만 아니라 AOT 컴파일러도 제공한다. 즉, C#이나

VB.NET으로 소스코드를 작성하여 컴파일하면, 해당 컴파일러는 CIL을

생성하고, 이 CIL이 CLR 위에서 JIT 컴파일러 등의 도움을 받으며 실행되

는 것이다. CLR은 가비지 컬렉션을 사용할 뿐만 아니라, JVM처럼 스택

머신이기도 하다.

The Java Virtual Machine Specification, Java SE 7 Edition

지난 2011년 7월 28일 오라클은 Jave SE 7을 발표하면서 JVM 명

세도 자바 SE 7 버전으로 업데이트하여 발표하였다. 1999년 “The

Java Virtual Machine Specification, Second Edition”을 발표한

이후 무려 12년 만에 개선판을 내놓은 것이다. 이 개선판은 12년 간

누적된 여러 변화와 수정 사항을 포함하고 있으며, 명세에 대해 더

명확하게 기술하고 있다. 또한 자바 SE 7과 함께 발표된 “The Java

Language Specification, Java SE 7 Edition”의 내용도 반영하고

있다. 주된 변화를 간단히 요약하면 다음과 같다.

• 자바 SE 5.0부터 도입된 Generics, 가변 인자 메서드 지원

• 자바 SE 6부터 변화된 바이트코드 검증 프로세스 기술

• 동적 타입 언어 지원을 위해 invokedynamic 명령어 및 관련 클래스 파

일 포맷 추가

• 자바 언어 자체의 개념에 대한 내용을 삭제하고, 자바 언어 명세에서 찾

도록 유도

• 자바 Thread와 Lock에 대한 내용을 삭제하고, 자바 언어 명세로 내용

을 넘김

이 중 가장 큰 변화는 역시 invokedynamic 명령어 추가이다. 자

바 SE 7부터 JVM 자체에서 자바 언어뿐만 아니라 다른 언어, 특

히 스크립트 언어들과 같이 타입이 고정되어 있지 않은 동적 타

입 언어를 지원함에 따라, JVM 내부 명령어에도 변화가 생긴 것

이다. 기존에 사용하지 않던 OpCode 186을 새로운 명령어인

invokedynamic에 할당하고, invokedynamic을 지원하기 위해 클

래스 파일 포맷에도 새로운 내용들이 추가되었다.

Page 182: The platform 2011

JVM Internal 181The Platform 2011

90: iconst_2

91: ireturn

92: iconst_0

93: ireturn

자바 소스코드에 비해 상당히 긴 바이트코드가 생성되었다. 먼

저 자바 바이트코드에서는 switch() 구문을 위해 lookupswitch

명령어를 쓰는 것을 알 수 있다. 그런데, 여기서는 하나의

lookupswitch가 아니라 2개의 lookupswitch를 사용한다.

switch() 구문에 int를 넣은 경우를 역어셈블해 보면 하나의

lookupswitch만 사용한다. 즉, String을 처리하기 위해 2개의

switch() 구문으로 나눈 것이다. 5, 39, 53번 바이트 명령어의 주석

들을 살펴보면 switch() 구문에서 String을 어떻게 처리하는지 알

수 있다.

5, 8번 바이트를 보면 먼저 hashCode() 메서드를 실행하여 그 결과

로 switch(int)를 실행한다. lookupswitch 명령어의 중괄호 안을

보면 hashCode 결과값에 따라 다른 위치로 branch한다. String

“abc”는 hashCode 값 96354이며 36번 바이트로 이동한다. String

“123”은 hashCode 값 48690이며 50번 바이트로 이동한다.

36, 37, 39, 42번 바이트를 보면 인자로 넘어온 str 변수의 값을

String “abc”과 equals() 메서드로 비교하는 것을 확인할 수 있다.

값이 같으면 지역 변수 배열 3번 인덱스에 숫자 ‘0’을 넣고 61번으

로 이동한다.

50, 51, 53, 56번 바이트에서도 마찬가지로 String “123”과

equals()로 비교한다. 값이 같으면 지역 변수 배열 3번 인덱스에 숫

자 ‘1’을 넣고 61번으로 이동한다.

61, 62번에서는 지역 변수 배열 3번 인덱스의 값, 즉 ‘0’, ‘1’ 또는 다

른 값을 lookupswitch하여 branch한다.

즉, 자바 코드로 보면, switch() 인자로 넘어온 str 변수의 값을

hashCode() 메서드와 equals() 메서드로 비교하여 그 결과 int 값

으로 switch()를 진행한다.

이 결과를 보면 컴파일된 바이트코드는 이전의 JVM 명세와 다르

지 않다는 것을 알 수 있다. 즉, 자바 SE 7의 새로운 기능인 String

in switch는 JVM 자체가 아니라 자바 컴파일러가 처리하고 있는

것이다. 마찬가지로 다른 자바 SE 7의 새로운 기능들도 자바 컴파

일러가 처리할 것이라고 유추할 수 있다.

C:\Test>javap -c SwitchTest.class

Compiled from "SwitchTest.java"

public class SwitchTest {

public SwitchTest();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/

Object."<init>":()V

4: return

public int doSwitch(java.lang.String);

Code:

0: aload_1

1: astore_2

2: iconst_m1

3: istore_3

4: aload_2

5: invokevirtual #2 // Method java/lang/

String.hashCode:()I

8: lookupswitch { // 2

48690: 50

96354: 36

default: 61

}

36: aload_2

37: ldc #3 // String abc

39: invokevirtual #4 // Method java/lang/

String.equals:(Ljava/lang/Object;)Z

42: ifeq 61

45: iconst_0

46: istore_3

47: goto 61

50: aload_2

51: ldc #5 // String 123

53: invokevirtual #4 // Method java/lang/

String.equals:(Ljava/lang/Object;)Z

56: ifeq 61

59: iconst_1

60: istore_3

61: iload_3

62: lookupswitch { // 2

0: 88

1: 90

default: 92

}

88: iconst_1

89: ireturn

Page 183: The platform 2011

기술개발 동향182 The Platform 2011

마치며

자바라는 도구를 잘 사용하기 위해서 도구를 어떻게 만들었는지 자

세히 살펴볼 필요까지는 없을 것이다. 실제로 많은 자바 개발자들

이 JVM을 깊게 이해하지 않고도 훌륭한 애플리케이션과 라이브러

리 결과물들을 만들어내고 있다. 그렇지만 JVM을 이해하면 자바

를 더 깊게 이해할 수 있고, 앞에서 본 사례와 같은 문제를 해결할

때에도 도움이 될 것이다.

여기에서 언급된 내용 외에도 JVM은 많은 다양한 특성과 기술들

을 내포하고 있다. JVM 명세는 JVM 벤더가 더 나은 성능을 제공

할 수 있도록 많은 부분에서 유연한 명세를 제공하고 있으며, 따라

서 벤더별로 다양한 기술이 적용되고 있다. 특히 가비지 컬렉션 같

은 경우는 JVM뿐만 아니라 가상 머신과 비슷한 형태의 사용성을

제공하는 대부분의 언어에서 사용되는 기술이며, 성능 면에 있어서

가장 첨단에 놓인 기술이라고 할 수 있다. 그러나, 그만큼 많이 다

루어지고 있기 때문에 이 기사에서는 설명하지 않았다.

JVM 내부 구조에 대해서 보다 자세히 알고 싶은 독자가 있다면

"Java Performance Fundamental"(김한도, 서울, 엑셈, 2009)라는

책을 참고하길 바란다. 한글로 작성되어 읽기 편하며, 이 기사를 작

성할 때에도 JVM 명세 다음으로 많이 참고한 책이다

Page 184: The platform 2011

JDK 7 183The Platform 2011

그런데 한 가지 이상한 점이 있다. OpenJDK는 소스코드만 배포하

고 바이너리는 배포하지 않는다. 앞에서 언급했듯이 일부 컴포넌트

의 저작권자가 오픈소스화를 거부했기 때문이다. 그럼 JDK 7을 사

용하려면 소스코드를 내려 받아 직접 빌드해야 하나? 다행히 그렇

지는 않다.

Java.net에는 또 하나의 JDK 7 프로젝트(이하, Oracle JDK 7)

가 있다. Oracle JDK 7은 OpenJDK의 JDK 7 기반에 추가로

OpenJDK에 포함되지 않는 컴포넌트까지 모두 갖춘 프로젝트이

다. 하지만 Oracle JDK 7은 오픈소스가 아니며, Oracle의 직원들

만 개발에 참여할 수 있다. Oracle의 공식 JDK 7은 바로 이 프로젝

트의 결과물이다.

Java SE 7, JDK 7, Java 7

이야기가 나온 김에 한 가지 더 짚고 넘어가자. Java 7, Java SE 7,

JDK 7 등의 용어가 혼용되는데 이들의 차이점은 무엇일까?

들어가며

2011년 7월 말 JDK 7이 정식으로 릴리스되었다. 비록 Lambda나

Jigsaw 같이 화제가 되었던 프로젝트는 JDK 8로 연기되어 김이 새

긴 했지만, 무려 5년 만에 나오는 새 버전인 만큼 전세계 Java 개발

자들의 이목이 집중되었다. The Platform에서도 JDK 7을 전체적

으로 살펴보고, 주요 기능은 좀 더 심도 있게 알아보기로 했다.

OpenJDK

Sun(현재 Oracle)이 JDK 7을 개발하기 시작할 때 이전과 다른 점

이 하나 있었다. 그것은 바로 Sun이 JDK를 오픈소스화하기 위해

2007년 OpenJDK를 만들었다는 것이다. Sun은 저작권자가 오픈

소스화를 거부한 일부 컴포넌트를 제외한 나머지 JDK 소스코드 전

부를 OpenJDK에 제공했고, OpenJDK는 이를 기반으로 JDK 7

프로젝트를 시작했다. 즉, 누구나 OpenJDK의 소스코드 저장소에

접근하여 소스코드를 살펴볼 수 있으며, 의지와 능력만 있으면 개

발에도 참여할 수 있다.

JDK 7

세상만사 부질없으니 흘러가는 대로 흐르렵니다.

어쩌면 무력함에 대한 변명일 지도 모르겠습니다만,

그렇다면 그런 거지 뭐 있겠습니까.

•웹플랫폼개발랩 _ 문종호

Page 185: The platform 2011

기술개발 동향184 The Platform 2011

에 Closure를 도입하는 것으로 자바 언어의 표현력을 크게 높일 수

있는 반면 함수형 언어에 익숙하지 않은 개발자들을 혼란에 빠뜨릴

수 있다. Generics 이후 최대의, 어쩌면 Generics보다 더 큰 언어적

변화를 가져올 수 있는 프로젝트이다.

Project Jigsaw는 JDK 클래스 라이브러리 자체를 모듈화하는 것

을 목표로, Java 클래스들을 묶어 모듈을 구성할 수 있도록 하는 프

로젝트이다. 여기서 모듈은 패키지나 JAR과는 다른 것으로, OSGi

의 모듈과 비슷하다고 할 수 있다.

이 두 프로젝트는 규모가 크고, 그만큼 많은 사람들의 관심을 끌고

있다. 이들에 비하면 JDK 7에 포함된 프로젝트는 미미해 보일 정

도이다. 아쉽기는 하지만, 그래도 JDK 7에는 꽤 유용한 기능들이

남아 있다. 지금부터 JDK 7에서 만나볼 수 있는 기능들을 찬찬히

살펴보도록 하겠다.

JDK 7의 주요 기능

JDK 7의 전체 기능은 JDK Features(http://openjdk.java.net/

projects/jdk7/features/)에서 볼 수 있다. 여기에서는 중요한 기능

들만 간단히 살펴보도록 하자.

JSR 292: Support for dynamically-typed languages

(InvokeDynamic)

JVM(Java Virtual Machine)에서 동작하는 언어는 Java만이 아

니다. 기존 언어가 JVM에서 동작하도록 구현한 JRuby, Jython 같

은 언어도 있고, Groovy, Scala 같이 처음부터 JVM 기반으로 만

들어진 언어도 있다. 그러나 JVM에는 아직 Java에 의존적인 부분

이 있기 때문에 다른 언어, 특히 동적 타입 언어가 JVM에서 동작

하는 데 걸림돌이 되는 부분이 있었다.

InvokeDynamic은 이러 걸림돌을 제거하는 작업의 일환으로,

JVM에서 동작하는 동적 타입 언어들의 효율성(바이너리 크기 등)

과 성능 향상을 이끌어낼 것으로 보인다. 다만 동적 타입 언어의 제

작자가 아닌 일반 개발자가 InvokeDynamic을 직접 사용할 일은

거의 없으므로 우리가 InvokeDynamic을 깊이 살펴볼 필요는 없

을 것이다.

Java SE는 Java Platform, Standard Edition의 약자로, Java

Platform의 근간을 이루는 ‘명세(Specification)’를 가리킨다. JDK

는 Java Development Kit의 약자로, 앞에서 말한 ‘명세’의 ‘구현

체’를 가리킨다고 보면 된다. 즉, Java SE 7은 JSR-336로 규정된

Java SE의 7번째 명세를 가리키는 용어이고, JDK 7은 그 구현체,

이를테면 OpenJDK JDK 7, Oracle JDK 7과 같은 것을 가리키는

것이다.

그런데 JDK 7은 Java SE 7이 확정되기 전에 개발되었다.

OpenJDK에서 명세와 구현을 동시에 진행하고 그 결과가

JSR(Java Specification Request)에 반영되어 명세가 결정되는 순

서로 진행되었던 것이다. 그래서인지 Java SE 7보다는 JDK 7이라

는 용어가 훨씬 더 많이 사용되었다. 따라서 여기에서도 Java SE 7

보다는 JDK 7이라는 용어를 사용하도록 하겠다.

한편 Java는 다양한 의미로 사용되지만 엄밀히 말하면 버전을 붙여

서 사용할 수 있는 용어는 아니다. 즉 Java 7은 정확한 표현이 아니

다. 하지만 Java SE 7이나 JDK 7보다 발음하기 편하고, 그냥 Java

7이라고 부른다고 해서 무슨 의미인지 혼란스러워 할 사람도 없기

때문에 흔히 Java 7이라고 부르기도 하는 것이다.

Plan B

그런데 JDK 7이 릴리스되기 전부터 JDK 8에 대한 이야기가 들려

오기 시작했다. 어떻게 된 것일까?

Java SE 6까지 Java SE는 약 2년을 주기로 메이저 버전이 릴리스

되어 왔다. 그래서 Java SE 7도 Java SE 6이 릴리스된 지 약 2년

후, 그러니까 2008년 말이나 늦어도 2009년 중에는 릴리스될 것

이라 생각되었다. 그러나 Oracle이 Sun을 인수하는 등 여러 일로

인해 JDK 7 개발은 자꾸 지연되었고, 이대로는 2012년에나 완성

될 것이라고 예측하기도 했다. 결국 2010년 9월, Oracle의 Mark

Reinhold는 마무리되어가고 있는 명세를 모아 2011년에 JDK 7으

로 릴리스하고 나머지는 2012년 말에 릴리스할 JDK 8으로 미루자

는 Plan B를 제안했다. 그리고 Oracle은 이 제안을 채택하여 2010

년 JavaOne에서 공식적으로 Plan B 채택을 발표했다.

JDK 8으로 미뤄진 프로젝트는 Project Coin의 일부와 Project

Lambda, Project Jigsaw 등이다. Project Lambda는 Java 언어

Page 186: The platform 2011

JDK 7 185The Platform 2011

• Upgrade class-loader architecture: 클래스 로더의 의존 관계에 사

이클이 존재하면 데드락(deadlock)이 발생하는 문제를 피하도록 수정

되었다.

• Locale enhancement: Java의 로캘(locale)은 현재 IETF BCP 47로

대체된 IETF RFC 1766 기반이다. 따라서 Java의 로캘을 IETF BCP 47

기반으로 변경했다.

• Unicode 6.0: Unicode 6.0을 지원한다.

• Enhanced JMX Agent and MBeans: JRockit에서 포팅된 기능으

로, 방화벽 내의 MBean 서버에 접근할 수 있게 하고, VM에 대해 더 많

은 정보를 제공하는 MBean을 제공한다.

• JDBC 4.1: JDBC 4.1로 업그레이드된다.

• Update the XML stack: JAXP, JAXB, JAX-WS의 최신 버전으로 업

그레이드된다.

이 글에서는 이 중 Project Coin과 NIO.2, Fork/Join 프레임워크

에 대해서 한층 더 깊이 살펴보도록 한다.

Project Coin

Project Coin은 JDK 7의 새 기능들 중 우리에게 가장 직접적으로

영향을 줄 기능이다. 그런데 갑자기 웬 동전일까? 아는 사람도 많겠

지만, 영어 사전을 찾아보면 동사 coin은 다음과 같은 뜻이다.

1. (새로운 낱말, 어구를) 만들다

2. (주화를) 주조하다

Project Coin에서 coin은 첫 번째 의미로 사용된다. Java의 문법

에 새로운 뭔가를 만들어 넣는다는 것이다. 그래도 의문은 남는다.

Project Lamda나 Project Jigsaw 등 다른 프로젝트들은 이름이

그 프로젝트의 내용을 나타내는데, Project Coin은 단지 '새로 만든

다'라는 의미뿐이다. 이렇게 된 이유는 Project Coin의 시작과 진행

과정에서 찾을 수 있다.

Project Coin은 2008년 12월, 프로젝트 리더인 Joseph D. Darcy

의 블로그 게시글에서 시작되었다. Sun(현재 Oracle)이 JDK 7에

‘작은 언어 변화’를 포함시키기로 했는데, 구체적으로 어떤 변화를

줄 것인지는 공개적으로 제안서를 받아서 선정하겠다는 이야기였

다. 그리고 여기에서 말하는 '작은 언어 변화'가 구체적으로 어떤 것

인지, 제안서를 어떻게 써야 하는지 안내하는 게시글이 이어졌다.

JSR 334: Small language enhancements (Project Coin)

Java 개발자들이 가장 직접적으로 큰 변화를 느낄 수 있는 부분이

바로 Project Coin이다. Project Coin은 Java 언어에 작은 변화를

주어서 개발자들의 가려운 곳을 긁어주자는 목적으로 시작됐으며,

구체적인 변화 내용에 대해서는 Oracle 내부 개발자뿐 아니라 누

구라도 참여할 수 있도록 공개적으로 제안서를 모집했다. 그 결과

70여 개의 제안서를 받았고, 그 중 다음과 같은 내용을 JDK 7에 포

함했다.

• String in switch: switch 문에서 int와 Enum 외에 문자열도 사용할

수 있다.

• Binary integral literals and underscores in numeric literals: 2진

수 리터럴 표현이 가능해지며, 모든 숫자 리터럴에서 언더스코어(_)를

사용하여 가독성을 높일 수 있다.

• Multi-catch and more precise rethrow: 하나의 catch 절에서 여

러 타입의 예외를 처리할 수 있다.

• Improved Type Inference for Generic Instance Creation:

Generic 객체를 생성할 때 타입 파라미터를 일일이 기술하지 않아도

된다.

• try-with-resources statement: try 절에서 사용되는 AutoClosable

을 구현한 자원의 close() 메서드가 불리는 것을 보장하여 자원이 새는

것을 막는다.

• Simplified Varargs Method Invocation: 가변 인자 메서드와

generic이 만나서 발생하는 경고(warning)를 해결할 수 없는 경우가

있다. 이 경고를 해결하는 방법을 제공한다.

JSR 203: More new I/O APIs for the Java platform (NIO.2)

java.io.File을 대체할 java.nio.Path가 추가되며, 파일시스템과 관

련된 많은 유틸리티 API, 비동기 채널, 파일 속성 관련 API, 파일시

스템 감시(watcher) 기능 관련 API 등 많은 API가 추가된다.

Concurrency and collections updates(jsr166y)

Fork/Join 프레임워크를 포함하여 Java 동시성(concurrency)과

관련된 클래스가 추가된다.

기타

그 밖에 한번 훑어볼 만한 기능은 다음과 같다.

Page 187: The platform 2011

기술개발 동향186 The Platform 2011

언더스코어는 숫자 사이 어디에든 넣을 수 있고, 언더스코어를 둘

이상 붙여서 쓸 수도 있다.

Strings in switch

지금까지 switch 문에는 int와 Enum 타입만 사용할 수 있었지만

JDK 7부터는 String도 사용할 수 있다. 번잡한 if ... else ... 체인 대

신 깔끔하게 switch 문을 사용할 수 있게 된 것이다. 다음은 String

을 사용한 switch 문의 예이다.

public void handle(String code, Something arg) {

switch (code) {

case "INSERT":

handleInsert(arg);

break;

case "DELETE":

handlerDelete(arg);

break;

default:

throw new IllegalArgumentException();

}

}

만약 switch 문에 null 값이 들어오면(위 예제에서 code의 값이

null이면) NullPointerException이 발생한다.

문법적으로 더 설명할 만한 것은 없으나 Strings in switch가 어떻

게 구현되는지 간단히 알아보자. Project Coin의 목표는 JVM 명

세는 변경하지 않으면서 변화를 주는 것이다. 즉 Strings in switch

는 자바 바이트코드 수준에서 지원되는 것이 아니라 컴파일러가 적

절한 코드로 변환하는 것이다. 그런데 Project Coin은 이런 변환

과정이 어떻게 이루어지는지, 어떤 코드로 변환하는지는 언급하지

않고 있다. 컴파일러에 따라 최적의 방식으로 구현할 것이며, 우리

는 구체적인 내용은 알 것 없이 그냥 쓰면 된다.

String을 사용할 수 있다면 그냥 모든 객체를 다 사용할 수 있게 하

면 안 되는지 궁금해하는 사람들도 있을 것이다. 하지만 그렇게 만

만한 일은 아니다. String을 지원하는 것은 자바 명세에 한 줄을 추

가하는 것으로 끝났지만, Object를 지원하려면 훨씬 많이 변경해

야 한다. 게다가 switch 문에서 굳이 Object를 사용해야 하는 경우

가 있을까?

그리고 2009년 2월 27일, Project Coin의 메일링리스트가 개설되

었고 3월 30일까지 약 한 달 동안 70여 개의 제안서가 제출되었다.

같은 해 8월, 이 중 Project Coin에 포함될 제안서들이 확정되었다.

이처럼 Project Coin은 구체적으로 어떤 변화를 줄 것인지 정해놓

고 시작한 것이 아니라 대중으로부터 제안을 받아 취합하는 형태로

진행된 프로젝트였기 때문에 이렇게 약간은 막연한 이름을 갖게 된

것이다.

이후 JDK 7의 진행이 지지부진해지면서 Project Coin도 함께 지

지부진하게 진행되었다. 그리고 JDK 7과 JDK 8의 분리가 결정

되면서 Project Coin도 JDK 7과 JDK 8에 포함될 것들로 구분

했다. 다행히 JDK 8으로 연기된 기능은 Language support for

collections 하나뿐이고, 나머지 기능들은 JSR 334에서 공식적인

절차에 따라 명세가 확정되었다.

그럼 이제부터 Project Coin의 기능들을 간단한 것부터 순서대로

살펴보도록 하겠다.

Binary integrals and underscores in numeric literals

이 기능은 두 가지 내용을 담고 있다. 하나는 16진수 리터럴처럼 2

진수 리터럴도 사용할 수 있게 하는 것이다. 16진수가 0x 또는 0X

로 시작하는 것처럼 2진수는 0b 또는 0B로 시작한다.

int x = 0b0110;

다른 하나는 가독성을 높이기 위해 숫자 리터럴 중간에 언더스코어

(_)를 넣을 수 있다는 것이다. 예를 들어 다음과 같은 상수를 보자.

private static final long TIMEOUT = 3600000;

private static final int BINARY_VALUE = 0b1001011011000011;

한눈에 파악하기 쉽지 않다. JDK 7에서는 이를 다음과 같이 쓸 수

있다.

private static final long TIMEOUT = 3600_000;

private static final int BINARY_VALUE =

0b1001_0110_1100_0011;

Page 188: The platform 2011

JDK 7 187The Platform 2011

반환

public List<Map<String, Object>> doSomething() {

……

return new ArrayList<>();

}

그러나 항상 이 기능을 사용하는 것이 바람직하지는 않을 것 같다.

메서드를 호출할 때, 특히 타입 파라미터를 가진 메서드를 호출할

때에는 상황이 복잡해질 수 있고, 최악의 경우 컴파일러가 추론에

실패하여 엉뚱한 타입을 집어넣거나 아예 컴파일 에러가 발생할 수

도 있다. 이때에는 뭐가 잘못된 것인지 상황을 분석하기도 쉽지 않

다고 한다.

하지만 변수 선언이나 대입의 경우에는 귀찮은 중복 코딩을 많이

줄여줄 유용한 기능이니 적극적으로 사용하길 권장한다.

Simplified Varargs Method Invocation

이 기능은 매우 간단하지만 왜 필요한지 이해하기는 쉽지 않다. 바

로 Generics가 얽힌 문제이기 때문이다. 한 문장으로 설명하자면 "

비정형적인(non-reifiable) varargs 파라미터를 가지는 메서드의

varargs 파라미터에 비정형적인 타입의 값을 전달하여 호출하면

반드시 경고가 발생하는데, 이를 없앨 수 있는 방법을 제공하겠다"

라는 것이다.

문제가 되는 상황은 다음과 같다.

Map<String, Object> foo = new HashMap<String, Object>();

Map<String, Object> bar = new HashMap<String, Object>();

List<Map<String, Object>> result = Arrays.asList(foo, bar);

// 경고 발생

세 번째 줄에서 ‘Type safety : A generic array of Map<String,

Object> is created for a varargs parameter’라는 경고가 발생한

다. 무엇이 문제인지 알아보기 위해 asList() 메서드를 살펴보자.

public static <T> List<T> asList(T... a) {

return new ArrayList<T>(a);

}

Improved Type Inference for Generic Instance Creation

(diamond)

이 기능은 많은 사람들이 반길 만한 기능이다. 기존에 작성했던 다

음과 같은 코드를 보자.

List<Map<String, Object>> mapList = new ArrayList<Map<String,

Object>>();

위 코드를 다음과 같이 작성할 수 있게 되었다.

List<Map<String, Object>> mapList = new ArrayList<>();

즉, generic 객체를 생성할 때, 타입 파라미터를 명시하지 않아도,

컴파일러가 추론해서 자동으로 채워주는 기능이다. 물론, 모든 상

황에서 가능한 것은 아니지만 OpenJDK, Tomcat, NetBeans 소

스를 대상으로 테스트한 결과 약 90%정도는 정확하게 추론해냈다

고 한다.

일반적으로 객체를 생성하는 경우는 다음과 같이 네 가지로 나눌

수 있으며, 네 경우 모두 diamond를 사용할 수 있다.

변수 선언

List<Map<String, Object>> mapList = new ArrayList<>();

변수 대입

List<Map<String, Object>> mapList;

……

mapList = new ArrayList<>();

메서드 호출

public void doSomething(List<Map<String, Object>> map) {

……

}

doSomething(new ArrayList<>());

Page 189: The platform 2011

기술개발 동향188 The Platform 2011

for (ValueHolder<String> holder : result) {

String value = holder.getValue(); //

ClassCastException 발생

System.out.println(value);

}

}

public static class ValueHolder<T> {

private T value;

public ValueHolder(T value) {

this.value = value;

}

public T getValue() {

return value;

}

}

위의 코드를 컴파일하면 경고만 하나 발생하고 컴파일이 완료된

다. 그런데 이를 실행시키면 ClassCastException이 발생한다. 이

는 doBadThing()에서 values에 ValueHolder<Integer>를 넣었

기 때문이다. 그리고 컴파일러가 이를 막지 못한 것은 type eraser

에 의해 values의 타입 파라미터 정보가 지워졌기 때문이다. 즉,

런타임에 VM이 values가 ValueHolder의 배열이라는 것만 알

뿐 ValueHolder<String> 배열이라는 것은 알 수 없다. 마찬가지

로 ValueHolder<Integer>를 생성하더라도 런타임 시에는 이것이

ValueHolder로만 취급된다. 따라서 JVM이 보기엔 아무런 문제

가 없다.

위 코드는 JVM에게 아래와 같은 코드로 보인다.

public static ValueHolder[] doBadThing(ValueHolder[] values)

{

Object[] objs = values;

objs[0] = new ValueHolder (10);

return values;

}

public static void main(String[] args) {

ValueHolder[] result = doBadThing(new ValueHolder[] {

아무리 봐도 잘못된 부분은 없다. 하지만 asList()를 호출하는 코드

에 @SuppressWarnings(“unchecked”)를 삽입하는 것 외에는 저

경고를 제거할 수 있는 방법이 없다.

이 문제는 컴파일러가 varargs 메서드 파라미터를 배열로 변환하

기 때문에 발생한다. asList()는 내부적으로 다음과 같이 변환된다.

public static <T> List<T> asList(T[] a) {

return new ArrayList<T>(a);

}

그리고 asList()를 호출하는 코드는 다음과 같이 변환된다.

List<Map<String, Object>> result = Arrays.asList(new

Map<String, Object> [] { foo, bar });

실제로 이 코드를 직접 작성하여 컴파일하면 new Map<String,

Object>[]에서 에러가 발생하고 컴파일이 중지된다. Java

Generics에서 사용하는 type eraser 때문에 형 안전(type safe)하

지 않은 상황이 발생할 가능성이 있어서 generic 배열을 생성할 수

없도록 했기 때문이다. 그런데 varargs 메서드를 처리할 때에는 컴

파일러가 직접 이렇게 변환하고는 경고를 한다. 그러면 다음과 같

은 문제가 발생한다.

public static <T> ValueHolder<T>[]

doBadThing(ValueHolder<T>... values) {

Object[] objs = values;

objs[0] = new ValueHolder<Integer>(10);

return values;

}

public static void main(String[] args) {

ValueHolder<String>[] result = doBadThing(new

ValueHolder<String>("foo"), new ValueHolder<String>("bar"));

Page 190: The platform 2011

JDK 7 189The Platform 2011

@SafeVarargs

public static <T> void addAll(List<T> list, T... items) {

for (T item : items) {

list.add(item);

}

}

@SafeVarargs를 아무데나 붙일 수 있는 것은 아니다. 다음 조건을

만족시키지 않는 메서드에 @SafeVarargs를 사용하면 컴파일 에러

가 발생한다.

• varargs 메서드여야 한다

• final 메서드나 static 메서드여야 한다.

또한, 명세에서는 다음과 같은 메서드에 @SafeVarargs를 달면 컴

파일러가 경고를 발생시키도록 권장하고 있다.

• varargs 파라미터가 reifiable 타입인 경우

• 위 예시 코드의 doBadThing()에서처럼 varargs 파라미터를 다른 변

수에 대입하는 등 메서드에서 잠재적으로 문제의 가능성이 있는 연산을

수행하는 경우

당연히 JDK 7의 라이브러리에는 @SafeVarargs가 적용되어 있다.

다음은 @SafeVarargs가 적용된 메서드이다.

• public static <T> List<T> java.util.Arrays.asList(T... a)

• public static <T> boolean java.util.Collections.

addAll(Collection<? super T> c, T... elements)

• public static <E extends Enum<E>> java.util.EnumSet<E>

EnumSet.of(E first, E... rest)

• protected final void javax.swing.SwingWorker.publish(V…

chunks)

Multi-catch and more precise rethrow

이제 남은 두 가지 기능은 모두 try/catch 절과 관련된 것이다. 그

중에 Multi-catch and more precise rethrow는 두 가지 내용을

담고 있는데, 이는 어떤 한 가지 문제 상황을 해결하기 위한 것이

다. 다음 예제를 통해 어떠한 문제인지 살펴보도록 하자.

ValueHolder ("foo"), new ValueHolder ("bar")});

for (ValueHolder holder : result) {

String value = (String) holder.getValue(); //

ClassCastException 발생

System.out.println(value);

}

}

public static class ValueHolder {

private Object value;

public ValueHolder(Object value) {

this.value = value;

}

public Object getValue() {

return value;

}

}

이제 어떤 문제가 발생할 수 있는지 알았다. 그런데 저런 일이 얼

마나 일어나겠는가? 일부러 문제를 일으키려고 작정하지 않은 이

상 이런 코드를 작성할 일은 거의 없다. 처음에 예를 든 Arrays.

asList()에서도 이런 문제가 발생할 일은 전혀 없다. 그럼에도 불구

하고 우리는 계속 무의미한 경고를 보지 않을 수 없었다.

JDK 7에서는 이러한 문제를 해결하기 위해 다음과 같은 조치를 취

했다.

• non-reifiable한 varargs를 가지는 메서드의 호출뿐만 아니라 선언에

도 경고를 발생시킨다.

• 해당 메서드에 아무런 문제가 없다면 메서드의 선언에 @SafeVarargs

어노테이션을 달 수 있다. 이 어노테이션을 달면 메서드의 선언과 호출

에 경고가 발생하지 않는다.

다음은 @SafeVarargs의 사용 예제이다. 이 메서드를 호출하는 코

드에는 경고가 발생하지 않는다.

Page 191: The platform 2011

기술개발 동향190 The Platform 2011

public class ParentException {

public void parentMethod() {

//……

}

}

public interface SomeInterface {

void someMethod();

}

public interface AnotherInterface {

void anotherMethod();

}

public FooException extends ParentException implements

SomeInterface, AnotherInterface {

// ……

}

public BarException extends ParentException implements

SomeInterface {

// ……

}

이렇게 multi catch에 사용된 파라미터 e는 final로 선언되지 않았

어도 암묵적으로 final로 취급된다. 물론 명시적으로 final로 선언

해도 되지만 스타일상의 이슈로 final을 쓰지 않을 것을 권장하고

있다. 사실 예외 파라미터에 다른 값을 대입할 일은 거의 없기 때문

에 문제가 되지는 않을 것이다.

multi catch와 관련한 또 하나의 제약 사항은 하나의 multi catch

절에서 처리하는 타입들 간에 부모 자식 관계가 존재하면 안 된

다는 것이다. 예를 들어 다음과 같이 FooException과 그 부모 클

래스인 ParentException을 함께 잡는 코드는 컴파일 에러가 발

생한다. 하지만 이 역시, ParentException 하나만 잡으면 당연히

FooException도 같이 잡히기 때문에 굳이 둘을 같이 처리할 필요

는 없으므로 별로 문제가 될 만한 제약은 아니다.

public void someMethod() throws FooException, BarException {

try {

canThrowFooException();

canThrowBarException();

} catch (FooException e) {

logger.log(e, e);

throw e;

} catch (BarException e) {

logger.log(e, e);

throw e;

}

}

someMethod()는 canThrowFooException()과 canThrowBar

Exception()을 호출하며, 이 메서드들은 각각 FooException과

BarException을 던진다. 이들이 예외를 던지면 someMethod()는

이를 잡아 로그를 남긴 후 다시 던진다.

그런데 FooException과 BarException에 대한 catch 절이 완전히

동일하다. 중복을 줄이기 위해서 FooException과 BarException

을 한꺼번에 처리할 수 있다면 좋을 것이다. 그래서 JDK 7에서는

다음과 같이 하나의 catch 절에서 여러 타입의 예외를 처리할 수

있는 multi catch 기능을 제공한다.

public void someMethod() throws FooException, BarException {

try {

canThrowFooException();

canThrowBarException();

} catch (FooException | BarException e) {

logger.log(e, e);

throw e;

}

}

이때 e는 FooException과 BarException이 공통으로 구현한 모

든 타입, 즉 공통의 조상 클래스를 상속받고 두 예외가 공통으

로 구현한 인터페이스를 구현한 타입으로 취급받는다. 예를 들

어 FooException과 BarException이 다음과 같이 선언되었다

면 위 코드의 예외 파라미터 e는 ParentException를 상속받고

SomeInterface를 구현한 타입으로 취급된다.

Page 192: The platform 2011

JDK 7 191The Platform 2011

다음 예시에서 canThrowWhat()이 어떤 예외를 던진다고 선

언해야 하는지, 즉 메서드 본문의 두 번째 catch 절에서 던지

는 예외가 어떤 타입인지 결정되는 과정을 통해 이 규칙이 어

떻게 적용되는지 살펴보겠다. 앞선 예시들에서 본 것처럼

canThrowFooException()과 canThrowBarException()은 각

각 FooException과 BarException을 던지며, FooException

과 BarExcept ion은 ParentExcept ion을 상속받는다.

canThrowAnotherException()은 AnotherException을 던

지며, AnotherException은 FooException, BarException,

ParentException과는 아무런 관계가 없고 Exception을 직접 상

속받는 예외이다.

public void canThrowWhat() throws ??? {

try {

canThrowFooException();

canThrowBarException();

canThrowAnotherException();

} catch (FooException e) {

// ……

} catch (ParentException e) {

System.out.println(e);

throw e; // e의 타입은?

} catch (AnotherException e) {

// ……

}

}

• catch 절에서 예외 파라미터 e에 다른 값이 할당되지 않으므로 e는

effectively final이다.

• try 절에서 던지는 예외는 FooException, BarException,

AnotherException이다.

• 앞선 catch 절에서 FooException을 잡으므로 남은 것은

BarException과 AnotherException이다.

• ParentException에 대입 가능한 것은 BarException이다.

따라서 canThrowWhat()은 BarException을 던진다고 선언하면

된다.

more precise rethrow와 관련해서 마지막으로 언급할 것은 이 기

능 때문에 소스 레벨에서 하위 호환성이 깨진다는 점이다. 즉, JDK

try {

canThrowFooException();

canThrowBarException();

} catch (ParentException | FooException e) { // 컴파일 에러

logger.log(e, e);

throw e;

}

다시 처음에 제기했던 문제로 돌아가 보자. 사실 multi catch 없이

코드 중복을 막을 수 있는 방법도 있다. 다음 코드를 보자.

public void someMethod() throws Exception {

try {

canThrowFooException();

canThrowBarException();

} catch (Exception e) {

logger.log(e, e);

throw e;

}

}

이렇게 하나의 catch 절에서 모든 예외를 잡으면 된다. 그런데 여

기에서 새로운 문제가 발생한다. 이렇게 모든 예외를 잡아서 다시

던지려면 someMethod()도 예외를 던지도록 선언해야 한다. 문제

는, 누가 봐도 FooException과 BarException 밖에 나올 수 없는

데 컴파일러는 그냥 catch절만 보고 throws Exception을 선언해

야 한다고 판단해 버린다는 것이다.

JDK 7에서는 바로 more precise rethrow로 이 문제를 해결했

다. 이제 Java 컴파일러는 catch 절에서 다시 예외를 던질 때 다음

과 같이 그 예외의 타입을 결정한다. catch 절에서 사용된 예외 파

라미터가 다음 조건을 만족하면 위의 코드에서 someMethod()는

FooException과 BarException을 던진다고 선언할 수 있다.

• final이거나 effectively final이다. effectively final은 JDK 7에서 추

가된 개념으로, final로 선언되지 않았지만 한 번 값이 할당된 후 변경되

는 일이 없는 경우를 가리킨다.

• try 절에서 발생할 수 있다.

• 앞선 catch 절에서 잡은 것이 아니다.

• 예외 파라미터에 대입할 수 있다.

Page 193: The platform 2011

기술개발 동향192 The Platform 2011

public interface AutoCloseable {

void close() throws Exception;

}

두 번째는 새로운 try with resource 문법이다. 다음과 같은 코

드를 작성하면 try 블록 안에서 예외가 발생하든 발생하지 않

든 resource.close()가 반드시 호출됨을 보장한다. 단, 여기서

SomeResource는 AutoCloseable을 구현한 클래스여야 한다.

try (SomeResource resource = new SomeResource()) {

// …

}

세 번째는 Throwable 클래스의 변경이다. try 절에서 예외가 발생

하여 자원의 close() 메서드를 호출했는데 거기서 또 예외가 발생

한 경우, 최종적으로 던져지는 예외는 try에서 발생한 예외이다. 하

지만 close()를 호출할 때 발생한 예외를 그냥 무시할 수도 없다. 이

를 해결하기 위해 suppressed exception이라는 개념이 도입됐고,

Throwable에 이와 관련한 메서드들이 추가되었다.

마지막으로, JDK의 플랫폼 라이브러리 중 각종 스트림과 채

널 등 try with resource에 사용될 수 있는 모든 클래스는

AutoCloseable을 구현하도록 수정되었다. 특히 java.io.Closeable

이 AutoCloseable을 상속받도록 수정됐기 때문에 Closeable을 구

현한 모든 클래스는 자동으로 AutoCloseable을 구현하게 된다.

그럼 이제부터 try with resource에 대해 자세히 살펴보도록 하자.

try (SomeResource resource = createSomeResource()) {

// ……

} catch (FooException e) {

// ……

} finally {

// ……

}

6까지 문제 없이 컴파일이 되던 코드가 JDK 7에서는 컴파일이 되

지 않을 수 있다. 하지만 걱정할 필요는 없다. 다행히도 이런 코드

는 실제로는 전혀 있을 수 없는 코드이고, 실제로 수많은 소스코드

를 검사하여 이런 패턴의 코드는 존재하지 않음을 확인했다고 한

다. 그러면 도대체 어떤 코드에 문제가 발생하는 것인지 알아보자.

try {

throw new FooException();

} catch (ParentException e) {

try {

throw e;

} catch (BarException e) {

// 도달할 수 없는 코드

}

}

JDK 6에서는 두 번째 throw가 ParentException을 던진다

고 판단하고 BarException을 잡는 catch 절이 실행 가능하다

고 판단한다. 하지만 JDK 7에서는 두 번째 throw가 던지는 예

외가 FooException이라고 더 정확하게 판단한다. 그 결과

BarException을 잡는 catch 절이 사용되지 않는 코드(dead code)

임을 알아차리고 컴파일 에러를 발생시킨다. 즉, 실제로 잘못된 코

드였으므로 컴파일되지 않는다고 해서 문제가 되진 않는다.

try-with-resource statement

이제 Project Coin의 마지막 기능이자 말도 많고 탈도 많았던 try

with resource이다. 이 기능은 자원을 사용한 후 적절하게 해제하

는 것이 너무 어렵기 때문에 언어적으로 이를 지원하여 개발자의

실수를 줄여주자는 의도에서 도입되었다. Joshua Bloch는 이 기능

을 제안하면서 Sun의 공식 문서에도 틀린 예제가 올라와 있었고,

자신이 쓴 책 Java Puzzler에서도 틀린 답이 있었는데 아무도 알아

채지 못했을 정도로 자원을 관리하는 것은 어렵다고 말했다.

이 기능은 네 부분으로 이루어져 있으며, 첫 번째는 try with

resource에 의해 자동으로 관리될 클래스가 구현해야 하는

AutoCloseable 인터페이스이다.

Page 194: The platform 2011

JDK 7 193The Platform 2011

변환된 코드에서 알 수 있는 사실은 다음과 같다.

• 자원의 초기화 도중 예외 E가 발생하면 E가 전파된다. try 블록은 실행

되지 않으며 자원의 close()도 호출되지 않는다.

• 자원의 초기화 도중 예외가 발생하지 않았고, 자원이 null이 아니면 자

원의 close() 메서드는 반드시 호출된다.

• try 블록 안에서 예외 E가 발생하고 자원의 close() 메서드에서 예외가

발생하지 않으면 E가 전파된다.

• try 블록 안에서 예외 E가 발생하고 자원의 close() 메서드에서 예외 F

가 발생하면 E가 전파되며 F는 E의 suppressed exception으로 추가

된다 .

• try 블록 안에서 예외가 발생하지 않고 자원의 close() 메서드에서 예외

E가 발생하면 E가 전파된다.

• 또한 왜 자원 명세에 선언된 변수가 try 블록 안에서만 보이는지 알 수

있으며, final로 선언된 것도 확인할 수 있다.

앞에서 언급한 것과 같이, 자원 명세부에서 세미콜론(;)으로 구분

하여 변수를 여러 개 선언할 수 있다. 이때 앞에서 선언한 변수를

뒤에서 사용할 수도 있다. 다음 코드에서 앞에서 선언한 some을

AnotherResource의 생성자에 전달하는 것이 그 예이다.

try (SomeResource some= createSomeResource();

AnotherResource another = new AnotherResource(some);)

{

// ……

} catch (FooException e) {

// ……

} finally {

// ……

}

이렇게 여러 개의 자원을 선언했을 때 어떻게 동작하는지도 컴파일

러가 이 코드를 어떻게 변환하는지 살펴보면 명확하게 알 수 있다.

컴파일러는 여러 개의 자원이 선언되었을 때 이를 재귀적으로 처리

한다. 우선은 첫 번째 자원인 some에 대해서 앞에서 본 것과 같은

방법으로 변환한다. 이때 차이점은 안쪽 try가 자원 명세에서 some

을 제외한 나머지 자원, 즉 another에 대한 try with resource라는

것이다.

try 뒤, SomeResource를 선언하고 있는 괄호 안을 자원 명세

(Resource Specificaiton)라고 한다. 이 곳에서 AutoCloseable을

구현한 클래스들의 변수를 하나 이상 선언할 수 있다. 이 변수들은

암묵적으로 모두 final로 처리되기 때문에 반드시 이 곳에서 초기

화되어야 한다. 또, 이 변수들의 scope는 자원 명세부와 try 블록으

로 한정되기 때문에 catch와 finally 블록에서는 이 변수들이 보이

지 않는다.

앞에서 언급했듯이 Project Coin은 JVM 명세를 전혀 변경하지 않

는다. 따라서 try with resource도 컴파일러가 코드를 변환하는 방

식으로 동작한다. 따라서 컴파일러가 이 코드를 어떻게 변환하는지

살펴보면 try with resource가 어떻게 동작하는지 알 수 있을 것이

다. 이 코드만 이해하면 try with resource에 대해 모든 것을 파악

한 것이나 마찬가지이다.

try {

final SomeResource resource = createSomeResource();

Throwable primaryException = null;

try {

// 원래 try 블록의 코드

} catch (Throwable t) {

primaryException = t;

throw t; // more precise rethrow를 활용

} finally {

if (resource != null) {

if (primaryException != null) {

try {

resource.close();

} catch (Throwable suppressedException) {

primaryException.addSuppressed(suppresse

dException);

}

} else {

resource.close();

}

}

}

} catch (FooException) {

// 원래 catch 블록의 코드

} finally {

// 원래 finally 블록의 코드

}

Page 195: The platform 2011

기술개발 동향194 The Platform 2011

primaryException2 = t2;

throw t2;

} finally {

if (another != null) {

if (primaryException2 != null) {

try {

another.close();

} catch (Throwable

suppressedException2) {

primaryException2.addSuppressed(

suppressedException2);

}

} else {

another.close();

}

}

}

} catch (Throwable t) {

primaryException = t;

throw t;

} finally {

if (some != null) {

if (primaryException != null) {

try {

some.close();

} catch (Throwable suppressedException) {

primaryException.addSuppressed(suppresse

dException);

}

} else {

some.close();

}

}

}

} catch (FooException) {

// 원래 catch 블록의 코드

} finally {

// 원래 finally 블록의 코드

}

자원이 세 개 선언되었다면 여기에서 변환이 한 번 더 일어난다. 이

와 같은 방식으로 재귀적으로 처리한다.

이렇게 두 개 이상의 자원이 사용되었을 때 변환되는 코드를 살펴

보면 다음과 같은 사실을 확인할 수 있다. 여러 자원을 선언했을 때

자원 명세에서 선언된 자원을 초기화하는 중에 예외 E가 발생하면

try {

final SomeResource some = createSomeResource() {

Throwable primaryException = null;

try (AnotherResource another = new

AnotherResource(some);) {

// 원래 try 블록의 코드

} catch (Throwable t) {

primaryException = t;

throw t;

} finally {

if (some != null) {

if (primaryException != null) {

try {

some.close();

} catch (Throwable suppressedException) {

primaryException.addSuppressed(suppresse

dException);

}

} else {

some.close();

}

}

}

} catch (FooException) {

// 원래 catch 블록의 코드

} finally {

// 원래 finally 블록의 코드

}

그리고 다음과 같이 안쪽 try with resource에 대한 변환을 거친다.

try {

final SomeResource some = createSomeResource() {

Throwable primaryException = null;

try {

final AnotherResource another = new

AnotherResource(some) {

Throwable primaryException2 = null;

try {

// 원래 try 블록의 코드

} catch (Throwable t2) {

Page 196: The platform 2011

JDK 7 195The Platform 2011

More New I/O APIs

JDK 7에서 두 번째로 살펴볼 부분은 NIO.2이다. “NIO”는 New

I/O의 약자이고 뒤에 붙은 “.2”는 물론 두 번째라는 의미이다. 즉,

NIO.2는 JDK에 두 번째로 추가되는 새로운 I/O API들을 나타내

며, 크게 파일시스템 API와 비동기 채널로 나눌 수 있다.

Java의 파일시스템 API에는 부족함이 많았다. 파일의 속성이야 워

낙 플랫폼에 의존적인 부분이라 플랫폼 독립성을 핵심 가치로 내세

우던 Java가 제공하기 난감한 부분이었겠지만, 파일 복사와 같은

기본적인 API조차 제공하지 않아 직접 만들어 쓰게 하는 것은 어찌

보면 성의 부족이라고까지 할 수 있었다. 당연히 개선을 요구하는

목소리가 꾸준히 있었지만 사실 파일을 다루는 것 자체가 치명적으

로 중요한 경우가 많지 않고 또 그렇게 근사해 보이는 분야도 아니

어서 차일피일 미루어져 왔던 것 같다. 그러다가 JDK 7에 이르러

서야 드디어 이를 보완한 것이다.

NIO.2의 두 번째 축인 비동기 채널은 I/O 작업을 비동기로 처리할

수 있도록 해주는 채널이다. 지금까지는 Apache MINA와 같은 별

도의 프레임워크를 사용해야만 가능했던 일이지만 이제는 JDK 자

체에서 이를 제공하게 되었다.

모든 기능을 세세하게 살펴보긴 어려우므로 NIO.2의 주요 기능들

에 대해서만 전체적으로 훑어보기로 하자.

Improved Filesystem Interface

NIO.2의 파일시스템 API는 기존의 java.io.File을 보완하는 것

이 아니라, 완전히 대체하는 것이다. 기존의 File 클래스는 사실 이

도 저도 아닌 애매한 모양새였기 때문에 이를 유지보수하는 것보

다는 아예 새 판을 짤 필요가 있었다. JDK 7에서는 File 클래스가

deprecate되진 않지만 사용을 자제할 것을 권장하고 있다.

Path

우선 첫 번째로 살펴볼 것은 File 클래스를 직접 대체할 java.nio.

file.Path이다. 이름에서 알 수 있듯이 파일보다는 경로 자체에 초

점을 맞추고 있다. 실제로 Path의 메서드들을 보면 순전히 경로를

다루는 메서드뿐이라는 것을 알 수 있다. 사실 기존의 File 객체가

가리키는 것은 어떤 경로가 가리키는 대상일 뿐, 그게 진짜 파일인

지는 알 수 없는 것이었으니 Path가 좀 더 정확한 의미일 것이다.

그 이후에 선언된 자원들은 초기화되지 않으며, 그 이전에 선언된

자원들 중 null이 아닌 자원에 대해서는 close()가 호출된다. 이 때

close() 호출 도중 예외가 발생하면 그 예외들은 E의 suppressed

exception으로 설정되며 최종적으로 전파되는 예외는 E이다.

보통 이렇게 새로운 기능에 대한 설명을 듣고 나면 왠지 모든 문제

를 해결해 줄 것이라는 기대를 갖게 된다. 하지만 이 세상에 완벽한

것은 없다. try with resource 명세 제정 과정에서 오고 간 논의의

한 토막을 살펴보자.

A: 자원을 자동으로 관리해 줄 수 있도록 try with resource를 만들자.

B: 그것으로는 lock을 처리할 수 없다.

A: 자원마다 사용하는 패턴이 다르다. try with resource는 획득 -> 사용 ->

반환의 패턴으로 사용되는 자원만 지원한다.

B: 일부에만 적용되는 복잡한 솔루션을 언어에 넣는 것은 바람직하지 않다.

게다가 JDK 8에 closure가 도입되면 언어에서 지원하지 않아도 라이브러

리 수준에서 해결할 수 있는 문제이다.

A: 일부가 아니라 대부분의 자원에 대해 적용할 수 있으며, 그것만으로도

충분한 가치가 있다. 그리고 JDK 8까지 기다리기만 할 수는 없다.

간단하게 정리하니 별 내용 아닌 것 같지만 Project Coin 메일링리

스트에서 가장 많은 메일이 오간 주제가 try with resource였다. 논

란이 컸던 이유는 ‘자원’이라는 애매모호한 용어 때문이 아닐까 한

다. 자원이라는 단어 하나가 가리키고 있는 것이 너무나도 많다. 스

트림, 채널, 소켓, 메모리, 커넥션, lock 등이 모두 자원이고 그 특성

이 제각각인데 그냥 하나로 묶어서 ‘자원’이라고 부르고 ‘자원을 자

동으로 관리해 주겠다’라고 하니 듣는 사람들은 엄청난 걸 떠올릴

수 밖에 없다.

결론은 try with resource가 모든 자원 관리의 해법이 될 수는 없

다는 것이다. 따라서 이 기능을 사용하고 싶다면 정말 try with

resource의 패턴에 맞는 자원인지, 즉 한 번 획득해서 사용한 후 곧

바로 반환해야 하는 자원인지 확인하고 사용해야 한다. 이렇게 적

절한 자원에 try with resource를 활용하면 쉽게 안전한 코드를 작

성할 수 있을 것이다.

Page 197: The platform 2011

기술개발 동향196 The Platform 2011

FileSystem을 기본 FileSystem으로 가지며, Path 등 파일시스템

관련 객체들은 기본 FileSystem을 통해 생성된다. 다음은 Path를

생성하는 예이다.

FileSystem fs = FileSystems.getDefault();

Path path = fs.getPath("foo/bar/sample.txt");

위 예에서 볼 수 있듯이 FileSystems.getDefault()를 호출하면 현

재 플랫폼에 맞는 기본 FileSystem이 반환된다. 이 FileSystem을

사용하여 Path 등 파일시스템 관련 객체들을 생성해 사용하면 된

다. 이렇게 매번 FileSystems.getDefault()를 호출하는 것이 번거로

운 사람들을 위해서 Paths라는 유틸리티 클래스도 제공한다. 다음

은 Paths를 사용하는 예이다.

Path path = Paths.get("foo/bar/sample.txt");

기본 FileSystem이 있다는 이야기는 기본이 아닌 FileSystem

도 있다는 뜻이다. 하나의 JVM에는 여러 개의 FileSystem을 등

록하고 이를 사용할 수 있다. 하지만 대부분의 개발자들은 기본

FileSystem만으로도 충분할 것이다.

FileSystem은 Path 외에도 아직 설명하지 않은 NIO.2의 주

요 타입들에 대한 팩토리 메서드들을 가지고 있다. Path 이외에

FileSystem으로 생성할 수 있는 객체는 다음과 같다.

표 1 _ FileSystem으로 생성할 수 있는 객체

객체 메서드 설명

PathMatcher getPathMatcher() Path가 glob 또는 정규표현식으로 표현된 패

턴과 부합하는지 확인하는 데 사용한다.

FileStore getFileSotres() 파티션, 볼륨 등과 같은 저장소를 나타내며

저장소에 대한 정보를 획득할 수 있다.

UserPrincipalLookup

Service

getUserPrincipal

LookupService()

사용자 또는 사용자 그룹의 principal을 구하

기 위해 사용한다.

WatchService newWatchService() WatchService를 생성한다.

Path가 제공하는 메서드는 다음과 같이 분류할 수 있다.

• Path의 이름 요소 처리: getName( ), getNameCount( ), iterator( )

• 관련된 Path 구하기: getFileName( ), getParent( ), getRoot( ),

subpath( )

• 상대 경로 처리: relativize( ), resolve( ), resolveSibling( )

• 경로 비교: compareTo( ) , startsWith( ), endsWith( )

• 경로 변환: normalize( ), toAbsolutePath( ),toRealPath( )

• 타입 변환: toFile( ), toString( ), toUri( )

• 기타: getFileSystem( ), isAbsolute( ), register( )

"이름 요소"는 경로를 구성하고 있는 디렉터리 또는 파일명을 뜻

한다. 즉 "foo/bar/sample.txt"라는 경로에서 이름 요소는 "foo",

"bar", "sample.txt"이다. getNameCount()는 이름 요소의 개수를,

getName(int)은 주어진 인덱스에 해당하는 이름 요소를 반환하는

메서드이다.

iterator()는 Iterable로부터 상속받은 메서드이며, 이 메서드를 호

출하여 얻은 이터레이터는 경로의 이름 요소들을 순회하는 데 사용

된다. 즉, "foo/bar/sample.txt"를 나타내는 Path의 이터레이터는

"foo", "bar", "sample.txt"를 차례로 반환한다.

Path는 또한 File을 사용하는 기존 코드와의 호환성을 위해

toFile()을 제공한다. 반대로 File에도 toPath()가 추가되었으니

File이 Path로 대체된다고 해서 호환성 문제가 생기지는 않을 것

이다.

그런데 Path는 File과는 달리 클래스가 아니라 인터페이스이다. 그

렇다면 Path는 어떻게 만들어 사용해야 할까?

FileSystem

파일시스템은 플랫폼에 따라 매우 다른 모습을 가지고 있다. 경로

를 가리키는 문자열, 즉 Path를 생성하기 위해 사용되는 경로 표기

법조차도 제각각이다. 따라서 플랫폼마다 적절하게 경로를 해석하

여 Path를 생성하는 방법을 제공해야 한다. NIO.2는 FileSystem

이라는 팩토리로 이 문제를 해결했다.

java.nio.file 패키지에는 FileSystem이라는 추상 클래스가 있

다. 이 클래스의 getPath()가 바로 Path를 생성해주는 팩토리 메

서드이다. JDK 7부터 JVM은 자신이 동작하는 플랫폼에 맞는

Page 198: The platform 2011

JDK 7 197The Platform 2011

• 디렉터리 탐색

•Visitor 패턴으로 디렉터리를 탐색하는 walkFileTree()

• 디렉터리의 엔트리들을 순회하는 데 사용하는 DirectoryStream의

팩토리 메서드 newDirectoryStream()

• 기타

•파일이 속한 FileStore를 반환하는 getFileStore()

•두 Path가 같은 파일을 가리키는지 확인하는 isSameFile()

•파일의 MIME 타입을 결정하는 probeContentType()

• 심볼릭 링크가 가리키는 대상 Path를 조회하는

readSymbolicLink()

파일 속성

파일의 일부 속성은 거의 모든 플랫폼에서 동일하기 때문에 명시적

인 API를 추가해도 큰 문제가 없다. 앞에서 살펴본 파일 속성 관련

메서드 중 대부분이 이러한 메서드이다. 하지만 이렇게 일반화하기

어려운 속성도 많다. 지금까지는 Java에서 이러한 속성에 접근할

수 있는 방법이 없었지만, NIO.2에서는 가능해졌다.

FileAttributeView

Java.nio.file.attribute 패키지에는 FileAttributeView와 그 하

위 인터페이스들이 포함되어 있다. 각 인터페이스는 특정 파

일 속성에 접근할 수 있는 메서드를 제공한다. 이름만 봐도 각

FileAttributeView가 어떤 속성을 다루는지 대략 짐작할 수 있을

것이다. 자세히 알고 싶다면 각 FileAttributeView의 javadoc을

살펴보기 바란다.

• FileAttributeView

- BasicFileAttributeView

- PosixFileAttributeView

- DosFileAttributeView

• FileOwnerAttributeView

- AclFileAttrivuteView

• UserDefinedFileAttributeView

Files

Path에는 파일 관련 메서드는 없고 오직 경로와 관련된 메서드뿐

이다. NIO.2에서 파일을 다루는 메서드는 모두 java.nio.file.Files

에 있다. Files에는 많은 메서드들이 정의되어 있어서 이를 하나하

나 살펴보기는 어렵다. 대신, 대부분의 유틸리티 클래스들이 그렇

듯이 어떤 메서드가 있는지 알아두는 것만으로도 충분히 유용하기

때문에, 여기서는 간단하게 Files의 메서드들을 종류에 따라 분류하

고 나열해 보도록 하겠다.

• 파일 연산

• 파일을 이동/복사/삭제하는 move(), copy(), delete(),

deleteIfExists()

•파일의 존재 여부를 체크하는 exists(), notExists()

• 각종 파일시스템 구성 요소를 생성하는 createFile(), createLink(),

createSymbolicLink(), createDirectory(), createDirectories()

• 임시 파일과 디렉터리를 생성하는 createTempFile(),

createTempDirectory()

• I/O

• 파일 I/O를 위한 객체들의 팩토리 메서드 newInputStream(),

newOutputStream(), newByteChannel(),

newBufferedReader(), newBufferedWriter()

•파일을 한꺼번에 바이트 배열로 읽고 쓰는 readAllBytes(), write()

•파일의 모든 줄을 한꺼번에 읽고 쓰는 readAllLines(), write()

• 파일 속성

• 파일의 특성을 체크하는 isDirectory(), isExecutable(), isHidden(),

isReadable(), isRegularFile(), isSymbolicLink(), isWritable()

• 파일 변경 시각을 조회, 설정하는 getLastModifiedTime(),

setLastModifiedTime()

•파일의 크기를 조회하는 size()

• 파일의 소유자를 조회하고 설정하는 getOwner().setOwner()

• POSIX 파일 권한을 조회하고 설정하는 getPosixFilePermissions(),

setPosixFilePermissions()

• 위에 언급한 기본 속성뿐 아니라 플랫폼 종속적인 파일의 속성들까

지 조회하고 설정하는 getAttribute(), getFileAttributeView(),

readAttributes(), setAttribute()

Page 199: The platform 2011

기술개발 동향198 The Platform 2011

Boolean value = (Boolean) Files.getAttribute( path,

"dos:hidden");

setAttribute()을 사용하여 동적 접근으로 속성을 쓸 수도 있으며,

readAttributes()로 여러 속성을 한꺼번에 읽어올 수도 있다.

디렉터리 탐색

NIO.2 이전에는 어떤 디렉터리의 엔트리들을 탐색하려면 File의

list() 또는 listFiles()를 호출하여 엔트리 목록을 한꺼번에 읽어 들

여야 했다. 그래서 엔트리의 개수가 아주 많은 경우에는 메모리 관

련 이슈가 발생하기도 했다. NIO.2에서는 이런 문제를 해결하고

좀 더 편리하게 디렉터리를 탐색할 수 있는 방법을 제공한다.

DirectoryStream

첫 번째 방법은 디렉터리의 엔트리들에 대한 이터레이터를 제공하

는 DirectoryStream을 사용하는 것이다. 다음은 디렉터리 내의 모

든 엔트리를 출력하는 예이다.

DirectoryStream<Path> ds = Files.newDirectoryStream(path);

for (Path entry : ds) {

System.out.println(entry);

}

ds.close();

DirectoryStream를 사용하고 나면 위 예제처럼 반드시 close()를

호출해야 한다. 그렇지 않으면 자원의 누수로 시스템에 문제를 일

으킬 수 있다. 그런데 자원을 반환하는 메서드를 제대로 호출하는

일은 생각보다 매우 어렵다. 그래서 앞에서 설명했듯이 JDK 7에

는 try with resource라는 새로운 문법이 추가되었다. 이를 사용하

면 try 또는 catch 절에서 예외가 발생하든 발생하지 않든, 사용한

자원의 close() 메서드가 반드시 호출되는 것을 보장한다. 다음은

JDK 7의 새로운 문법을 사용하도록 수정한 예이다.

FileAttributeView는 Files의 getFileAttributeView() 메서드를 호

출해서 얻을 수 있다. 이때 원하는 FileAttributeView의 타입을 명

시해야 한다.

BasicFileAttributeView attrs = Files.

getFileAttributeView(path, BasicFileAttributeView.class);

if (attrs != null) {

// 속성 처리

}

플랫폼마다 지원하는 FileAttributeView가 다르므로 지원

하지 않는 FileAttributeView를 요청하면 null이 반환된다.

BasicFileAttributeView는 모든 플랫폼에서 제공된다. 어떤

FileAttributeView가 지원되는지 미리 확인하려면 다음과 같이 파

일이 속한 FileStore를 통해 조회한다.

if (Files.getFileStore(path).supportsFileAttributeView(AclFi

leAttributeView.class)) {

AclFileAttributeView view = Files.

getFileAttributeView(path, AclFileAttributeView.class);

// 속성 처리

}

FileAttributeView를 직접 사용하지 않고 파일 속성들에 접근할

수 있는 방법도 있다. BasicFileAttributeView와 그 하위 인터페이

스들이 제공하는 속성은 다음과 같이 Files의 readAttributes()를

호출하면 바로 가져올 수 있다.

DosFileAttributes attrs = Files.readAttributes(path,

DosFileAttributes.class);

Dynamic Access

모든 파일 속성에는 문자열로 표현되는 이름이 있다. 이 이름은

“{뷰 이름}:{속성 이름}”의 형식이며, 이렇게 이름으로 파일의 속

성에 접근하는 것을 동적 접근이라 한다. 동적 접근을 위한 API도

Files에 있다. 다음은 DosFileAttribute의 hidden 속성을 가져오는

예이다.

Page 200: The platform 2011

JDK 7 199The Platform 2011

FileVisitor<Path> visitor = new FileVisitor<Path>() {

@Override

public FileVisitResult preVisitDirectory(Path dir,

BasicFileAttributes attrs) throws IOException {

System.out.println("entering: " + dir);

return FileVisitResult.CONTINUE;

}

@Override

public FileVisitResult visitFile(Path file,

BasicFileAttributes attrs) throws IOException {

System.out.println("visit: " + file);

return FileVisitResult.CONTINUE;

}

@Override

public FileVisitResult visitFileFailed(Path file,

IOException exc) throws IOException {

System.out.println("visit failed: " + file);

return FileVisitResult.CONTINUE;

}

@Override

public FileVisitResult postVisitDirectory(Path dir,

IOException exc) throws IOException {

System.out.println("leaving: " + dir);

return FileVisitResult.CONTINUE;

}

};

Files.walkFileTree(path, visitor);

walkFileTree()는 주어진 디렉터리를 루트로 깊이 우선 탐색을 수

행하며 FileVisitor의 메서드를 호출한다. 메서드 이름에서 알 수

있듯이 디렉터리에 들어가기 전에 preVisitDirectory(), 파일을 방

문할 때 visitFile(), 디렉터리에서 나올 때 postVisitDirectory(), 어

떤 이유에서든 파일 방문에 실패했을 때 visitFileFailed()가 호출된

다. 방문 순서에 대해서는 깊이 우선 탐색이라는 점 이외에 정해진

것이 없다. 즉, 한 디렉터리 안의 엔트리들이 어떤 순서로 방문될

지 알 수 없으며, 심지어 파일과 디렉터리 중 어느 것이 먼저 방문

될지도 알 수 없다.

try (DirectoryStream<Path> ds = Files.

newDirectoryStream(path)) {

for (Path entry : ds) {

System.out.println(entry);

}

} catch (IOException | DirectoryIteratorException e) {

System.err.println(e);

}

DirectoryStream을 생성할 때, DirectoryStream.Filter 객체를 함

께 전달하여 조건에 부합하는 엔트리만을 걸러내도록 할 수도 있

다. 단순히 경로의 패턴을 가지고 엔트리를 걸러낼 경우 glob 패턴

문자열로 걸러낼 수도 있다. 다음은 DirectoryStream.Filter를 사

용하여 디렉터리의 목록만을 출력하는 예이다.

DirectoryStream.Filter<Path> filter = new DirectoryStream.

Filter<Path>() {

@Override

public boolean accept(Path entry) throws IOException {

return Files.isDirectory(entry);

}

};

try (DirectoryStream<Path> ds = Files.

newDirectoryStream(path, filter)) {

for (Path entry : ds) {

System.out.println(entry);

}

} catch (IOException | DirectoryIteratorException e) {

System.err.println(e);

}

FileVisitor

DirectoryStream은 대상 디렉터리에 직접 포함된 엔트리만을 순

회하며, 하위 디렉터리의 내용은 포함되지 않는다. 만약 하위 디렉

터리들까지 모두 탐색하려면 FileVisitor를 사용한다. 이름에서 알

수 있듯이 visitor 패턴을 사용하여 파일 트리를 탐색한다.

Page 201: The platform 2011

기술개발 동향200 The Platform 2011

// 1. WatchService 획득

FileSystem fs = FileSystems.getDefault();

WatchService watcher = fs.newWatchService();

Path target = fs.getPath("D:\\temp");

// 2. 감시 대상에 WatchService 등록

target.register(watcher, StandardWatchEventKind.ENTRY_

CREATE,

StandardWatchEventKind.ENTRY_DELETE,

StandardWatchEventKind.ENTRY_MODIFY);

for (;;) {

// 3. 이벤트 대기

WatchKey key = watcher.take();

// 4. 이벤트 처리

for (WatchEvent<?> event: key.pollEvents()) {

Path path = (Path) event.context();

if (event.kind() == StandardWatchEventKind.ENTRY_

CREATE) {

System.out.println("created: " + path);

다음은 위 코드의 각 단계에 대한 설명이다.

1. WatchService를 사용하기 위해 우선 WatchService 객체를 얻어 온

다. 앞에서 설명한 것처럼 FileSystem의 newWatchService()를 호출

하여 WatchService를 얻는다.

2. 감시할 대상, 여기서는 Path 타입의 변수인 target의 register() 메서

드를 호출하여 WatchService를 등록한다. register()를 호출할 때 어

떤 이벤트를 감시할 것인지도 함께 명시하는데, JDK 7에서 모니터링

할 수 있는 이벤트는 StandardWatchEventKind에 정의된 EVENT_

CREATE, EVENT_DELETE, EVENT_MODIFY로, 각각 디렉터리 내

에 어떤 엔트리가 생성, 삭제, 수정되는 이벤트를 나타낸다.

3. WatchService의 take() 메서드를 호출하여 이벤트가 발생한

WatchKey를 가져온다. take() 메서드는 이벤트가 발생할 때까지 블록

되는 메서드이다.

4. WatchKey는 WatchService와 Watchable 사이의 등록을 나타내

는 객체이며, 이 객체의 pollEvents()를 호출하면 이벤트를 나타내는

WatchEvent의 List가 반환된다.

WatchKey의 kind() 메서드를 호출하면 발생한 이벤트의 종류가 반환

되고, context()를 호출하면 이벤트와 관련된 부가 정보가 반환된다.

FileVisitor의 각 메서드는 FileVisitResult라는 enum 타입을 반환

하도록 선언되어 있으며, 이 반환값에 따라 파일 트리 탐색을 계속

할지 결정한다. 위의 예처럼 CONTINUE를 반환하면 탐색을 계

속하고, TERMINATE를 반환하면 탐색을 즉시 중단한다. SKIP_

SUBTREE는 preVisitDiretory()에서만 사용할 수 있는 값으로 이

디렉터리 아래에 있는 것들은 탐색하지 말라는 의미이다. 마지막으

로 SKIP_SIBLING은 현재 엔트리와 부모가 같은 엔트리들은 더

이상 탐색하지 않겠다는 의미이다.

WatchService

설정 파일이 변경되면 자동으로 감지하고 읽어 들여 반영시키라는

요구 사항이 있다면 어떻게 구현해야 할까? 이제까지는 파일을 읽

어 들인 후 수정 시각을 기록하고, 타이머를 돌려 주기적으로 파일

의 수정 시각을 체크하고, 수정 시각이 바뀌면 파일이 변경된 것으

로 보고 파일을 읽어 들여야만 했다.

NIO.2는 이런 상황에 활용할 수 있는 WatchService를 제공한다.

다음은 D:\temp 디렉터리의 변경 사항을 모니터링하는 예이다.

다.

// 1. WatchService 획득

FileSystem fs = FileSystems.getDefault();

WatchService watcher = fs.newWatchService();

Path target = fs.getPath("D:\\temp");

// 2. 감시 대상에 WatchService 등록

target.register(watcher, StandardWatchEventKind.ENTRY_

CREATE,

StandardWatchEventKind.ENTRY_DELETE,

StandardWatchEventKind.ENTRY_MODIFY);

for (;;) {

// 3. 이벤트 대기

WatchKey key = watcher.take();

// 4. 이벤트 처리

for (WatchEvent<?> event: key.pollEvents()) {

Path path = (Path) event.context();

if (event.kind() == StandardWatchEventKind.ENTRY_

CREATE) {

System.out.println("created: " + path);

Page 202: The platform 2011

JDK 7 201The Platform 2011

비동기 채널은 두 가지 패턴으로 사용할 수 있는데, Future를 사

용한 방법과 CompletionHandler를 사용하는 방법이 있다. 모

든 비동기 채널은 이 두 가지 패턴의 I/O 메서드를 지원하는데,

I/O 메서드의 구체적인 시그니처는 각 채널의 특성에 따라 조

금씩 다르다. 여기서는 혼자서도 간단하게 테스트해 볼 수 있는

AsynchronousFileChannel을 위주로 설명한다.

Future

먼저 Future를 사용한 비동기 I/O 예를 살펴보자.

AsynchronousFileChannel ch = AsynchronousFileChannel.

open(path);

ByteBuffer buffer = ByteBuffer.allocate(1024);

Future<Integer> future = ch.read(buffer, 0);

// 다른 작업 처리

Integer readBytes = future.get();

이처럼 비동기 채널은 Future를 반환하는 I/O 메서드를 가지고 있

으며, 이를 호출하면 I/O 작업이 끝나지 않았더라도 즉시 Future를

반환한다. 그리고 다른 작업을 처리하다가 나중에 Future로부터 I/

O 작업의 결과를 확인할 수 있다. Future를 사용하는 기존의 다른

API들과 다를 바 없다.

CompletionHandler

비동기 채널을 사용하는 또 다른 방법은 CompletionHandler를

사용하는 것이다. 이는 I/O 작업이 끝났을 때 호출할 콜백 함수를

등록하는 것이라 생각하면 된다. 예시 코드부터 살펴보자.

AsynchronousFileChannel ch = AsynchronousFileChannel.

open(path);

ByteBuffer buffer = ByteBuffer.allocate(1024);

CompletionHandler<Integer, ByteBuffer> handler = new

CompletionHandler<Integer, ByteBuffer>() {

@Override

public void failed(Throwable exc, ByteBuffer attachment)

{

context()는 디렉터리에 대한 엔트리 생성, 삭제, 수정 등의 이벤트가

발생한 엔트리를 나타내는 Path 객체를 반환하며, 이 Path는 감시 대상

으로부터의 상대 경로이다.

5. 이벤트를 모두 처리한 후에는 반드시 WatchKey의 reset() 메서드

를 호출해야 하며, reset()을 호출하지 않으면 더 이상 이벤트를 통보

받을 수 없다. reset() 메서드는 이 WatchKey가 유효한지를 나타내는

boolean을 반환하며, false가 반환되면 이 WatchKey는 더 이상 사용

할 수 없다.

WatchService를 위의 예처럼 무한루프를 도는 전용 스레드에서

만 사용해야 하는 것은 아니다. WatchService의 poll() 메서드

를 사용하여 원하는 때에만 이벤트를 확인하는 방식으로 사용할

수도 있다. 또한, 감시할 대상이 하나뿐이라면 WatchService의

take()나 poll()을 호출할 필요 없이, register()를 호출할 때 반환된

WatchKey로 직접 pollEvents()를 호출해도 된다.

이처럼 WatchService를 사용하면 디렉터리 내에서 발생하

는 이벤트들을 쉽게 통보받을 수 있어 상당히 유용하다. 하지만

WatchService는 디렉터리에 대해서만 사용할 수 있으므로 특정

파일만을 감시하고 싶을 때에는 약간 번거롭다. 감시할 파일의 부

모 디렉터리에 ENTRY_MODIFY 이벤트에 대해 WatchService

를 등록한 후, 이벤트가 발생하면 WatchEvent의 context()를 호출

하여 이벤트가 발생한 파일이 내가 감시하고자 하는 파일이 맞는지

확인하는 과정을 거쳐야만 한다.

Channel API

이제 NIO.2의 다른 한 축인 비동기 I/O에 대해 살펴보도록 하겠

다. 사실, NIO.2의 한 축이라고 했지만 파일시스템 API에 비하면

분량이 매우 적다.

Asynchronous I/O

NIO.2는 세 가지 비동기 채널을 제공한다. AsynchronousFile

Channel, AsynchronousServerSocketChannel, Asynchronous

SocketChannel이며, 각각 FileChannel, ServerSocketChannel,

SocketChannel의 비동기 버전이다. 즉, 다른 점은 모두 똑같고 I/

O를 요청하는 메서드가 비동기로 동작하는 차이점만 있다고 보면

된다.

Page 203: The platform 2011

기술개발 동향202 The Platform 2011

채널을 생성할 때, 즉 AsynchronousSocketChannel이나

AsynchronousServerSocketChannel의 open()을 호출할 때, 새

로 생성될 채널이 속할 AsynchronousChannelGroup을 지정할

수 있다. 채널이 속할 AsynchronousChannelGroup을 지정하지

않으면 시스템의 기본 그룹에 포함된다.

AsynchronousFileChannel도 마찬가지로 Completion Handler

호출 등에 사용할 스레드 풀이 필요하지만 Asynchronous

ChannelGroup에 속하지 않는다. 대신 채널을 생성할 때, 사용할

ExecuterService를 지정할 수 있으며, 따로 지정하지 않으면 시스

템 기본 스레드 풀을 사용한다.

Fork/Join Framework

Fork/Join 프레임워크는 재귀 알고리즘을 병렬적으로 수행하여

멀티프로세서 환경을 충분히 활용할 수 있도록 해주는 프레임워크

이다. 이 프레임워크는 Doug Lea의 주도하에 작성됐으며, 그의 논

문 “A Java Fork/Join Framework”에 기반을 두고 있다.

병합 정렬

재귀 알고리즘을 설명할 때 자주 사용되는 예인 병합 정렬을 예로

들어 Fork/Join이 어떤 것인지 감을 잡아보도록 하자. 두 개의 워

커 스레드가 크기 4인 배열에 들어있는 데이터에 대해 병합 정렬

(Merge Sort)을 수행하는 시나리오이며, M(a, b)는 배열의 a번째

값부터 b번째 값까지의 병합 정렬 작업을 나타낸다.

1. 각 워커 스레드에는 자신만의 작업 큐가 있다. 처음 Fork/Join 프레임워

크에 M(0, 3) 작업을 전달하면, 놀고 있던 스레드 중 하나가 이 작업을

가져가 처리한다. 여기서는 1번 스레드가 먼저 작업을 가져간다.

Thread1 Thread2

M(0, 3)

.

System.out.println(exc);

}

@Override

public void completed(Integer result, ByteBuffer

attachment) {

byte[] bytes = new byte[result];

attachment.flip();

attachment.get(bytes);

System.out.println("read " + result + " bytes");

System.out.println(new String(bytes));

}

};

ch.read(buffer, 0, buffer, handler);

// 다른 작업 처리

CompletionHandler를 사용하는 I/O 메서드를 호출하면, I/O 작

업의 완료와 상관 없이 즉시 반환된다. I/O를 요청한 스레드는 이

제 I/O를 신경쓰지 않고 다른 작업을 수행해도 된다. I/O가 완료됐

을 때 다른 스레드에서 CompletionHandler의 메서드를 호출하여

뒤처리를 할 것이기 때문이다.

I/O가 성공적으로 완료되면 CompletionHandler의 completed()

가 호출되고, 실패하면 failed()가 호출된다. completed()는 I/O

작업의 결과인 result와 I/O작업을 요청할 때 전달한 attachment

를 파라미터로 받고, failed는 I/O 실패를 나타내는 예외와 함께

attachment를 파라미터로 받는다.

attachment는 I/O 요청과 완료 처리 간의 문맥을 유지시키기 위

한 것으로, 어떤 값이든 전달할 수 있다. 위 예에서는 파일 읽기 작

업을 요청하면서 읽기 버퍼를 attachment로 전달하므로 버퍼로 읽

어 들인 값을 completed()에서 출력할 수 있다.

Groups

바로 앞에서 CompletionHandler는 다른 스레드에서 호출된다고

했는데, 이 스레드의 정체에 대해서 간단히 이야기해보자.

네트워크와 관련된 비동기 채널들은 모두 AsynchronousChannel

Group에 속한다. 같은 그룹에 속한 채널들은 스레드 풀 등의 자

원을 공유하며, 바로 이 스레드 풀이 CompletionHandler를 호출

한다.

Page 204: The platform 2011

JDK 7 203The Platform 2011

5. 이때 2번 스레드가 일거리를 찾는다. 자신의 작업 큐는 여전히 비어있

기 때문에, 1번 스레드의 작업 큐에서 M(0, 1)을 훔친다.

Thread1 Thread2

M(2, 3) M(0, 1)

M(0, 3)

M(2, 2)

M(3, 3)

이것이 바로 work stealing이라는 정책으로, Cilk에서 처음 사용

된 개념이다. 여러 스레드에 작업을 분배할 때, 어떤 작업이 얼마나

걸릴지 미리 예측하기 어렵기 때문에, 처음부터 각 스레드에 공평

하게 작업을 분배해 준다는 것은 거의 불가능하다. 따라서 어떤 스

레드는 일이 없어서 놀고 있는데, 어떤 스레드는 쉬지 않고 일해도

할 일이 산더미처럼 쌓여 있게 되는 상황이 벌어질 수 있다. 이런

경우에 놀고 있는 스레드가 바쁜 스레드의 작업을 가져다 처리하는

것이 work stealing이다. 따라서 각 프로세서에 골고루 작업이 분

배되어 멀티프로세서 환경을 충분히 활용할 수 있다.

여기서 하나 눈여겨볼 점은, 다른 스레드의 작업 큐에서 작업을 훔

쳐올 때는 큐의 반대쪽 방향에서 꺼내온다는 것이다. 여기에는 두

가지 이유가 있다. 하나는 작업 큐의 주인인 스레드와 작업을 훔쳐

가려는 스레드 간의 경합을 피하기 위한 것이다. 다른 하나의 이유

는 work stealing의 횟수를 줄이기 위한 것이다. Fork/Join 프레임

워크의 특성상, 먼저 큐에 들어간 작업이 더 큰 작업일 가능성이 높

다. 위의 그림에서는 큐의 가장 안쪽에 M(0, 1)이 있는데, 이 작업

이 큐에 있는 작업들 중 가장 크기가 크다는 것을 알 수 있다. 자잘

한 일을 훔쳐오면 금방 일이 끝나서 또 할 일이 없어져 또 다시 일

을 훔쳐와야 하니, 되도록이면 큼직한 일을 가져와 이런 수고를 덜

게 하는 것이다.

6. 이제 두 스레드가 모두 열심히 일을 하고 있다. 1번 스레드는 M(2, 3)를

잠시 접어두고 M(3, 3)를 꺼낸다. 2번 스레드는 M(0, 1)을 처리하기 시

작했고, 그 결과 파생된 M(0, 0)과 M(1, 1)을 자신의 작업 큐에 넣는다.

2. M(0, 3)은 M(0, 1)과 M(2, 3) 두 하위 작업을 파생시킨다. 1번 스레드

는 이 두 작업을 자신의 작업 큐에 넣는다. 2번 스레드는 아직 일거리가

없어 놀고 있다.

Thread1 Thread2

M(0, 3)

M(0, 1)

M(2, 3)

3. M(0, 3)은 파생된 두 하위 작업이 끝나야만 일을 마무리할 수 있다. 따

라서 1번 스레드는 M(0, 3)를 잠시 한 쪽에 치워두고, 작업 큐에서 다음

작업을 꺼내서 처리한다.

Thread1 Thread2

M(2, 3)

M(0, 3)

M(0, 1)

4. M(2, 3)은 또다시 M(2, 2), M(3, 3)으로 나누어진다. 이번에도 1번 스

레드는 이 두 작업을 자신의 작업 큐에 넣는다. 눈치 빠른 사람들은 이

쯤에서 1번 스레드가 자신의 작업 큐의 한쪽(그림에서는 아래쪽)에서

만 작업을 넣고 꺼내고 있다는 것을 알아챘을 것이다. Fork/Join에서 워

커 스레드는 자신의 작업 큐를 LIFO(Last In, First Out)로 관리한다.

Thread1 Thread2

M(2, 3)

M(0, 3)

M(0, 1)

M(2, 2)

M(3, 3)

Page 205: The platform 2011

기술개발 동향204 The Platform 2011

9. 1번 스레드는 M(2, 3)의 처리를 끝냈다. 하지만 아직 M(0, 1)의 처리가

끝나지 않았기 때문에 M(0, 3)을 시작할 수 없다. 훔쳐올 일이 있는지 2

번 스레드의 작업 큐를 살펴보지만, 그쪽도 남은 일이 없다. 2번 스레드

는 이제 M(0, 1)의 처리를 시작한다.

Thread1 Thread2

M(0, 1)

M(0, 3)

10. 2번 스레드가 M(0, 1)을 마쳤다. 1번 스레드는 M(0, 3)를 마무리한

다. 이로써 모든 작업이 완료된다.

Thread1 Thread2

M(0, 3)

이제 Fork/Join 프레임워크의 실체를 살펴보기로 하자. 다음은 위

에서 설명한 병합 정렬을 처리하는 코드이다.

public class Sample {

public static void main(String[] args) {

ForkJoinPool threadPool = new ForkJoinPool();

int[] values = new int[] { 10, 3, 4, 1, 2, 5, 7, 9 };

threadPool.invoke(new SortTask(values, new int[8], 0,

7));

System.out.println(Arrays.toString(values));

}

Thread1 Thread2

M(2, 3) M(0, 1)

M(0, 3)

M(2, 2)

M(3, 3)

7. 1번 스레드가 처리하던 M(3, 3)는 더 이상 하위 작업을 파생시키지 않

고 끝난다. 2번 스레드는 M(0, 1)을 한쪽으로 치워두고 M(1, 1)을 처리

한다.

Thread1 Thread2

M(2, 2) M(1, 1)

M(2, 3) M(0, 1)

M(0, 3)

M(0, 0)

8. 1번 스레드의 M(2, 2) 역시 하위 작업을 추가시키지 않고 끝났다. M(2,

2)과 M(3, 3)이 모두 끝났으니 기다리던 M(2, 3)을 마무리할 수 있게

되었다. 2번 스레드도 M(1, 1)을 끝내고 M(0, 0)을 처리한다.

Thread1 Thread2

M(2, 3) M(0, 0)

M(0, 1)

M(0, 3)

Page 206: The platform 2011

JDK 7 205The Platform 2011

우선 눈여겨봐야 할 코드는 main() 메서드에서 ForkJoinPool을

생성하고 invoke()를 호출하여 SortTask를 전달하는 부분이다. 이

것이 앞에서 본 그림 중 첫 번째에 해당하는 부분, 즉 모든 것이 시

작되는 부분이다.

그리고 SortTask의 compute() 메서드에서 하위 작업을 나타내는

SortTask 객체들을 생성하고 invokeAll()을 통해 이들을 호출하는

부분도 잘 살펴보기 바란다. invokeAll()은 인자로 전달된 모든 작

업을 작업 큐에 넣고, 그들의 실행이 완료될 때까지 현재 작업을 멈

추는 메서드이다. 앞의 그림 설명에서 ‘하위 작업들을 작업 큐에 넣

고 현재 작업을 잠시 한쪽으로 치워두는’ 것이 invokeAll()에서 이

루어진다.

ForkJoinPool, ForkJoinTask

Fork/Join 프레임워크를 사용하기 위해 알아야 하는 것은

ForkJoinPool과 ForkJoinTask 둘뿐이다.

ForkJoinPool은 워커 스레드들을 관리하며, 외부에서 이 스레

드 풀에 작업을 요청할 때 사용할 수 있는 API를 제공한다. 간

단하게 말하자면, ForkJoinPool은 work stealing이 가능한

ExecutorService라고 할 수 있다. 실제로 ExecutorService를 구현

하고 있기 때문에 Runnable이나 Callable 객체를 ForkJoinPool

에서 실행시킬 수도 있다. 하지만 이 경우, ForkJoinPool의 혜택을

모두 누릴 수는 없다.

ForkJoinPool이 제공하는 효율적인 병렬 처리 효과를 제대로 보

려면 ForkJoinTask를 사용해야 한다. ForkJoinTaks는 하위 작업

을 작업 큐에 넣는 fork(), invoke(), invokeAll() 등의 메서드와 작

업이 종료되길 기다릴 때 사용하는 join() 등의 메서드를 제공한다.

이를 활용하여 추상 메서드인 execute()를 구현하면 된다.

Fork/join 프레임워크는 ForkJoinTask를 상속한 Recursive

Action과 RecursiveTask도 제공한다. 예시 코드에서 사용된

RecursiveAction은 리턴값이 없는 재귀 작업을 좀 더 간단하게 구

현하게 해주며 RecursiveTask는 리턴값이 있는 재귀 작업을 위한

것이다.

지면 관계상 각 클래스와 메서드들에 대해 깊이 살펴볼 수는 없으

므로 관심 있으신 사람들은 JDK 7의 Javadoc을 살펴보기 바란다.

private static class SortTask extends RecursiveAction {

private int[] a;

private int[] tmp;

private int lo, hi;

public SortTask(int[] a, int[] tmp, int lo, int hi) {

this.a = a;

this.lo = lo;

this.hi = hi;

this.tmp = tmp;

}

@Override

protected void compute() {

if (hi == lo) {

return;

}

int m = (lo + hi) / 2;

invokeAll(new SortTask(a, tmp, lo, m), new

SortTask(a, tmp, m+1, hi));

merge(a, tmp, lo, m, hi);

}

}

private static void merge(int[] a, int[] b, int lo, int m,

int hi) {

if (a[m] <= a[m+1])

return;

System.arraycopy(a, lo, b, lo, m-lo+1);

int i = lo;

int j = m+1;

int k = lo;

while (k < j && j <= hi) {

if (b[i] <= a[j]) {

a[k++] = b[i++];

} else {

a[k++] = a[j++];

}

}

System.arraycopy(b, i, a, k, j-k);

}

}

Page 207: The platform 2011

기술개발 동향206 The Platform 2011

Fork/Join 프레임워크 제작자인 Doug Lea가 Javadoc에 굉장히

상세한 설명을 달아놓았기 때문에 다른 레퍼런스가 거의 필요 없을

정도이다.

그 밖에 추가된 것들

JDK 7에서 java.util.concurrent 패키지에 추가된 클래스는 위에

언급한 것들 외에 몇 가지가 더 있다. 이들은 대부분 Fork/Join 프

레임워크의 구현에 필요하여 작성된 것이지만, 다른 곳에서도 유용

하게 사용될 수도 있기에 포함된 것들이다. 이들에 대해 간단히 살

펴보도록 하자.

• TransferQueue: BlockingQueue를 상속받은 인터페이스로,

transfer() 메서드를 호출하여 큐에 값을 넣으면, 누군가가 그 값을 꺼

내갈 때까지 블록되는 큐이다.

• LinkedTransferQueue: TransferQueue의 구현체이다.

• Phaser: CyclicBarrier와 유사하지만, 좀 더 유연하고 다양한 기능을 제

공한다.

• ThreadLocalRandom: java.util.Random은 여러 스레드에

서 동시에 사용할 경우 경합이 벌어져 성능에 영향을 줄 수 있다.

ThreadLocalRandom은 이런 상황을 피할 수 있게 해준다.

맺음말

JDK 7은 분명 오래 기다린 것에 비해서는 아쉽다. 게다가 JDK 7가

나오자마자, 버그가 있으니 JDK 7을 사용하지 말라는 소리가 릴리

스 소식보다 더 많이 들려온 것을 보면 앞날이 평탄치 않을 것 같기

도 하다. 어쨌든 우리는 좋으면 쓰고 안 좋으면 안 쓰면 되는 것이

고, JDK 7에는 써서 득 볼 수 있는 부분이 분명 있다고 본다.

짧은 글로 JDK 7의 모든 면을 샅샅이 살펴보기는 불가능하다. 여

기에서 미처 설명하지 못한 기능들이 많으며, 설명했다 하더라도

세세한 옵션이나 커스터마이징할 수 있는 부분에 대해 거의 언급하

지 못 했다. 그러니 이 글을 JDK 7의 전부가 아니라 JDK 7을 알아

가는 출발점으로 여기기 바란다.

Page 208: The platform 2011

구르믈 버서난 달처럼 207The Platform 2011

사람들은 왜 구름 위로 올라가고 싶어할까?

영어에 ‘Cloud Nine’이라는 표현이 있다. 단테의 신곡에서 천국으

로 가는 마지막 아홉 번째 계단을 지칭하는 말에서 유래했다고도

하고, 미국 기상청에서 구름을 위치에 따라 9단계로 나눈 가운데

제일 위의 구름이라고 하기도 하는데, ‘가장 행복한 순간, 절정의 순

간’이라는 의미로 사용한다. 그리고 유명한 노래도 있는 듯 하다. 또

나쁜 뜻으로도 사용되어서 담배 이름에 사용했다가 구설수에 오르

기도 했다. 아무튼 흔히 ‘구름 위를 걷는 기분’이라는 말을 하는데,

클라우드, 클라우드 컴퓨팅, 클라우드 서비스가 무엇이고 왜 필요

한지 먼저 살펴보도록 하겠다.

‘클라우드 컴퓨팅’의 정의부터 알아보자. 다양한 견해와 꽤 많은 정

의가 있지만 필자는 그 중에 ‘8000미터 상공을 나는 비행기에서 노

트북을 사용하는 것’이라는 표현이 맘에 든다. 물론 좀 더 기술적

인 정의가 있다. 많은 사람이 미국 국립 표준 기술 연구소(NIST)에

서 발표한 “The NIST Definition of Cloud Computing(http://

csrc.nist.gov/groups/SNS/cloud-computing/)”의 정의를 인용

한다.

지난 한두 해 동안 ‘클라우드’(정확히는 ‘클라우드 컴퓨팅’이라는

용어가 맞다)에 관심을 두고 살펴보면서 든 느낌은 “아! 이거, 말 그

대로 ‘뜬구름 잡는 이야기’구나.”였다. 클라우드가 구름이니 당연

한 걸까? 그러다가 사내 TF에 모여서 서로 논의하고, 구체적인 준

비를 위해 좀 더 현실적인 사안들을 고민하다 보니 이제는 어스름

이 걷히고 달이 구름에서 좀 벗어난 느낌이 든다. 이제 박흥용 작가

님의 만화 원작과 이준익 감독의 영화로 잘 알려진 “구르믈 버서난

달처럼”의 제목을 빌려서 ‘클라우드’에 대한 이야기를 해 보려 한다

(물론 원작의 내용과는 상관은 없지만 표현이 너무나 멋있어서 차

용했다).

이야기는 “사람들은 왜 구름 위로 올라가고 싶어할까?”부터 시작

한다. 이 질문에 대한 답변으로 클라우드의 개념과 필요성을, “구

름에도 새털구름부터 뭉게구름, 비구름 등 여러 종류가 있다”고 하

면서 클라우드 서비스의 분류(IaaS, PaaS, SaaS 등)와 몇 가지 예

를 살펴보고, “구름 제조 공장”에서 플랫폼 서비스에 필요한 기술

을 설명한다. 끝으로는 “구름에서 비가 내리면”, 그러니까 클라우

드 서비스 플랫폼을 개발한다면 어떤 모습일지 짧게 이야기하려고

한다. 자 그럼 이제 뜬 구름을 잡으러 가 볼까?

구르믈 버서난 달처럼아껴둔 술을 열고 좋아하는 친구를 부르는 날이,

그날이 가장 좋은 날이어야 합니다.

그날 만큼은 compiler warning이 생각나지 않고,

그날 만큼은 SQL 문법이 떠오르지 않아야 진정 좋은 날입니다.

그렇지 못한 분들과 함께 저는 불행한 개발자입니다.

•서비스플랫폼개발센터 _ 박기은

Page 209: The platform 2011

기술개발 동향208 The Platform 2011

• Measured Service: 사용량이 자동으로 측정되고, 제어되고, 보고된

다. 사용자에게뿐만 아니라 제공자에게도 자원 사용 감시가 자동으로

이루어진다.

주위에 '클라우드 서비스'라고 주장하는 서비스가 위 요건을 모두

구비하고 있는지 따져보는 것도 나름 재미있는 일이지 않을까?

다음으로 클라우드 컴퓨팅은 세 가지 서비스 모델과 네 가지 적용

모델이 있다고 정의하고 있다. 이 내용은 조금 있다가 클라우드 서

비스의 분류에서 살펴보도록 하겠다.

정리하면 클라우드 서비스를 '네트워크, 서버, 스토리지, 애플리케

이션, 서비스 등의 IT 자원을 소유하지 않고 필요할 때마다 인터넷

을 통해 일정 기간 동안 구매하여 편하고 빠르게 이용하는 방식'이

라고 정의할 수 있다. 그러니까 내가 개발한 프로그램을 실행할 웹

서버와 DB 서버가 필요할 때, 웹에서 간단히 클릭 몇 번만 하면 쓸

수 있다는 것이다. 또한 필요한 기능과 필요한 저장 용량의 웹 오피

스 서비스를 즉시 구매해서 사용하다가 중단할 수 있는 것도 분명

클라우드 서비스이다. 그러면 "그냥 웹 서비스와는 무슨 차이지?"

라는 의문이 들 테고, 그때는 위에서 살펴본 다섯 가지 속성들을 대

입해 보면 된다.

이제는 "클라우드 서비스가 왜 필요한가요?"라는 질문을 던져 보

자. 쉽게 답할 수 있다. 클라우드는 '싸고 좋은 것'이라서 필요하다.

흔히 말하는 TCO(Total Cost of Ownership) 절감이다. 클라우

드 서비스가 있으면 기업은 자체 IT 인프라를 소유하지 않아도, 구

축이나 운영에 대한 전문 지식이 없어도 IT 자원을 사용할 수 있게

된다. 게다가 서비스 부하에 따른 실시간 확장을 지원받고, 사용한

만큼 비용을 지불한다. 누구나 알고 있는 사실이지만 기업 입장에

서는 서버 한 대를 사 놓으면 서버의 하드웨어 가격뿐만 아니라 그

서버를 놓을 장소, 그 서버에 들어가는 전기, 그 서버를 관리할 사

람, 그 서버에 필수적으로 필요한 소프트웨어 구매 등등 이른바 IT

인프라 비용이 발생한다.

서비스가 대박날 것을 대비해서 서버를 넉넉히 준비해 놓고 있었는

데, "아뿔싸!" 이용자는 늘지 않고…… 돈이 부족해서 서버를 몇 대

준비 못했는데 폭발적인 반응에 “아이고, 서버만 더 있으면 저것이

다 돈인데”라고 하면서 급하게 서버를 증설하느라 가격도 제대로

깎지 못하는 상황. 이런 상황을 생각해 보면, ‘아~ 클라우드가 정말

필요하구나’라고 느끼게 된다. 경제 용어로 하면 리스크 헤징(risk

그림 1 무엇이 클라우드 컴퓨팅일까?

NIST에서는 "Cloud computing is a model for enabling

convenient, on-demand network access to a shared pool of

configurable computing resources (e.g., networks, servers,

storage, applications, and services) that can be rapidly

provisioned and released with minimal management effort

or service provider interaction. This cloud model promotes

availability and is composed of five essential characteristics,

three service models, and four deployment models”라고 세밀

하게 정의했다.

이 정의에서 이야기하는 클라우드 서비스의 다섯 가지 중요 특성을

살펴보면 다음과 같다.

• On-demand self-service: 소비자는 서비스 제공자 쪽 사람의 도움 혹

은 개입 없이 자동으로 필요한 컴퓨팅 자원을 사용할 수 있다.

• Broad network access: 다양한 클라이언트 플랫폼(스마트폰, 노트북,

데스크톱, 서버 할 것 없이)에서 네트워크를 통해 접근할 수 있다.

• Resource pooling: 컴퓨팅 자원을 여러 사용자가 공유하면서도 각자

독립된 환경으로 느끼는 multi-tenant(이걸 뭐라고 할까? 다중 세입

자?) 모델로 제공되고, 물리 자원은 필요한 사용자에게 동적으로 할당

된다. 사용자는 사용하는 자원들의 실제 위치나 실제 구성을 모른다.

• Rapid elasticity: 사용자가 언제든지 얼마만큼이든지 즉시 구매해서

사용할 수 있고, 필요에 따라 쉽고 빠르게 - 어떨 때는 자동으로 - 규모를

늘리거나 줄일 수 있도록 탄력적으로 제공된다.

Page 210: The platform 2011

구르믈 버서난 달처럼 209The Platform 2011

• 서비스 자동화 및 지속성을 통해 운용, 보수, 갱신을 합리화할 수 있다.

• 다른 클라우드 서비스를 사업자 측에 결합해 저렴한 가격으로 다양한

서비스를 제공할 수 있다.

• 사용자의 수와 상관없이 서비스를 공급할 수 있는 분산, 가상화의 특징

이 있다.

이제 사람들이 왜 자꾸 구름위로 올라가려고 하는 지 알 수 있

겠는가? 그렇다, 합리화, 최적화, 그리고 비용 절감이다. 아마존

(Amazon)이 자주 사용하는 70/30 스위치(switch)를 표현하는 그

림은 클라우드의 이점을 단순 명료하게 설명하고 있다.

그림 3 70/30 Switch2

그럼 우리가 잘 알고 있는 대형 솔루션 벤더의 움직임을 살펴보자.

클라우드에 선구자 격인 아마존, 워낙 유명한 구글(Google), 나름

선전하고 있는 마이크로소프트(Microsoft), 생소하지만 굳건히 자

리잡고 있는 salesforce.com, 업계에서 잔뼈가 굵은 노병인 IBM과

Oracle 등 모두 자신만의 서비스와 솔루션을 내놓고 있다.

2 AWS 101 Cloud Computing Seminar, Amazon, 2010

hedging)이라고 할 수 있다. IT 인프라에 미리 투자하지도 너무 늦

게 투자하지도 않고 필요한 만큼 필요한 시기에 사용하는 것이다.

그것이 서버를 직접 구매해서 운영하는 것 보다 비싸게 보일 수도

있지만 결국은 TCO 절감이라는 IT 업계의 절대 화두에 부응하는

길이다.

그림 2 Gartner의 클라우드 컴퓨팅 정의1

구체적으로 사용자 측면에서 클라우드 서비스의 장점은 다음과 같다.

• 자본 비용과 운용, 유지의 부담 없이 저렴하게 고기능의 정보 처리 기능

을 이용할 수 있다.

• 시스템 구축, 개발 기간이 단축되고 수요 변동에 유연하고 즉각적으로

대응할 수 있다.

• 네트워크를 통해 협업, 데이터 수집, 제어가 용이하다.

• 장비 분실 등에 따른 정보 유출 위험을 감소시킨다.

• 클라우드 기반의 지속성에 의해 사업의 연속성이 향상된다.

• 사용한 비용만 청구받고 지불하면 된다.

사업자 측면에서 본 클라우드 컴퓨팅의 장점은 다음과 같다고 할

수 있다.

• 가상화, 분산 처리 기술을 활용함으로써 다양한 정보 처리 수요에 대해

자원을 효율적으로 제공할 수 있다.

• 애플리케이션 표준화를 통해 소프트웨어 자원을 효율적으로 활용할 수

있다.

1 The Cloud Computing Scenario, Gartner, 2008

Page 211: The platform 2011

기술개발 동향210 The Platform 2011

비하면 많이 좋아진 것이다. 아, 그리고 한 반나절 정도 쓰다가 정

지시켜 놓을 수 있다. 돈은 사용한 시간만큼만 낸다.

그런데, 이렇게 눈에 보이는 차이만이 클라우드 서비스의 전부가

아니다. 가장 핵심은 서비스 제공 업체가 가상 서버를 사용하게 해

주는 과정에서 사람이 개입하지 않는다는 점이다. 이는 제공 업체

의 원가에 지대한 영향을 미치고, 결국 상품의 가격 경쟁력으로 이

어진다. 그래서 사용자는 빠르고 쉽게, 그리고 싸게 서비스를 구매

할 수 있게 된다.

AWS의 S3 서비스는 EC2와 더불어 가장 유명한 클라우드 상

품이며, 이제는 우리 모두에게 너무나 친숙한 클라우드 스토리

지 서비스이다. N드라이브 같은 개인 파일 저장 서비스와 차

이는, S3는 REST API를 사용하는 플랫폼으로 제공된다는 점

이다. NHN의 분산 파일 시스템인 OwFS(Owner-based File

System)를 웹에서 필요한 용량만큼 구매해 사용할 수 있다고

보면 된다. 가끔 유용하게 사용하는 SlideShare(http://www.

slideshare.net)도 AWS의 EC2, S3, EBS, SQS, MapReduce 등

을 이용해서 제공되는 서비스라고 한다4. 이처럼 웹 서비스 제공

업체도 굳이 인프라에 대규모 투자를 하지 않고 클라우드를 이

용해 사업할 수 있는 환경이 되었다.

구글

구글은 Gmail부터 문서 도구까지 기업을 위해 클라우드 기반 오

피스 환경을 제공하는 Google Apps 서비스도 있지만, 구글의 인

프라에서 웹 애플리케이션을 실행할 수 있는 환경을 제공하는

Google App Engine 클라우드 서비스가 유명하다.

App Engine은 제공되는 툴킷을 이용해 자바나 파이선으로 프로그

램을 개발하고 제공하는 인프라(웹 서버, 네트워크, 스토리지 모두)

에서, 관리나 확장에 대한 고민 없이 프로그램을 실행할 수 있다.

4 http://www.jonathanboutelle.com/how-slideshare-uses-amazon-web-services

아마존

아마존은 EC2(Elastic Compute Cloud), S3(Simple Storage

Service)로 대표되는 AWS(Amazon Web Services) 클라우드 서

비스를 제공하고 있다. AWS는 클라우드 서비스의 대표 주자답

게 다양한 유형의 서비스 제품이 있다. 이 원고를 쓰기 며칠 전

에도 AWS 메일링 리스트로 Oracle RDS(Amazon Relational

Database Service) 서비스가 시작되었다는 뉴스가 날아왔는데, 정

말 하루가 다르게 새로운 서비스 제품을 내놓고 있다.

그림 4 Amazon Web Services3

아마존의 EC2 서비스는 클라우드 플랫폼 서비스의 최강자이면서

표준이라고 할 만큼 잘 만들어져 있다. Xen 가상화 기술 기반으로

사용자에게 서버를 제공하는데, 수 분만에 웹을 통해 서버를 할당

받아 사용할 수 있다. VM을 사용하게 해 주는 것이 뭐 대수라고 쉽

게 생각할 수 있는데, 시스템 관리자의 개입 없이 여러 IDC에서 수

만대의 서버에 VM을 자동으로 올리고, 자동으로 할당하고, 자동

으로 환경을 구성하고, 문제가 생기면 자동으로 이전시켜 주고, 그

것도 온 세상 사람들을 대상으로 웹 인터페이스를 통해서라고 생각

하면 그리 녹록한 일이 아님을 알게 된다. 서버에 사용할 VM 이미

지(OS와 다양한 패키지 구성을 다 합한)도 마음대로 지정하고, 심

지어 자신만의 VM 이미지를 만들어서 업로드해 사용할 수도 있으

니 말이다. 신청하고 며칠에서 일주일 정도 기다려야 사용할 수 있

고, 보통 월 단위로 돈을 내야 하던 기존의 호스팅 업체 서비스에

3 http://aws.amazon.com/

Page 212: The platform 2011

구르믈 버서난 달처럼 211The Platform 2011

그림 6 Windows Azure6

salesforce.com

salesforce.com은 웹 기반 CRM 솔루션(영업 자동화, 파트너 관계

관리, 마케팅 자동화, 고객 서비스 및 자원 자동화 등)을 서비스하

는 대표적인 회사이다. 클라우드 기반의 플랫폼 서비스 업체로 거

듭나기 위해 내부적으로 사용하던 클라우드 컴퓨팅 기술을 서비스

화시켜 Force.com이라는 서비스를 내놓았다. 자사의 CRM 애플

리케이션을 사용자별로 구동시키면서 인프라 자원을 효율적으로

사용할 수 있게 하는 개발 툴과 환경을 따로 서비스로 제공하겠다

는 것이다. 이는 책을 파는 아마존이 서비스 개발과 제공을 위해 내

부적으로 갖춰 놓은 환경을 AWS로 상품화한 것과 같다. 클라우드

서비스가 어느 날 갑자기 시작되었다기보다는, 다들 자기 서비스

운영의 효율화를 위해 자체적으로 구축하고 있던 노하우와 운영 환

경을 상품화한 것임을 알 수 있다.

6 http://www.livbit.com/article/2010/02/02/microsoft-azure-platform-is-now-

officially-open-for-business/

사용료는 HTTP 요청 수와 CPU 사용 시간, 네트워크 사용량에 따

라 지불한다.

그림 5 Google 애플리케이션 개발자 도구5

마이크로소프트

마이크로소프트는 패키지 소프트웨어의 강자답게 “소프트웨어 +

서비스”라는 전략을 바탕으로 Windows Azure와 SQL Azure,

AppFabric 클라우드 서비스를 내놓고 있다.

Windows 기반의 가상 서버와 SQL Server를 클라우드에서 구매

해 사용할 수 있다. Visual Studio와 SQL Server를 이용해서 내

PC에서 프로그램을 작성하면 그대로 클라우드로 보내(deploy) 실

행할 수 있어서, Microsoft 개발 패키지 소프트웨어의 강점을 그대

로 살리고 있다.

5 http://code.google.com/intl/ko/googleapps/

Page 213: The platform 2011

기술개발 동향212 The Platform 2011

층운이고 천둥 번개를 동반하는 강한 비구름은 적란운이다. 이렇듯

구름에도 여러 종류가 있듯이 '클라우드 서비스'에도 여러 종류가

있다. 앞서 살펴보았던 NIST의 클라우드 컴퓨팅 정의에 따른 분류

를 알아보도록 하겠다.

NIST의 문서에서는 클라우드 컴퓨팅을 서비스 모델(Service

Model)에 따라 SaaS와 PaaS , IaaS로 나누고, 이용 모델

(Deployment Model)에 따라 Private과 Community, Public,

Hybrid로 나눈다.

• SaaS(Cloud Software as a Service): 공급자의 서비스 애플리케이션

이 클라우드 인프라에서 실행되는 형태. 소비자는 하부의 클라우드 인

프라와 애플리케이션 프로그램에 대해서는 관리하거나 제어하지 않는

다.

• PaaS(Cloud Platform as a Service): 공급자가 제공하는 프로그래밍

언어나 도구로 작성하거나 미리 준비된 애플리케이션을 클라우드 인프

라에서 실행할 수 있는 형태. 소비자는 하부의 클라우드 인프라에 대해

서는 관리하거나 제어하지 않지만, 사용하는 애플리케이션 프로그램과

실행 환경 설정을 제어한다.

• IaaS(Cloud Infrastructure as a Service): 소비자가 OS를 포함한 소

프트웨어를 설치해 사용할 수 있도록 미리 구성된 컴퓨팅 환경(서버, 스

토리지, 네트워크 등)이 제공되는 형태. 소비자는 하부의 클라우드 인프

라에 대해서는 관리하거나 제어하지 않지만 OS와 스토리지, 이용하는

애플리케이션 프로그램, 제한된 네트워크 설정까지 제어한다(그림 9).

SaaS는 클라우드 서비스 중 가장 일반적인 유형으로서, 대부분의

사용자가 언젠가 사용해 본 경험이 있는 유형이다. SaaS 클라우

드 모델에서는 서비스 제공자가 모든 인프라와 소프트웨어 제품

을 제공하고, 사용자는 웹 기반의 프론트엔드를 사용하여 서비스를

사용한다. 이러한 서비스는 Gmail 같은 웹 기반의 메일에서부터

Dropbox와 같은 데이터 싱크 솔루션, Mint와 같은 금융 소프트웨

어에 이르기까지 다양하다.

PaaS는 서비스 제공자가 자체 하드웨어 인프라에서 호스트하

는 소프트웨어와 제품 개발 도구를 제공하는 클라우드 서비스로

서, 사용자는 제공된 API와 플랫폼 또는 개발용 툴을 사용하여 애

플리케이션을 개발할 수 있다. 이러한 유형의 서비스에 대한 일

반적인 사례로는 salesforce.com의 Force.com과 Google App

Engine, Microsoft의 Azure 플랫폼 등을 들 수 있다. AWS 중에서

MySQL이나 Oracle을 클라우드로 사용할 수 있게 하는 RDS 서

비스의 경우도 PaaS라고 보면 되겠다.

그림 7 salesforce.com7

구름에도 새털구름부터 뭉게구름,

비구름 등 여러 종류가 있다

그림 8 구름의 분류8

세계기상기구(WMO)에서는 구름을 10종류로 구분한다고 한다.

푸른 하늘에 하얗고 얇게 떠 있는 새털구름은 상층운 중에 권운, 날

씨가 좋은 봄이나 가을에 자주 등장하는 뭉게구름은 하층운 중에

적운에 해당한다. 보통 비가 오기 전에 몰려오는 검은 비구름은 난

7 http://www.salesforce.com/

8 http://www.doopedia.co.kr/doopedia/master/master.do?_method=view&MAS

_IDX=101013000828688

Page 214: The platform 2011

구르믈 버서난 달처럼 213The Platform 2011

이외에도 다양한 XaaS 용어가 쓰이고 있는데, DaaS(Database

as a Service)는 PaaS의 일부로 데이터베이스

를 클라우드 형태로 제공하는 것을 지칭하고,

MaaS(Management as a Service)는 인프라스

트럭처의 운영 관리를 아웃소싱 서비스로 제공

하는 것을 의미한다. 클라우드 붐이 일면서 이것

저것 가져다 붙인다는 느낌도 좀 있지만, 사용자

에게 디바이스, 위치, 네트워크와 상관없이 데스

크톱 환경을 제공하고, 사용자 데이터를 효율적

으로 관리할 수 있도록 하는 클라우드 서비스를

‘Desktop as a Service’라고 부르는 사람도 있다.

더 다양한 클라우드 분류가 궁금하다면 ‘Cloud

Taxonomy’ 사이트(http://cloudtaxonomy.

opencrowd.com/taxonomy/)를 참고하기 바란

다(그림 10).

‘SaaS 생태계(Ecosystem)로서의 PaaS’라는 개

념도 있다. 시장 조사 전문 회사인 Forrester의 리

포트인 “Forrester’s Platform-As-A-Service

Reference Architecture”10에서 제시된 PaaS의

참조 아키텍처에서는 SaaS의 기반이 되는 PaaS

의 측면을 강조하고 있다. 일반 사용자에게 클

라우드 형태의 다양한 서비스를 제공하는 SaaS

ISV(Independent Software Vendor)들이 독자

적인 클라우드 시스템을 갖는 것이 아니라 PaaS

제공자의 플랫폼 위에 SaaS 애플리케이션을 제

공하는 유형이 많다는 것이다. 이 리포트에서 제

시하는 PaaS 기반으로 SaaS 애플리케이션을

이용한 클라우드 서비스 유형은 이미 시장에서 다양하게 나타나

고 있다. IaaS((Infrastructure as a Service) 클라우드 서비스는

Quora(http://www.quora.com)의 조회 결과에서 볼 수 있듯이

이미 일반 사용자의 사용보다는 SaaS ISV가 더욱 많이 사용하고

있다.

10 http://www.forrester.com/rb/Research/forresters_platform-as-a-service_

reference_architecture/q/id/53987/t/2

그림 9 IaaS와 PaaS, SaaS의 정의9

IaaS는 기본 빌딩 블록에 대한 액세스를 서비스 형태로 제공하는

클라우드 서비스로서, 사용자는 이러한 빌딩 블록을 이용해서 애

플리케이션을 실행하는 데 필요한 인프라를 구축할 수 있다. IaaS

와 관련된 가장 유명한 사례는 AWS(Amazon Web Services)와

Rackspace이다.

9 http://www.katescomment.com/iaas-paas-saas-definition/

Page 215: The platform 2011

기술개발 동향214 The Platform 2011

ISV(Independent Software Vendor)들이 독자적인 클라우드 시스

템을 갖는 것이 아니라 PaaS 제공자의 플랫폼 위에 SaaS 애플리케

이션을 제공하는 유형이 많다는 것이다. 이 리포트에서 제시하는

PaaS 기반으로 SaaS 애플리케이션을 이용한 클라우드 서비스 유

형은 이미 시장에서 다양하게 나타나고 있다. IaaS((Infrastructure

as a Service) 클라우드 서비스는 Quora(http://www.quora.

com)의 조회 결과에서 볼 수 있듯이 이미 일반 사용자의 사용보다

는 SaaS ISV가 더욱 많이 사용하고 있다.

그림 10 Cloud Taxonomy11

‘SaaS 생태계(Ecosystem)로서의 PaaS’라는 개념도 있다. 시장 조

사 전문 회사인 Forrester의 리포트인 “Forrester’s Platform-As-

A-Service Reference Architecture”12에서 제시된 PaaS의 참조 아

키텍처에서는 SaaS의 기반이 되는 PaaS의 측면을 강조하고 있다.

일반 사용자에게 클라우드 형태의 다양한 서비스를 제공하는 SaaS

11 http://www.opencrowd.com/assets/images/views/views_cloud-tax-lrg.png

12 http://www.forrester.com/rb/Research/forresters_platform-as-a-service_

reference_architecture/q/id/53987/t/2

Page 216: The platform 2011

구르믈 버서난 달처럼 215The Platform 2011

으로 AWS의 EC2와 같은 개념의 N-Server 클라우드 서비스를 제

공할 수 있는데, N-Server는 탐력적인 컴퓨팅과 탄력적인 저장소

(Elastic Storage, AWS의 EBS 개념)를 모두 제공하기 위해 VM과

ViSTO 솔루션을 활용한다.

PaaS 부분은 X- Storage 서비스, X-RDBMS 서비스 등과 같은

클라우드 플랫폼 서비스를 제공하기 위해 사용하는 시스템 혹은 솔

루션을 표시한 부분으로서, 플랫폼 서비스에 접근하는 REST API

를 제공하는 API 게이트웨이, API 기반의 스토리지 서비스를 제공

할 수 있는 OwFS, AWS RDS와 같은 가상의 RDBMS 인스턴스

와 탄력적인(elastic) 데이터 볼륨을 지원하는 N-RDBMS 서비스

등에 활용될 CUBRID, Microsoft Azure Queue나 AWS SQS와

같은 큐 서비스를 제공하는 데 활용할 수 있는 Jamie, OwFS 기반

의 심플 스토리지 서비스에서 대용량 파일 다운로드를 효과적으로

지원하기 위한 P2P 솔루션 등이 포함된다.

그리고 클라우드 시스템 관리 기능으로 빌링 시스템, 인증 계정 관

리, PROV(서버/플랫폼 프로비저닝과 모니터링을 위한 시스템),

CoVI, Nsite, Ntree 등이 표시되어 있다. 마지막으로 SaaS 부분에

는 다양한 클라우드 서비스가 있을 수 있다. 예를 들어 OwFS를 파

일 저장소로 사용하는 웹 스토리지 서비스인 N드라이브 솔루션 같

은 경우가 여기에 해당한다.

제시한 PaaS 참조 아키텍처는 SaaS의 기반 시스템으로서의 PaaS

라는 의미도 내포하고 있는데, PaaS 유형의 플랫폼 서비스를 직접

사용하는 개인 사용자들도 있을 수 있지만, 대부분의 클라우드 서

비스 시장은 SaaS 중심으로 이루어지므로 PaaS 서비스 제공자는

결국 SaaS를 구현하는 ISV들을 대상으로 하는 B2B 서비스일 것이

라고 가정할 수 있다. 즉, SaaS 생태계로서의 PaaS라는 개념이다.

그림 11 AWS 상위 10개 고객 사이트13

구름 제조 공장

여러분이 플랫폼 클라우드 서비스를 만든다고 생각해 보자. 예를

들어 가상 서버를 제공하는 X-Server 상품, 스토리지 API를 제공

하는 X-Storage 상품, CUBRID 같은 RDBMS를 사용할 수 있는

X-RDBMS 상품 등을 포함하여 다양한 클라우드 서비스 상품을

제공할 수 있는 클라우드 시스템은 어떤 모습일까?

이와 관련해서 TF에서는 보유하고 있는 기술 자산(플랫폼 제품) 가

운데 클라우드로 서비스할 수 있는 플랫폼은 무엇이고, 어떤 구조

를 가질 것이며, 어떻게 구현할 수 있을까 등등을 연구하고 논의했

다. 여러 클라우드 서비스와 솔루션을 분석하고, 클라우드 시스템

아키텍처도 그려 보고, 각 플랫폼 별로 기능도 정의하고, 가상의 서

비스 시나리오를 통해 타당성도 살펴보는 등의 일을 했다. 이중에

PaaS 참조 아키텍처 그림을 먼저 살펴보도록 하겠다.

PaaS 참조 아키텍처는 크게 5가지 부분으로 이루어져 있다. 가

장 하부의 데이터 센터는 실제 물리적인 자원과 운영 부분을 표시

한 것이다. IaaS 부분에는 가상 서버(VM)을 중심으로 가상 스토

리지(ViSTO)나 분산 처리(Hadoop) 등 탄력적인 컴퓨팅(Elastic

Computing)을 구현할 수 있는 시스템 혹은 솔루션이 포함된다.

IaaS 부분은 직접적으로 외부에 IaaS 클라우드 서비스를 제공할 수

도 있고 상위 PaaS 부분에 활용될 수도 있다. 예를 들어 IaaS 부분

13 http://www.quora.com/Who-are-the-top-10-Amazon-AWS-customers

Page 217: The platform 2011

기술개발 동향216 The Platform 2011

구성 요소 플랫폼 추가 요구 사항

스토리지

시스템

OwFS •API G/W와의 연동

•모니터링 시스템과의 연동

메시징

시스템

Jamie •제품의 안정성 확보

•사실상(De-facto)의 표준 프로토콜 지원

•확장성 측면에서의 시스템 구조 개선

P2P 시스템 Neobit •트래픽 증가에 따른 시스템 확장성 측면

•API G/W와의 연동

아 래 그 림 은 미 국 연 방 조 달 청 인 G S A 에 서 클 라 우 드

RFQ(Request For Quotation, 견적 요청)로 정의한 "정부 클

라우드 컴퓨팅 프레임워크"(Government Cloud Computing

Framework)이다. 그림을 살펴보면 데이터 센터 시설에서부터 보

안과 개인 정보 관리, 서비스 관리와 프로비저닝 등의 측면까지 다

양한 요소를 정의한 것을 알 수 있다. 이와 같이 클라우드 서비스

시스템은 사용자에게 드러나는 서비스 이외에도 다양한 기능 요소

가 필요함을 알 수 있다(그림 13).

클라우드 서비스 시스템, 이른바 '구름 제조 공장'을 구축하

기 위해서 제일 먼저 갖춰야 할 설비는 위의 그림에서 "Cloud

Service Delivery Capabilities"로 명명된 영역, 그러니까 "Cloud

Management Platform" 이라고 불리는 솔루션일 것이다. 그리

고, "Cloud User Tool"로 명명된 영역도 '구름 제조 공장'에 꼭 필

요한 설비이다. 클라우드 OS라고도 불리는 클라우드 관리 플랫폼

은 Xen이나 KVM, VMware 등의 하이퍼바이저 기술을 기반으

로 보유하고 있는 데이터 센터 인프라 환경을 클라우드화해 준다.

어떻게 보면 IaaS 수준의 클라우드 환경을 제공하는 솔루션이라

고 할 수 있다. 유명한 솔루션으로는 Cloud.com의 CloudStack,

RightScale의 RightScale Cloud Management Platform,

enomaly의 Elastic Computing Platform, Eucalyptus Systems

의 Eucalyptus, C12G Labs의 OpenNebula 등이 있다. 모두 오픈

소스 버전과 상용 버전 제품이 있으며, 다른 많은 오픈 소스 도구를

활용하고 있다. 기업에서 자체적으로 사용할 Private 클라우드 구

축을 위한 솔루션도 있고, 외부에 클라우드 서비스를 제공할 목적

으로 사용할 Service Provider 에디션도 있다.

그림 12 PaaS 참조 아키텍처

TF에서는 플랫폼 서비스 준비에 대한 나름의 결론으로, 참조 모델

에 있는 각 구성 요소별로 추가 개발해야 할 내용을 표로 정리했다.

표 1 PaaS 아키텍처 구성 요소

구성 요소 플랫폼 추가 요구 사항

API

게이트웨이

Open API

G/W

•클라우드 방식을 위한 인증 방식 정의와 관련 기능

•빌링 시스템과의 연동 방식 정의와 관련 기능

• Auto-scaling을 지원하기 위한 구조와 기능(트래픽 증가에

따른 시스템 확장성)

•플랫폼 서비스 유형별 API G/W 기능의 차이에 따른 변경

빌링 시스템 빌링 시스템 • 대량의 과금 정보를 처리할 방법론(예를 들어 Billing

Mediation Platform)

•플랫폼 서비스 상품 기획에 따른 서비스 과금 로직

•트래픽 증가에 따른 시스템 확장성

프로비저닝

시스템

CoVI, PROV,

Ntree 등

•VM(서버 프로비저닝)에서부터 플랫폼 소프트웨어까지 통

합 관리 기능

•다양한 VM 이미지 설정 배포 기능

• 클라우드 방식을 위한 완전 자동화된 리소스 프로비저닝(서

버 및 네트워크 할당 자동화)

•모니터링 시스템과의 연계(Auto-scaling)

•서비스 가입자 포털과의 연계(API G/W와의 연동 포함)

모니터링

시스템

Nsite • 사용자 경험에 기반하는 서비스 모니터링의 정의와 관련 기

•사용자 단위의 리소스 및 서비스 모니터링 기능

•플랫폼 서비스 유형별 모니터링 항목의 정의와 관련 기능

•서비스 가입자 포털과의 연계(API G/W와의 연동 포함)

데이터베이스

시스템

CUBRID •Elastic 데이터 볼륨 확장 기능(예를 들어 ViSTO 연동)

•과금을 위한 사용량 측정 메커니즘 및 과금 시스템과의 연

• 플랫폼 서비스 상품 기획에 따른 백업 등 운영 관리 정책 및

방법론

•모니터링 시스템과의 연동

Page 218: The platform 2011

구르믈 버서난 달처럼 217The Platform 2011

그림 13 미국 연방 조달청에서 정의한 정부 클라우드 컴퓨팅 프레임워크14

그림 14 클라우드 컴퓨팅 맵15

14 Cloud Computing Request For Quotation, GSA, 2009

15 http://broadcast.oreilly.com/2010/06/cloud-computing-mind-map.html

Page 219: The platform 2011

기술개발 동향218 The Platform 2011

화면에 나온 로그인 정보를 이용해 할당 된 서버에 들어가서

CUBRID 클러스터 테스트 시작.

이틀 뒤에 필요한 테스트는 다 되었으니 반납. 예산 신청 시에 부여

받은 컴퓨팅 자원 포인트가 서버 3대 x 2일 치만큼 줄어들었군.

새로운 플랫폼인 nSpider의 설계는 끝났고, 코딩을 하려니 시험해

보면서 쓸 DB가 필요한데 개발용 DB 장비는 어떻게 세팅하지? 난

CUBRID는 설치할 줄 모르는데. DB 용량은 얼마가 된다고 해야

하지? 테스트 데이터가 꽤 큰데.

사내 가상 RDB 신청 사이트에서 CUBRID 서버 한 대 신청서를

일단 작성하고 꾹~

화면에 나온 DB 접속 정보를 JDBC URL로 사용해서 개발 시작.

테스트 데이터를 부어 넣다 보니 DB 볼륨 용량이 좀 아슬아슬하니

스토리지 용량 추가 버튼 꾹~

지금까지 DB에 넣은 테스트 데이터를 나중에 다시 쓰려고, 가상

RDB 백업 버튼 꾹~

지난 주에 새로 오픈한 서비스의 트래픽이 점점 늘어나서 긴급으로

웹 서버 10대를 추가로 투입했는데, 지금 모니터링 하니까 다시 트

래픽이 줄고 있네.

아깝다 저 서버 팽팽 놀고 있네. 개발 서버로나 쓸 수 있으면 좋으

련만.

클라우드 컴퓨팅 관련 기술과 관련 업체, 솔루션을 모두 나열하기

는 어렵지만, 인터넷에서 또 다음과 같은 공장 설계도(The Cloud

Computing Map)를 구할 수 있었다. 그림이 너무 커서 한번에 잘

안 보이는데, 뿌리인 Cloud Computing에서 뻗어 나온 굵은 가지

들은 Characteristics, Deployment Models, Types of Clouds,

Cloud Stack, Benefits, Barriers, Vendors, Projects이다. 그 중에

Cloud Stack로 뻗어 나온 가지만 살펴보겠다. 전체 그림은 http://

broadcast.oreilly.com/2010/06/cloud-computing-mind-

map.html에서 볼 수 있다(그림 14).

구름에서 비가 내리면

이제 하늘에 구름이 잔뜩 끼었다. 아무래도 곧 비가 내릴 것 같다.

구름에서 비가 내리면 우리에게는 어떤 이로운 점이 있을까? 땅은

비옥해지고, 곡식은 잘 자라고, 먹을 물도 풍부해지고, 더운 날씨도

식혀주고, 이로운 점이 많다. 그럼 클라우드 서비스가 개발자인 당

신의 업무에 적용되면 어떤 이로운 점이 있을까? 한번 상상해 보자.

예를 들어 가상 서버, 가상 스토리지, 가상 RDB 이런 서비스가 있

을수 있는데, 사내 클라우드 서비스는 개발자에게도 여러 변화를

가져올 것이다. 아마 다음과 같은 가상의 시나리오가 가능하지 않

을까?

CUBRID 클러스터를 잠깐 테스트해야 하는데 팀 내 테스트 서버

의 현황이 어떻지? 세 대 정도 비어 있는 서버가 있나? 테스트 서버

가 다 사용 중이군. 잡아 놓은 예산은 있지만 한두 번 쓰자고 새로

서버를 신청할 수도 없고, 신청해도 최소 일주일은 있어야 될 텐데.

사내 가상 서버 신청 사이트에서 VM 세 대 신청서 작성하고 꾹~

Page 220: The platform 2011

구르믈 버서난 달처럼 219The Platform 2011

마치며

끝으로, 궁금해할지도 모를 “cafe24 호스팅과 같은 서버 호스팅 업

체에서 하는 가상 서버 호스팅과 아마존의 EC2의 차이가 무엇일까

요?”와 같은 질문에 답해 보겠다. 호스팅과 클라우드 IaaS의 경계

는 애매모호하다. GoGrid나 Rackspace 같은 대형 호스팅 업체는

“클라우드 호스팅(Cloud Hosting)”이라는 용어를 쓴다. 아마 클라

우드 호스팅의 반대되는 용어는 전용 호스팅(Dedicated Hosting

혹은 Managed Hosting)일 것이다. 아무튼 클라우드 컴퓨팅의 개

념은 호스팅보다는 더 큰 개념이고 더 많은 영역을 이야기한다. 그

리고 호스팅 서비스가 NIST에서 이야기하는 5가지 클라우드 컴퓨

팅 속성을 만족시킨다면 그것이 클라우드일 것이다. 아무튼 cafe24

의 가상 서버와의 차이는 “on-demand self-service로 사용할 수

있느냐”와 “사용량 기반으로 과금하느냐”일 것이다. EC2에서는 디

스크 용량이 부족하면 사용자가 웹에서 스스로, 그리고 즉시 EBS

스토리지를 추가할 수 있고, CPU나 메모리가 부족하면 서버의 내

용을 그대로 유지하면서 상

위 VM 모델로 바꿀 수 있

다. cafe24에서도 이런 서비

스가 가능할까?

‘클라우드’라는 주제에 대해

서 나름 설명을 해 봤다. 어

떻게 이제는 구름 사이로 달

빛이 좀 비추기 시작하는지,

아니면 아직도 뜬 구름 잡기

(분명히 저기 있는데 손에는

안 잡히는)라고 생각되는지

모르겠다.

이번 서비스는 가상 서버 관리 API를 이용해서 트래픽이 늘어나고

시스템 부하가 임계치를 넘으면 auto-scaling API로 자동으로 웹

서버를 늘리고 가상 부하 분산 기능 설정되도록 해 놓았으니 장애

걱정이 좀 주는군.

트래픽이 줄면 auto-scaling API로 웹 서버 수를 줄이는 것도 빨

리 코딩해야지.

이런 소설 같은 이야기를 가능하게 해 주는, 클라우드 서비스의 아

키텍처는 아마도 다음 그림과 같이 되지 않을까 싶다.

그림 15 클라우드 서비스 아키텍처

Page 221: The platform 2011

기술개발 동향220 The Platform 2011

장소로 Microsoft SQL Server를 이용하고 있다. 그리고 RDBMS

가 테라바이트나 페타바이트 단위의 대용량 데이터를 처리하기에

는 부담이 있다고 하지만, 샤딩(sharding)으로 극복할 수 있는 경

우가 많다. NHN의 카페, 블로그, 뉴스 서비스도 RDBMS를 샤딩

하여 쓰고 있다.

NoSQL은 비관계형(non-relational) 데이터 모델, 나아가서 스키

마가 없는(schema-free) 데이터 모델을 제공한다. 그 때문에 시스

템의 수평적인 확장이 용이하다. 대신 RDBMS보다는 덜 구조화

되었고, ACID(Atomicity, Consistency, Isolation, Durability)

를 보장하지 않는다. 즉, INSERT 오퍼레이션이 성공한 이후

SELECT 오퍼레이션을 실행했을 때 다른 값을 얻을 수 있다. 또한

NoSQL 데몬이 다운되어 복구한 이후 저장된 값이 다운되기 이전

상태와 다를 수 있다. 당연히 트랜잭션이나 JOIN 같은 오퍼레이션

은 사용할 수 없다. 그러나 NoSQL은 ACID를 버린 대신 데이터

저장에 대한 유연성, 확장성, 가용성을 증대했다. 그렇기 때문에 폭

발적인 대량의 데이터 처리에 더 적합하다는 장점이 있다.

NoSQL이 대두되면서 다양한 서비스 영역에서 NoSQL 제품을

데이터 저장소로 사용하려 한다. 그렇다면 NoSQL 제품은 어떤

특성이 있고, 어떤 서비스 영역에서 사용하기에 적합할까? 이 글

에서는 NoSQL 제품 가운데 잘 알려진 카산드라(Cassandra)와

HBase, MongoDB를 대상으로 성능을 비교하고 그 특성을 파악

해 본다.

왜 NoSQL인가?

NoSQL에 대한 관심이 증가하는 까닭은 처리해야 하는 데이터가

점점 늘고 있기 때문이다. 구글, 페이스북과 같은 세계적인 인터넷

기업은 폭증하는 데이터를 처리하기 위해서 저마다 NoSQL 솔루

션을 가지고 있다.

왜 이들은 RDBMS 대신 NoSQL을 사용하고 있을까?

트위터는 아직도 MySQL을 잘 이용하고 있고, 페이스북에 비하여

규모가 작기는 하지만 세계적 SNS 기업인 마이스페이스도 핵심 저

왜 NoSQL인가?10여 년간 DBMS 연구 개발을 해 왔으며 Query Processing 분야에 대한

전문성을 가지고 있습니다.

OpenSource DBMS인 CUBRID를 개발하였으며,

현재는 NHN에서 분산 데이터베이스인 nStore를 개발하면서

NoSQL 및 차세대 DBMS에 대해 관심을 두고 연구하고 있습니다.

•저장시스템개발팀 _ 이혜정

Page 222: The platform 2011

왜 NoSQL인가? 221The Platform 2011

카산드라의 최신 버전은 0.8.0이지만 안정성에 문제가 있어 이전

버전인 0.7.4를 선택했다. HBase의 경우, Hadoop-append 버전

이 아니면 HDFS(Hadoop Distributed File System)의 지속성

(durability)이 떨어지는 문제가 있어 Hadoop-append 버전을 선

택했다.

테스트 워크로드는 다음과 같다.

• Insert Only

비어 있는 데이터베이스에 1KB 크기의 레코드를 5천만 건 입력한다.

• Read Only

1KB 크기의 레코드가 5천만 개 들어 있는 데이터베이스에서 1시간 동

안 Zipfian 분포로 키를 조회한다.

• Read & Update

Read Only와 동일한 조건에서 Read 오퍼레이션과 Update 오퍼레이

션을 1대 1로 실행한다.

시험 장비는 3대이며, 각 장비의 규격은 모두 동일하게 다음과

같다.

• CPU: Nehalem 6 Core x 2

• RAM: 16GB

각 제품은 복제본 3개로 복제와 분산을 실행한다. 단, MongoDB

는 분산과 복제를 모두 구성하면 성능이 비정상적으로 떨어져서 복

제 구성(Replica Set)만으로 테스트했다. 제품별로 설정한 파라미

터는 다음 표와 같다. 기타 항목은 모두 기본값으로 설정했다.

표 1 제품별 설정 파라미터

제품 파라미터

카산드라 • ConsistencyLevel, Read=ONE, Write=ONE

복제 실행 중 최소한 하나만 성공하면 복제 완료에 대한 확인(confirm) 여부

를 애플리케이션에 반환한다

• commitlog_sync: batch, commitlog_sync_batch_window_in_ms: 1

periodic(기본 값), 혹은 batch로 설정한다. periodic은 커밋 로그(commit

log)를 주기적으로 디스크에 쓰며(fsync), batch는 매번 디스크에 쓰는 작

업(fsync)을 모아서 주기적(1ms)으로 실행한다.

•key_cached=1.0

키 위치를 캐싱하는 정도. 1.0이면 모두 캐싱한다.

대용량 데이터를 처리하는 방법에 하나의 정답은 없는 것 같다. 각

자의 상황이 다르니, 회사 사정에 맞는 솔루션을 선택하여 서비스

를 잘 하는 게 최고의 가치일 것이다1.

그러나 RDBMS를 이용하여 대용량 데이터를 처리하는 노하우를

충분히 보유하고 있더라도, NoSQL에 대한 관심과 학습을 계속할

필요가 있다고 본다. 어쨌든 NoSQL의 핵심은 대용량 데이터의 처

리이고, 아직 RDBMS에 비하여 이런저런 기술적(또는 기능적) 제

반 요소가 부족하지만 시간이 지남에 따라 해결될 것이기 때문이

다.

이 글에서는 대표적으로 많이 쓰고 있는 NoSQL 제품 가운데 카

산드라(Cassandra)와 HBase, MongoDB의 아키텍처와 직접 수

행한 벤치마크 테스트로 각 제품의 특성을 확인하고, 그럼으로써

NoSQL 제품이 개발하려는 인터넷 서비스에서 기대하는 역할을

정말로 충족할 수 있는지 검토해 볼 것이다.

YCSB를 사용한 벤치마크 테스트

YCSB(Yahoo Cloud Servicing Benchmark)는 야후!가 개발한

테스트 프레임워크다. YCSB는 여러 데이터 스토리지 제품에 맞

도록 추상화한 기본 오퍼레이션을 제공하며, 이 오퍼레이션으로

특정 워크로드를 만들 수 있다. 기본 오퍼레이션에는 INSERT,

UPDATE, READ, SCAN이 있고, 오퍼레이션을 조합한 몇 가지

기본 워크로드 세트를 제공한다. 물론 새로운 워크로드를 제작할

수도 있다.

현재 YCSB가 지원하는 제품은 카산드라와 HBase, MongoDB,

Voldemort, JDBC이다. 그 외의 스토리지 제품에 대한 테스트가

필요하다면 YCSB가 제공하는 테스트 인터페이스에 맞춰 오퍼레

이션과 워크로드를 개발하면 된다.

테스트 대상으로 선택한 제품의 버전 정보는 다음과 같다.

• Cassandra-0.7.4

• HBase-0.90.2 (Hadoop-0.20-append)

• MongoDB-1.8.1

1 RDBMS 중 Oracle은 예외로 두어야 할 수도 있다. 대용량 데이터 처리, 데이터 동기화 등 다른

RDBMS보다 훨씬 우월한 성능과 기능을 가지고 있기 때문이다. 그러나 상당한 비용 부담이 발생

한다는 것이 문제다. 규모에 따라서는 Oracle 라이선스를 지불하는 것보다 NoSQL을 개발하는

것이 더 저렴하다.

Page 223: The platform 2011

기술개발 동향222 The Platform 2011

카산드라와 HBase 아키텍처

두 제품은 모두 구글의 빅테이블(BigTable)의 영향을 받았다. 물론

카산드라에 직접적인 영향을 끼친 것은 아마존의 Dynamo이다.

빅테이블은 다차원 정렬맵(Multidimensional Sorted Map) 형

태로 데이터를 저장하는 칼럼 기반(Column-Oriented) 데이터베

이스며, 데이터 구조는 <row key, column family, column key>

로 되어 있다. 여기서, column family는 서로 관련이 있는 column

group으로 데이터를 저장하는 기본 단위이다.

빅테이블에서 데이터 쓰기는 기본적으로 추가(append) 방식이다.

즉, 데이터를 변경하는 경우 저장 파일 내에 해당 데이터의 위치에

서 갱신(in-place update)하지 않고 파일에 데이터를 추가한다. 다

음 그림은 빅테이블의 데이터 쓰기/읽기 경로를 보여준다.

그림 2 빅테이블의 내부 구조2

쓰기를 실행할 때에는 우선 memtable이라는 메모리 공간

에 데이터를 넣는다. 그리고 memtable이 차면 전체 데이터를

SSTable(Sorted String Table)이라는 파일에 저장한다. Memtable

의 데이터를 SSTable에 쓰기 전에 서버가 다운되면 데이터 유실이

발생할 수 있으므로 memtable에 데이터를 쓰기 전에 매번 그 이력

을 태블릿 로그(tablet log)에 저장하여 지속성을 제공한다. 읽기를

실행할 때에는 먼저 memtable에서 해당 키를 찾는다. 만약, 키를

찾지 못하면 SSTable에서 해당 키를 찾는다. 이때 조회해야 하는

SSTable이 여러 개가 될 수 있다.

2 http://static.googleusercontent.com/external_content/untrusted_dlcp/labs.

google.com/ko//papers/bigtable-osdi06.pdf

제품 파라미터

HBase HeapSize, HBase=8G, HDFS=2G

MongoDB •--oplogSize=100G

수행 내역 로그(Oplog)의 크기. 수행 내역 로그는 마스터가 복제를 위해 쌓

는 로그이다.

각 제품별 처리량(throughput)을 비교한 결과는 다음 그림과 같

다. 단위는 OPS(operation per second)이다.

그림 1 제품별 처리량 비교

모든 제품에서 읽기보다 쓰기에 대한 처리량이 훨씬 좋았다. Insert

Only에서는 카산드라가 2만OPS를 넘어 월등했고, HBase도 상대

적으로 좋은 성능을 나타냈다. Read Only의 경우, Insert Only에

비해 제품 간 성능 격차가 크지 않았으며, HBase가 가장 좋고 카산

드라는 HBase보다 조금 떨어졌다. Read & Update에서는 카산

드라가 가장 좋았는데 카산드라의 우수한 쓰기 성능 때문에 Read

Only보다 더 높게 나온 것으로 보인다. MongoDB는 세 경우 모두

가장 낮게 나왔다. MongoDB는 Memory Mapped File(mmap)

을 사용하는데, 테스트 데이터의 규모가 커서 메모리의 용량을 넘

어섰기 때문인 것으로 보인다.

그럼, 제품별로 이런 성능 차이를 낸 이유에 대해 살펴보자.

Page 224: The platform 2011

왜 NoSQL인가? 223The Platform 2011

카산드라는 정합성 레벨(consistency level)이라는 개념도 사용해

가용성을 높이고 있다. 이 개념은 복제 수행과 관련된 것이다. 복제

수, 그리고 복제 수행 완료에 대한 확인(confirm) 여부를 시스템 파

라미터로 조절할 수 있게 한다. 예를 들어, 복제본 3개를 유지하고

있다고 하면 쓰기 오퍼레이션을 실행할 때 세 복제에 대한 실행이

끝나야 이 오퍼레이션이 성공했다고 말할 수 있다. 하지만, 카산드

라는 전체 중 N( < 3 )개의 실행만 확인하고 바로 성공 여부를 반환

한다. 그래서, 일부 복제 노드에 장애가 나더라도 쓰기 오퍼레이션

을 성공으로 처리하여 가용성을 높인다. 그리고, 실패한 쓰기 오퍼

레이션의 이력을 복제 노드가 아닌 제 3의 노드에 기록했다가 추후

쓰기 오퍼레이션을 다시 시도하기도 한다4.

모든 복제에 대한 쓰기 오퍼레이션의 성공 여부를 보장하지 못하기

때문에 데이터 정합성은 읽기 시점에 체크한다. 일반적으로 읽을

때에는 여러 복제본 중에 한 곳에서만 읽어 온다. 하지만, 카산드라

는 모든 복제가 일치하지 않을 가능성을 고려하여 세 복제본에서

데이터를 모두 읽어와 동일한지 확인하고 정합성이 깨진 경우 최

신 데이터로 복구한다. 이를 ‘Read Repair’라고 하는데, 카산드라

에서 쓰기 오퍼레이션이 읽기 오퍼레이션보다 느린 가장 큰 이유이

다.

HBase

이번엔 HBase를 살펴보자. HBase는 빅테이블과 동일한 구조이

다. 빅테이블이 태블릿(tablet) 단위로 동작하듯이 HBase는 영역

(region) 단위로 분산과 복제를 수행한다. 키는 영역 기반으로 정렬

되어 있으며 키가 저장된 영역의 위치 정보를 메타 데이터로 저장

하고 있으며, 이를 메타 영역(meta region)이라 한다. 그래서, 키가

들어오면 메타 영역을 먼저 조회하여 해당 영역을 찾고, 이후 클라

이언트가 이 정보를 캐시에 저장해 둔다. HBase는 처음 일련의 쓰

기가 들어오면 분산 없이 하나의 영역에 넣으며 해당 영역이 커지

면 분할(split)하는 방식이다. 이 부분은 미리 영역을 만드는 등의

튜닝이 필요한 부분이다.

데이터 저장과 복제는 HDFS가 담당한다. HDFS는 하둡

(Hadoop)의 분산 파일 시스템으로 파일을 쓸 때에는 여러 복제본

에 쓰고(Synchronous Replication), 읽을 때에는 그 중 한 곳에서

4 이를 hinted handoff라고 한다

이런 아키텍처를 사용하면 쓰기에 이점이 있다. 쓰기를 실행하면,

우선 메모리에만 기록하고 일정 용량 이상 데이터가 쌓였을 때만

실제 디스크에 옮겨 쓰기 때문에 I/O가 동시에 일어나지 않는다.

하지만 읽기를 실행하는 경우에는 memtable이 아닌 SSTable에서

읽어야 할 때 성능이 상대적으로 낮아진다. 카산드라와 HBase 모

두 SSTable에 키가 있는지 없는지를 빨리 판단하기 위해 블룸 필터

(Bloom filter)3를 쓰고, 인덱스도 만든다. 하지만 SSTable의 개수

가 많다면 읽기는 확실히 I/O를 많이 발생시킨다. 그래서 읽기 성

능을 높이기 위해 컴팩션(Compaction)을 실행한다. 컴팩션은 두

SSTable을 머지 정렬(merge sort)하여 하나의 SSTable로 만드는

것이다. 컴팩션을 실행하면 SSTable의 개수가 줄고, 따라서 컴팩션

을 많이 실행한 상태일수록 읽기 성능이 좋아진다.

카산드라

두 제품은 이런 공통된 특징이 있지만 차이점도 있다. 이 차이점은

한 마디로 카산드라는 AP(Availability & Partition tolerance)를

지향하고, HBase는 CP(Consistency & Partition tolerance)를 지

향한다는 것이다.

분산 컴퓨팅 분야에서 CAP 정리란 이론이 있다. 이 이론에 다

르면 일관성(Consistency)과 가용성(Availability), 확장 가능성

(Partition tolerance)을 모두 제공할 수 있는 시스템은 없다. 예를

들어, 가용성을 높이기 위해 데이터를 여러 노드에 복제하면 복제

한 데이터 간에 일관성을 맞춰야 하는데, 네트워크를 확장해도 가

용성이 높게 동작하게 하려면 결국 복제한 데이터 간에 일관성을

보장하기 어려워진다. 그래서, CAP 중 일부만 지원할 수 있다.

카산드라에 직접 영향을 미친 것은 아마존의 Dynamo이다.

Dynamo는 컨시스턴트 해싱(consistent hashing)으로 키 기반 저장

소(key-value store)로 데이터를 분산하며, 높은 가용성을 제공하는

시스템이다. 컨시스턴트 해싱은 키를 해시한 값(slot)을 순서화해서

링(ring) 형태로 펼쳐 놓았다고 생각하고, 클러스터를 구성하는 여러

노드가 이 링의 특정 구역(region)을 맡아서 처리할 수 있게 하는 방

법이다. 클러스터에서 노드가 삭제되거나 클러스터에 노드가 추가

될 때마다 다시 해시하지 않고 링의 인접 노드가 해당 구역을 맡거

나 나눠주는 방식이다. 카산드라 역시 컨시스턴트 해싱을 사용한다.

3 블룸 필더에 대한 자세한 사항은 http://en.wikipedia.org/wiki/Bloom_filter를 참조한다.

Page 225: The platform 2011

기술개발 동향224 The Platform 2011

MongoDB는 데이터 스케일보다 고성능을 우선시하여 메모리 기

반에서 동작한다. 가용 메모리 내에서 읽기/쓰기를 한다면 높은 성

능을 보여 주지만, 그 범위를 넘어서면 성능을 예측하기가 어렵다.

따라서 MongoDB는 중소 규모의 저장소로 사용하는 것이 적합하

다. 낮은 사양의 서버로 카산드라와 HBase에서 구성한 테스트 워크

로드를 구성하고, 레코드를 5천만개 대신 30만개 넣고 시험했을 때

MongoDB는 카산드라와 HBase보다 더 좋은 성능을 내기도 했다.

MongoDB는 데이터 저장을 위해 BSON을 사용한다. 여기서,

BSON은 ‘Binary JSON’이라고도 할 수 있지만, 데이터 인코딩 형

식이나 표현 자료형 면에서는 JSON보다는 Hessian과 유사하다고

할 수 있다 이렇게 BSON을 이용하여 표현하는 논리적인 단위를

문서(Document)라고 한다. 이 문서는 RDBMS의 Row에 대응하

는 개념이지만, 스키마(schema)가 없다는 것이 가장 큰 차이다. 이

러한 문서가 모인 것이 컬렉션(Collection)이다. RDBMS의 테이

블(table)에 대응하는 개념이다.

기본 질의 방식은 컬렉션에서 조건에 맞는 문서를 검색하는 방식

이다. 조건 검색을 빨리 하려면 인덱스가 필요한데, MongoDB는

컬렉션이나 문서 내에서 특정 필드에 대해 인덱스를 생성할 수 있

다. 이 인덱스는 RDBM의 인덱스와 매우 비슷한 형태인 B-트리

(B-Tree)로 구현되어 있다. 각 문서마다 객체 식별자를 저장하는

‘_id’라는 기본 필드가 있는데, 기본적으로 컬렉션마다 이 _id 필드

에 대해 인덱스가 생성되어 있다. MongoDB의 강점 중 하나는 인

덱싱과 JOIN 등을 지원하여 RDBMS와 비슷한 기능을 가지고 있

다는 것이다.

이제 구조에 대해 잠시 살펴보자. MongoDB는 복제와 분산 구성

을 설정할 수 있다. MongoDB가 권장하는 시스템 구성은 3개의

샤드(Shard) 서버 그리고 각 샤드별로 3개의 복제본 노드(Replica

Set)를 구성하는 것이다(즉, 9개의 노드로 구성). 고가용성(HA) 구

성은 마스터/슬레이브(Master/Slave) 방식이나 혹은 Replica set

으로 구성하는 방식이 가능한데, Replica Set으로 구성하는 방식

은 자동 페일오버(failover)를 지원한다. Replica set으로 구성하면

하나의 마스터와 여러 슬레이브가 있고 쓰기는 마스터에게만 보낼

수 있다. 만약 마스터가 다운되면 남은 슬레이브 중 하나를 마스터

로 선정하여 서비스를 재개한다. MongoDB가 제공하는 분산 방

식은 샤딩이다. 컬렉션마다 샤딩할 수 있는데, 컬렉션의 필드를 샤

드 키(shard key)로 정할 수 있다. 샤딩은 HBase와 같이 순서 기

반 파티셔닝(Order-Preserving Partitioning) 방식으로 동작한다.

만 읽는다. 따라서, 데이터 정합성을 보장할 수 있고, 읽기 오퍼레이

션에 오버헤드가 적다. 다만, HBase와 HDFS는 별개로 동작하기

때문에 memtable과 SSTable이 있는 노드의 위치가 달라질 수 있

으며, 이로 인해 부가적인 네트워크 I/O가 발생할 수 있다.

만약 노드에 장애가 나면, 무슨 일이 일어날까? 영역 서버(region

server)가 다운되면 다른 노드에서 해당 영역을 구성하는 작업을

한다. 즉, 그 영역에 대한 커밋 로그를 재수행하여 memtable을 구

성한다. 장애 노드에 HDFS의 데이터 노드도 있었다면 그 노드에

속한 데이터 청크(chunk)를 모두 다른 노드로 마이그레이션한다.

여기까지 두 제품에 대한 공통점과 차이점을 살펴봤다. 그렇다면,

비교 시험에서 둘 간의 쓰기 성능 차이는 왜 났을까?

우선 커밋 로그에 차이가 있다. 카산드라는 커밋 로그를 로컬 파일

시스템에 한 번 쓴다. 하지만 HBase는 커밋 로그를 HDFS에 쓰

고 HDFS는 복제를 수행한다. 이 과정에서 데이터 로컬리티(data

locality)가 고려되지 않기 때문에 파일 쓰기 요청이 네트워크상에

있는 다른 물리적인 노드(Physical Node)로 전해질 가능성이 있

다. 두 번째로 카산드라는 항상 쓰기 요청을 세 노드에서 모두 받지

만, HBase는 초기에 단일 영역에만 ‘쓰기’를 하기 때문에 한 노드

만 요청을 받고 있다는 점이다.

그럼 읽기에서의 차이점은 어떨까? 여러 상이한 점이 있겠지만 무

엇보다도 카산드라는 읽기 요청 한 번에 대해 실제 세 번의 데이터

읽기가 발생하고, HBase는 하나의 데이터만 읽으면 된다. 이 점은

처리량에 영향을 주고 있다.

MongoDB 아키텍처

MongoDB는 앞서 설명한 카산드라나 HBase와는 다르다. 사실

NoSQL이라고 부르는 대상에 대한 기능군이 너무 다양하여 세 제

품을 동일선상에서 비교하기는 어려운 감이 있다.

카산드라와 HBase는 본격적인 대용량 데이터의 처리를 위한 것이

라 할 수 있고, MongoDB는 일정 크기 이내의 데이터를 사용할 때

빠르고 자유롭게(Schema-Free) 쓸 수 있도록 만들어진 것이다.

Documented-Oriented 형식을 하고 있어서, 키 기반 저장소나 칼

럼 기반 저장소보다는 RDBMS와 유사성이 더 높다.

Page 226: The platform 2011

왜 NoSQL인가? 225The Platform 2011

연산 같은 것은 값을 갱신하는 연산으로 바뀌어 로그에 저장된다.

MongoDB는 복제나 페일오버 방식, 쿼리 파워 면에서 RDBMS

와 비슷한 점이 많다. 아무래도 가장 큰 차이점은 스키마가 없는

(schema-free) 데이터를 저장하면서 애플리케이션에 데이터 저장

에 대한 유연성을 제공한다는 점이 아닐까 생각한다.

RDBMS 아키텍처

RDBMS는 NoSQL과 달리 ‘읽기’ 오

퍼레이션 중심이라고 할 수 있다. 구체

적인 방식은 제품마다 다르지만, 일반

적으로 NoSQL 제품은 대용량 데이터

를 빠르게 ‘쓸’ 수 있도록 구현되어 있

다. 반면 RDBMS는 읽기 중심인데, 빠

르게 읽을 수 있게 한다는 것뿐 아니

라, 많은 함수와 조회 기능을 제공한다.

DBMS의 경우 색인(index) 때문에 빠

르게 값을 조회할 수 있지만, 색인 때문

에 ‘쓰기’가 느려지기도 한다. 쓸 때마

다 색인을 구성해야 하기 때문이고, 색

인이 많을 수록 인덱스 구성에 시간이

필요하다.

ACID와 BASE로 RDBMS와 NoSQL 차이를 비교하기도 하지만,

읽기 대 쓰기 관점에서 비교해 볼 수도 있을 것이다. 구축하려는 비

즈니스가 쓰기보다는 읽기가 많고, SUM()이나 AVG()같은 기본적

인 집계 함수는 물론 복잡한 읽기 방식이 필요하다면 NoSQL보다

는 RDBMS를 선택하는 것이 좋다.

마치며

지금까지 카산드라와 HBase, MongoDB에 대한 벤치마크 테스트

및 제품 간 차이점을 살펴보았다. 카산드라와 HBase는 쓰기 성능

이 탁월하지만, 읽기 성능은 좋지 않았다. 그 이유는 기존 RDBMS

와 달리 쓰기에 최적화된 구조 때문이며 이로 인해 읽기에서 많은

concurrent I/O를 유발시켰다. MongoDB는 RDBMS와 비슷한

구조를 보이며 데이터 모델링 관점에서는 더 높은 유연성을 보여

준다.

청크는 샤드 내에서 연속된 데이터 범위인 <collection, minkey,

maxkey>에 해당하는 영역이며 크기는 최대 64MB이다. 최대 크

기를 넘어선 청크는 분할되어 두 청크로 나눠진다. 나눠진 청크는

다른 샤드 서버로 마이그레이션될 수 있다. 샤드와 replica set으로

구성된 MongoDB의 모습은 다음과 같다.

그림 3 MongoDB 구성5

클라이언트는 mongos라는 프로세스에 최초로 접속하는데,

mongos는 클라이언트 요청을 라우팅하거나 코디네이션한다. 만

약, 코디네이션 과정에서 여러 mongod에 요청을 나누어 보낸 경

우, 그 결과를 모아서 반환하기도 한다. 설정 서버(config server)는

샤드와 청크 등에 대한 메타 정보를 유지한다.

이제 좀 더 내부적인 부분을 보자. MongoDB의 복제는 비동

기(asynchronous) 방식이다. 마스터는 쓰기가 오면 그 이력을

Oplog에 저장한다. Oplog는 capped collection이라는 고정 크

기의 FIFO 방식 컬렉션이다. Oplog에 쌓인 로그는 슬레이브가 주

기적으로 가져와서(poll) 읽어가서 반영한다. 만약 Oplog가 가득

찰 동안 슬레이브가 로그를 다시 수행하지 못했다면 슬레이브는 로

그 기반의 복제를 중지하고 마스터의 데이터로 동기화해야 한다.

Oplog의 또 다른 특징은, 로그를 재수행할 때 저장된 오퍼레이션

을 멱등적(idempotent)으로 수행할 수 있다는 점이다. 오퍼레이션

로그가 수행 순서대로 저장되어 있기 때문에 다시 오퍼레이션을 수

행할 때 그 시점 이후부터 수행하면 되기 때문이다. 이를 위해 증가

5 http://www.mongodb.org/display/DOCS/Introduction

mongod mongod mongod mongod

mongod mongod mongod mongod

mongod mongod

mongod mongod

mongod

mongod mongod

C1 mongod

C1 mongod

C1 mongod

shard2

...

...

shard3 shard4shard1

replics set

config servers

Page 227: The platform 2011

기술개발 동향226 The Platform 2011

그림 1 카산드라의 컨시스턴트 해싱1

1  http://horicky.blogspot.com/2010/10/bigtable-model-with-cassandra-and-hbase.

html

NoSQL은 RDBMS에 비해 어떤 장점을 가지고 있을까? NoSQL

제품은 저마다 고성능, 확장성, 가용성 등 RDBMS가 제공하기 어

려웠던 부분을 강조한다. 하지만 모든 면에서 완벽한 제품은 없다.

NoSQL 제품을 면밀히 들여다보면 확실한 장점이 있기는 하지만

취약한 부분도 있다. 그래서 이런 제품을 사용하려고 할 때는 검증

이 필요하다. 이 글에서는 운영 관점에서 ‘분

산’과 ‘가용성’에 대해 분석해 보려고 한다.

그 대상은 카산드라(Cassandra)와 HBase,

MongoDB다.

카산드라의 장애 처리와 데이터 정합성

카산드라는 데이터 분산과 가용성 면에

서 뛰어나다. 우선 분산에 대해 살펴 보자.

카산드라는 컨시스턴트 해싱(consistent

hashing)을 사용하여 데이터를 분산한다.

Key space represented as points on the ring circumfrence

KeyAB owned by B,replicated in C, D

When B crashes, ownership ofkeyAB and keyAB transferred to C

keyAB owned by C,replicated in D, E

Node D is hosting1. replicas of keyAB, keyBC,2. primary copy of keyCD

A

B

C

D

E

NoSQL가용성과 운영 안정성

•저장시스템개발팀 _ 이혜정

Page 228: The platform 2011

NoSQL 가용성과 운영 안정성 227The Platform 2011

• 관리 도구에서 명시적으로 노드를 삭제하면 삭제할 노드가 담당했던 데

이터를 남은 노드에 마이그레이션한 다음 해당 노드를 삭제한다.

• 신규 노드를 추가하면 추가된 노드는 먼저 기존 노드(seed node)와 통

신하여 노드가 추가되었다는 사실을 알리고 링에서 특정 범위를 할당

받는다.

• 신규 노드가 담당할 노드에 마이그레이션을 수행한다

(bootstrapping)

• 마이그레이션을 마치면 신규 노드는 서비스 가능한 상태가 된다.

노드 장애 후 신규 노드 추가

노드에 장애가 발생한 후 신규 노드를 추가하는 시나리오와 재현

결과는 다음과 같다.

• 노드가 다운되면 그 노드가 맡았던 데이터를 마이그레이션하지 않으며,

복제본 2개로 서비스가 지속된다. 즉, 이 기간 동안 서비스 요청이 와도

오류를 반환하지 않는다.

• 신규 노드를 추가하면 링에서 특정 범위를 할당받는다. 하지만 부츠트

래핑(bootstrapping)은 수행하지 않는다. 부츠트래핑은 마이그레이

션 대상이 되는 데이터의 복제본 수가 3개로 유지된 상태에서만 수행할

수 있기 때문이다.

• 추가한 노드는 데이터가 없는 상태지만 서비스가 가능한 상태이므로 요

청을 처리한다. 만약, 이 시점에 읽기 요청이 들어오면 해당 키에 대한

데이터가 없다고 반환한다. 만약, 복제 개수(replica factor)가 3이고

읽기 정합성 레벨이 1이라면, 들어온 읽기 요청 가운데 1/3정도는 데이

터가 없다고 반환할 수 있다. 그리고, 정합성 레벨을 정족수로 설정했다

면 1/6 확률로 빈 데이터를 반환한다. 즉, 장애 복구가 끝나기 전까지는

읽기 정합성이 보장되지 않는다. 실제 레벨이 1인이면 코디네이터 역할

을 하는 노드는 신규 노드로부터 먼저 응답을 받을 가능성이 높다. 그 이

유는 신규 노드에서는 데이터가 없어서 읽기 I/O가 발생하지 않기 때문

이다. 따라서 빈 데이터를 반환할 확률은 더 높아진다.

• 관리 도구로 신규 노드에서 Read Repair를 수행하면 다른 노드에서 복

제본 데이터를 읽어서 노드를 구축한다. 그리고 Read Repair가 끝날

때까지 읽기 정합성이 깨져 있게 된다.

카산드라는 노드 장애 상황에도 오류를 발생하지 않고 서비스할 수

있다. 하지만 쓰기 측면에서의 가용성은 높은 반면 읽기 측면에서

는 Rread Repair가 오래 걸리면서 데이터 불일치 상태가 지속되

컨시스턴트 해싱은 메타 정보를 조회하지 않아도 클러스터에서 키

가 저장된 노드를 바로 찾아갈 수 있는 방법이다. 키의 해시 값을

구해서 키를 찾고, 이 해시 값만으로 노드를 찾아갈 수 있다. 좀 더

자세히 살펴보면, 컨시스턴트 해싱은 일련의 해시 값을 가상의 링

(ring)에 순서대로 나열했다고 가정하고 각 노드는 링에서 각자 맡

은 범위만 처리하는 방법이다. 만약 노드를 추가하면 특정 노드(많

은 데이터를 가진 노드)가 맡고 있던 범위를 분할하여 새 노드에 할

당한다. 노드를 삭제할 때는 링에서 삭제된 노드가 맡고 있던 범위

를 인접 노드가 맡는다. 따라서 서비스 중에 노드의 추가/삭제로 인

해 영향을 받는 노드 수를 최소화할 수 있다.

카산드라는 마스터 없이 동작한다. 즉, 데이터 분산이나 복구를

관장하는 별도의 서버가 없다. 따라서, SPoF(Single Point Of

Failure, 단일 고장점)이 없다. 마스터가 없는 대신 각 노드가 주기

적으로 서로 메타 정보를 주고 받는다. 이를 가십 프로토콜(gossip

protocol)이라 한다. 가십 프로토콜을 통해 노드는 다른 노드가 살

아 있는지 여부를 알 수 있다.

카산드라는 정합성 레벨(consistency level)을 제공하여 가용성

을 높인다. 레벨이 낮으면 노드가 다운되어도 서비스의 다운타임

(downtime)이 전혀 발생하지 않을 수 있다. 예를 들어, 키에 해당

하는 복제 데이터를 저장하는 노드가 3대 있고 이 가운데 노드 하

나가 다운되었다면, 일반적으로는 쓰기 요청이 왔을 때 다운된 노

드에 복제 데이터를 쓰지 못하기 때문에 요청 결과에 대해 바로 성

공을 반환하지 않는다. 하지만, 레벨을 정족수(quorum)나 1로 설

정하면 동작하고 있는 노드의 수가 설정한 수만큼 있는 경우 바로

성공을 반환한다. 따라서, 노드 3대가 모두 다운되지 않는 한 요청

오류가 발생하지 않는다.

그럼 정말 노드에 장애가 발생해도 데이터를 읽고 쓰는 데에 문제

가 없을까? 이를 확인해 보려고 서비스 요청이 지속적으로 들어오

는 상황에서 노드 장애를 발생시켜 신규 노드를 추가하는 운영 시

나리오를 재현해 보았다. 결과는 다음과 같다.

노드 삭제 후 신규 노드 추가

노드를 삭제한 후 신규 노드를 추가하는 시나리오와 재현 결과는

다음과 같다.

Page 229: The platform 2011

기술개발 동향228 The Platform 2011

HRegionServer는 데이터 분산, HMaster는 HRegionServer

의 모니터링을 담당한다. 그리고, HDFS는 데이터 저장과 복제,

Zookeeper는 HMaster의 위치 정보 유지 및 마스터 선출을 담당

한다. 만약, 각 컴포넌트를 이중화하지 않으면 이들 컴포넌트는 모

두 SPoF가 된다.

HRegionServer에 대해 좀 더 살펴보면 다음과 같다. HRegion

Server는 영역(region)이라는 단위로 데이터를 분산하는 역할을

한다. 영역이란 정렬된 데이터를 저장한 큰 테이블을 정렬 키 범

위로 나눈 것이다(빅테이블에서 태블릿과 같다). 각 영역이 맡은

키 범위 정보는 별도 영역에 저장하는데, 이것을 메타 영역(meta

region)이라 한다. 그리고, 메타 영역의 위치를 저장하는 것을 루트

영역(root region)이라 한다. 즉, 영역 서버(region server)는 루트

영역, 메타 영역, 데이터 영역이 계층적으로 구성된 트리를 저장하

고 있다. 만약, 영역 서버 하나가 다운되면 다운된 서버가 맡고 있

어 가용성이 떨어진다. 따라서, 장애 상황에서 읽기 정합성을 유지

하기 위해서는 다음과 같은 방법을 적용해야 한다.

• 읽기 정합성 레벨을 all로 설정하고 읽기를 수행한다. 이 경우, 모든 복제

본 중에서 최신 데이터를 얻을 수 있다.

• 데이터를 읽어오지 못하면 읽기를 재시도한다. 첫 번째 읽기에서 Rread

Repair를 수행하여 두 번째 읽기 시 복구된 데이터를 얻을 수 있기 때문

이다. 단, 이 방법은 두 번째로 읽기 전에 Rread Repair가 완료됨을 가

정한 것이다2.

HBase의 장애 요소와 복구 방식

HBase는 다음과 같이 여러 컴포넌트로 구성되어 있다.

그림 2 HBase의 컴포넌트 구성3

2 정합성 레벨이 낮으면 read repair는 읽기 오퍼레이션 처리 스레드와 별개의 스레드로 백그라운드에

서 수행된다

3 http://ofps.oreilly.com/titles/9781449396107/architecture.html

Page 230: The platform 2011

NoSQL 가용성과 운영 안정성 229The Platform 2011

MongoDB의 복제 및 장애 복구 방식

MongoDB는 마스터에서 슬레이브로 데이터를 비동기 방식으로

복제한다. 비동기 복제의 장점은 복제로 인한 마스터의 성능 저하

가 거의 없으며, 슬레이브를 추가해도 서비스 성능이 떨어지지 않

는다는 점이다. 하지만 데이터가 대부분 불일치하기 때문에 장애가

생기면 데이터 유실이 발생한다.

MongoDB는 DBMS의 HA와 유사한 방식이다. 즉, 장애가 나면

마스터를 선출하는 방식이다. 아래 두 가지 시나리오를 살펴보자.

노드 장애

노드 3대를 마스터와 슬레이브, 슬레이브로 구성하고, 마스터 프로

세스를 중단시킨다. 그러면 자동으로 슬레이브 하나를 마스터로 선

출한다. 이 때 장애 발생 후 마스터 선출까지 수 초 정도 걸린다. 즉,

다운타임은 길지 않다. 하지만 마스터와 슬레이브로 구성된 상태에

서 다시 마스터가 다운되면 마스터가 선출되지 않는다.

노드 추가

마스터에 데이터를 입력한다. 이 때, 데이터의 양은 5GB로서 메모

리 크기보다 작다고 하자. 그리고 마스터에 새로운 슬레이브를 추

가한다. 이 때 슬레이브 추가가 마스터의 성능에는 영향을 주지 않

는다. 그리고, 추가한 슬레이브에서 전체 데이터를 복제하는데 수

분 정도 소요된다.

MongoDB에서 장애나 증설로 인한 성능 저하는 거의 없다. 하

지만, 마스터/슬레이브 간 복제 데이터가 일치하지 않을 때 장애

가 난다면, 슬레이브가 복제하지 못한 데이터는 유실될 수 있다.

MongoDB의 복제 방식을 살펴보면, 마스터가 수행 내역을 로컬

에 있는 로그(Oplog)에 쓰고 슬레이브는 그 로그를 읽어서 자기 데

이터베이스에 반영한다. 만약, 슬레이브가 마스터의 로그를 다 읽

어 가지 못한 상태에서 장애가 난다면, 읽지 못한 데이터는 유실된

다. 또한, 마스터의 로그가 가득 찼지만 슬레이브가 복제를 다 하지

못한 경우, 로그 기반 복제 대신 마스터의 데이터를 읽어 와서 슬레

이브에 반영한다(data sync). 이런 상황에서 마스터에 장애가 발생

하면, 역시 데이터가 유실되며 유실되는 양도 커진다.

던 영역을 다른 서버에서 맡기 전까지 해당 영역은 서비스할 수 없

다. 따라서, 영역이 복구되기 전까지 서비스 다운타임이 발생한다.

그럼, 이 다운타임 어느 정도 지속될까? 장애 복구 과정을 살펴 보

면서 다운타임을 가늠해 보자.

영역 서버 장애

영역 서버에 장애가 발생하면 다음 단계로 데이터를 복구한다.

• HMaster가 장애를 감지하고 장애 서버의 영역을 다른 서버가 대체하

여 서비스하도록 지시한다.

• 새로운 영역을 담당하도록 지시 받은 HRegionServer는 먼저 새 영역

의 WAL(Write Ahead Log)을 읽어서 그 영역의 MemStore를 복구

한다.

• MemStore 복구가 완료되면 HMaster는 영역의 위치 정보를 저장하

고 있는 메타 영역을 수정하여 해당 영역의 서비스를 재개한다.

• 디스크에 저장되어 있는 영역의 데이터는 HDFS에 의해 새로 복구된다.

결국, 복구 시간은 장애를 감지하고 HDFS에서 로그를 읽어 새로

운 영역을 만드는 시간 정도라고 할 수 있다. 복구된 영역을 맡은

서버도 HDFS의 데이터 파일에 접근할 수 있기 때문에 복구 과정

에서 HDFS상의 데이터 이동은 발생하지 않는다. 따라서, 다운타

임이 그리 길진 않다.

HDFS 장애

HDFS는 하나의 이름 노드(name node와 여러 개의 데이터 노드

(data node)로 구성된다. 여기서, 이름 노드는 메타 데이터를 저장

하는 노드이므로 다운되면 서비스 장애가 발생할 수 있다. 하지만,

데이터 노드 중 하나가 다운되는 경우에는 데이터가 복제되어 있

기 때문에 서비스 장애가 발생하지 않는다. 다만 다운된 데이터 노

드가 저장했던 데이터를 다른 노드에서 구축하여 복제 개수를 정상

상태로 맞춘다(복구). 이때, 대량의 데이터 복사가 발생할 수 있고

이 시점에 서비스나 애플리케이션에서 읽기 요청이 들어오면 읽기

가 느려질 수 있다. 읽기를 위한 디스크 I/O가 데이터 복사로 인해

영향을 받기 때문이다.

Page 231: The platform 2011

기술개발 동향230 The Platform 2011

결론

지금까지 카산드라와 HBase, MongoDB의 장애 처리 부분을 살

펴 보았다.

카산드라는 쓰기 측면에서 고가용성을 제공한다. 하지만 장애 시

데이터 복구 시간이 오래 걸린다. 복구 과정상 복구할 데이터를 모

두 알아낸 다음 각 데이터마다 최신 버전을 읽어서 쓰는 방식이기

때문이다. 그리고 추가한 노드가 데이터 복구를 하는 동안, 서비스

요청도 처리하기 때문에 읽기에 대해 잘못된 결과를 반환할 수 있

으며, 복구 대상인 데이터를 읽을 경우 Rread Repair를 이중으로

수행한다. 노드 장애뿐 아니라 오퍼레이션 수행 실패에 대해서도

로그 방식의 복구(hinted handoff)를 제공하고 있지만 복구가 되

기 전에 해당 데이터를 읽으면 틀린 결과를 반환할 수 있다. 따라

서, 정합성 레벨을 높이지 않는다면 읽기 처리가 필요한 서비스에

서는 사용하기 어렵다.

HBase는 구성상 장애 요소가 많다. 하지만, 카산드라가 프로세스

에서 장애가 발생할 때에도 데이터를 복구해야 하는 반면, HBase

는 HDFS 장애가 아닌 이상 데이터 복구가 발생하지 않아 다운타

임이 길지 않다. HDFS에 장애가 발생하더라도 다운타임은 생기

지 않으며, 복구 중에 읽기 성능은 떨어질 수 있지만 정합성은 보

장된다. 그리고, SPoF 부분을 이중화한다면 가용성을 좀 더 높일

수 있다.

MongoDB는 자동 페일오버(fail over)를 제공하며 다운타임이 짧

다. 하지만, 비동기식 복제로 인해 페일오버 후 데이터가 유실될 가

능성이 높다.

각 제품을 사용하기 전에 이런 특성을 한 번쯤 염두에 두어야 할 것

이다.

Page 232: The platform 2011

성능 향상을 위한 SQL 작성법 231The Platform 2011

다면 아마도 B-Tree 계열 인덱스를 의미할 것이다. CUBRID는

B+-Tree를 이용하고 있다. B+-Tree는 B-Tree의 한 종류로서, 일

반적인 B-Tree와 달리 데이터 포인터를 리프(Leaf) 노드에만 저장

한다. 리프 노드의 상위 레벨인 비리프(Non-Leaf) 노드는 전형적

인 B-Tree 로 구성되며 리프 노드를 빠르게 찾는 인덱스 역할을 한

다. 리프 노드에는 키와 키에 대응하는 데이터의 포인터가 저장되

어 있다. 다음 그림은 전형적인 B+-Tree 모습이다.

그림 1 B+-Tree 구조

DBMS는 저장되어 있는 데이터를 효율적으로 검색할 수 있게 인

덱스를 사용한다. 웹 애플리케이션의 백엔드 성능을 높이려고 종종

실행하는 SQL 튜닝이란, SQL이 DBMS의 인덱스를 활용하도록

SQL을 수정하는 것이라고 할 수 있다. 그러니 인덱스를 잘 이해하

고 있다면 더 좋은 SQL을 작성할 수 있을 것이고, 훨씬 더 성능 좋

은 애플리케이션을 만들 수 있을 것이다.

이 글에서는 CUBRID 2008 R4.0에 적용된 다양한 인덱스 기법을

중심으로 인덱스 구조와 인덱스 활용 기법을 설명하겠다. MySQL

이나 MS-SQL, 오라클 등 다른 DBMS도 이 글에서 설명하는 기

법과 같거나 유사한 인덱스 기법을 사용하고 있

기 때문에 CUBRID를 사용하지 않더라도 자신

이 사용하는 DBMS 인덱스를 이해하는 데 부

족함이 없으리라고 생각한다.

인덱스 구조

거의 모든 DBMS는 B-Tree 계열 인덱스를 사

용한다. 인덱스 종류에 대한 특별한 언급이 없

Non-Leaf

Pages

성능 향상을 위한 SQL 작성법NBP DBMS개발랩에서 CUBRID를 개발하고 있는

딸바보 아빠입니다.

CUBRID가 MySQL보다 많이 쓰이는 날이 빨리 왔으면 좋겠습니다.

CUBRID Forever!!

•DBMS개발랩 _ 강동완

Page 233: The platform 2011

기술개발 동향232 The Platform 2011

Leaf

Pages

Table

Pages

인덱스 스캔을 이용한 질의 처리 과정

다음 그림은 CUBRID에서 아래의 SQL 질의로 테이블과 인덱스

를 생성하고 데이터를 입력했을 때 인덱스 리프 노드와 테이블 데

이터의 관계를 나타낸 그림이다. 왼쪽 인덱스 리프 노드에는 인덱

스 키와 키에 대응되는 OID(레코드의 물리적 주소 값)가 저장되어

있다.

CREATE TABLE tbl

(a INT NOT NULL,

b STRING,

c BIGINT);

CREATE INDEX idx ON tbl

(a, b);

INSERT INTO tbl VALUES

(1, 'ZZZ', 123456),

(4, 'BBB', 123456789),

(1, 'AAA', 123'),

(이하 생략)

그림 3 인덱스 리프 노드와 테이블 데이터의 관계

인덱스 스캔

B+-Tree의 리프 노드는 링크드 리스트(linked list)로 서로 연결

되어 있고, 저장된 키는 정렬되어 있어서 순차 처리가 용이하다. 그

렇기 때문에 범위를 검색하는 데 유리하다. 테이블에서는 처음부

터 끝까지 모든 레코드를 읽어야 완전한 결과 집합을 얻을 수 있지

만, 인덱스는 키-칼럼 순으로 정렬되어 있기 때문에 특정 위치에서

검색을 시작해서 검색 조건이 일치하지 않는 값을 만나는 순간 검

색을 멈출 수 있다. 이것을 인덱스 범위 스캔(Index Range Scan)

이라고 한다. CUBRID는 범위 스캔을 B+-Tree 검색의 기본 연산

으로 제공하고 있다. 범위 스캔에는 두 개의 키가 필요한데, 범위의

양 끝을 표현하는 하위 키(Lower Key)와 상위 키(Upper Key)가

그것이다.

인덱스 범위 스캔은 다음 그림과 같이 두 단계로 진행된다. 첫 번째

단계에서는 루트에서부터 트리를 순회하여 리프 노드에서 하위 키

를 찾는다. 두 번째 단계에서는 첫 번째 단계에서 찾은 키에서부터

상위 키까지 순차적으로 레코드를 읽어 처리한다. 상위 키가 현재

노드에서 발견되지 않으면 다음 노드를 읽어 상위 키를 가진 노드

까지 검색을 계속해 나간다. 상위 키까지 순차 검색이 끝나면 전체

범위 검색이 완료된다.

그림 2 인덱스 범위 스캔

두 번째 단계에서 상위 키까지 찾아가는 과정은 레코드에서 키를

읽어와 상위 키와 비교하는 과정의 연속이다. 상위 키가 최대 키이

면 현재 노드의 키부터 마지막 노드까지 모두 검색 결과에 포함되

기 때문에 비교 연산을 할 필요가 없어져서 검색의 성능이 좋아진

다. 검색 성능을 위해 옵티마이저는 입력된 쿼리를 재작성(rewrite)

하며, CUBRID는 특정 키를 찾는 검색도 범위 검색으로 변환하여

수행한다. 특정 키를 찾는 검색을 범위 검색으로 변환할 때에는 하

위 키와 상위 키 모두를 찾으려는 키로 동일하게 설정한다.

lowerkey

upperkey

� �

12

Index Leaf Pages Table Heap Pages

1 AAA OID3

1 CCC OID11

1 BBB OID7

1 DDD OID14

1 ZZZ OID1

2 ABC OID9

2 AAA OID5

2 CCC OID13

3 ZZZ OID8

4 DBB OID2

4 DAA OID15

4 DDD OID6

4 ZZZ OID12

5 BBB OID16

5 AAA OID10

5 CCC OID4

1

2

1

2

1

2

1

ZZZ

AAA

BBB

ABC

CCC

CCC

DDD

123456

12

123456789

1234567

12345

123456

123456789

1 AAA 123

4 DBB

...

...

...

...

...

...

...

...

...

...

123456789

5 CCC 1234

Page 234: The platform 2011

성능 향상을 위한 SQL 작성법 233The Platform 2011

그림 4 인덱스 스캔을 통한 질의 처리 과정

두 개 이상의 칼럼을 묶어서 인덱스를 만들 때는 칼럼의 순서가 매

우 중요하다. 이런 인덱스를 다중 칼럼 인덱스(Multi-Column

Index) 또는 복합 인덱스라고 한다. 복합 인덱스에서는 WHERE

절에 인덱스 키의 첫 번째 칼럼을 사용해야 인덱스 스캔을 수행한

다. 인덱스가 여러 칼럼으로 조합되어 있을 때 칼럼 가운데 한 가

지 칼럼만 사용해도 무방하다고 알려져 있는데, 잘못 알려진 것이

라고 할 수 있다. 첫 번째 칼럼이 없는 상태에서는 두 번째 칼럼이

정렬된 상태라고 할 수 없기 때문에 범위를 정의할 수 없다. 따라서

반드시 첫 번째 칼럼이 조건에 있어야 하며, 첫 번째 이후의 칼럼은

조건에 없어도 상관없다.

인덱스는 값의 대소 비교를 토대로 트리를 구성한다 따라서 값의

대소 비교가 아닌 조건은 B+-Tree를 사용해서 값을 찾을 수 없다.

<>, != 와 같이 부정형 조건이나 NULL 비교는 인덱스를 사용할 수

없다. 인덱스의 칼럼을 조건절에서 가공할 때도 인덱스를 사용할

수 없다. 다음은 인덱스를 사용하지 못하는 쿼리와 튜닝 후 인덱스

를 사용하도록 수정한 쿼리의 예이다.

SELECT * FROM tbl

WHERE a > 1 AND a < 5

AND b < 'K'

AND c > 10000

ORDER BY b;

위와 같은 SELECT 질의에서 WHERE 절에

있는 검색 조건은 다음과 같이 3가지로 나눌

수 있다.

• Key Range: 인덱스 스캔 범위로 활용하는 조

건이다(a > 1 AND a < 5).

• Key Filter: Key Range에 포함할 수 없지만 인

덱스 키로 처리 가능한 조건이다(b < ‘K’).

• Data Filter: 인덱스를 사용할 수 없는 조건이

다. 테이블에서 레코드를 읽어야만 처리 가능한

조건이다(c > 10000).

CUBRID의 질의 처리 과정은 다음과 같다.

1. 인덱스 스캔인 경우 먼저 Key Range와 Key Filter를 적용하여 조건에

부합하는 OID 리스트를 만든다. 이 과정은 Key Range의 시작부터 끝

까지 계속된다.

2. OID를 이용해 데이터 페이지에서 해당 레코드를 읽어 Data Filter를 적

용하거나 SELECT 리스트에 기술된 칼럼 값을 읽어와 결과를 저장하는

임시 페이지에 기록한다.

3. ORDER BY 절이나 GROUP BY 절이 있으면 임시 페이지에 저장된 레

코드를 정렬하여 최종 결과를 생성한다.

그림 4는 위의 SELECT 질의가 처리되는 과정이다.

인덱스 사용하기

옵티마이저가 인덱스를 사용하게 하려면 WHERE 절에 범위

(Range) 조건이 있어야 한다. 범위 조건은 값의 비교 조건, 즉 크

다, 작다, 크거나 같다, 작거나 같다, 같다와 같은 비교문으로 기술

한다. 만약 범위 조건이 없다면 옵티마이저는 테이블 순차 스캔을

시도할 것이다.

OID Buffer

Sorting

KeyFilter

KeyRange

DataFilter

Index Leaf Pages Table Heap Pages

1 AAA OID3

1 CCC OID11

1 BBB OID7

1 DDD OID14

1 ZZZ OID1

2 ABC OID9

2 AAA OID5

2 CCC OID13

3 ZZZ OID8

4 DBB OID2

4 DAA OID15

4 DDD OID6

4 ZZZ OID12

5 BBB OID16

5 AAA OID10

5 CCC OID4

1

2

1

2

1

2

1

ZZZ

AAA

BBB

ABC

CCC

CCC

DDD

123456

12

123456789

1234567

12345

123456

123456789

1 AAA 123

4 DBB

...

...

...

...

...

...

...

...

...

...

123456789

5 CCC 1234

OID5

OID5

OID10

OID10

OID9

OID9

Page 235: The platform 2011

기술개발 동향234 The Platform 2011

대부분의 DBMS가 페이지(또는 블록) 단위로 I/O를 수행하며

CUBRID도 예외는 아니다. 즉, 하나의 레코드에서 하나의 칼럼만

읽으려 해도 레코드가 속한 페이지 전체를 디스크로부터 읽어온다.

따라서 질의 성능을 좌우하는 가장 중요한 성능 지표는 I/O를 수행

하는 페이지 개수이며, 이 개수는 옵티마이저의 판단에 가장 큰 영

향을 미친다. 옵티마이저가 인덱스를 읽을지, 테이블을 읽을지 결

정하는데 있어 가장 중요한 판단 기준은 읽어야 할 레코드가 아니

라 읽어야 할 페이지 개수인 것이다.

디스크 I/O는 메모리 액세스에 비해 비용이 아주 크다. 질의 수행

에 필요한 모든 데이터 페이지와 인덱스 페이지를 데이터베이스 버

퍼에 로드해서 처리할 수 있다면 좋지만 그러기에는 한계가 있다.

결국 디스크 I/O를 최소화하고 대부분의 연산을 데이터베이스 버

퍼에서 처리할 수 있도록 질의 처리 과정에서 액세스하는 페이지

수를 최소화하는 것이 튜닝의 핵심이다. 액세스하는 페이지 수가

적으면 자연스럽게 물리적으로 디스크에서 읽어야 할 페이지 수도

줄기 때문에 데이터베이스 버퍼 히트율(DB buffer hit ratio)이 높

아져서 데이터베이스의 전체적인 성능이 높아진다. 그러면 지금부

터 인덱스 스캔 과정에서 액세스해야 할 페이지 수를 줄일 수 있는

기법을 알아보자.

Key Filter 활용

앞서 설명한 바와 같이 Key Filter는 Key Range에는 포함되지 않

지만 인덱스 키로 처리할 수 있는 조건이다. 이러한 Key Filter가

WHERE 조건절에 포함되면 데이터 페이지에 접근하는 횟수를 줄

일 수 있다. 데이터 페이지는 랜덤 액세스로 읽기 때문에 인덱스 페

이지 스캔보다 많은 비용이 든다. 따라서 WHERE절에 Key Filter

를 포함하면 성능 향상에 유리하다.

또한 Data Filter가 Key Filter로 적용될 수 있도록 인덱스에 칼

럼을 추가하는 것도 방법이 될 수 있다. 예를 들어 user 테이블에

(groupid, name)으로 구성된 인덱스 idx_1이 있는 상태에서 아래

질의를 수행한다고 하자.

SELECT * FROM user

WHERE groupid = 10

AND age > 40;

표 1 인덱스를 사용하도록 튜닝한 쿼리

튜닝 전 튜닝 후

SELECT *

FROM student

WHERE grade <> 'A';

SELECT *

FROM student

WHERE grade > 'A';

SELECT name, email_addr

FROM student

WHERE email_addr IS NULL;

SELECT name,email_addr

FROM student

WHERE email_addr = '';

SELECT student_id

FROM record

WHERE substring(yymm, 1, 4) = ‘1997’;

SELECT student_id

FROM record

WHERE yymm BETWEEN ‘199701’ AND

‘199712’;

SELECT *

FROM employee

WHERE salary * 12 < 10000;

SELECT *

FROM employee

WHERE salary < 10000 / 12;

작성한 SQL 질의가 DBMS에서 실행될 때 인덱스 스캔을 이용하

는지 확인하려면 질의 실행 계획을 출력해 봐야 한다. CUBRID

Manager의 질의 편집기에 질의 실행 계획을 출력하는 기능이 있

다. 다음 그림은 CUBRID Manager에서 질의 실행 계획을 출력한

예다. 질의 실행 계획에서는 테이블 스캔(sscan)이나 인덱스 스캔

(iscan) 여부, 예상되는 CPU 및 I/O 비용, 예상 결과 집합의 레코

드 개수, 예상 페이지 접근 개수 등을 볼 수 있다.

그림 5 질의 실행 계획

인덱스 활용 최적화

B+-Tree는 특성상 어떤 리프 페이지에 접근하든 거의 동일한 비

용이 든다. B+-Tree를 사용할 때 가장 큰 비용이 드는 부분은 Key

Range의 시작부터 끝까지 인덱스 리프 노드를 따라 진행하는 스캔

과 여기에 대응하는 테이블 데이터의 스캔이다.

Page 236: The platform 2011

성능 향상을 위한 SQL 작성법 235The Platform 2011

을 가능성이 높다는 점에서 디스크 I/O를 줄이는 데 큰 역할을 한

다. 따라서 레코드 크기에 비해 인덱스 키의 크기가 작고, 커버링

인덱스를 이용하는 질의가 자주 수행되는 것이 확실하다면, 커버링

인덱스를 사용하여 SELECT 질의 성능을 크게 향상시킬 수 있다.

정렬 연산 대체

인덱스 스캔으로 생성한 결과 집합은 인덱스 칼럼 순으로 정렬된

상태이므로 ORDER BY 절이나 GROUP BY 절에 의한 정렬 연

산을 생략하도록 질의를 작성할 수 있다.

정렬 연산을 생략하려면 인덱스 칼럼의 순서대로 ORDER BY 절

이나 GROUP BY 절에 칼럼이 지정되어야 한다. 단 인덱스 칼럼

이 조건절에서 ‘=’ 연산자로 동등 비교되는 경우에는 해당 칼럼

이 ORDER BY나 GROUP BY 절에서 중간에 생략되어도 된

다. 예를 들어, 인덱스 키가 (a, b)로 되어 있다면 “ORDER BY

a” 또는 “ORDER BY a, b”처럼 정렬할 컬럼이 명시되어야 한다.

“ORDER BY b” 는 정렬 연산을 대체할 수 없다. 인덱스 (a, b)에

서 b는 a가 같은 값일 때 만 정렬된 상태가 보장되기 때문이다. 하

지만, “a = 2”와 같은 조건이 WHERE 절에 있다면 “ORDER BY

b”도 정렬 연산을 대체할 수 있다.

GROUP BY에 대한 정렬 연산 대체는 CUBRID 2008 R4.0부터

지원된다. 아래 질의는 정렬 연산 없이 인덱스 스캔만으로 정렬된

결과를 만들어 내는 예이다.

SELECT * FROM tbl

WHERE a = 2 AND b < 'K'

ORDER BY b;

SELECT COUNT(*) FROM tbl

WHERE a > 1 AND a < 5

AND b < 'K'

AND c > 10000

GROUP BY a;

ORDER BY b;

정렬 연산이 인덱스 스캔으로 대체되는지 확인하려면 다음 그림과

같이 질의 실행 계획에 skip ORDER BY 또는 skip GROUP BY

가 표시되는지 확인하면 된다.

groupid=10인 조건을 만족하는 레코드가 100건이고 그 중 age >

40인 레코드가 10건이라고 하면, 인덱스 스캔으로 100건의 OID를

가져온 후, 최악의 경우 데이터 페이지로 100회의 액세스를 수행

할 것이다. 그러나, idx_1 인덱스에 age 칼럼을 추가하여 (groupid,

name, age)로 만들면 age > 40 조건이 Key Filter 조건으로 처리되

어 인덱스 스캔으로 10건의 OID만 추출할 수 있다.

커버링 인덱스

만약 사용하는 인덱스로 SELECT 질의에 대한 결과를 모두 얻을

수 있는 상황이라면, 데이터 페이지에 저장되어 있는 레코드를 읽

어오지 않아도 인덱스 키의 값만으로도 결과를 얻을 수 있다. 이와

같이 인덱스가 하나의 질의를 모두 ‘커버’한 경우를 ‘커버링 인덱스

(Covering Index)’라고 한다. CUBRID 2008 R4.0는 커버링 인덱

스를 지원한다.

SELECT a, b FROM tbl

WHERE a > 1 AND a < 5

AND b < 'K'

ORDER BY b;

위의 SQL 질의에서 커버링 인덱스를 적용할 수 있다. 질의에 사용

한 칼럼은 a, b 뿐이고 모두 인덱스 칼럼이기 때문이다. 질의에 커

버링 인덱스가 활용되는지 확인하려면 다음 그림과 같이 질의 실행

계획에 covers라는 표시가 있는지 보면 된다.

그림 6 커버링 인덱스 질의 실행 계획

커버링 인덱스는 데이터 페이지를 읽지 않는다는 점, 그리고 해당

질의를 자주 사용하면 인덱스가 데이터베이스 버퍼에 캐시되어 있

Page 237: The platform 2011

기술개발 동향236 The Platform 2011

LIMIT 최적화

LIMIT 절은 질의의 최종 결과 개수를 제한한다. CUBRID 2008

R4.0에서는 Data Filter가 없는 질의에 LIMIT 절이 있으면 Key

Range에 해당하는 키 값 전부를 스캔할 필요 없이 LIMIT 절에 기

술된 개수만큼의 결과를 확보했을 때 바로 스캔을 중단할 수 있다.

이렇게 하면 범위의 끝까지 스캔하고 나서 결국 버리게 되는 페이

지를 액세스하지 않기 때문에 불필요한 I/O를 줄일 수 있다.

SELECT * FROM tbl

WHERE a = 2 AND b < 'K'

ORDER BY b

LIMIT 3;

이 SQL 질의는 LIMIT 최적화로 필요한 결과를 얻은 후 인덱스 스

캔이 중단되는 예다. 만약 a = 2인 인덱스 키가 10페이지에 걸쳐 저

장돼 있더라도 LIMIT 절에 명시한 3개의 키 값만 스캔하므로 1개

의 페이지만 읽게 된다.

IN 절을 사용한 질의에도 LIMIT 최적화를 적용할 수 있다.

CUBRID는 인덱스 칼럼이 IN 절에 사용되면 Key Range를 IN 절

에 사용된 개수만큼 생성하고, 각 Key Range에 대해 인덱스 스캔

을 수행한다. 다만, 아래 질의처럼 LIMIT 절에 결과 개수가 명시되

면 3번의 인덱스 스캔에 대해 각각 3건의 결과만 구하고 인덱스 스

캔을 중단한다. 즉, 각 인덱스 스캔에 대해서 LIMIT 최적화가 적용

되는 것이다.

SELECT * FROM tbl

WHERE a IN (2, 4, 5)

AND b < 'K'

ORDER BY b

LIMIT 3;

ORDER BY 절은 전체 결과에 대한 정렬을 의미하기 때문에 Key

Range가 여러 개이면 각 인덱스 스캔 결과를 모아서 다시 정렬해

야 한다. 하지만 인덱스 스캔의 결과로 정렬을 대체할 수 있을 때에

는 스캔 과정에서 바로 병합(merge)할 수 있다. CUBRID는 이 과

정을 ‘In-Place Sorting’이라고 부른다.

그림 7 정렬 연산 대체 질의 실행 계획

앞에서 인덱스 스캔을 하려면 조건절에 인덱스의 첫 번째 칼럼이

명시돼야 한다고 설명했다. 하지만 인덱스 칼럼에 NOT NULL 제

약 조건이 설정돼 있다면, 옵티마이저는 조건절에 인덱스 첫 번째

칼럼이 없더라도 최소 키 값과 최대 키 값으로 Key Range를 자동

으로 추가하여 인덱스 스캔을 할 수 있게 최적화한다. 즉, 인덱스

리프 노드의 처음부터 끝까지 스캔하는데, 이를 오라클에서는 인덱

스 전체 범위 스캔(Index Full Range Scan)이라고 한다.

SELECT * FROM tbl

WHERE b < 'K'

ORDER BY a;

위 SQL 질의는 옵티마이저에 의해 인덱스 전체 범위 스캔이 수행

되는 예다. 위 질의문의 실행 계획을 확인하면, 다음 그림처럼 Key

Range가 자동으로 추가되어 ORDER BY 정렬 연산이 생략되는

것을 알 수 있다.

그림 8 옵티마이저에 의해 정렬 최적화된 질의 실행 계획

Page 238: The platform 2011

성능 향상을 위한 SQL 작성법 237The Platform 2011

• 인덱스 키의 크기는 되도록 작게 설계해야 성능에 유리하다.

• 분포도가 좋은 칼럼(좁은 범위), 기본 키, 조인의 연결 고리가 되는 칼럼

을 인덱스로 구성한다.

• 단일 인덱스 여러 개보다 다중 칼럼 인덱스의 생성을 고려한다.

• 업데이트가 빈번하지 않은 칼럼으로 인덱스를 구성한다.

• JOIN 시 자주 사용하는 칼럼은 인덱스로 등록한다.

• 되도록 동등 비교(=)를 사용한다.

• WHERE 절에서 자주 사용하는 칼럼에는 인덱스 추가를 고려한다.

• 인덱스를 많이 생성하는 것은 INSERT/UPDATE/DELETE의 성능 저하

의 원인이 될 수 있다.

• 인덱스 스캔이 테이블 순차 스캔보다 항상

빠르지는 않다. 보통 선택도(selectivity)가

5~10% 이내인 경우에 인덱스 스캔이 우수

하다.

정리하면, 데이터베이스 튜닝의 핵심은 적

절한 수의 인덱스를 생성하고 질의가 이

인덱스를 활용할 수 있도록 질의를 최적

화하는 것이다. 이를 위해서는 DBMS에

구현된 인덱스 구조와 다양한 활용 기법

을 이해하고, 질의 패턴과 사용 빈도, I/O

비용, 저장 공간에 대한 비용을 전체적으

로 고려해야 한다.

다음 그림을 보면서 자세히 설명하면, 먼저 첫 번째 범위(a = 2

AND b < ‘K’)를 스캔하여 3건의 OID를 확보한다. 그 다음 두 번

째 범위(a = 4 AND b < ‘K’)에 대한 스캔을 시도하는데, 이 범위의

첫 번째 키(4, ‘DAA’)는 첫 번째 범위의 마지막 스캔 키(2, ‘CCC’)

보다 b 칼럼의 값이 크기 때문에 바로 스캔을 중단한다. 마찬가지

로 다음 세 번째 범위인 a = 5 AND b < ‘K’에 대한 스캔에서도 두

번째 키를 읽은 후 바로 스캔을 중단한다. 이처럼 In-Place Sorting

기법은 인덱스 스캔 범위를 더욱 축소하고, 최종 결과에 대한 별도

의 정렬을 수행하지 않기 때문에 성능 향상에 많은 도움을 준다.

그림 9 In-Place Sorting 기법에 의해 최적화된 질의 처리 과정

OID Buffer

In-placeSorting

Index Leaf Pages Table Heap Pages

1 AAA OID3

1 CCC OID11

1 BBB OID7

1 DDD OID14

1 ZZZ OID1

2 ABC OID9

2 AAA OID5

2 CCC OID13

3 ZZZ OID8

4 DBB OID2

4 DAA OID15

4 DDD OID6

4 ZZZ OID12

5 BBB OID16

5 AAA OID10

5 CCC OID4

1

2

1

2

1

2

1

ZZZ

AAA

BBB

ABC

CCC

CCC

DDD

123456

12

123456789

1234567

12345

123456

123456789

1 AAA 123

4 DBB

...

...

...

...

...

...

...

...

...

...

123456789

5 CCC 1234

OID5

OID10

OID9

은 총알은 없다(No Silver Bullet)

인덱스가 좋다고 인덱스를 많이 만드는 것은 능사가 아니다. 오히

려 인덱스 관리 비용이 증가하고 INSERT, UPDATE, DELETE

의 성능 저하의 원인이 될 수 있다. 인덱스 사용 시 다음 내용을 고

려하자.

Page 239: The platform 2011

기술개발 동향238 The Platform 2011

이번 콘퍼런스에 참석해서 웹 커뮤니티의 동향도 느낄 수 있었

다. 이번 콘퍼런스에서 느낀 최근의 경향은 웹 성능 최적화(WPO,

Web Performance Optimization), 클라우드, 모바일, 자바스크립

트의 4가지로 요약할 수 있다. 웹 성능 최적화는 사업적인 경쟁력

을 바탕으로 점점 더 중요해지고 있다. 빠른 사이트는 더 많은 고객

을 불러오고, 그 결과 매출이 늘어난다는 이론이 더 명확해 지고 있

는 것이다. 또한 AWS(Amazon Web Services)나 OpenStack과

같은 클라우드 솔루션으로 손쉽게 IT 인프라를 구성하고 사용하는

방법이 이제는 일반화되었다는 것을 알 수 있었으며, 자바스크립트

는 바야흐로 최고의 전성기를 앞두고 있는 것으로 보인다. 또한 이

미 대세가 되어 버린 모바일에 대한 여러 이슈도 다루어졌다.

다음 절부터는 2011년 Velocity 콘퍼런스에서 다루었던 여러 주제

중에서 웹 성능 최적화와 모바일 및 자바스크립트에서 ‘서비스 속

도 혁신’과 연관이 있는 주제를 소개할 것이다.

O’REILLY가 주관하고 여러 기업이 후원하는 Velocity 콘퍼런스

는 2008년에 실리콘밸리의 웹 개발자들과 운영자들이 모여서 시

작했다. 이후, 이 콘퍼런스는 대형 웹 사이트를 '효율적으로' 개발하

고 '중단 없이' 운영하는 지식을 공유하는 대표적인 콘퍼런스가 되

었다. NHN은 2010년부터 이 콘퍼런스에 참가했다. 이 글에서는

Velocity 2011 콘퍼런스 참관기를 공유한다.

Velocity 2011

2011년의 Velocity 콘퍼런스는 2010년보다 훨씬 더 많은 숫자인

2,000명이 넘는 개발자와 운영자가 참석했다. 매해 급격하게 늘

고 있는 참가자 수는 웹 환경이 급속도로 발전하고 있다는 것을 방

증하는 것이 아닐까 한다. 참가자 대부분이 실리콘밸리의 사람이

었지만, 유럽에서도 많은 사람들이 왔고 다음(Daum)이나 바이두

(Baidu)와 같은 아시아 회사도 이번 행사에 참여했다. 또한 2010년

에 비해 배가 넘는 서른 다섯 개 이상의 업체가 스폰서를 할 정도로

Velocity 콘퍼런스는 중요한 콘퍼런스로 자리잡았다.

Velocity 2011, 속도혁신 최근 동향항상 해외 콘퍼런스에 다녀오면서 하는 생각은......

"영어 공부 열심히 하자~"인데 작심삼일입니다.

꾸준히 하기가 어렵네요.

그런데 작심삼일도 자주 하면 영어가 많이 늘지 않을까요?

회사 님, 해외에 자주 보내 주세요~

•성능엔지니어링랩 _ 장재완

Page 240: The platform 2011

Velocity 2011, 속도혁신 최근 동향 239The Platform 2011

Page Speed

Page Speed(http://code.google.com/intl/ko-KR/speed/page-

speed/)는 파이어버그(Firebug)에서 웹 페이지 로드 속도를 측

정할 수 있는 도구이다. 이번 업데이트로 WebPagetest처럼 Page

Speed를 온라인에서도 사용할 수 있게 되었고, Page Speed API를

이용해서 다른 사이트에서도 Page Speed 기능을 쉽게 이용할 수

있게 되었다.

크롬 개발자 툴

크롬 개발자 툴의 업데이트가 가장 많았다. 새로운 성능 관련 지표

(performance.memory.*)도 지원하기 시작했고, 자바스크립트 디

버깅을 위해서 힙(heap) 프로파일러도 들어갔다. 그리고 원격에서

디버깅할 수 있는 기능도 추가되었다.

Show Slow

웹 성능 최적화 도구

웹 성능을 최적화할 때 가장 필수적인 것이 웹 성능을 측정하고 모

니터링하는 도구이다. 이번 Velocity 콘퍼런스에서도 모니터링 도

구에 대한 이야기가 많이 나왔다. 이미 우리에게 익숙한 도구에 새

로운 기능이 추가되었다는 소식도 들었고, 생소한 도구도 많이 알

수 있었다.

WebPagetest

과거 WebPagetest(http://webpagetest.org)는 페이지 하나만 테

스트하는 사이트였다. 그러나 Advanced Settings에 있는 스크립

트(Script) 기능을 이용하여 페이지 여러 개를 순서대로 방문하면

서 테스트할 수 있게 되었다. 또한 과거에는 아이디와 패스워드를

입력해야 하는 사이트는 테스트할 수 없었지만, 이제는 자동으로

아이디와 패스워드를 채워서 테스트할 수 있는 기능이 추가되었다.

이 외에도, 사용자의 자바스크립트 코드를 삽입할 수 있는 기능도

추가되었다.

YSlow

YSlow(http://developer.yahoo.com/yslow/)는 야후!에서 제작

한 도구로, 웹 페이지를 분석하여 성능을 향상시킬 수 있는 다양한

방법을 제안해 준다. 그동안 파이어폭스 플러그인만 있었는데 이제

는 크롬에서도 사용할 수 있다. 그리고 모바일 페이지의 웹 성능 최

적화를 테스트할 수 있는 기능이 새로 추가되었다.

Page 241: The platform 2011

기술개발 동향240 The Platform 2011

요즘 PC의 웹 브라우저는 개발자 도구를 이용해서 웹 페이지의

HTML, CSS, 자바스크립트 등을 분석(inspect)할 수 있다. weinre

라는 도구는 모바일에서도 비슷한 방식으로 웹 페이지를 정밀하게

분석할 수 있는 도구이다. 모바일 페이지에 weinre 자바스크립트

를 넣어 두고 원격으로 PC에서 분석하는 방식이다.

그 외 기타

Selenium을 이용해서 웹 페이지 테스트를 자동화하는 방법,

BrowserMob 프록시나 Charles 프록시를 이용해서 모바일 네트

워크를 에뮬레이션하고 Har 파일을 추출하는 방법, Harpoon을

이용해서 수행한 모든 테스트의 Har 파일을 저장해두고 분석할 수

있는 도구 등이 소개되었다.

웹 성능 최적화 적용 사례

Google 순간 검색

Google은 순간 검색(Instant Search)을 자랑했다. 그 중에 굉장히

신선한 아이디어가 바로 Hover-to-Click 지연 시간을 이용한 프

리페치(prefetch)였다. 보통 링크를 클릭할 때 클릭 전에 그 링크로

마우스 포인터를 가져가는데, 링크 위에 마우스 포인터를 올려 두

고 클릭해서 손가락을 뗄 때까지 대략 200ms~300ms 정도의 지연

시간이 있다고 한다. 그 시간에 그 링크에 있는 HTML이나 이미지

등을 먼저 가져오는 방식을 이용하는 기발한 아이디어였다.

야후 메일의 디스플레이 광고

Show Slow(http://www.showslow.com/)는 Yslow, Page

Speed, dynaTrace의 성능 지표를 하나로 모아 볼 수 있게 해 주는

사이트이다. 이제 새롭게 Google Analytics의 지표와 비교할 수

있는 기능을 지원하고, 이를 통해서 성능 지표와 비즈니스 지표를

대조할 수 있다고 한다.

HTTP Archive

HTTP Archive(http://httparchive.org)는 WebPagetest의 자료

를 이용해서 수많은 웹 사이트의 웹 성능 지표의 과거와 현재를 기

록하는 사이트이다. 이를 통해서 웹의 현재 동향을 파악할 수 있게

된다. 예를 들어, 최근에는 웹 사이트가 포함하고 있는 이미지의 크

기가 점점 커지는 추세라고 한다. 이 외에도 매우 흥미로운 통계 자

료도 많이 있었다.

weinre

Page 242: The platform 2011

Velocity 2011, 속도혁신 최근 동향 241The Platform 2011

• 3G 네트워크를 안 쓰면 자동으로 끄는데, 다시 켤 때 데이터를 주고 받

기 위해서 2초 이상 기다려야 한다.

• 안드로이드 전체에 HTTP 트랜잭션(transaction)을 처리하는 스레드

가 4개 존재한다(넥서스S의 경우).

• HTTP 파이프라인(HTTP Pipelining)을 쓴다. 그래서 생각지도 못한 결

과가 나오는 경우가 있다.

• 안드로이드 단말기의 persistent cache(플래시 메모리에 저장되는

캐시)는 그 오브젝트를 서버에서 받아왔을 때 명시된 캐시 만료 시간

(expiration date)을 기준으로 가까운 만료시간을 가진 오브젝트부터

캐시에서 빼내는 알고리즘을 운용한다. 그러므로 가급적 캐시 만료 시

간을 아주 먼 미래로 설정하는 것이 유리하다.

이 세미나는 모바일 디바이스의 성능이 브라우저 코어인 웹킷

(Webkit) 엔진뿐만 아니라 네트워크 성능과 운영 체제의 정책에

의해서도 많이 영향을 받는다는 것을 보여 주었고, 앞으로 웹 성능

을 높이기 위해서는 브라우저만이 아니라 고객의 운영 체제에 대해

서도 많이 알아야 함을 시사하였다.

PhantomJS

PhantomJS라는 재미있는 도구도 소개되었다. 웹킷과 자바스크립

트 엔진을 붙여 놓은 프로그램으로, 커맨드라인에서 웹 사이트를

방문하여 DOM에 접근하거나, 필요한 부분만 렌더링해서 이미지

파일로 받을 수 있는 등 GUI 없이 디버깅과 테스트 자동화를 할 수

있는 신기한 프로그램이다.

야후!는 웹 2.0 방식의 야후! 메일에서 디스플레이 광고를 어떻게

비즈니스 충돌 없이 잘 보여 주면서도 웹 성능도 희생하지 않을 수

있는가에 대한 케이스 스터디를 발표했다. 그 발표 중에 놀라운 사

실은 웹 1.0 방식의 야후! 메일에서 광고를 잠시 뺐더니, 페이지 로

드 속도가 73%나 향상되었다는 것이다. 그래서 광고를 효율적으로

로드하기 위해 광고주 또는 대행사에 광고 제작 시 필요한 라이브

러리를 사용하게 했고 광고를 보여 주는 로직도 개선했다고 한다.

모바일

모바일 웹 최적화 팁

모바일 웹 최적화는 지금까지 진행했던 PC용 웹 최적화보다 더 까

다롭다. 모바일 디바이스는 하드웨어 성능이 PC에 비해서 매우 낮

고, 모바일 디바이스의 종류도 매우 다양하기 때문에 최적화가 더

어렵다. 그렇기 때문에 다양한 최적화 팁에 대한 이야기가 더 많이

나왔다. 몇 가지 팁만 요약하면 다음과 같다.

• HTML5의 시맨틱 태그(semantic tag)인 <header>, <footer>,

<nav> 등을 이용해서 필요한 HTML 코드의 크기를 줄인다.

• 인라인 이미지(inline image) 등을 이용해서 가급적 서버로의 요청 횟

수를 줄인다.

• ‘ontouchend’ 이벤트를 사용한다. onclick 이벤트보다 300~500ms

더 빠르다

• 애니메이션은 자바스크립트로 만들지 말고, CSS3의 transition 속성이

나 애니메이션 기능을 사용한다.

• 보이지 않는 부분은 지연 로딩(lazy-loading) 기법을 사용한다.

• HTML5에 소개된 localStorage를 캐시로 이용한다.

하지만 모바일 디바이스는 워낙 디바이스마다 특성이 다르기 때문

에 어쩔 수 없이 개발 단계에서 테스트를 많이 해 볼 수 밖에 없다

는 것이 모바일 관련 세미나의 결론이었다. 그래서인지 이런 테스

트를 자동화하는 솔루션에 대한 광고도 많았다.

안드로이드 스마트폰

스마트폰 중에서는 오픈 소스인 안드로이드에 대한 최적화 기술이

많이 나왔다. 특히 퀄컴에서 발표한 안드로이드 성능 최적화에 대

한 세미나가 흥미로웠다.

Page 243: The platform 2011

기술개발 동향242 The Platform 2011

참고 자료

발표 제목을 이 글에서 다룬 기준으로 나누어서 정리했다. 자세한

사항은 Velocity 콘퍼런스 사이트(http://velocityconf.com)의 발

표 자료 슬라이드와 비디오를 참조하기 바란다.

웹 성능 최적화

Workshop

• Performance tools (Google, Dynatrace, Blaze, truTV)

• Automated Web Performance Test (Neustar)

콘퍼런스 첫째 날

• Performance Measurement and Case Studies at MSN (MSN)

• WebPageTest update (Google)

• Web Site Acceleration with Page Speed Technology

(Google)

• Lightening Demos Wednesday

콘퍼런스 둘째 날

• Making the Web Instant (Google)

• The impact of Ads on Performance and Improving Perceived

Performance (Yahoo)

• Lightening Demos Thursday

Mobile

Workshop

• Analyzing the Performance of Mobile Web: Challenges and

Techniques (Sencha)

• Mobile Web & HTML5 Performance optimization (ITMaster

Professional Training)

콘퍼런스 첫째 날

• Testing and Monitoring Mobile Apps (Keynote)

• Understanding Mobile Web Browser Performance

(Qualcomm)

자바스크립트

자바스크립트에 대한 이야기도 많이 나왔다. 특히 자바스크립트 엔

진은 비약적으로 빨라졌지만, 자바스크립트로 DOM에 접근할 때

매우 느리다는 이야기를 여러 번 들었다. 그래서 새로운 데이터를

저장할 때에는 가급적 DOM 구조에 저장하지 말고 자바스크립트

변수에 넣어 두고 사용하라고 한다. 그리고 몇몇 코딩 가이드가 있

었다.

• eval, with 등은 느리니까 가급적 피한다.

• 루프 안에서 오브젝트를 생성하지 않는다.

• 밀집 배열(dense array)을 쓴다.

• 타입이 고정된(type-stable) 코드를 만든다. 한 변수의 타입이 고정되

어 있어야 자바스크립트 엔진이 JIT 컴파일러로 빨리 실행할 수 있다고

한다. 예를 들어 a라는 변수에 문자열을 넣었다가 정수형을 넣으면 빨리

실행하기 어려워진다는 것이다.

• 자바스크립트의 크기도 중요하지만 자바스크립트가 실제로 로드되어

메모리를 얼마나 사용하는지도 성능의 고려 대상이다.

이 외에도 짧게 설명하기 어려운 많은 트릭이 있었다. 더 많은 트릭

을 알고 싶으면 이 글 마지막의 참고 자료에 있는 자바스크립트 관

련 발표의 슬라이드를 꼭 보기 바란다.

클라이언트 사이드는 물론 Node.JS를 이용한 서버 사이드 제작 이

야기도 많이 나왔다. 또한 클라이언트 쪽에서나 서버 쪽에서나 동

일하게 실행할 수 있는 자바스크립트를 만드는 이야기도 나왔다.

웹 성능 최적화 전망

웹의 속도, 성능의 문제는 이제 시작이라고 생각한다. 이미 서비스

속도혁신실, Front-end성능TF와 같은 많은 실무 조직에서 고민하

고 토론했던 웹의 속도 문제를 웹의 본고장 실리콘밸리에서도 똑같

이 고민하고 있다는 점에서 우리가 늦게 가고 있는 것은 아니라는

자신감도 느낄 수 있었다. 그 누구도 더 앞서 나간다고 하기 어려우

며 눈덩이처럼 쌓여 있는 문제를 풀 수 있는 다양한 아이디어와 기

획이 나오고 있다. 한국도 마찬가지라고 생각한다. 미국과는 매우

다른 인터넷 환경과 인터넷 익스플로러 사용자가 아직도 전체 사용

자의 90%를 넘는 기형적인 환경에서 웹의 속도를 향상시킬 수 있

는 아이디어와 기획은 NHN이 아니면 국내에선 그 누구도 하기 쉽

지 않겠다는 생각에 어깨가 무거워지는 것을 느낄 수 있었다.

Page 244: The platform 2011

Velocity 2011, 속도혁신 최근 동향 243The Platform 2011

콘퍼런스 둘째 날

• 10 Tricks for Mobile Performance (Torbit)

• iOS vs. Android vs. Blackberry (Blaze.io)

자바스크립트

Workshop

• Performance Enhancing Programming with NodeJS

(Creationix)

콘퍼런스 첫째 날

• JavaScript & Metaperformance (Yahoo)

• Writing fast client-side code: lessons learned from

SproutCore (Strobe)

• Oh, To be single again – Building a single codebase in a

client-server world (Yahoo)

• Know your engines: How to make your javascript fast

(Mozilla)

콘퍼런스 둘째 날

• Holistic Performance (mozilla)

• Instrumenting the real-time web: Node.js, Dtrace and

Robinson Projection (Joyent)

Page 245: The platform 2011

기술개발 동향244 The Platform 2011

이런 환경에서 분산 버전 관리 시스템이 점점 부각되기 시작했다.

특히 Bitkeeper나 Plastic SCM 등과 같은 상용 DVCS를 대체할

수 있는 Git나 Mercurial 등의 공개 도구가 등장하고, Linux 커널

등과 같은 대규모의 프로젝트에서도 사용되기 시작하면서 빠른 속

도로 확산되기 시작했다. 특히 DVCS는 이력(History)에 대한 개

념을 달리하면서 강력한 브랜치, 병합 등이 대표적인 장점이 되었

다. 무엇보다 가장 큰 장점은 분산 환경에 기반하고 있으므로, 로컬

저장소(Repository)만으로 잘 동작하고, 짧은 역사를 만회하기 위

해 기존 VCS와의 협업을 다양하게 지원한다는 점이다. 이러한 점

은 개발자가 개발 환경에서 많은 비용을 들이지 않고 가벼운 마음

으로 시도해 볼 수 있도록 한다.

DVCS를 사용해 보고 싶은 마음이 들었다면, 어떤 도구를 사용

할 수 있는지 궁금할 수 있다. Darcs, Bazzar, arch, SVK, Fossil,

Mercurial, Git 등의 공개 도구와 Bitkeeper, Plastic SCM 등

의 상용 도구가 있는데, 이 중에서 현재 가장 대세에 가까운 Git와

Mercurial을 비교해 보도록 하겠다.

기존의 버전 관리 시스템(Version Control System)에서는 애자일

(Agile) 스타일의 개발 방법론으로 개발을 진행할 때 잦은 충돌로

인해 브랜치를 병합하는 과정에서 예상보다 더 많은 비용이 드는

사례가 늘고 있다. 이런 환경에서 부각되고 있는 것이 분산 버전 관

리 시스템(DVCS, Distributed VCS)이다. 이 글에서는 분산 버전

관리 시스템의 대표적 제품인 Git과 Mercurial을 비교해 보겠다.

DVCS로의 이주

버전 관리 시스템(이하 VCS)은 IDE(Integrated Development

Environment)와 개발자가 가장 자주 접하는 도구이다. VCS의 경

우에는 IDE와는 달리 개발자에게 주어진 선택의 자유가 그렇게 많

지 않다. 그 이유는 조직 내의 모든 구성원이 동일한 VCS를 사용해

야 하기 때문이다. 하지만 아이러니하게도 개발자가 열심히 일할수

록 종래의 VCS는 많은 문제를 일으킨다. 애자일(Agile) 개발 방법

론을 적용하여 기능 단위의 변경이 동시에 병렬적으로 일어나는 환

경에서는 자주 충돌이 발생한다. 브랜치를 병합하는 과정에서 역시

많은 충돌이 발생하고 병합에 들어가는 비용이 높아 종래의 의도대

로 브랜치와 병합 기능을 사용하는 경우는 드물다.

Git vs. Mercurial 잠 와요.

졸려요.

부작용 없는 잠 깨는 비약 혹은 비법을 주세요.

•EC전시서비스개발팀 _ 정지웅

Page 246: The platform 2011

Git vs. Mercurial 245The Platform 2011

확장 명령을 만들기가 쉽지 않다. 대신 필요하다 싶은 기능은 대부

분 이미 번들 확장(Extension)에 포함되어 있다.

반면에 Git은 필수적인 기능 세트를 콤팩트하고 세련된 형태로 갖

춘 맥가이버 칼이다. 그래서 핵심적인 부분만을 봤을 때, 기능의 종

류도 적고 기본적인 기능만 제공된다고 생각할 수 있다. 하지만 셸

스크립트를 사용해 기본적인 명령을 확장하거나 연결해서 새로운

명령을 만들어 자신의 손에 딱 맞는 형태의 도구를 생성할 수 있다.

그래서 때로는 Mercurial과 동등하거나 더 나은 사용성을 얻을 수

있다.

Mercurial은 또한 잘 정리된 별칭3이 미리 만들어져 있으며, 대

부분의 경우에 설정 없이 혹은 사용자 이름을 설정하는 정도만으로

바로 시작할 수 있는 환경을 제공한다. 그에 반해 Git은 일반적인

설정을 전혀 제공하지 않으므로, Mercurial에 비해 조금 더 많은

설정이 필요할 수 있다.

더 깊이

Git과 Mercurial의 차이점을 위에서 간략하게 살펴보았다. 이러한

차이는 구현 언어 외에도 구현 방식에서도 드러나는데, 대표적인

구현 방식상의 차이는 저장소의 구조와 브랜치에 있다.

저장소의 구성

먼저 Git의 저장소는 스냅샷 기반이다. 모든 변경이나 파일을 포함

하는 대상은 오브젝트로 표현되며, 오브젝트의 종류에는 Commit,

Tree, BLOB, Tag가 있다. BLOB은 Leaf 노드로 실제 관리되는 파

일이 여기에 들어간다. 그 외의 오브젝트는 다른 오브젝트를 참조

하는 형식으로 아래와 같은 트리 구조를 만든다(그림 1).

여기서 BLOB에서의 관리 대상은 특정 시점의 파일 전체 내용이

다. 그래서 Git의 저장소는 크기가 빠른 속도로 증가하는데, 이를

해결하기 위해 gc 명령을 제공한다. gc 명령을 수행하면서 접근 불

가능한 브랜치는 삭제하고, 오래된 변경 집합(Changeset)은 diff

형태의 파일을 압축한 형태로 저장하여 저장소 효율을 높인다.

3 ‘status’의 별칭인 ‘st’, ‘commit’의 별칭인 ‘ci’, ‘update’의 별칭인 ‘up’ 등

Git와 Mercurial

개요

Git과 Mercurial은 거의 비슷한 철학을 가지고 있다. 이는 아마

도 DVCS가 가지는 특성 때문일 것이라고 생각할 수 있다. 가볍고,

규모 확장이 쉬운 VCS. 이 특성은 DVCS(Distributed VCS)에서

‘D’가 가지고 있는 특징을 그대로 나타낸다. 특히 변경에 대한 이력

을 시간에 의존적인 선형 구조로 나타내는 것이 아니라, 복수의 부

모 변경과 복수의 자식 변경을 표시하는 그래프로 나타낸다. 두 제

품에서 차이가 있다면, Git은 태생부터 수많은 병렬 브랜치를 전

제로 하여 설계되었다는 점이다. Mercurial은 그런 장점이 없는

대신에 배우고 사용하기 쉽도록 많은 노력을 들였다. 이는 Git과

Mercurial 사이의 선택에서 가장 중요한 차이점이다.

실행 환경

Git은 C 언어와 bsh(Bourne Shell), Perl을 이용하여 전체가 작성

되어 있으며, Mercurial은 거의 전체가 파이썬으로 작성되었다1.

바꿔 말하자면 Git은 Mercurial에 비해 상당히 Linux 친화적인 성

격을 가진다. 그렇기 때문에 Windows 환경에서 Git는 mingw32

와 같은 에뮬레이션 환경이 필요하며, 상대적으로 Mercurial이

Git에 비해서 Windows 환경에서 더 나은 성능을 보여 준다. 하지

만 동시에 Git은 셸 스크립트(shell script)를 사용할 수 있다면, C

언어를 사용하지 않아도 명령을 확장할 수 있다는 장점이 있다. 그

에 반해서 Mercurial은 파이썬으로 된 코드(Core Code)나 명령어

(Command) 구성을 이해하지 못하면 확장 명령을 만들기가 Git에

비해서는 어렵다는 특징이 있다2.

사용성

사용성 측면에서 공구에 비유해 보자면 Mercurial은 종합 공구 세

트에, Git은 맥가이버 칼에 비교할 수 있을 것 같다.

Mercurial은 이미 패키지 안에 모든 도구가 각각 고도로 완성된 상

태로 들어 있는 종합 공구 세트이다. 물론 확장을 할 수는 있지만,

1 Mercurial에 ‘거의’란 말을 덧붙인 것은 Binary Diff와 관련된 부분은 C로 작성되어 있기 때문이다.

이것을 명시하지 않은 이유는 Mercurial이 가지는 Cross-Platform에 대한 강점을 없애거나 줄이지

않는다고 생각했기 때문이다.

2 대신 풍부한 번들 확장을 가지지만, 아무래도 확장의 자유도 측면에서는 조금 아쉬운 부분이 있다.

Page 247: The platform 2011

기술개발 동향246 The Platform 2011

그림 1 Git의 오브젝트 트리 구조

반면에 Mercurial은 각 파일별 변경분만 추적한다. 저장소에는 실

제 관리 대상이 되는 파일의 트리 구조와 동일한 형태의 .i를 확장

자로 하는 변경 기록용 파일이 있으며, 그 파일에는 해당 파일이 가

리키고 있는 파일의 변경 이력이 바이너리(binary) 형태로 저장되

어 있다. 그로 인해 저장소는 변경분에 비례해서 증가하게 되며, 일

반적으로 Git에 비해 완만한 속도로 저장소의 크기가 증가한다4.

그림 2 Mercurial의 메타 정보 구조5

4 gc 명령이 실행되면, 거의 비슷하거나 Git의 저장소 크기가 좀 더 작다.

5 영문자 앞의 '_'는 해당 문자가 대문자 임을 나타내며, '__'는 '_'로 이스케이프 된다. 이는 영대소문자를

구분하지 않는 파일 시스템에서도 파일이름을 유지하기 위해 사용된다.

Commit Commit Tree

Tree

Blob

tree tree blob

blob

LICENNSE=======

......

File Content

......

Size Size Size

Size

Size

Comit Message Comit Message

e730a 0de24 e8455

e8455

README

lib_spec.rb

parent parent tree

author author tree

committer committer

98ca9 nil 10af9 lib

Gildong Gildong b70f8 spec

Gildong Gildong

08ae4... 98ca9... 0de24...

b70f8...

e8455...

Working Copy Meta Repository

lib_spec.rb.i

specspec

lib

081ae .patch

73c88 .patch

spec

spec

README

_r_e_a_d_m_e.i

lib_spec.rb

lib_spec.rb.i

Page 248: The platform 2011

Git vs. Mercurial 247The Platform 2011

전반적으로 Mercurial이 제공하는 변경 이력 그래프는 Git에 비

해 좁은 폭을 지니고 SVN의 이력 관리와 상당히 유사한 면모를 보

인다. 그로 인한 대표적인 차이점이 Mercurial은 로컬 저장소에 있

는 변경 이력에 대해 SVN의 리비전(Revision) 번호와 같은 리비

전 번호를 제공한다. 다만 이 변경 이력이 복제된 다른 저장소에서

도 유지된다는 것을 보장하지 못하므로, 식별자로서 사용하는 것은

의미가 없다. 그래서 이 차이점은 초기 적응에는 도움이 될 수 있지

만, 최종적으로는 Mercurial에 대한 오해

를 불러 일으키는 원인이 되기도 한다.

DVCS의 브랜치는 기본적으로는 저장소

의 복제를 기반으로 동작한다. 하지만 Git

과 Mercurial은 둘 다 태깅(Tagging)을

기본으로 하는 브랜치 기법을 제공한다.

분기된 특정 커밋에 태그를 붙이면, 해당

태그가 브랜치의 이름처럼 동작한다. 거의

동일하지만 큰 차이 중 하나는 저장소를

원격 저장소에 반영하거나 혹은 그 반대

의 작업을 할 때, 브랜치에 대한 동작이다.

Mercurial의 경우에는 모든 브랜치를 한

번에 반영한다. 그에 반해 Git은 현재 작업

중인 브랜치만 반영한다.

성능

Git 같은 경우 프로젝트의 규모에 비례하여 저장소의 규모가 상당

히 빠른 속도로 증가하며, 대조적으로 Mercurial은 완만하게 선형

적으로 증가하게 된다7. 대신 스냅샷 기반이므로 대규모의 프로젝

트에서도 일정한 성능을 유지하며, 대체로 Mercurial보다 빠르다.

반면 Mercurial의 경우 차이점 기반이며 디스크 I/O가 적으므로,

대량의 읽기/쓰기가 발생하는 상황에서도 비교적 안정적이다. 그

러나 프로젝트 규모가 커지고, 변경 이력이 쌓일 수록 패치를 병합

하는 비용이 증가한다. 또 Mercurial의 저장소는 Append 기반으

로 동작함에 따라, 디스크에 오류가 있을 때에 저장소가 영향을 적

게 받는다는 점도 Mercurial의 장점이 된다.

7 SVN과 함께 사용할 경우 내부에 SVN Meta정보를 보관하므로 오히려 Git보다 빠른 속도로 저장소

가 커질 수 있다.

그래서 별도의 저장소에 대한 관리 작업이 필요하지 않다는 장점이

있다. 대신 패치(patch)의 생성이나 변경 이력의 추적에는 Git에

비해서 용이하나, 스냅샷의 생성이나 업데이트, 커밋(Commit) 작

업에는 대부분의 경우 Git에 비해서 높은 비용을 요구한다.

병합과 변경 이력

그림 3 선형 구조와 Revision 구조의 차이6

병합과 브랜치는 DVCS가 SVN등의 기존 VCS에 비해서 더

나은 가장 큰 장점 중 하나다. 분산 환경을 전제로 하고 개발

된 시스템이므로 기본적으로 모든 변경 이력은 위 그림과 같이

DAG(Directed Acyclic Graph)로 나타나며, 그로 인해 SVN의

단순 스냅샷 기반의 3-Way 병합에 비해 영리한 병합 작업이 가

능하다. 그것은 각 변경 집합이 부모 변경 집합에 대한 참조를 가

지고 있기 때문인데, 이로 인해 충돌이 발생할 가능성이 현저하

게 떨어지게 된다. 다만 구현상의 차이라기 보다는 동작상에 차이

가 있다. Git은 정말 대규모의 브랜치를 고려하므로 n-Way 병합

을 지원한다. 반면에 Mercurial의 경우 기본적으로 Git과는 다른

익명 브랜치(혹은 동적 브랜치)를 기본 사양으로 하므로, 2-Way

병합을 기본으로 하여, N개의 병합 대상 브랜치가 있다면 N-1의

병합을 실행하게 된다.

6 최신 변경 집합이 선형에서는 항상 하나인데 반해서 DAG는 복수의 최신 변경 집합을 가질 수 있다.

Merge

5

4

3

1 1

2 3

4 5

67

8

2

DAGLine

Timeline

Page 249: The platform 2011

기술개발 동향248 The Platform 2011

그림 4 중앙집중형 저장소 구성

물론 DVCS답게 중앙 저장소를 가지지 않는 형태의 구성도 가능하

지만, SVN과 함께 쓴다면 이런 형태 외에는 나오기가 힘들다. 다

만 SVN과 DVCS를 같이 쓰거나 SVN에서 DVCS로 마이그레이

션할 때, DVCS에서는 svn:externals와 같은 기능은 제공하지 않

는다는 것, 하위 디렉터리별 체크아웃이 불가능하다는 점은 염두에

두어야 한다. 후자의 경우는 저장소의 재구성을 통해 해결하여야

하며, 전자의 경우는 유사한 확장(Mercurial - SubRepos, Git -

SubModule)이 나와 있으나 기능적인 면에서 많은 차이9를 보이

니 사용 전에 미리 고려해 보는 것이 좋다.

결론

둘 다 DVCS의 대표적인 것들이며, 활발하게 수정이 일어나고 있

다. 적용되고 있는 프로젝트도 늘어나고 있으며, Kernel이나 모질

라(Mozilla), 안드로이드, OpenJDK 등 대규모 프로젝트의 이전

도 눈에 많이 띈다. 뿐만 아니라 개발자 개인 환경에만 영향을 주는

설정이 가능하며, 둘 다 SVN과 나쁘지 않은 연동을 제공한다. 그렇

게 봤을 때, 더구나 대부분의 소프트웨어가 그렇듯 서로의 장점을

9 Mercurial의 경우 최근(2011-08-01) 1.9.1에서 svn:external의 subrepo 변환 기능이 추가되

었다.

Subversion과의 연계

가장 첫머리에 밝혔다시

피 일반적으로 개발자가

VCS를 마음대로 선택할

수 있는 경우는 드물다. 그

러므로, 기존의 VCS와 함

께 사용해야 할 텐데 가

장 많이 쓰이는 VCS가

SVN(Subversion)이므로

SVN과의 연동을 기본으로

비교해보고자 한다.

둘 다 기본적인 동작은

SVN의 각 커밋 내용을 받

아서 로컬 저장소에 다시 커

밋하는 형태를 취한다. 이

과정에서 Git의 경우 중간

에 동작을 멈추는 경우가

종종 있었으나 최근에는 해결되었는지 어지간한 규모의 프로젝트

에서도 잘 동작한다. 그리고 기본적인 성능의 차이로 인해 Git이

Mercurial에 비해 조금 더 빠르다. Mercurial의 경우는 자체 메

타 데이터 내에 SVN의 메타 데이터도 함께 보관한다. 그래서 일반

적인 저장소에서의 상황과는 달리 SVN과 사용할 경우에는 Git의

저장소가 훨씬 콤팩트한 형태가 된다. 대신 SVN과의 연동 자체를

Mercurial이 Git보다 먼저 시작하였고 오래된 만큼 안정적이다8.

이러한 문제가 생기는 원인은 SVN은 선형적으로 변경 사항의 목

록을 관리하기 때문이다. DVCS의 프론트 엔드로 SVN을 쓸 때 가

장 자주 부딪치는 문제가 이 그래프 형태의 변경 이력을 선형적인

형태로 바꾸는 작업이다. 이 문제를 해결한 방법이 Mercurial과

Git 양쪽 공히 rebase란 작업이다. 이 rebase 작업을 Mercurial의

경우 직접 해야 하고, Git의 경우는 원격 저장소에 반영하는 작업

속에 이 rebase가 녹아 있다.

일반적인 환경의 구성은 그림 4와 같다.

8 적절하게 명령을 실행했을 때에 한하며, 명령을 잘못 수행하면 오히려 Git보다 복구가 힘들다.

Subversion or DVCS

Personal Repository Personal Repository Personal Repository

pull, clone

update, revert

C:�NMP�nmp-hermes C:�nmp-hermes

commit

Folder Folder FolderFile2 File2File2

File3 File1 File3 File4File3File1

push, dcommit

X

Server Centrol

Repository

Cloned

Repository

Working

Copy

Desktop

C:�User�nmp�nmp-hermes

Page 250: The platform 2011

Git vs. Mercurial 249The Platform 2011

부록

Mercurial 번들 확장 목록10

다음 표는 Mercurial의 번들 확장 목록 중에서 자주 쓰는 일부

번들 확장이다. 더 자세한 목록이 필요하다면 공식 위키(http://

mercurial.selenic.com/wiki/CategoryBundledExtension)를 참

조할 수 있다.

Name Page Description

acl AclExtension 저장소의 접근 경로별 커밋 권한을 설정한다. Mercurial 특성

상 읽기 권한은 구분할 수 없다.

alias AliasExtension 사용자가 정의한 커맨드 이름의 별칭을 사용할 수 있다.

bookmarks BookmarksExtension 특정 변경 집합의 수정 사항을 따라가는 마커를 생성한다(Git

의 Fast Branch와 유사한 기능을 제공).

bugzilla BugzillaExtension 버그 ID 기록으로 버그질라의 항목을 업데이트할 수 있다.

children ChildrenExtension 특정 리비전이 부모인 자식 리비전을 보여 준다.

churn ChurnExtension 사용자별 작업의 통계를 보여 준다.

convert ConvertExtension 다른 VCS를 Mercurial로 변경한다. CVS, SVN, Git, Bazaar,

Perforce 등을 지원한다.

color ColorExtension diff, status 등 일부 명령의 출력을 색깔로 표시한다.

eol EolExtension End-line 문자를 작업 파일과 저장소간에 변환을 해준다.

extdiff ExtdiffExtension 외부 프로그램을 이용해서 diff 결과를 출력한다.

fetch FetchExtension pull, merge, update를 한방에!

gpg GpgExtension GPG를 이용해 change set을 digest하고 결과를 확인한다.

graphlog GraphlogExtension 리비전 그래프를 ASCII로 출력한다.

hgcia HgciaExtension CIA(http://cia.navi.cx/)로 알림을 보낸다.

highlight HighlightExtension Mercurial의 웹 서버 상에서 파일 내용을 표시할 때 Syntax

Highlight를 제공한다.

mq MqExtension 패치를 큐(queue) 형태로 관리할 수 있도록 해준다.

progress ProgressExtension 일부 명령을 수행할 때 진행 상태를 표시한다(1.5 이상 필요).

rebase RebaseExtension 변경 집합의 부모를 변경한다.

win32mbcs Win32mbcsExtension 윈도우 상에서 파일명을 shift_jis/big5로 쓸 수 있도록 한다.

10 VCS를 이용하는 활동에 대한 각종 통계 정보를 제공하는 서비스

흡수하며, 기능적으로는 빠른 속도로 닮아가고 있다. 그래서 선택

에서 있어서는 기호에 맞추어 마음에 드는 것을 고르면 된다고 생

각한다. 다만 간단하게 나마 고민을 줄일 수 있도록 몇몇 비교 가능

한 요소를 위에서 정리한 내용의 일부를 포함하여 다음 표와 같이

간단하게 정리해 보았다.

참고로, 는 기본적으로 제공되거나, 상황에 따라 달라질 여지가

없는 경우를, 는 추가 설정이 필요하거나, 경우에 따라서 달라질

수 있는 경우 혹은 일부만 지원되는 경우를, 는 현재는 적용할

수 있는 방법이 없는 경우를 의미한다.

표 1 Gif와 Mercurial 비교

비교 조건 Git Mercurial 설명

SVN을 쓴 경험이 많고, 러닝

커브를 최대한 줄여야 하는지

실행 환경이 윈도우 계열인지,

xNIX 계열인지xNIX 윈도우

윈도우 환경에서는

Mercurial이 압도적으

로 빠르므로, Mercurial

을 추천함

새로운 것을 경험해 보거나

DVCS에 대한 콘셉트를 잘 알

고 싶은지

VCS가 아닌 다양한 용도로 사

용할 필요가 있거나, 강력한 사

용자화 혹은 명령 조합이 필요

한지

호스팅 서비스https://github.com

https://

bitbucket.org

하위 디렉터리 체크아웃

외부 저장소 링크

- submodule - subrepo

번들 확장으로 포함

권한 보존실행 권한만

실행 권한

변경 이력 모델스냅샷

패치

(Patch)

구현 언어C, Bourne Shell,

Perl파이썬

Mercurial은 바이너리

비교기능(binary diff)만

C언어로 구현

브랜치 병합 n-Way 2-Way

Page 251: The platform 2011

기술개발 동향250 The Platform 2011

참고 자료

Git Official Site http://git-scm.com/

Mercurial Official Site http://www.selenic.com/mercurial

Git vs. Mercurial: Please Relax

http://importantshock.wordpress.com/2008/08/07/git-vs-

mercurial/

The Differences Between Mercurial and Git

http://www.rockstarprogrammer.org/post/2008/apr/06/

differences-between-mercurial-and-git/

The Git Object Model

http://book.git-scm.com/1_the_git_object_model.html

Page 252: The platform 2011

iOS 플랫폼 소개 251The Platform 2011

아이폰의 등장

그림 1 아이폰 3Gs

iOS는 4년 전 애플(Apple)에서 출시하여 스마트폰의 방향을 바꾼

아이폰의 운영체제다. iOS는 키패드나 스타일러스를 사용하지 않

고 손가락을 이용하는 멀티터치 인터페이스, Wi-Fi 및 GPS 지원,

PC용 브라우저와 거의 동일한 스펙의 웹브라우저 지원 등 지금은

보편화되어 있으나 당시에는 상상하기 힘든 기능을 지원하면서 화

려하게 등장했다. 아이폰 출시 당시 스티브 잡스는 기존의 휴대폰

용 운영체제로는 스마트폰의 문제를 해결할 수 없어서 Mac의 운

영체제인 Mac OS X을 그대로 탑재했다고 말했다. iOS가 Mac

OS의 운영체제와 동일하다고 말한 것에는 일부 과장이 있겠지만,

Mac OS에서는 제공하지 않는, 휴대폰에 특화된 다양한 기능을 추

가로 제공하고 앱 스토어나 APN(Apple Push Notification)과 같

은 서비스 인프라와 연동된 기능을 운영체제 기능의 일부로 제공하

고 있다. 따라서 어떤 면에서는 Mac OS보다 앞서 있다고 볼 수 있

겠다.

스마트폰 플랫폼으로서 iOS의 특징을 살펴보고, 아이클라우드

(iCloud)라는 새로운 기능을 포함하여 곧 출시할 예정인 iOS 5의

새로운 기능도 알아볼 것이다.

iOS 플랫폼 소개 "이제 곧 소프트웨어 개발자들의 시대가 옵니다!"

•모바일앱개발팀 _ 이종권

Page 253: The platform 2011

기술개발 동향252 The Platform 2011

이런 시점에 등장한 아이폰은 여러 가지 면에서 혁신적이었는데 그

중 몇 가지를 되돌아보도록 하자.

브라우저

애플은 리눅스의 KDE 프로젝트에서 개발한 코드를 기반으로 웹킷

(Webkit)이라는 오픈소스 브라우저 엔진을 개발해서 Mac OS의

브라우저인 사파리에 사용하고 있다. 사파리는 브라우저의 웹 표준

준수 여부를 테스트하기 위한 ACID2 테스트를 가장 먼저 통과한

브라우저이기도 하며, 웹킷 엔진은 구글의 크롬 및 안드로이드의

브라우저에도 사용하고 있다. 아이폰은 Mac OS에 탑재된 것과 동

일한 엔진을 사용한 모바일용 사파리 브라우저를 탑재해서 일반 웹

사이트를 문제없이 보여줄 수 있었는데, 윈도우 모바일이나 심비안

의 웹브라우저가 성능이나 기능면에서 많이 부족했던 것과는 대조

되는 것이었다.

당시 휴대폰에서 WAP을 버리고 웹을 바로 지원해야 한다는 요구

가 컸는데, 오페라는 서버에서 웹 페이지를 변환해서 경량화한 다

음 자바 기반의 브라우저로 볼 수 있게 하는 오페라 미니라는 제품

을 내놓고 있었고, 국내에서도 윈도우 서버를 이용해 윈도우 서버

의 인터넷 익스플로러에서 로드한 화면을 이미지로 변환해서 클라

이언트로 전송하는 방식의 브라우저 솔루션들이 나와 있었다. 휴대

폰용 브라우저에서 일반 웹 페이지를 변환 없이 직접 렌더링하는

건 무리라는 것이 일반적인 인식이었는데 아이폰은 이 상식을 뛰어

넘은 것이었다.

아이폰의 브라우저가 기존의 문제점을 해결할 수 있었던 것은 단지

웹킷을 잘 포팅했기 때문만은 아니다. 멀티터치 인터페이스 덕분

에 화면을 쉽게 확대하고 축소할 수 있고, 웹 페이지의 특정 영역을

살짝 두 번 누르면(더블탭) 해당 영역만 화면에 꽉 차게 볼 수 있다.

자바 애플릿과 플래시를 지원하지 않은 것 역시 브라우저의 성능에

는 크게 도움이 되는 점이었다. Wi-Fi를 기본으로 지원한 점도 브

라우저의 장점을 부각시킨 요소였다.

사실 사파리 브라우저는 아이폰이 처음 출시될 당시 아이폰의 플랫

폼에서 가장 중요한 요소였다고 볼 수 있다. 애플의 초기 정책은 아

이폰의 서드파티(3rd party) 콘텐츠는 모두 웹앱(Web App)으로

개발하게 한다는 정책이었다. 휴대폰용으로 적합한 앱은 한가지 목

적을 위해 만든 간단한 형태가 바람직하고, 여기엔 웹이면 충분하

아이폰은 2007년 1월 맥월드(Macworld)에서 처음으로 소개됐

고, 그 해 6월에 시장에 나왔다. 아이폰의 등장은 센세이션을 불러

일으켰다. 애플에서 휴대폰을 만든다는 것도 의외의 일이었고, 아

이폰은 기존의 스마트폰과 크게 차별화된 획기적인 제품이었다. 당

시 스마트폰 운영체제의 주류는 노키아가 주도하고 있는 심비안

(Symbian)이었고, 마이크로소프트의 윈도우 모바일(Windows

Mobile)은 통신사업자 및 제조사의 정책적인 견제로 배척당할 뿐

아니라 휴대폰에 최적화되지 못해 부족한 부분이 많았다. 업계에서

는 리눅스에 기반하는 심비안의 대안을 모색하고 있었다. 구글 역

시 리눅스에 자바를 탑재해서 자신만의 플랫폼을 준비하고 있었지

만 안드로이드가 나온 때는 아이폰보다 약 1년 정도 늦은 시점이었

다.

그림 2 노키아의 스마트폰

아이폰 이전의 스마트폰은 휴대폰이라기 보다 PDA에 가까웠다.

손에 들고 다닐 수 있는 미니 컴퓨터였다. 여기에 전화 기능을 탑재

해 통화를 할 수 있었고, 통신사업자의 데이터망을 이용해 데이터

를 다운로드하거나 제한적으로 웹 서핑을 할 수 있었다. 마우스를

대신한 스타일러스와 쿼티 키패드가 있었지만 일반인이 사용하기

에는 너무 복잡하고 거추장스러운 휴대폰이었다. 반면에 일반 휴대

폰은 점점 넓고 컬러풀한 액정을 갖고 다양한 기능을 추가하면서

발전해 왔다. 휴대폰 카메라의 성능이 좋아지면서 디지털 카메라를

대체하기 시작했고, WAP으로 인터넷 서비스를 이용하고, 자바로

만든 게임을 다운로드할 수 있게 됐다. 하지만 이동통신망 속도의

증가와 CPU 및 하드웨어의 성능 향상을 따라가기에 기존 휴대폰

플랫폼은 역부족인 상황이었다.

Page 254: The platform 2011

iOS 플랫폼 소개 253The Platform 2011

Wi-Fi 지원

Wi-Fi도 아이폰을 더 매력적으로 보이게 하는 중요한 요소였다.

덕분에 웹 사이트를 빠르게 브라우징할 수 있었다. 지도는 거의 실

시간으로 다운로드하고, 아이폰으로 찍은 사진을 바로 웹 서버로

동기화하고, 유튜브 비디오를 비교적 높은 화질로 볼 수 있었다.

사용자는 아이폰의 다양한 서비스를 요금 걱정 없이 더 빠르게 이

용할 수 있었던 것이다. 지금은 통신사업자 입장에서도 Wi-Fi 지

원이 자신들의 무선 트래픽 부하를 줄여 주는 긍정적인 요소로 인

식하고 있지만 당시엔 데이터 수익을 위협하는 위험한 요소였고

Wi-Fi를 지원하더라도 데이터 요금제와 연동하려 했다. 아이폰이

없었다면 적어도 국내에서는 무료로 Wi-Fi가 지원되는 휴대폰이

나오지 못했을 것이다.

아이폰의 서비스 플랫폼

iOS는 단순히 범용적인 운영체제가 아니라 서비스와 긴밀하게 연

결되어 있는 운영체제다. iOS에서 다양한 서비스를 지원하면서도

휴대폰으로서의 안정성을 확보하고 있는 측면을 살펴보자.

앱 서비스 환경

브라우저와 함께 모바일 서비스 플랫폼에서 중요한 자리를 차지하

고 있는 요소는 앱 서비스 플랫폼이다. 일반 휴대폰에서는 모바일

자바가 대세이고, 거의 모든 휴대폰이 Sun(지금은 오라클에 인수

된)에 로열티를 지불하고 자바를 탑재하고 있다. 자바 앱은 통신사

업자가 운영하는 WAP 서버에서 다운로드한다. 반면에 스마트폰

은 자바 이외에도 다양한 개발 환경을 제공하고 있고, 아이폰 이후

앱 스토어와 같은 고유한 다운로드 플랫폼을 갖추고 운영체제 제공

업체에서 직접 서비스를 운영하고 있다.

표 1 휴대폰별 앱 서비스 환경

단말기 운영체제 개발언어 및 환경 앱 스토어

일반 휴대폰 다양한 RTOS 자바(MIDP) 통신사업자가 운영

아이폰 iOS 코코아 터치(Cocoa

Touch) 기반의

Objective-C(네이티브

SDK)

앱 스토어(App Store): 애플이 운영.

고 보안 측면에서나 배터리 관리의 측면에서도 웹앱이 유리하다는

생각이었다. 하지만 애플은 곧 정책을 변경하고 네이티브 앱을 만

들 수 있는 SDK와 이를 이용해 만든 앱을 다운로드할 수 있는 앱

스토어를 공개했다. 이때 정식으로 모습을 드러낸 아이폰의 운영체

제 이름이 아이폰 OS였는데 나중에 iOS로 이름을 바꿨다.

멀티 터치 인터페이스

아이폰이 출시될 당시 사람들의 눈을 가장 사로잡았던 것은 터치

인터페이스였을 것이다. 키패드도 달려있지 않고 스타일러스도 없

이 손가락으로 모든 걸 조작할 수 있었다. 손가락 두 개로 사진을

늘였다 줄였다 할 수 있고, 연락처 목록을 한 페이지씩 스크롤하는

것이 아니라 손가락으로 밀어올리면 손가락을 따라 스크롤되고, 손

가락을 떼면 밀어올리던 손가락의 가속도에 맞춰 스크롤되다가 천

천히 멈춘다. 기존의 스마트폰은 정압식 터치패널이라서 스타일러

스 같은 펜을 이용하거나 손톱을 이용해야 했던 반면에 아이폰은

정전식 터치패널이라 손가락을 사용할 수 있다.

GPS 및 다양한 센서의 지원

아이폰이 GPS를 탑재한 최초의 휴대폰은 아니지만 GPS를 가장 효

과적으로 사용한 휴대폰인 것은 분명하다. 아이폰에서 GPS의 1차

적인 용도는 지도(구글 지도)를 제공하는 용도였겠지만 이후 다양

한 위치 기반 서비스를 가능하게 만든 중요한 요소가 됐다. 기존 휴

대폰은 통신사업자의 기지국 정보를 이용해 위치 정보를 제공하는

방식이 일반적이었고, 국내에서는 이 위치 정보를 이용하기 위해

서 사용자가 비용을 지불해야 한다. 그 외에도 국내에서는 위치 정

보를 이용한 서비스를 제공하려는 사업자에게 많은 제약이 있었다.

이러한 점들이 아이폰의 국내 도입을 지연시키는 요인이 되기도 했

고, 아이폰 도입 이후에는 관련 법이 간소화되기도 했다.

아이폰에 내장된 자이로센서는 아이폰을 들고 있는 방향이 가로 방

향인지 세로 방향인지를 구분할 수 있게 하고, 아이폰이 어느 방향

으로 흔들리는지를 판단할 수 있게 한다. 이 정보를 이용해 사진 등

을 화면 크기에 맞게 채워서 볼 수 있고 게임 컨트롤에 유용하게 사

용할 수 있다. 그 밖에 아이폰을 귀에 가까이 대는 것을 감지하는

근접 센서와 주변의 밝기에 따라 화면 밝기를 자동으로 조절할 수

있게 하는 밝기 센서도 있다.

Page 255: The platform 2011

기술개발 동향254 The Platform 2011

통신사업자에 독립적인 단일 마켓

기존 자바 서비스는 통신사업자 단위로 분리되어 있었다. 개발 업

체는 여러 통신사업자에 각각 등록하고, 심사받고, 정산받아야 했

다. 외국 통신사업자를 통해 서비스하는 건 더 어려웠다. 앱 스토어

에 등록된 앱을 전세계를 대상으로 서비스할 수 있는 건 획기적인

변화였다.

독자적인 신용카드 결재 시스템 구축

전세계를 대상으로 한 신용카드 결재 시스템 구축 역시 불가능해

보이는 일이었다. 그러나 앱 스토에서는 전세계 이용자 누구나 앱

을 구매하여 신용카드로 결재할 수 있으며, 은행계좌와 연계해서

개발자 수익 정산까지 이루어지고 있다.

개발자 접근성

개발자로 등록하려면 일년에 $199나 비용을 지불해야 함에도 불구

하고, 누구나 온라인으로 개발자로 등록해서 앱을 제공할 수 있다

는 건 큰 장점이다. 기존 통신사업자에 개발 업체로 등록하는 건 그

자체가 커다란 장벽이며, 개인 개발자는 시도하기도 어려운 일이

다.

세련되고 편리한 인터페이스

앱 스토어 자체의 세련된 인터페이스도 과거 자바 서비스의 WAP

소개 페이지에 비교해서 큰 차이가 있다. 앱에 대한 충분한 소개와

스크린샷을 제공하고, 다운로드한 사람의 리뷰와 별점을 볼 수 있

다. 앱의 업데이트 정보가 실시간으로 제공되고 손쉽게 업데이트할

수 있다. 랭킹별로 앱 노출 순서가 자동으로 결정된다. 앱 스토어의

디자인도 기존의 WAP 페이지뿐만 아니라 안드로이드 마켓과 비

교가 안 될 정도로 뛰어나다.

광고 수익모델

통신사업자가 운영하는 자바 서비스에서는 앱을 무료로 제공하고

광고로 수익을 얻는 모델이 허용되지 않았다. 광고 수익을 모델로

하려면 통신사업자와 별도 계약을 통해 광고 수익을 나눠야 했고,

아주 드문 케이스였을 뿐이다. 앱 스토어는 이를 제한하지 않으며,

개발자는 애드몹(AdMob) 같은 광고대행사를 통해 손쉽게 광고를

넣을 수 있다.

단말기 운영체제 개발언어 및 환경 앱 스토어

안드로이드폰 리눅스 기반의

안드로이드

자바(안드로이드)

컴포넌트를 C로 만들 수

있는 네이티브 SDK

구글이 운영하는 앱 스토어와 통신사

업자 및 제조사가 운영하는 앱 스토어

가 각각 존재

심비안폰 심비안 QT 기반의 C++

자바(MIDP)

오비스토어(OviStore): 노키아가 운영

윈도우폰 윈도우 폰 실버라이트

XNA(게임 라이브러리

를 지원하는 C#, 네이티

브 SDK는 제공 안 됨)

마켓플레이스(Marketplace): 마이크

로소프트가 운영

블랙베리 블랙베리 OS Java(MIDP) 앱 월드(AppWorld): RIM이 운영

팜프리 webOS 자바스크립트

컴포넌트를 C로 만들 수

있는 네이티브 SDK

앱 카탈로그(App Catalog): HP가 운

앱 스토어

아이폰에 앱 스토어가 탑재된 이후 앱 스토어는 스마트폰의 기본

요소가 되었고, 스마트폰 운영체제의 경쟁력을 좌우하는 요소가 됐

다. 앞서 언급했듯이 게임 등의 앱을 휴대폰에서 다운로드할 수 있

게 하는 서비스는 아이폰 이전에 이미 모든 휴대폰에서 제공되던

것이었다. 그렇다면 앱 스토어가 유독 성공한 이유는 무엇일까? 개

발자들이 앱 개발 분야로 뛰어들어 양질의 앱을 만들어 내는 에코

시스템을 가능하게 한 것은 우수한 개발 환경 덕분이기도 하지만

앱 유통의 중심에 있는 앱 스토어도 핵심적인 역할을 했다.

그림 3 아이폰 앱 스토어

Page 256: The platform 2011

iOS 플랫폼 소개 255The Platform 2011

iOS 4.0 이전엔 앱 실행 도중 홈 버튼을 누르거나 전화가 오면 앱이

바로 종료됐다. 이와 같은 형태의 라이프사이클은 특별한 것은 아

니었는데, 일반 휴대폰에서 자바 앱의 라이프사이클도 이와 같다.

휴대폰의 성능이나 배터리 소모를 고려하면 이런 형태의 라이프사

이클을 적용한 것이 자연스런 선택이라 볼 수 있다. 그러나 이 라이

프사이클은 다음과 같은 문제점이 있다.

• 종료 직전의 상태를 기억 못하고, 앱 재실행 시 항상 처음 화면에서 실

행됨

• 전화 통화 등으로 앱이 원하지 않는 시점에 종료됨

• 음악 앱 등 백그라운드로 실행되는 앱을 개발할 수 없음

• 데이터를 주기적으로 업데이트하거나 서버로부터 이벤트를 전달받을

수 없음

백그라운드에서 항상 실행 상태로 있어야 하는 대표적인 앱이 메신

저인데, 상대방이 나에게 메시지를 보냈는지 주기적으로 확인하고

있다가 새로운 메시지가 도착하면 알려줘야 하기 때문이다. iOS에

서는 이와 같은 형태의 서비스를 지원하기 위해 iOS 3.0부터 APN

서버라는 푸시 서버를 제공하고 있다.

그림 5 아이폰의 푸시 서비스 구조

iOS 자체적으로 하나의 서버를 바라보고, 서드파티 앱의

서버에서 받은 푸시 정보를 APN 서버를 통해 전달받는 방

식이다. 여러 개의 앱이 각자 자신의 서버에 연결해서 트래

픽과 CPU를 낭비하는 것보다 훨씬 효율적인 방식이다. 하

나의 안정적인 APN 서버를 운영하는 것이 애플 입장에서

는 부담이 되겠지만 효율적인 방식인 것은 분명하고, 이후 안드로

이드 역시 2.2 버전부터 C2DM이라는 동일한 서비스를 제공하고

있다.

Wi-Fi

앱 스토어의 성공엔 다양한 요인이 얽혀 있지만 아이폰이 Wi-Fi를

지원하는 덕분에 무료로 인터넷을 이용할 수 있다는 것도 빠질 수

없는 요인이다. 앱 스토어에 어떤 앱이 있는지 둘러보는 것만으로

도 데이터 요금이 발생하고, 앱을 다운로드하는 데이터 요금이 앱

의 가격보다 더 비쌌다면 어땠을까? 기존 통신사업자들이 운영하

던 자바 서비스 환경이 그랬다.

멀티태스킹

이제 iOS의 가장 독특한 측면인 멀티태스킹 기능에 대해 살펴보자.

멀티태스킹 기능은 iOS 4.0에서부터 지원됐다. Mac OS는 당연히

멀티태스킹이 지원되는 운영체제이고, iOS 4.0 이전에도 아이폰에

내장된 뮤직 플레이어나 메일 클라이언트 등은 백그라운드에서 동

시에 실행할 수 있었으므로 iOS에서는 원래 멀티태스킹이 지원됐

다. 그러나 다운로드한 앱에는 제한을 두었다고 보는 것이 맞겠다.

iOS 4.0에서는 단순히 이 제한을 없앤 것이 아니라 독특한 방식으

로 멀티태스킹 기능을 지원하기 시작한다.

그림 4 iOS 3.0의 앱 라이프사이클

User taps application icon

main()

UIKit Your code

UIApplicationMain()

EventLoop

System asks application to terminate

Application execution termitates

applicationDidFinishLaunching:

Handle event

applicationWillTerminate:

Page 257: The platform 2011

기술개발 동향256 The Platform 2011

고 있는데, 이와 같은 방식은 앱의 백그라운드 실행을 통

제하면서도 앱을 종료시키지 않아서 앱 간 전환을 자연

스럽게 한다. 일반 앱도 필요한 경우 백그라운드 상태에

서 멈추지 않고 진행하던 작업을 계속할 수 있도록 추가

적인 시간을 요청할 수 있는데, 이 추가시간은 10분으로

제한된다.

앱의 백그라운드 실행에 대한 이와 같은 제한은 아이폰

의 배터리가 불필요하게 낭비되거나 사용자가 모르게

네트워크 트래픽이 발생하는 문제 등을 최소화할 수는

있으나, 더욱 다양한 서비스를 만드는 데 걸림돌이 되는

것은 분명하다. 예를 들어, 새벽 특정한 시간에 앱이 실

행돼서 아이폰으로 찍은 사진을 웹 서버에 백업하는 서

비스나, 특정한 시간 간격으로 뉴스나 웹툰 데이터 등을

미리 다운로드해 두는 서비스는 개발할 수 없는 것이다.

이 중 두 번째 케이스는 iOS 5에서 다른 방식으로 지원

할 예정이기는 하다.

메모리 관리와 앱 우선순위

아이폰의 RAM 용량은 아이폰 3GS가 256MB, 아이폰 4가

512MB다. 이 정도면 PC 못지 않은 충분한 용량의 메모리다. 하지

만 대부분의 스마트폰 운영체제와 마찬가지로 iOS 역시 디스크 페

이징을 지원하지 않는다. 그렇기 때문에 앱이 사용할 수 있는 메모

리는 RAM 용량으로 제한되며, 운영체제 및 시스템 프로세스가 사

용하는 메모리를 제외하고 남은 공간을 여러 앱이 공유해야 한다.

PC에서는 응용 프로그램이 여러 개 실행되어 메모리를 많이 차지

하면 전반적으로 성능이 저하될 뿐이지만 스마트폰에서는 메모리

가 부족하면 앱이 실행되지 못한다. 이 경우 사용자가 원하는 앱이

실행되지 못하면 안 되므로, 운영체제에서 다른 앱을 종료시켜서

메모리를 확보한 다음 앱을 실행한다. iOS에서 메모리 확보를 위해

다른 앱을 종료시키는 기준은 정확하게 기술되어 있지는 않으나 대

략 다음과 같이 동작한다.

• 시스템에 전반적으로 메모리가 부족해지면 실행 중인 모든 앱에 메모리

부족을 통지한다.

• 앱은 이 메시지를 확인해서 캐시 데이터 등 불필요한 메모리를 삭제할

수 있다.

그림 6 iOS 4의 앱 라이프 사이클1

iOS 4.0부터는 멀티태스킹이 지원되기 시작했으나 기존의 제한이

모두 없어진 것은 아니다. 우선 백그라운드 상태에서 실행할 수 있

는 앱과 그렇지 않은 앱이 구분되고, 특정 종류의 앱만 특정 상태

에서 백그라운드로 실행할 수 있다. 특정한 종류의 앱에는 음악 앱,

VoIP 앱, 위치 정보 앱의 3가지가 있다.

• 음악 앱: 백그라운드 상태에서 음악 재생이 가능함. 단, 음악 재생만 가

능하고 다른 작업은 음악이 재생되는 동안에만 지속할 수 있다.

• VoIP 앱: 서버와 네트워크 연결을 유지하기 위해 keepAliveTimer

를 등록해서 주기적으로 깨어나 데이터를 전송하거나 확인할 수 있

음. 깨어난 이후 최대 30초까지 실행할 수 있으며, 이후 다시 멈춰

야(suspend) 한다. 앱이 종료된 상태에서도 수신 소켓(incoming

socket)에 데이터가 수신되면 운영체제에서 앱을 다시 실행시킨다.

• 위치 정보 앱: 유일하게 백그라운드 상태에서 아무 제약 없이 실행할 수

있는 형태의 앱. 자동차 내비게이션 앱 등이 여기에 해당된다.

그 외에 다른 앱도 홈 버튼을 누를 때 종료되지 않고 실행되던 상

태 그대로 멈췄다가(suspend) 앱을 다시 선택하면 실행을 재개

(resume)한다. 애플에서는 이것을 Fast App Switching이라 부르

1 http://developer.apple.com/library/ios/#documentation/iPhone/Conceptual/iPho-neOSProgrammingGuide/Introduction/Introduction.html

User taps application icon

main()

UIKit Your code

UIApplicationMain()

EventLoop

System asks application to quit foreground

Application moves to background

application:didFinishLaunchingWithOptions:

Handle event

applicationWillResignActive:

applicationDidEnterBackground:

Page 258: The platform 2011

iOS 플랫폼 소개 257The Platform 2011

로 앱 스토어의 앱을 구매하거나 백업할 수 있고, 아이폰에 저장된

주소록이나 일정, 사진 등도 백업할 수 있으며, 운영체제도 업그레

이드할 수 있다.

아이팟의 강력한 무기였던 아이튠즈가 아이폰에도 마찬가지로

강력한 도구였을까? 아이팟과는 다르게 아이폰은 스스로 서버

에 접속해서 MP3를 다운로드할 수 있고, 앱을 다운로드할 수 있

다. 그것도 Wi-Fi를 이용해서 아주 빠르게. 케이블을 PC와 연결해

서 동기화해야 하는 아이튠즈는 이제 아주 거추장스럽고 답답한 도

구가 된 것이다. PC에 케이블을 연결할 필요 없이 PC와 아이폰이

Wi-Fi로 통신해서 알아서 주기적으로 백업하면 편리하지 않을까?

아쉽게도 PC, 특히 윈도우가 운영체제인 PC는 대부분 Wi-Fi 인터

페이스가 없다. 게다가 아이폰과 아이패드 때문에 사람들이 PC를

켜는 시간이 줄어들고 있고, 조만간 PC 자체가 줄어들 것으로 예상

되고 있다.

이러한 환경 변화에 대한 애플의 선택은 PC용 아이튠즈를 없애고

클라우드 서비스를 제공하겠다는 것이다. 이 클라우드 서비스가 바

로 아이클라우드이다. iOS 5부터는 아이폰의 데이터를 아이클라우

드를 통해 백업할 수 있게 된다. 앱은 아이클라우드 관련 API를 이

용해 특정 데이터나 파일을 아이클라우드의 저장소에 저장할 수 있

다. 그렇게 함으로써 사용자가 아이클라우드로 데이터 백업을 설

정해 두지 않아도 앱에서 필요한 데이터는 아이클라우드에 보관할

수 있다. 아이클라우드가 PC용 아이튠즈에 비해 한발 더 나아간 것

은 아이폰 하나만을 대상으로 하지 않고, 아이폰, 아이패드, Mac을

하나의 계정으로 통합 지원한다는 것이다. 아이폰으로 찍은 사진이

아이패드와 Mac으로 자동 동기화되고, 아이폰에서 구매한 음악을

아이패드에서도 들을 수 있다. 아이폰에서 앱을 구매해 설치하면

아이패드에도 자동으로 설치되며, 이 앱에서 저장한 데이터 역시

양쪽 기기에서 동기화된다. 아이클라우드 저장소는 무료로 5GB까

지 제공되며, 추가로 구매할 수 있다.

뉴스스탠드

앞에서 iOS의 멀티태스킹 제한 때문에 뉴스 데이터 등을 미리 읽어

오는 서비스가 불가능하다고 언급했는데, iOS 5에 추가되는 뉴스

스탠드(Newsstand) 서비스와 NewsstandKit API를 이용하면 이

와 같은 서비스를 구현할 수 있게 된다. 뉴스스탠드 서비스는 아이

북과 유사한 UI를 가진 서비스이며, 뉴스나 잡지 같은 앱이 뉴스스

• 앱의 화면 처리를 담당하는 기본 요소인 UIViewController는 메모리

부족 메시지를 받으면 컨트롤러가 담당하는 UI 요소를 해제해서 메모리

를 늘린다. 그렇기 때문에 적절하게 처리하도록 앱을 개발하지 않으면

화면이 올바르게 나타나지 않을 수 있다.

• 메모리가 충분히 확보되지 않으면 시스템은 실행 중인 백그라운드 앱을

먼저 종료시킨다.

• 여전히 메모리가 부족하면 멈춰 있는 앱을 종료시킨다.

아이폰에서 앱의 백그라운드 실행을 엄격히 제한하고, 실행 중인

앱의 목록을 따로 보여주지 않는 이유에는 이런 메모리 문제도 있

을 것이다. 백그라운드로 실행 중인 앱도 언제든 종료될 수 있으므

로 수명을 보장할 수 없다.

iOS 5와 iOS의 진화

휴대폰과 클라우드 서비스

그림 7 아이클라우드

멀티태스킹 지원이 iOS 4의 가장 큰 변화였다면, iOS 5의 가장 큰

변화는 아이클라우드라는 서비스다. 아이폰의 큰 장점이자 동시에

단점이기도 한 기능에 아이튠즈(iTunes)라는 PC용 도구와 연동 기

능이 있다. 아이팟이라는 MP3 플레이어를 위해 음악을 구매하고,

다운로드하고, 백업하는 등 MP3 플레이어 지원 기능을 제공하는

막강한 도구이다. 아이튠즈가 아이폰을 지원하게 되면서 아이튠즈

Page 259: The platform 2011

기술개발 동향258 The Platform 2011

탠드로 다운로드된다. 뉴스스탠드 형태의 앱은 APN을 통해 푸시

메시지를 받으면 사용자의 액션이 없어도 백그라운드 상태에서 실

행되어 신규 콘텐츠를 다운로드할 수 있다. 물론 이때도 앱이 실행

가능한 시간은 콘텐츠를 다운로드하는 동안으로 제한된다.

위젯

안드로이드 등에서는 제공되지만 iOS에서 제공되지 않던 대표적

인 기능이 위젯이다. 위젯의 특징은 바탕 화면에서 바로 콘텐츠를

보여줄 수 있다는 것인데, 주기적으로 위젯의 콘텐츠를 업데이트

할 수 있어야 하고, 바탕 화면이 노출된 상태에서는 백그라운드 상

태에서도 실행할 수 있어야 한다. 위젯이 지원될 것이라는 기대와

는 달리 iOS 5에 위젯 기능은 추가되지 않았다. 그러나 알림 센터

(Notification Center)라는 기능이 추가되면서 iOS 5에 기본으로

탑재되는 내장 앱인 날씨, 주가, 트위터는 위젯 형태로 노출된다. 머

지 않아 일반 앱도 유사한 형태로 실행할 수 있는 기능이 제공될 것

으로 보인다.

마치면서

아이폰의 등장 이후로 iOS는 스마트폰 플랫폼의 방향을 제시하고

있다. 예전처럼 일반 휴대폰과 스마트폰의 용도가 확연하게 구분되

는 것이 아니라 스마트폰이 휴대폰의 영역을 거의 대체하가고 있다

는 측면에서 보면 iOS가 전체 휴대폰 플랫폼의 방향을 제시하고 있

다고 봐도 과언이 아닐 것이다. 아이폰의 등장으로 휴대폰 업계의

판도가 바뀌었고, 모바일 서비스에서 통신사업자의 영향력이 크게

약화되었으며, 개인 및 소규모 업체들의 앱 개발 분야로 러시가 이

어지고 있다. 안드로이드는 iOS의 특징을 효과적으로 벤치마킹하

면서 이제 거의 동등한 수준에서 경쟁하고 있는 상황이고, 몰락하

는 노키아는 마이크로소프트와 손잡고 윈도우폰 7을 탑재한 스마

트폰을 준비하고 있다. 어느 제조사가 만든 어떤 모델이냐보다 어

떤 운영체제의 어떤 버전을 탑재하고 있느냐가 점점 더 중요해지고

있고, 하드웨어 스펙보다 소프트웨어의 기능이 더 중요해지고 있

다. 바야흐로 스마트폰 플랫폼의 춘추전국시대다.

Page 260: The platform 2011

259

Page 261: The platform 2011
Page 262: The platform 2011
Page 263: The platform 2011
Page 264: The platform 2011