당신의 서비스에서 모두의 프라이버시 지키기

에드워드 스노든이 2013년에 내부 고발한 NSA의 광범위한 사찰까지 얘기 하지 않아도 ISP, 통신사나 여러분의 고용주가 컴퓨터 네트워크 위의 통신을 들여다보는 일은 비일비재하다. 하지만 조금만 더 노력을 기울이면 이런 프라이버시를 침해하는 일을 막거나 훨씬 어렵게 만들 수 있다. 이 글에서는 표준 프로토콜인 SSL/TLS를 어떻게 일반적인 웹 서버 프로세스나 응용 프로그램 프로세스에 적용할지 설명한다.

SSL/TLS

요즘의 웹 / 네트워크 환경에서는 인터넷 위로 통신하는 부분의 안전성을 위해서 TLS (transport layer security) 를 사용한다. 많은 곳에서 SSL (secure socket layer)이라고 부른다. 하지만 가장 나중 버전인 SSL v3 도 IETF에서 2016에 나온 RFC 7568에서 사용을 피하고 TLS v1.2 를 쓰라고 한다. 그래서 이 글에서는 TLS 라는 용어로 지칭하겠다.

TLS 가 제공하는 것

크게 다음 세 가지 기능을 제공한다.
• 인증 (authentication): 지금 통신 중인 서버가 연결하려고 한 서버가 맞는지 확인
• 보안 (confidentiality): 주고 받는 메시지를 남이 해독할 수 없게 변환
• 무결성 (integrity): 주고 받는 메시지를 남이 조작했는지 확인

TLS 표현 방식

wikipedia-tls.png

파이어폭스에서 확인한 위키 TLS 설정
구글 크롬이나 파이어폭스의 보안 탭을 열어보면 — 혹은 다른 OpenSSL 기반의 설정을 해봤다면 — 아래와 같은 문자열을 볼 수 있다. (아래 예는 위키백과 접속했을 때 사용한 알고리즘이다)
ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
대략 아래와 같은 의미이다.

• ECDHE 키교환: 타원곡선 디피-헬만 알고리즘 (ephemeral 키 방식)
• ECDSA 인증서 알고리즘: 타원곡선을 이용한 디지털 서명 알고리즘
• CHACHA20 스트림 암호화: 메시지를 암호화할 때 CHACHA20 스트림 암호화 알고리즘
Poly1305 MAC, SHA-256 HMAC: 메시지 무결성 검증 알고리즘 두 가지

TLS 표기 방식의 의미

인증을 담당하는 부분은 여기서 ECDSA 하나에 해당한다. 이 부분은 해당 웹 사이트에서 제공하는 인증서 서명 알고리즘에 해당한다.
보안에 해당하는 부분은 키 교환메시지 암호화 (블럭 혹은 스트림 암호화) 에 해당하는 ECDHE 와 Chacha20이다.
무결성 검증을 위해서 사용한 부분이 MAC에 해당하는 Poly1305 와 SHA-256 알고리즘이다.
이 중에서 보안무결성 보장을 위해서 사용한 부분은 웹 서버나 응용 프로그램에서 설정하는 부분이다. 즉, 아파치 웹 서버나 nginx 설정을 하다보면 설정해야하는 부분이다. 모질라 재단에서 제공하는 웹 서버용 권장 설정이나 권장 암호화 알고리즘 목록 의 모범 설정 예시를 참고하면 좋다. 이에 대해서는 이전 블로그 글에서 설명했다.

TLS 인증 기능?

TLS 의 (서버 쪽) 인증 기능은 서버 설정만으로 동작하지 않는다. TLS의 인증 기능이 어떻게 동작하는지 아래에서 살펴보겠다.

TLS 인증서

TLS 인증서는 아주 간단히 말하면 “믿을 수 있는 누군가가 특정 형식으로 전자 서명한 공개키” 다. 여기서 “믿을 수 있는 누군가” 에 해당하는 역할을 담당하는게 “인증 기관” 이다. 그리고 특정 형식에 해당하는게 X.509 형식이다.
주의할 점은 믿을 수 있는 누군가 에 대해서는 강제적인 합의가 있는 것이 아니라서, 웹 브라우저 / OS에 따라서 특정 인증 기관을 신뢰할 수도 있고, 신뢰하지 않을 수도 있다. 예를 들어 올해 초에 한국 전자 인증 (crosscert) 에서 권한없이 인증서를 발급해서 모질라 재단 쪽에서 문제를 재기했고, 이에 대해 관리/모니터링할 책임이 있는 시만텍 인증서 사업 부문에 대한 조사가 이뤄졌다. 그 결과 구글 크롬과 모질라 파이어폭스에선 조만간 시만텍에서 발급한 서버 인증서를 모두 신뢰하지 않게 된다.

전자 서명 구조

전자 서명은 단순히 특정 키 + 서명 형식이 아니고, 공개키 인프라 (PKI) 를 이용해서 키를 단계적으로 서명한다. 우선 인증 기관들의 공개키를 루트 인증서 (root certificate) 이라고 한다. 이 루트 인증서는 웹 브라우저나 운영 체제 수준에서 포함하고 있다. 그리고 이 루트 인증서로 서명한 중간 단계 인증서 (intermediate certificate) 혹은 이걸로 다시 서명한 인증서를 생성한다. 이렇게 몇 단계를 거쳐서 생성한 서명키(=공개키)로 실제 웹 서버의 인증서(=공개키)를 전자 서명 알고리즘으로 서명하면 TLS 인증서가 완성된다. 즉, 서버의 TLS 인증서에는 루트 인증서 바로 아래에 있는 서명키부터, 서버의 공개키까지를 포함한다.
그리고 이런 루트 인증 기관, 혹은 이를 대행하는 인증 기관들은 이런 공개키 서명할 때 비용을 청구한다. 흔히 “인증서 발급 비용” 이라고 말하는게 이런 부분이다.

자가 서명 인증서

인증 기관을 통하지 않고 스스로 서명한 인증서 (self-signed certificate) 를 만들 수도 있다. 하지만, 해당 인증서는 클라이언트 프로그램 (웹 브라우저 혹은 게임 클라이언트 등등) 에서 확인할 방법이 없어서 절대로 피해야 한다. 대안은 무엇일까? 공짜로 인증서를 만들 순 없을까?

공짜로 인증서를 만들 방법은 없을까?

서두에서 언급한것처럼 스노든의 내부 고발 이후로 프라이버시에 대한 관심/걱정은 대폭 증가했고, 2014년에 전자 프런티어 재단 (EFF), 모질라 재단, 미시건 주립대 등이 참여해서 Let’s Encrypt 라는 새로운 CA 를 만들었다. Let’s Encrypt 의 설립 목적이 “자동화된 방법”으로, “비용없이” 원하는 모두에게 TLS 인증서를 발급하는 것이라, 대부분의 경우에서 사용할 수 있는 인증서쉽게(?) 발급할 수 있다.

Let’s Encrypt 에서 TLS 인증서 발급해서 적용하기

foo.example.com처럼 도메인 주소를 가지고 있는 서버라면 아래 과정으로 TLS 인증서를 발급할 수 있다. 이를 DV; domain validated인증서라고 한다. 이외에 OV 나 EV 처럼 웹 브라우저에 기관 이름 혹은 사업자 이름이 뜨는 인증서도 있지만 Let’s Encrypt 에선 발급할 수 없다. 이런 인증서는 발급할 때 추가 확인절차가 있어서 “자동화” 발급만 지원하는 Let’s Encrypt 등에선 지원할 수 없다.

리눅스 서버에 적용하기

리눅스에서 인증서를 발급받는다면 다음과 같은 과정으로 TLS 인증서를 발급할 수 있다.
1. 웹 서버를 설치한다. (nginx, apache2, …; 여기서는 nginx 를 예제로 사용) bash $ sudo yum install -y nginx # centos; nginx $ sudo apt-get install -y nginx # ubuntu; nginx

2. 웹 서버에서 80번 포트로 접근할 때의 루트 디렉터리를 지정한다. 예를 들어 nginx 라면 아래와 같은 설정이 필요하다.
>server {
# 80
번 포트에 대한 설정
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    #
이 경로를 기억해두자.
    root /usr/share/nginx/html;

    location / {}

    # Let’s Encrypt 용 추가 설정
    location ~ /.well-known { allow all; }
   }

3. certbot 프로그램을 설치한다. 아래 명령 중에 사용 중인 서버 OS에 맞는 명령을 고르자.
   $ sudo yum install -y certbot # centos
   $ sudo apt-get install -y letsencrypt # ubuntu
   $ pip install certbot # python 사용 가능한 임의의 운영체제 (virtualenv 여도 무방)

4. 인증서 발급. 발급하려는 모든 도메인을 -d some.domain.com 하는 형식으로 여러번 지정할 수 있다. 2에서 설정한 웹 서버 루트 경로 (위에선 root 설정으로 지정) 를 webroot -w 의 인자로 전달해야 한다.
   $ sudo letsencrypt certonly –webroot -w /usr/share/nginx/html -d foo.example.com -d http://www.foo.example.com
해당 명령 실행 후에 /etc/letsencrypt/live 밑에 도메인 이름으로 심볼릭 링크가 걸린 디렉터리가 생긴다.

5. 서버 설정에 반영
   server {
    listen 443 default_server;
    listen [::]:443 default_server;
    server_name foo.example.com;
# 서버 도메인 지정
    root /usr/share/nginx/html;

# TLS 패러미터들 (인증을 제외한 나머지)
    ssl on;
    ssl_certificate /etc/letsencrypt/live/foo.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/foo.example.com/privkey.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
    ssl_prefer_server_ciphers on;
 }

윈도우즈 지원

공식은 아니지만 Microsoft Windows 용 클라이언트 가 있으며, 위 웹 사이트의 Release 페이지에서 다운로드할수 있다. 이 커맨드라인 프로그램으로 유사한 작업을 해서 인증서를 발급받을 수 있다.

요약

• 현대적인 방법으로 프라이버시를 보호하는데 사용할 수 있는 TLS 레이어가 있다.
• TLS는 인증, 기밀 유지, 무결성 검증 기능을 제공한다. 이중 뒷쪽 두 가지는 서버 설정에 따라 동작한다.
• 인증을 위해서 인증 기관들이 존재하며, 여기서 발급받은 인증서가 필요하다.
• 인증서 발급을 자동화 + 무료로 처리하는 Let’s Encrypt 가 있으며, 서버에서 자동으로 처리할 수 있다.

아이펀팩토리 김진욱 CTO

Python 기본 개념에 대한 간단한 퀴즈

Python은 그 단순함과 유연성, 다양한 라이브러리의 존재 등의 장점을 가진 언어이다. 웹 서비스로부터 통계, 최근에는 머신 러닝에 이르기까지 다양한 분야에서 사용된다.
이는 게임 산업에서도 마찬가지이다. 실시간 PVP등 성능이 중요한 게임에서는 GIL로 인한 멀티스레딩 불가 문제(C python, Pypy 등의 경우이다. IronPython 등 GIL이 없는 환경도 있다.), 스크립트 언어가 가지는 성능상의 불리함 등의 이유로 게임 개발에 직접로 사용되는 경우는 상대적으로 적지만( Eve online, 듀랑고 등 게임 구현에 Python을 적극적으로 도입하는 게임들도 있다.), 웹 서버 기반의 게임 서버 작성, 기능/부하 테스트를 위한 더미 클라이언트, 각종 배치 작업 등 각종 도구를 작성하는 용도 등으로 널리 사용된다.

Python의 단순함과 명료함은 툴 개발과 같은 높은 생산성이 있어야 하는 작업에 적합하다. 하지만 이것이 Python의 전부는 아니다. Python을 메인 언어로 하는 게임 개발 프로젝트는 많지 않기에 단순하고 생산성이 높은 언어로만 알고 있는 경우가 많지만, 이외에도 다양한 프로그램 패러다임을 지원할 수 있도록 명료하면서도 다양한 개념들이 존재한다.

아래의 문제들은 Python에서 지원하는 몇몇 개념에 대한 것들이다. 본사에서 후원한 Pycon 2017 에서 이벤트 진행에 사용한 문제들로, 특별한 언급이 없다면 Python 3.6 버전을 기준으로 한다. https://www.python.org/dev/peps/pep-0020/ 에 쓰인 것처럼, Python의 아름다움(?)을 느끼는 데 조금이나마 도움이 되길 바란다.

Q. Python의 에러 처리 메커니즘인 Exception에 대한 설명 중 옳은 것은?

1. 별도의 try… except 를 통해 처리되지 않은 exception 들을 처리하는 함수를 지정 가능하다.
2. 새로운 Exception을 임의로 정의할 수 없다.
3. 처리(handling)을 위해 특정 코드를 건너뛸 수는 없다.
4. 예외가 발생하지 않은 경우에만 실행되는 코드를 따로 지정할 수 없다.
5. 임의로 발생시킬 수 없다.

정답 : 1

Python에서는 내장 Exception 클래스를 상속받아 유저가 임의의 Exception을 정의할 수 있다. 다른 언어의 exception과 마찬가지로, exception 발생 시 코드 실행을 중지하고 가장 가까운 handler(except 문)으로 이동하므로, 특정 코드를 건너뛸 수 있으며 finally 문을 이용한, Exception 발생 여부와 관계없이 실행되는 블록을 작성할 수 있다.
이외에도 else 문을 사용하여 Exception이 발생하지 않은 경우에만 실행되는 블록을 작성할 수 있다.

1끝으로, 별도의 핸들링(except)을 거치지 않은 Exception은 sys.excepthook 함수에서 확인, 필요 시 처리 가능하다.

1-2

Q. 다음 중, Python Closure에 대한 설명으로 옳지 않은 것은?

1. 상위 영역의 변수에 접근이 필요한 경우, __dict__ 멤버에서 해당 변수를 찾으려 시도한다.
2. 일반적으로, nested function 형태이다.
3. Callable이다.
4. 다른 변수에 대입(binding)할 수 있다.
5. parent scope에 존재하는 변수를 참조할 수 있다.

정답 : 1.

Closure란, 어떤 함수 및 해당 함수의 실행 시 필요한 환경(각종 변수 등)을 모두 포함한 것을 뜻한다. 예를 들어, 아래 예제의 cl 은 inner 함수 및 inner의 실행에 필요한 변수(outer_var)를 모두 가지고 있는 Closure이다.

2-1

Python의 함수는 first class 값이므로, 일반적인 변수나 객체처럼 인자로 넘겨지거나 함수의 반환 값으로 사용 가능하다. Closure는 함수이므로 당연히 Callable의 일종이며 다른 변수에 대입(binding)할 수 있다. 또한 위의 예에서처럼, 실행에 부모 스코프(outer 함수의 스코프) 내의 변수가 필요하다면 해당 변수 또한 참조 가능하다.

Python의 각종 타입들은 실행 시 필요한 많은 정보, 혹은 동작들을 특수한 이름 규칙을 따르는 변수/함수로 관리한다. 예를 들어 함수의 이름은 __name__ 이라는 변수에 저장되며, 두 오브젝트간의 동등 비교(a == b) 시에는 __eq__(self, other) 라는 함수를 호출한다. 특정 오브젝트의 어트리뷰트들은 __dict__ 에 저장된다. 하지만 Closure 실행 시 필요한 변수들__dict__ 가 아닌, __closure__ 라는 멤버 변수에 저장된다.

Q. Python decorator에 대한 다음 설명 중 옳지 않은 것은?

1. 클래스 멤버 함수(method)는 decorated될 수 없다.
2. 하나의 함수가 둘 이상의 decorator로 decorated 될 수 있다.
3. 다른 모듈 내에 정의된 함수도 decorator로 사용할 수 있다.
4. 함수 정의 구문 위에 @ 기호를 붙여 사용한다.
5. decorator로 감싸진 함수의 __name__ 등 몇몇 변수의 값은 감싸지지 않은 함수의 그것과 다르다.

정답 : 1.

Python의 decorator는 closure 개념을 이용, 함수의 동작을 확장/변경하는 개념이다.

3

위의 예제는 DocratedSample 라는 클래스의 decorated_func 라는 Method를 doc_1, doc_2라는 두 개의 Decorator 로 감싸는 예제이다.

decorator 자체는 closure를 반환하는 함수이므로 다른 모듈에 속해 있어도 사용할 수 있다. 또한 어떤 함수가 decorator로 감싸지면, 이름이 같은 closure로 대체되므로 __name__ 등 몇몇 내장 변수의 값이 원래 함수의 그것과는 다르다.

Python에서 object의 method는 자기 자신을 가리키는 인자를 하나 더 받는 함수이므로 decorator 내에서 해당 처리( inner, inner_2 함수에서 self를 넘겨주는 동작)만 해 준다면 method도 decorator로 감쌀 수 있다.

Q. 다음 중, python 2.7이후의 내장 자료형인 set에 대한 설명으로 옳은 것은?

1. set 객체 자체는 hashable하지 않다.
2. 보기 내의 다른 모든 선택지가 옳다.
3. 원소를 추가/삭제할 수 있다.
4. 순회(iterate)할 수 있다.
5. set의 원소들은 해당 set 내에서 유일하다.

정답 : 2

우선 set의 객체는 collection.Iterable 의 instance이므로 순회(iterate)가능하며, set은 동일 set 객체 내의 원소들이 해당 set 객체 내에서 유일함을 보장해 준다.

Python의 내장 자료구조 중 set, dict 의 key 등 유일성이 보장된 값들을 관리하는 자료 구조들은, hashable 한 값들만을 원소로 취할 수 있다. 이러한 자료 구조들에서는 두 원소를 비교하기 위해 __hash__ 함수와 __eq__ 함수의 반환 값을 참조한다. 이 때, __eq__ 를 이용한 비교 결과 두 객체가 동일하다면, __hash__ 의 값도 동일한 경우를 hashable하다고 한다. (Python glossary) list, set 등 원소들을 변경 가능한(mutable) 자료 구조들의 경우, 값이 변경되었을 때 동일성을 판단하기 모호한 경우가 생긴다.

4

따라서 Python 내장 자료 구조 중 변경 가능한(mutable) 자료 구조들은 모두 hashable하지 않다. set은 필요에 따라 원소 추가, 제거가 가능하므로 mutable 한 자료 구조이며, 따라서 set은 hashable하지 않다.

Q. Python 2.x에서, 다음 코드에 대한 설명으로 옳은 것은?

5

1. type(B) 는 의 값은 ‘class __main__.B’ 이다.
2. Class B는 A를 상속받지만, A클래스 내의 now 필드는 상속받지 않는다.
3. a 객체의 now 필드는 a 객체가 생성된 시점의 시각이다.
4. j 필드는 b 객체에서 접근할 수 없다.
5. Class B 의 object 생성 시, 반드시 생성자의 j에 해당하는 값을 넘겨야 한다.

정답 : 2

Python 에서 object property 는 __init__ 함수에서 초기화함으로써 만들어지며, 설령 상속을 받았다 하더라도 super(…).__init__(…) 등의, 부모 클래스의 __init__을 호출해 주지 않으면 부모 클래스에서 선언된 property 는 사용할 수 없다.

이외에, 의외로 많은 실수를 하는 부분이 3번, 함수의 기본 인자값과 관련된 내용이다. Python에서 함수의 기본 인자값은 해당 문장이 interpreter에서 해석되는 시점에 평가된다. 즉 함수가 호출되는 시점에 평가되지 않는다.

5-1

위의 예제를 보면, obj 생성 시점은 21:33:34 임에도 불구하고, __init__ 에서 datetime.now()의 값으로 초기화하는 obj.now의 값은 그보다도 이전, 대략 5초 전이다. class 선언문 위에서의 시간 값과 비교해보면 class 선언문이 interpereter에서 해석된 시점의 것과 거의 유사함을 알 수 있다. 또한 5초 sleep후에 다시 생성해봐도 객체 내 now 의 값은 변함이 없음을 확인할 수 있다. 의외로 실수하기 쉬운 부분이므로, 함수 인자의 기본값을 함수의 반환값으로 지정하는 경우 주의가 필요하다.

Python 은 간결하고 명확한 언어이지만 그것이 다는 아니다. 개발자가 상당히 깊은 부분까지 커스터마이징이 가능하며, 우아한 코드를 작성할 수 있는 방법들을 제공한다. 위에서 언급한 개념들이 Python을 이용한 제품 설계, 개발 시 조금이라도 도움이 되기를 바라며 글을 마친다.

아이펀팩토리 민영기 TD

간단한 게임 서버 모니터링 시스템 구축

이번 글에서는 게임 서버 모니터링에 도움이 되는 간단한 테크닉에 대해서 알아보도록 하겠다.

게임 개발 과정은 고난의 연속이다. 출시를 목표로 피곤에 쩔은 몸과 피폐해진 정신을 인내하지만, 클라이언트-서버 개발 완료로 끝날 것이라 생각되었던 그 고난들은 안타깝게도 서비스 출시 이후까지도 계속된다. 단순히 계속되는 정도가 아니라, 많은 이들이 주지하다시피 사실 출시부터가 본격적인 고난의 시작인 경우가 대부분이다.

출시가 끝이 아니라 여정의 시작이 되는 이유는 여러가지가 있는데, 먼저 서비스가 죽지 않고 안정적으로 동작하도록 해야되며, 유저 피드백에 따라 게임의 운영이나 업데이트를 신속하게 조정해야되는 것이 큰 이유 중 하나이다. 출시 후 첫 업데이트에 대해서 사전 계획을 가지고 준비하고 있던 경우라 할지라도, 많은 경우 급작스레 계획이 수정 되곤 한다.

여기서 말하는 유저 피드백은 공식 까페 등에서 유저들이 올리는 글이라기 보다는 KPI 라고 불리우는 객관적인 성능 지표를 의미한다. 까페의 글들은 정량화 되기 어렵고 개인 취향에 따른 것들도 많기 때문이다.

KPI 종류

KPI 는 관점에 따라 1) 재무적인 KPI, 2) 게임 콘텐츠 상의 KPI, 3) 서버 운영에 있어서의 KPI 로 구분할 수 있다. 재무적인 KPI 는 우리가 일반적으로 많이 들었던 수익(revenue), 유료 유저 비율 (paying user rate), 유저당 평균 수익 (ARPU), 유료 유저당 평균 수익 (ARPPU), 유저 생애 가치 (LTV) 등 돈과 직접적인 관련이 있는 중요한 것들이다. 이런 지표들은 회사의 사업담당자가 주로 관심을 갖는 지표이다.

재무적 KPI 가 이익 창출을 위한 궁극적인 KPI 라면, 게임 콘텐츠 상의 KPI 는 게임 안에서 벌어지고 있는 일들에 대한 KPI 로서 재무적 KPI 의 선행 지표격이라고 할 수 있다. 여기에는 일간 활동 사용자수 (DAU), 월간 활동 사용자수 (MAU), 잔존율 (retention rate) 등 게임에 공통적인 KPI 와 게임 콘텐츠 각 단계별 통과 비율 (funnel analysis), 게임내 경제의 인플레이션 흐름 (전체 재화 수준, 혹은 재화별 수준) 등 게임에 따라 다른 의미를 갖게 되는 KPI 로 나눌 수 있다. 이런 지표들은 주로 게임을 설계하는 기획자나 프로듀서가 관심있게 봐야되는 KPI 에 해당한다.

앞의 두 KPI 종류와 다르게 서버 운영에 있어서의 KPI 는 서비스의 안정성에 관련된 KPI 이다. 이 때문에 이 KPI 는 급격한 변화를 목표로 하기 보다는 주로 안정적인 수치 안에서의 관리를 목표로 한다. 여기에는 수용 가능 최대 동접, 클라이언트 패킷에 대한 반응시간, CPU 나 메모리 등의 리소스 사용량, DB 나 캐시와 같은 외부 시스템에 대한 요청 처리량 및 반응 시간 등이 포함된다. 이 KPI 들은 주로 서버 개발자가 주의깊게 살펴봐야되는 지표다.

KPI 모니터링

재무적인 KPI 나 게임 콘텐츠 KPI 에 대해서는 꽤 오래전부터 사람들이 많은 관심을 가져왔고, 많은 상용 솔루션이 나와있다. 그리고 존재하는 솔루션의 전형적인 형태는 클라이언트에 원격 로그용 SDK 를 심고, 로그인, 로그아웃, 결제 등과 같은 주요 이벤트에 대해서 SDK 를 통해서 기록을 남기는 방법이다. 클라이언트 SDK 를 통해서 수집된 내용은 분석 서버에서 통합되어 의미있는 KPI 로 생산되게 된다. 그런데 이런 솔루션들은 주로 일반화된 KPI 가공에 촛점이 맞춰져 있기 때문에 게임에 특화되어있는 지표는 여전히 게임에서 직접 생산해야되는 경우가 많다.

재미있는 것은 서버 관련 KPI 가 안정적인 서비스를 위해서 필수불가결함에도 이를 위한 솔루션들이 거의 존재하지 않는다는 점이다. 그 때문에 많은 경우 서버 개발자는 일반적인 ganglia 나 nagios 등 OS 수준의 모니터링 툴이나 프로세스 모니터링 툴, 혹은 DB 성능 툴들을 대신 사용하곤한다.

하지만, 이런 툴들이 따로따로 존재하는 경우 서비스의 현 상황을 한눈에 파악하기 힘들고, 그리고 앞의 게임 콘텐츠 KPI 에서 설명한 것처럼 일부 커스텀 기능의 추가 때문에 직접 개발해야되는 경우를 피할 수 없는 경우도 종종 있다.

이런 이유들 때문에 게임 콘텐츠 KPI 및 서버 운영 관련 KPI 를 모니터링 하는 툴을 직접 만들어야되는 경우 손쉽게 적용할 수 있는 구조로 다음과 같은 두 가지 방법을 생각할 수 있다.

방법1: 게임 서버에 내부 카운터를 유지하는 방법

이 방법은 게임내의 정보를 카운터로 노출하고 외부에서는 이를 단순히 그래프로 표시해주는 방법이다. 주로 후처리 없이 지속적으로 변동되는 휘발성 정보를 기반으로 하는 경우에 해당하며 다음과 같은 두 가지 layer 로 구성된다.

  1. 기초 데이터 생성 layer: 게임 서버는 주기적으로 주요 지표들을 (이를 counter 라고 부르겠다) 특정 DB 에 저장하는 역할을 한다.
  2. 표시 (presentation) layer: 앞에서 DB 에 저장된 데이터를 읽어들여 그래프로 표시해준다. 보통 dashboard 형태가 된다. Django 나 Flask 와 같은 framework 을 이용해 쉽게 구성할 수 있다.

기초 데이터 생성 layer 를 위해 게임 서버는 counter 라는 hash map 이나 dictionary 를 관리한다. 그리고 기초데이터에 해당되는 주요 숫자가 바뀌게 되면 이를 반영해준다. 예를 들어, 각 패킷 별로 처리 지연 시간을 보고 싶다면, counter 에 <패킷 이름, 패킷 개수, 처리 지연 시간의 누적값> 을 관리할 수 있을 것이다. 그리고 이를 통해서 각 패킷별 처리 시간의 평균과 표준 편차를 DB 에 저장할 수 있을 것이다.

이런 형태로 모니터링 될 수 있는 데이터는 CPU/메모리 등 OS 리소스 사용량, 앞에서 예를 든 것처럼 패킷 별 처리시간, 패킷 별 트래픽량, DB 요청 숫자 및 반응 시간 등이 있을 수 있다.

방법 2: 로그 분석을 이용한 방법

앞의 방법은 별도의 가공이 필요없는 데이터를 기반으로 하였으나, KPI 생성을 위해 가공이 필요한 데이터를 기반으로 하는 경우는 이를 로그로 저장하고 로그를 분석해야된다. 이 때는 다음과 같은 세 가지 계층 구조를 갖게 된다.

  1.기초 데이터 생성 layer: 게임 서버는 주요 상황에 대해서 유실되지 않는 방법으로 로그를 남긴다. DB 에 저장하는 것도 가능하고 파일로 남기는 것도 가능하다. 로그인, 로그아웃, 결제 등을 로그로 남기는 것을 생각할 수 있겠다.
  2. KPI 생성 layer: 기초 데이터를 읽어들여 KPI 를 생성하는 작업을 한다. 예를 들어 로그인 정보를 통해 평균 플레이 시간이나, 잔존율 등을 계산 할 수 있을 것이다. 매번 KPI 를 계산하는 것이 큰 부하를 줄 수 있으므로 한번 계산된 KPI 는 DB 에 저장하는 역할도 한다. 
  3. 표시 (presentation) layer: 앞의 방법1 에서와 마찬가지로 생성된 KPI 를 표시해주는 dashboard 라고 생각하면 된다.

주의할 점은 위의 두 방법이 상호 배타적인 방법이 아니라 데이터의 성격상 후처리가 필요한지 여부에 따라 구분이 될 뿐이라는 점이다. 그리고 많은 경우 모니터링 툴은 두가지 데이터 형태 모두를 필요로 한다. 따라서 “표시 layer” 로 언급된 dashboard 는 두 방법에 의한 데이터를 모두 표시하는 것이 일반적이다.

아이펀팩토리 문대경 대표

분산 서비스에서 쉽고 확장 가능하게 인증처리하기

웹/게임 서비스 구조의 변화


2000년대 초/중반에 웹 서비스나 게임 서비스를 만든다고 하면 영속적인 데이터 저장소 (데이터베이스나 SAN와 애 플리케이션 계층처럼 2계층이나 데이터 저장소 – 비지니스 로직 – 프리젠테이션의 3계층 형식으로 구성하는게 흔했다. 하지만 요즘은 여러 곳에서 경험하고 있다시피, 여러 개의 작은 서비스로 쪼개서 서비스끼리 통신하는 형태로 구성하 는 마이크로서비스로 만드는 경우가 많아졌으며, AWS 같은 클라우드 플랫폼들은 이런 구조를 권장한다.

즉, 유저와 서비스가 통신하는 것으로 끝나는게 아니라 유저와 서비스 1, 2, … 그리고 서비스 1과 서비스 2, … N 이 통 신하게 되는 것이다. 그런데 서버/클라이언트 구조의 모노리딕 서비스를 만들던 시절이랑 똑같은 문제는 여전히 남아 있다.

  • 서비스에 접속한 유저/다른 서비스가 주장하는 유저/서비스가 맞는가 (authentication)
  • 해당 유저/서비스가 요청한 기능을 실행해도 좋은가 (authorization)

많은 경우,

  • 서비스에 접속한 유저 (이제부터 유저는 다른 서비스와 상호 교환가능한 단어로 생각하자) 가 그 유저가 맞는지를 인증 한다. 즉 ID/PW를 데이터베이스 비교하거나, Kerberos 나 LDAP 같은 외부 인증 서비스에 확인한다.
  • 유저가 요청한 내용이 허용되는 내용인지를 유저의 종류나 역할 (role) 정보를 데이터베이스에서 확인하거나, 외 부 서비스 (역시나 Kerberos나 LDAP 류)에 묻는다.

같은 구현을 만든다. 이렇게 만든 서비스가 규모가 커가면 이 부분에서 병목이 생기리란 점은 쉽게 예측 가능하다. 우선 저런 인증 및 권한 시스템 자체에 부하가 걸린다. 비슷하게, 데이터베이스에 권한 시스템을 만든다면 데이터베이스 의 부하가 오를 것이다. 다음으로, 간과하기 쉬운 부분인데 지연 시간이 오르기 시작한다. 10G를 넘어 40G 이상이 대 세가 되어가는 데이터 센터 내의 통신이 빠르다곤 하지만 네트워크 요청의 응답 지연 시간은 여전히 분산 시스템 전체 의 성능에서 꽤 중요한 요소다. 마이크로 서비스 구조는 그 자체가 다른 시스템에 뭔가를 요청하는 시간이 들 수 밖에 없고, 이로 인한 지연 시간은 다음과 같은 경우 모두에서 올 수 있다 — 그리고 모두 성능에 악영향을 준다:

  • 네트워크 사용이 짧은 시간에 몰리는 특성
  • 하드웨어/OS 오버헤드 (스위치, 라우터, …, OS 의 네트워크 스택)
  • 다른 서비스 (데이터베이스, Kerberos, …) 에 보낸 요청이 대기열에서 기다리는 시간
  • 인증 서비스가 정말로 외부에 있는 경우 외부 인증 서비스까지 다녀오는 시간:

auth-token-traditional

물론 이 중 많은 부분은 최적화하거나 개선할 수 있고, 적절한 캐시 계층은 많은 부하를 최소화하리라 생각된다. 그렇지만, 인증 / 권한 확인에 들어가는 부하와 지연 시간을 제거할 방법은 없을까?

인증토큰


많은 시스템들에서 토큰 기반의 인증/권한관리 시스템을 구현한다. 토큰에는 대략 다음과 같은 정보가 포함된다.

  • 토큰 소유자가 누구인지
  • 토큰 소유자가 무엇을 할 수 있는지 (혹은 그 중에 이 토큰으로 무엇을 할 수 있는지)
  • 누가/언제 토큰을 만들었는지 (그리고 언제 만료되는지)

토큰을 전달받은 마이크로 서비스는 토큰의 정보를 이용해서,

  • 인증 처리: 토큰 소유자가 누구인지가 토큰에 들어있다.
  • 권한 확인: 토큰 소유자가 할 수 있는 일이 무엇인지 들어있다.

즉, 인증/권한 서비스에 접근하지 않고도 토큰을 들여다보는 것만으로 토큰을 받은 서비스가 알아서 할 수 있게 된다.

auth-token

토큰을 어째서 신뢰해도 좋은가?

위에서 설명한 토큰의 내용 중에 의도적으로 포함하지 않은 부분이 있다. 인증 토큰이 유효하다는 걸 증명하기 위해서 메시지 인증 코드 (Message Authentication Code) 를 붙인다. 이 MAC 코드는 암호화를 하는 것은 아니고, MAC이 붙어있는 메시지가 변경 사항이 없다 는 점만 확인할 수 있다. 그래서 토큰의 신뢰성은 a. MAC 알고리즘을 믿을 수 있 는가? b. 토큰을 다른 유저가 접근할 수 없는가? 두 가지 요소가 결정짓는다.

우선 MAC 알고리즘 부분을 살펴보자. 크게 두 종류의 알고리즘이 있는데, 토큰을 어떤 주체와 주고 받을지에 따라서 선택할 수 있는 알고리즘이 다르다. 같은 운영 주체 안에서만 쓴다면 비밀 값 을 하나 정해서 안에서 공유할 수 있다. 이 경우에는 암호학적인 해시함수를 이용한 HMAC 을 이용한다. 보통 SHA1, SHA-2 (SHA-256, SHA-512, …) 나 심지 어 MD5 알고리즘을 이용한다. 해시 값 자체를 이용하는게 아니라 조금 다른 생성 방식을 쓰기 때문에 아직 MD5가 현역 인 거의 마지막 분야다. 그리고 몇 번의 해시 함수와 XOR 연산 외에는 없기 때문에 엄청나게 빠르다.

하지만 여러 운영 주체가 토큰을 교환하는 경우가 있다. 예를 들어서 Google 이 발급한 토큰을 사용해서 내 서비스에 접근하는데, Google 과 내 서비스가 특정 비밀 키를 합의하기는 어렵다. 그래서 HMAC 말고 공개적으로 확인할 수 있 는 다른 대안이 필요하다. 이 경우에는 전자 서명 알고리즘을 이용한다. 즉, 토큰 내용의 해시 값에 암호학적인 서명을 추가한다. (암호학적인 해싱 + RSA, DSA, ECDSA, 혹은 EdDSA 등의 공개키 알고리즘으로 서명) 다만 이 경우 서명과 서명 확인 모두 일반적인 해싱보다 훨씬 (대략 수십에서 수천배 정도)느리다.

알고리즘의 선택에 따라 HMAC 자체의 구조 혹은 공개키 알고리즘이 제공하는 보안성 만큼 믿을 수 있다. 만약 해당 토큰이 노출되면 토큰 자체의 기밀성은 없기 때문에 토큰을 탈취한 쪽이 인증/권한을 뺏어갈 수 있다.

사례 분석: OpenStack Keystone

IaaS 혹은 그 이상을 구축할 수 있게 해주는 오픈소스 구현체인 OpenStack 에는 Keystone 이라는 인증 및 권한 처리 를 담당하는 서비스가 있다. Ubuntu Linux 와 유사하게 매년 두 번 릴리즈하는데, 2012년의 두 개의 릴리즈 (Essex, Folsom) 동안에는 토큰을 사용하긴 하지만 토큰 자체를 항상 Keystone API에 확인받는 구조였다. 즉, 위에서 언급했 던 외부 서비스에 확인하는 방식으로 동작했다. 오픈스택 자체가 상호연관된 수많은 마이크로서비스로 만들어져있고, 하나의 API 서비스가 여러 개의 인스턴스가 분산 환경에서 동작하기에 Keystone API와 이 API가 사용하는 데이터베 이스 단의 부하가 꽤 컸다. 실제로 VM을 띄우는 노드가 수십개 수준일 때에도 상당한 양의 CPU 싸이클이 Keystone API와 데이터베이스에서 소진되는 걸 경험했다.

그래서 이 다음 릴리즈인 2013년의 Grizzly 버전에서는 공개키 인프라스트럭처 (PKI) 기반의 토큰 기능을 제공하기 시작했다. 예상할 수 있는 바처럼, Keystone 에서 토큰을 발급하고나면, Nova 나 Neutron 같은 다른 OpenStack 서비스에서는 공개키 서명을 확인해서 토큰을 검증하기 때문에 전반적인 API호출 수가 줄어든다.

개념 확장하기


인증 토큰은 단순하면서도 강력한 개념이다. 그래서 같은 개념을 사용해서 네트워크 기반 서비스에서 활용하는 일이 적지 않다.

좀 더 안전한 쿠키 / 클라이언트 세션

웹 사이트에서 보낸 쿠키를 클라이언트에서 변조해도 웹 사이트에서 이를 확인할 방법이 없다. 그래서 쿠키 데이터를 직렬화 (serialize) 할 때 MAC으로 HMAC 이나 전자 서명을 추가한다. 웹 서버에서 다시 쿠키를 받았을 때, MAC 내 용과 쿠키 내용을 비교하고, 이 내용에 대해서 변조된게 있는지 확인하는 방식으로 처리한다. 그리고 이런 기능을 이용 하면 분산 서버들이 공유 캐시나 공유 데이터베이스, 혹은 로드밸런서 수준의 sticky session 을 사용하지 않아도 세션 을 만들 수 있다.

실제로 여러 웹 프레임워크에서 이런 기능을 제공한다:

  • Python itsdangerous: MAC과 만료 시한을 포함한 데이터를 만들 수 있게 도와주는 라이브러리. 해당 기능을 이 용해서 flask 의 세션을 구현한다.
  • Rails: Rails의 세션 구현 역시 이런 기능을 이용한다.

이런 구현이 널리 쓰이는 것은 공유 세션 저장소를 만드는 것은 데이터베이스를 쓰거나 memcached 나 redis를 써도 해당 부분에 대해서 고가용성을 확보하면서 성능을 올리는게 그리 쉬운 일이 아니라서 그렇다.

메일에 보내는 URL에 특정 정보를 포함시키기

일부 이메일 리스트 등에서 탈퇴 처리를 위한 링크를 보낼 때가 있다. 해당 링크에는 서명 혹은 암호화를 하고 만료 시한을 지정한 정보를 지정하는 경우가 있다. 즉, 링크 내용의 일부를 디코딩하고, 암호화한 경우 복호화까지 하면,

  • 유저가 하려는 행동을 알 수 있다. 예를 들어 이메일 리스트에서 탈퇴, 혹은 패스워드 재설정
  • 유저에게 특정 시간까지만 이를 허용한다: 만료 시한을 데이터의 일부분으로 넣으면 기간이 지난 링크는 사용할 수 없다.
  • 유저가 의도한 유저인지 알 수 있다. MAC을 포함해서 보내면 데이터를 변조한 경우 알 수 있다.

분산 서비스에 토큰을 적용해보자


인증 토큰을 이용하면,

  • 공유 저장소의 사용을 줄이거나 (클라이언트 세션, 이메일 내의 링크 데이터)
  • 서비스 요소간의 (불필요한) 통신을 줄이거나 (서비스 내 인증 토큰)
  • 분산 처리를 쉽게 하거나 (특정 서버와 계속 통신하지 않아도 되는 인증 토큰)

하는 등의 이득을 볼 수 있다. 다만 인증 토큰의 무효화 처리 (revocation) 은 쉽지 않으니, 만료 처리나 블랙리스트 처 리를 구현해서 좀 더 편하게 분산 서비스를 만들어봅시다.

아이펀팩토리 김진욱 CTO

MariaDB에서의 쿼리 계획(Query plan) 활용

게임 서비스 시 서버 측면에서 성능상 문제가 되는 부분은 DB와 관련된 부분일 것이다. 과거에는 거의 모든 게임 데이터 관리 및 최종 동기화 등을 RDBMS에 의존하였기 때문에 쿼리 최적화는 게임 서버 최적화에서 매우 중요한 부분이었다. 최근에는 성능상의 이유로 캐시나 NoSQL 을 이용하는 경우가 많지만, 결제 관련 내용이나 사용자 간 거래 등 atomic 한 처리가 필요한 데이터들을 관리하는 데는 여전히 RDBMS를 사용하는 경우도 많다. 또한 이러한 결제나 거래 관련 데이터들은 통계 처리 등의 이유로 복잡한 쿼리의 대상이 되는 경우가 많기에 아직도 RDBMS 쿼리 튜닝은 게임 서버 성능 최적화에서 적지 않은 비중을 차지한다.

쿼리 최적화시에는 실행 시간을 기반으로 한 프로파일링이 크게 의미가 없다. 쿼리의 실행 시간에 가장 큰 영향을 주는 요소들은 RDBMS가 설치된 머신의 스펙과 쿼리의 대상이 되는 레코드의 수, 그리고 인덱스 접근/ 사용 방식과 중첩 쿼리의 실행 순서 등 쿼리 자체의 실행 과정이다. 이 중 머신의 스펙과 대상 레코드의 수는 최적화의 대상으로 보기는 어렵다. 따라서 최적화의 대상은 쿼리 실행 과정인데, 이는 단순히 실행 시간만을 측정해서는 알 수 없기 때문이다.

따라서 여러 RDBMS(Mysql, Mariadb, Oracle, Postgresql 등)에서는 어떠한 쿼리가 주어지면 해당 쿼리를 어떤 순서로 어떤 데이터를 활용하여 처리하겠다는, 쿼리 계획(query plan) 을 보여주는 기능을 제공한다.

Mariadb 에서는 쿼리 계획을 확인하는 명령어를 두 개 제공한다. EXPLAIN과 ANALYZE가 그것이다. EXPLAIN는 예상되는 실행 계획을 보여주고, ANALYZE는 쿼리를 실제 실행한 후 실행한 쿼리 계획을 보여준다. 언뜻 보기엔 ANALYZE가 더 유용해 보이지만, 쿼리 실행 시 디비에 부하가 가해질 수 있고, 레코드가 늘어나거나 하면 실행 계획 또한 바뀔 수 있으므로 실 서비스에서 실행시와 동일한 정보를 알려주지는 않는다. 두 명령어가 제공하는 쿼리 계획에 대한 정보는 같다.

Mariadb는 (10.0.1 이후부터) SELECT, UPDATE, DELETE 쿼리의 실행 계획을 조회하는 기능을 제공한다. UPDATE와 DELETE의 경우 각각 수정/삭제할 대상 레코드를 어떻게 찾느냐에 대한 계획을 보여주기에 실행 계획은 SELECT의 그것과 큰 차이가 없다.

EXPLAIN을 이용하여 쿼리 계획을 보는 방법은 간단하다. 실행할 쿼리 앞에 EXPLAIN을 붙여 주면 된다.

이미지

EXPLAIN을 실행하면 위와 같은 결과가 출력된다. JOIN이나 서브 쿼리가 포함되어 여러 단계의 처리가 필요한 경우, 각 단계별로 실행 계획을 보여준다.

쿼리 계획에는 다음 항목들이 표시된다.

● id : 대상 쿼리문에 JOIN이 포함되어 있을 때, 어떠한 순서로 테이블이 JOIN되는지를 나타내는 값이다.

● select_type : 각 단계를 실행할 때 어떤 종류의 SELECT가 실행되었는지를 나타낸다. 최적화 시에는 크게 중요하지는 않으나, 값이 DEPENDENT SUBQUERY, 혹은 DEPENDENT UNION 인 경우 의존성 등의 문제로 쿼리가 특정 순서로만 실행되어야 함을 뜻하므로 비효율적인 쿼리일 가능성이 있다.

● table : 해당 단계에서 접근하는 테이블의 이름이다. 실제 테이블, 혹은 임시 테이블일 수 있다.

● type : 테이블 내에서 접근이 필요한 레코드를 어떻게 찾았는지에 대한 정보이다.
인덱스 접근 여부 및 방식 등에 대한 내용도 포함하므로 쿼리 최적화 시 반드시 확인해야 할 값이다. 몇 가지 중요한 값에 대해서만 부연 설명하겠다.
 ○ system : 테이블 내에 레코드가 1개 이하인 경우이다. 이 경우에는 레코드를 더 추가한 후 다시 쿼리 계획을 확인하는 것이 좋다.
 ○ const, eq_ref : 해당 단계가 PK 나 유니크 인덱스 검색을 이용해 레코드에 접근함을 뜻한다. 일반적으로 가장 빠른 검색 방법이다.
 ○ ref : 인덱스를 이용하여 동등 비교 연산을 통해 레코드에 접근함을 뜻한다. 위의 두 개만은 못하지만, 역시 매우 빠른 검색 방법이다.
 ○ fulltext : 전문 인덱스(Fulltext Index) 를 이용하여 레코드에 접근함을 뜻한다. 전문 인덱스는 일반적인 비교 연산으로 접근이 어려운 경우에 주로 사용되므로 최적화하기 어려운 경우가 많다.
 ○ range: 인덱스를 이용하여 값 비교 연산 ( , BETWEEN 등 )을 이용하여 레코드에 접근함을 뜻한다.
 ○ index : index 전체를 스캔해야만 필요한 레코드에 접근할 수 있음을 뜻한다. 풀 테이블 스캔보다는 빠르지만, 인덱스가 매우 큰 경우 등에는 비효율적이다.
 ○ ALL : 인덱스를 이용하여 필요한 레코드를 검색할 수 없어, 전체 테이블을 스캔해야만 함을 뜻한다. 당연히 테이블 내 레코드 수에 따라 실행 시간이 매우 길어지므로 적절한 인덱스 추가나 HINT 문 사용 등을 통해 최적화하는 것이 좋다.

● possible_keys : 레코드에 접근하기 위해 사용할 수 있는 키, 혹은 인덱스 목록을 보여준다. 실제로 사용된다는 의미가 아니므로 실제로 어떠한 키가 사용되었는지는 key 항목을 확인해야 한다.

● key, key_len : 레코드에 접근하기 위해 어떠한 index를 참조하는지, 인덱스 중 몇 바이트를 참조했는지에 대한 정보이다. key_len 은 둘 이상의 컬럼으로 구성된 인덱스를 참조했을 경우에만 의미가 있다.

● ref: 인덱스 검색 시 비교 연산 등에 사용되는 기준값을 보여준다. 최적화 시에는 큰 의미는 없다.

● rows : 필요한 레코드들을 추려내는 과정에서 몇 개의 레코드에 접근해야 하는지를 예측하여 보여준다.

● extra : 이상의 항목 외의 특이 사항들이 있다면 해당 내용을 표시해준다. 예를 들어, 접근해야 하는 컬럼이 모두 인덱스에 포함되어 있어 인덱스 검사만으로 필요한 값을 반환할 수 있다면 Using index 가 표시된다. 때에 따라 성능에 영향을 줄 수 있는 값들이 있으므로, 최적화 시에 이 컬럼이 비어 있지 않다면 확인할 것을 권한다.

한 가지 유의할 점은, MariaDB는 쿼리 계획을 만들 때 인덱스의 크기나 수, 레코드의 수 등을 같이 고려하므로 같은 쿼리라 하더라도 실행 계획 조회 시점에 따라 실행 계획이 달라질 수 있다. 그러므로 유효한 데이터를 얻기 위해서는 될 수 있는 한 실 서비스에서, 혹은 실 서비스와 최대한 비슷한 환경에서 실행 계획을 조회하는 것이 좋다.

지금까지 RDBMS 의 쿼리를 최적화할 때 쿼리 계획 조회가 왜 필요한지, 그리고 MariaDB 에서 쿼리 계획 조회 시 어떠한 정보들을 알 수 있는지에 대해 알아보았다. 쿼리 계획은 최적화에 필요한 모든 정보를 제공하지는 않지만 기본이 되는 정보를 제공해 주기에, 쿼리 최적화 시 꼭 한번은 쿼리 계획을 확인할 것을 권하고 싶다.

아이펀팩토리 아이펀 디플로이 테크니컬 디렉터 민영기

개발 환경, 테스트 환경, 그리고 라이브

서버 개발자에게 컴퓨팅 자원은 곧 비용이다. 대충 한 열 명의 동접마다 한대씩 서버를 할당한다면 누구나 더 편하게 서버를 개발할 수 있을지도 모르겠지만, 현대의 기술은 아직 우리에게 이런 여유(혹은 낭비)를 허용하지 않고 있다. 게다가 열 명당 하나씩 서버를 둘 수 있다손 치더라도, 이로 인해 엄청나게 늘어나는 서버의 수를 관리하는 서버에 몰리는 부하를 처리해야하는 골치아픈 문제는 결국 그대로 남아있게 된다. 그러므로 결국 서버를 최대한 최적화하고 적당한 하드웨어를 찾아서 배포하는 방법론은 당분간 어쩔 수 없는 서버 개발자의 숙명이다.

클라이언트 개발자가 여러 대의 단말에서 테스트 한다면, 서버 개발자는 다양한 환경에서 서버 환경을 구축하고 테스트하게 된다. 끝없이 수정한 코드를 다시 실행해볼 개발 서버, 개발팀, 기획팀, 사내, QA팀을 위한 테스트 서버, FGT나 CBT등을 위한 일정 규모의 라이브에 준하는 서버, 그리고 실제로 서비스하는 라이브 서버, 마지막으로 게임이 성공해서 새로운 서버를 열어야하는 경우까지.. 안드로이드와 iOS라는 꽤나 다른 OS에서 동일한 어플리케이션을 동시에 준비하는 클라이언트 개발자들의 노력도 대단하지만, 상황에 따라 소규모부터 대규모까지 동시에 서버 환경을 만들어내야하는 서버 개발자의 고생도 더하면 더하지 덜하진 않다.

그렇다면 각각의 상황에 맞춰 필요한 하드웨어의 미덕과 준비 방법은 어떻게 될까. 이번 칼럼에서는 이를 이야기해보려고 한다.

1. 개발 환경

의외로 많은 개발자들이 개발 환경 하드웨어의 중요성을 무시하곤한다. 개발 환경의 서버는 최적화되기 전의 코드를 수행한다. 또한 끝없이 코드를 컴파일해야 한다. 그러므로 개발 생산성과 테스트 용이성을 위하여, 개발 환경의 하드웨어는 그 어떤 환경보다도 빠르고 많은 CPU 코어, 이래도 될까 싶을 정도로 많은 메모리와 디스크 용량, 가능한 예산 내에서 무조건 최대한 빠른 형태의 IO를 지원하는 디스크를 확보해야한다.
강력한 하드웨어는 개발과 동시에 빠른 피드백이 이루어지는 팀 테스트와 수정을 동시에 진행하는 경우에도 도움이 된다. 예를들어 스크럼 같은 개발 방법론을 활용하여 빠르게 개발해보고 포스트모템을 진행하는 경우, 강력한 개발 환경에서 단시간 안에 코드를 바꾸고 컴파일해서 테스트 해볼 수 있을 때 가장 효율이 좋다.

2. 테스트 환경

테스트 환경은 다양한 파트의 요청사항에 따라 끝도 없이 늘어날 수 있다는 것이 특징이다. 동일한 환경을 쉽고 간편하게, 그리고 필요에 따라 적절한 수준의 하드웨어에 배포할 수 있도록 배려하는 것이 중요하다. 스트레스 테스트를 위해서는 가능한 분리된 인스턴스에서 각각의 로그와 프로파일링 정보를 얻어내는게 중요하다면, 펀테스트를 위해서는 소수의 인원이 즐겁게 즐길 수 있도록 유도하는 환경이 되어야 한다.

보통 테스트 환경에서 코드를 컴파일할 일은 거의 없다. 즉, 새로 만든 패키지를 빠르게 배포하는 것이 중요하고, 사용자의 수나 컨텐츠의 양에 따른 하드웨어 능력의 분배가 중요하다. 또한 쉽게 배포하는 만큼이나 만들어진 서버 환경을 쉽게 회수할 수 있는 것 역시 중요하다.
그러므로 내부에서 사용할 때에는 vm으로 여러 개의 인스턴스를 관리할 수 있도록 하거나, 클라우드 등에서 저렴한 하드웨어를 필요할 때마다 만들어서 사용하는 것이 좋다.

3. 라이브 환경

라이브 환경을 결정하는 것은 보통 서버 개발자가 아니라 퍼블리셔인 경우가 많다. 물론 그렇긴해도 일반적으로 대형 퍼블리셔가 관리하는 IDC거나, 클라우드로 서버를 배포하는 경우가 가장 흔하다.
이 경우 서버 개발자가 준비해야하는 것은 동접자에 따른 전체적인 서버의 수와 구성 방식이다. 그리고 이를 위해서는 잘 구성된 스트레스 테스트 환경과 성능 프로파일링 방법이 필요하다. 그러므로 라이브 환경 설정은 테스트 환경의 연장선에 있다고 할 수 있다. 라이브 서버에 새로운 서버를 추가하는 경우에도 이는 마찬가지이다. 이 때 사용자의 분산과 추가 작업의 양을 결정하는 것은 비슷한 환경에서 얼마나 테스트를 해보았느냐에 달려있다.
또한 라이브 환경 서버 설정은 비용과 밀접한 관련을 가지게 되므로, 예상 동접은 소화할 수 있되, 노는 서버가 없도록 조율할 필요가 있다. 동시에 넉넉한 서버를 확보하기위하여 서버 개발자는 프로젝트의 책임자에게 잘 설명하여 퍼블리셔를 설득할 수 있는 이론적 기반을 마련해줄 수 있다면 금상첨화일 것이다.

지금까지 서버의 환경에 따른 구성과 이를 위해 중요한 부분은 무엇인지, 어떻게 준비해야하는지를 간단히 살펴보았다. 실제로 어떤 서버를 얼마나 준비해야하는지는 서비스 별로 가이드를 하기에 녹록치 않은 것이 사실이다. 게임마다 로직이 다르고, 장르마다 필요한 서비스가 다르다. 또한 어떤 기능을 어떻게 최적화했느냐에 따라 필요한 하드웨어의 사양은 천차만별이다. 그렇다보니 물고기를 주기보다는 물고기를 잡는 방법을 알려주라는 탈무드의 말과 같이, 주로 원론적인 내용 위주로 정리하게 되었다. 이번에도 미약하나마 서버 개발자들의 고민을 조금이라도 해결해주기를 바라며 이 칼럼을 마친다.

아이펀팩토리 박근환 TD

SQL 의 CAP 이론과 NoSQL 의 BASE

CAP 이론

분산 시스템 설계에 많이 인용되는 CAP 이론은 UC Berkeley 의 Eric Brewer 교수님이 제안한 개념이다. 분산 시스템 설계니 당연히 한 대의 서버로 이루어진 시스템이 아니라 여러 서버로 이루어진 시스템을 가정하는데, Consistency, Availability, Partition Tolerance 라는 세 가지 속성의 약자를 따서 CAP 라고 이름이지어진 이 개념은 Partition tolerance 한 시스템을 구현하기 위해서는 Consistency 나 Availability 둘 다를 얻을 수 없음을 의미한다. 좀 더 쉽게 말하면 C, A, P 이 세 속성 중에 기껏해야 두 개 밖에 취할 수 없다는 뜻이다.

여기서 Consistency 는 다른 서버에서도 가장 최근에 쓰여진 데이터가 읽혀야 됨을 의미한다. 예를 들어 A 와 B 라는 서버가 분산 시스템을 구성하고 있다면, A 라는 서버에 데이터를 쓰면 그 바로 뒤에 B 서버에서 누군가 그 데이터를 읽어들일 때 가장 최근의 데이터를 읽을 수 있어야 됨을 뜻한다.

Availability 는 보낸 요청에 대해서는 응답을 줄 수 있어야 됨을 의미한다. 그 응답이 가장 최근의 응답이 되었든, 아니면 살짝 이전의 데이터가 되었든말이다. 이 자체로는 너무 쉽고 당연한 것 같지만, 시스템이 “가장 최근의 데이터만을 줘야된다” 라는 제약이 붙게 되면, 가장 최근인지 아닌지 판단을 할 수 없는 경우 응답을 보내지 못할 수도 있게 된다는 점을 기억하자.

Partition tolerance 는 서버간의 통신에서 설령 네트워크가 끊긴 수준의 오류가 있더라도 시스템이 동작해야됨을 의미한다.

.

RDBMS 와 ACID 속성

RDBMS 는 Relational Database Management System 의 약자이다. 우리가 보통 DB 라고 말하는 것은 원칙적으로 DBMS 를 의미한다. 그러니 RDBMS 도 보통 RDB 라는 이름으로 부르기도 한다. R에 해당하는 Relation(al) 은 “데이터를 테이블 형태” 로 관리함을 의미한다. SQL 서버들이 테이블을 만들고 그 안에 데이터를 저장하는 것을 기억하는가? SQL 쿼리 문도 “CREATE TABLE …” 로 시작한다. 바로 SQL 이 RDB 이기 때문이다. 우리가 일반적으로 사용하는 DB 들은 이 RDB 형태들이다.

전통적인 RDB 에서는 Network Partition 을 크게 고려하지 않았다. DB 는 같은 데이터센터 안에, 심지어 같은 랙에 존재하고 있고 그 때문에 서버간 통신은 굉장히 잘 관리가 되는 안정적인 상황이었기 때문이다.

대신 RDB 에서는 ACID 라는 속성을 중요하게 생각해왔다. Atomicity, Consistency, Isolation, Durability 의 약자를 모아서 단어를 만든 것이다.

Atomicity 는 관련된 작업들은 전부 반영되거나 아니면 하나도 반영이 안되거나 해야된다는 것을 의미한다. “All or nothing” 인데, 쉽게 생각해서 우리가 SQL 에 transaction 을 만들면 그 안의 작업들이 모두 다같이 반영되어야 되는 것을 생각해보면 이해가 쉬울 것이다.

Consistency 는 CAP 의 consistency 와는 약간 다르다. CAP 에서는 서로 다른 서버라 하더라도 가장 최근 데이터를 반환함을 의미하지만, 여기서 consistency 는 DB 의 상태가 늘 일관된 상태를 유지해야됨을 의미한다. 예를 들어 DB 데이터의 속성이나 테이블 내 제약 등이 지정되어있는 경우 이를 준수하는 상황이 계속 일관되게 이어져야 된다는 것이다.

Isolation 은 여러 작업이 실행되더라도 그것이 순차적으로 실행된 것과 같은 결과를 내야됨을 의미한다. 만일 두 작업이 겹치는 것이 아예 없다면 이는 전혀 상관없이 동시에 실행될 수 있어야 되고, 만일 겹치는 부분이 있으면 순서대로 처리가 되어야지 겹쳐서 처리되어 데이터가 이상한 상태가 되어서는 안된다는 것을 뜻한다.

Durability 는 일단 작업이 완료 되었다고 리포팅이 되었다면 그게 DB 에 영구적으로 반영이 되어야 함을 의미한다. 설령 DB 가 크래쉬 하더라도 말이다.

.

NoSQL 등장의 배경

앞서 언급한 것처럼 DB 에서는 Partition 의 상황보다는 ACID 를 훨씬 중요한 가치로 생각을 하고 그쪽으로 많은 연구들이 이루어졌다. 그러나 구글이나 아마존, 페이스북 등 인터넷 회사들의 경우 데이터 센터가 여러곳에 분산되는 것이 일반적이다. 그런데 DB 는 한 곳에서만 가져다가 써야된다고 하면 이는 성능상의 큰 병목이 될 것이다. 그때문에 DB 역시 여러곳에 분산을 해야되는 상황이 되었고, 이 때문에 partition tolerance 를 중요한 가치로 인식하게 되었다.

앞에서 Partition tolerance 를 취하면 consistency 나 availability 둘 중 하나 밖에 취할 수 없다고 말을 했다. 그런데 실제 상황에서 consistency 가 그렇게까지 강하게 요구되지 않는 경우들이 많다는 것을 알게 됐다. 구글 검색을 했는데, 방금 crawler 가 긁어온 최신의 데이터를 꼭 보여줘야될 필요는 없지 않은가. 친구가 facebook 에 글을 쓰고 1-2초 지나서 내 화면에 뜬다고 문제가 될 건 없는 경우 등도 마찬가지다.

그래서 consistency 를 좀 약하게 보장하고 대신 시스템의  availability 를 좀 더 보장하는 방법들이 소개 되기 시작했다. 이 때 나온 개념이 eventual consistency 이다. 어느 한쪽이 데이터를 쓰더라도 다른쪽이 가장 최근의 데이터가 아니라 그 전의 데이터를 볼 수 있음을 의미한다. 이렇게 되면 “요청에 응답을 보내야 한다” 라는 availability 기준이 “이전 데이터라도” 보낼 수 있게 되니 크게 향상될 수 있다.

기존에 데이터를 쓰면 다음에 데이터를 읽는 쪽에서는 가장 최근의 데이터를 받아야 된다는 것은 이와 대비해서 strong consistency 라는 표현을 쓴다.  그리고 이런 속성의 DB 는 strong consistency 의 RDB 와는 사뭇 다르다. 그래서 기존의 SQL 과 다른 DB 로 NoSQL 이라는 이름을 얻게 되었다.

이런 속성으로 SQL 의 ACID 에 대응하는 NoSQL 의 주요 속성으로 BASE 를 이야기 한다. Basically Available, Soft-state, Eventual consistency 의 약자를 딴 것이다.

.

게임에서의 SQL 과 NoSQL 의 적용

NoSQL 이 처음 소개되었을 때에는 그것이 새로운 개념이다보니 기존 SQL 을 대체하는 우월한 개념처럼 잘못 인식되곤 했다. 하지만 SQL 과 NoSQL 은 가정하는 상황이 완전히 다른 별개의 솔루션들이다. 풀어야 되는 문제가 다르면 다른 솔루션을 적용해야되는 것처럼, SQL 과 NoSQL 역시 필요에 따라 다르게 사용하는 것이 바람직하다.

앞서 설명한 것처럼 SQL 은 strong consistency 를 보장한다. 그 때문에 consistency 가 중요한 상황에서는 SQL 이 더 적합하다. 그 때문에 많은 경우에 유저 데이터를 SQL 에 저장하는 것이다.

NoSQL 은 partition tolerance 와 availability 가 중요한 상황에서 유용하다. 디비가 먼 지역에 분산되어있고, 반드시 최신 데이터를 보여주지 않아도 되는 경우에 유용할 수 있다. 아니면 적어도 몇초 나 몇분 단위로 싱크만 맞아도 되는 경우라면 적합하다. 이런 이유로 많은 경우에 로그데이터를 NoSQL 에 저장하기도 한다.

이번 컬럼에서는 SQL 과 NoSQL 이 어떤 이유로 나오게 되었고 각각 어떤 것을 중요하게 생각하는지를 설명했다. 어떤 경우도 만능의 솔루션은 없다. 한 선택에 의한 트레이드 오프는 필연적이라고 할 수 있다. 각 솔루션의 배경에 맞게 적절하게 사용하는 것이 무엇보다 중요하다.

아이펀팩토리 문대경 대표