C++11 서버 라이브러리

안녕하세요, 틱톡 개발을 담당하고 있는 조직, Studio M 서버개발팀 개발자 임유빈입니다. 이번 포스팅에는 틱톡을 개발하면서, 소켓 데몬을 손쉽게 만들기 위한 libpw 를 개발하고, 더 나아가 오픈소스로 세상에 공개까지 한 이야기를 풀어보고자 합니다.

C++? 도입부터 쉽지 않았습니다.

많은 서비스를 node.js나 스프링 프레임워크로 개발하면서, 대부분 서비스 로직에 집중하지 I/O에 큰 신경을 쓰지 않습니다. 그만큼 신뢰가 많이 쌓여 있으며, 협업하기 좋은 프레임워크이기 때문입니다.

반면 C++은 유구한 역사를 가지고 있고, 자유로운 언어이지만, OS에 매우 의존적이며, 서버 데몬을 개발하기에 적절한 프레임워크도 적당하지 않습니다. 이러한 C++로 접속지향 서버 데몬을 기능에 맞게 많은 종류로 만들어내기란 쉽지 않습니다. 과거에는 적당히 데몬을 하나 만들고, 필요에 따라 해당 데몬 소스를 복제해서 로직만 수정해서 사용했으나, 공통부분에 심각한 수정사항이 생기면 여러 데몬을 보수하기 쉽지 않았습니다.

그래서 C++ 프레임워크를 만들어 봤습니다. 작동 환경도 모든 OS를 다 아우르며, 모든 패턴을 다 제공하는 것은 지원하지 않습니다. 환경을 제한하여, 로직에 집중하게 하는 것이 목표였습니다. – 사족을 달자면, ACE(http://www.cs.wustl.edu/~schmidt/ACE.html)는 오랜 기간 우리를 지켜온 훌륭한 프레임워크입니다. 다만 일반화 쪽에 신경을 많이 써, 작은 데몬을 여러 종류를 만들기에는 해야할 일이 꽤 있습니다. 일반화와 특수화 사이의 고민이죠.

그리하여 만들어진 C++ 프레임워크를 이용하여 작은 로직을 처리하는 여러 소켓 데몬을 만들고 틱톡을 비롯한 Studio M에서 개발한 실제 서비스에 투입하였습니다.

개발, 협업 그리고 오픈소스로…

처음에는 이것을 이용하여 데몬을 개발하도록 하는 것이 고민이었습니다. 개발한 라이브러리가 아무리 좋아도, 써주는 사람이 없다면, 아무런 의미가 없습니다. 그래서 어떻게 하면 이것을 쓰도록 할 것인가를 고민하였습니다.

내가 다른 라이브러리를 쓸 때 어떻게 접근했었지? 음, 그래… 튜토리얼이나 예제가 있으면, 꽤 쉬웠지. 예제를 만들자!

그렇게 예제를 만들 것을 고민하였으나, 너무 간단한 예제를 만드는 것에 시간과 노력을 쏟을 가치가 있는지, 막연한 생각이 들었습니다. 마침 크리티컬하지 않지만, 소켓 데몬 수요가 있던 터라 하나하나 데몬을 만들었습니다.

처음에는 가벼운 웹 캐시 데몬을 만들어 각종 설정을 캐시하도록 하였습니다. 서비스 데몬이 직접 호출하던 구조를, 캐시를 통해 호출하는 구조로 변경하여 뒷단 서비스의 부하를 줄이고 응답속도를 높였습니다.

그림 1. HTTP 캐시

다음으로 APNS, GCM을 지원하는 푸쉬 서비스 데몬을 개발하여, 신규 서비스에 투입하였습니다. 그 외에도 틱톡 서비스에서 어뷰징 관련 데몬을 개발하였습니다. 또한 데몬 외에도 흔히 사용하지만, 매번 만들어 사용하던 유틸리티도 개발하였습니다.

이렇게 서비스 데몬을 개발하면서, libpw 에 많은 부분을 다듬던 도중, 제법 큰 소켓 데몬 수요가 생겼습니다. 현재 해외에도 서비스 하던 틱톡에, 내부 응답속도 개선을 위해 데이터베이스 캐시가 필요하게 되었습니다. 이 프로젝트에 참여한 양성우 매니저님은 libpw 를 선택하여 개발하였습니다. 프로젝트를 진행하면서, 예기치 못하게 환경을 바꾸거나, 크게 수정하는 일이 종종 있었으나. 서로 조율하여, 서비스를 성공적으로 라이브하였습니다.

서비스를 라이브 할 때 즈음, 김태양 실장님이 libpw와 결과물을 오픈소스화 하자고 제안하셨습니다. 처음에 소스를 오픈하는 것에 대해 대단히 걱정이 많았지만, 큰 프로젝트에 투입하여 사용하는 것을 보고, 더 많은 사람이 사용했으면 하는 바람이 생겼습니다. 다만, 우리 회사에는 소스를 오픈하는 것에 대한 프로세스가 없었습니다. 실장님의 적극 도움으로, IPR팀, 기술기획팀, SQE팀이 한 곳에 모여 프로세스 정립에 대해 논의하고 빠르게 프로세스를 만들기 시작했습니다. github계정 정리, OSS검증, 라이선스결정 및 검증… 또한 실제 서비스에 사용한 만큼, 보안 검증까지 마치고, 기존 소스 트리에서 libpw만 분리하고 정리하여 github에 세상에 나왔습니다.

어떻게 생겼나?

그림 2. 프레임워크 모듈 요약

프레임워크 모듈은 크게 5가지로 볼 수 있습니다.

  • 네트워크 입출력
  • 잡 매니저
  • 유틸리티
  • 로그
  • 인스턴스

네트워크 입출력

그림 3. 네트워크 모듈 요약

네트워크 입출력은 소켓을 요람에서 무덤까지 관리해주는 소소한 관리모듈입니다. 사용하는 소켓 객체는 모두 비동기 방식 사용을 기반하며, 싱글스레드를 목표로 만들어진 모듈입니다.

OS에서 제공하는 소켓 API를 단순히 감싸고, 이벤트 처리만으로 끝난다면, ACE나 libevent 등과 다를 바가 없습니다. libpw는 조금 더 개발 환경을 옭아매어서, 프로토콜 해석부분만 남기고 자주 쓰이는 구조를 모두 자동화 하였습니다. 단적으로, 사용자가 프로토콜 수신부만 개발하면, 동작합니다. – 당연하게도 이 철학에 맞지 않는 개발요건이면, 이 프레임워크를 사용하지 말아야 합니다.

또한 평문과 암호화(SSL/TLS) 통신을 기본으로 지원하며, 평문과 암호화 통신 사이에 동일 서비스 로직을 적용하는 것이 가능합니다. 예를 들면 HTTP 페이지를 만들고, 설정 또는 포트만 변경해서 동일한 페이지를 HTTPS로도 제공하는 것입니다. – 자세한 내용은 examples/http 예제를 확인하세요.

간단하지만, HTTP, APNS, REDIS 프로토콜을 미리 구현하여 제공하고 있으며, 이 프로토콜 모듈은 실제 프로젝트에서 자주 사용하였습니다.

잡 매니저

작업 이벤트에 대한 명세표를 관리하는 모듈입니다. 모든 소켓은 비동기이며, libpw는 요청한 작업에 대해 응답을 기다리지 않습니다. 정확히는 바쁜 대기상태로 기다리지 않습니다. 요청할 때, 적당히 어디에 요청서를 작성해놓고, 응답이 왔을 때, 응답에 기록한 요청서 아이디를 보고, 요청서를 꺼내 이벤트를 처리합니다.

예상 시나리오를 적으면 아래와 같습니다.

사용자는 아이디, 암호를 입력해서, 앞단(frontend-)서버로부터 인증을 받고자 한다.

매우 간단하지만, 이것을 풀어보면 아래와 같습니다.

  • 클라이언트가 사용자 아이디와 암호를 앞단 서버에 전송한다. (A)
  • 앞단 서버는 뒷단(backend-)에 인증 API를 호출한다. (B)
  • 응답이 오면, 클라이언트가 요청한 채널로 응답을 보낸다. (C)

이때 B, C 사이를 바쁜 대기 상태로 기다릴 수도 있지만, libpw에서 job 클래스로 요청 명세서를 적도록 하고, 요청 처리를 종료하여, 다른 요청에 대한 처리를 합니다. 요청한 채널로부터 응답이 오면, 요청 명세서를 찾아 이벤트를 처리하도록 하여, 전체적으로 스레드를 사용하지 않더라도, 다중 요청 응답을 처리할 수 있도록 하였습니다.

그림 4. 잡 매니저 요약

좀 더 코드에 가깝게 그려본 그림입니다.

사용자가 직접 요청서를 간단히 구조체 등으로 만들어도 괜찮지만, 제공하는 모듈로 작성할 경우 타임 아웃이나 오류 처리에 대한 이점이 주어집니다!

유틸리티

잡다한 유틸리티 그룹입니다. 흔히 사용할 수 있는 토크나이저나, 문자열 제어, 인코딩, 압축(zlib), OpenSSL 래핑으로 암호화, 해쉬 등을 지원합니다. 그 외에도 여러 유틸리티가 존재합니다. – 여담이지만 C++11 그리고 이후 나올 C++ 표준이 꽤 강력하여, 중복인 코드는 안타까움을 뒤로 한 채 삭제하였습니다.

로그

매우 간단한 로그 시스템입니다. 스레드 환경에 안전하며, 순환도 시켜주고, 레벨도 지원합니다.

인스턴스

데몬을 시작할 때, 환경 설정 읽어와야 하고, 리스너 포트 열어야 하고, 각종 시그널 초기화 하고, 채널 초기화 하고… 다른 데몬을 만들 때도 같은 걸 반복해야 하고…

그래서 반복되는 부분을 최대한 많이, 그리고 미리 짜둔 것이 인스턴스입니다.

인스턴스는 초기화와 설정 다시 읽기를 위해 매우 많은 가상메소드를 가지고 있습니다. 그러나 대부분 기본 코드가 들어가 있어, 포트 개방과 설정 부분만 신경 쓰면 됩니다. 특히 libpw는 환경 설정(config.ini) 파일에 설정에 따라 싱글프로세스와 멀티프로세스를 코드 수정 없이 변경할 수 있습니다. – 단, 데몬 재시작은 필수입니다.

실제 프로젝트에서도 main 함수에는 단순히 라이브러리 초기화(PWINIT 매크로)와 인스턴스의 start 메소드를 호출하고 반환 값을 OS에 반환하는 것으로 끝납니다.

#include <pw/pwlib.h>
using namespace pw;

int main(int argc, char* argv[])
{  
	PWINIT(); 
	MyInstance inst("myservice"); 
	return inst.start(argc, argv); 
}

글을 마치며…

라이브러리를 공개하고, 실제 서비스에도 적용하며, 팀 내 동료도 멋진 프로젝트에 libpw를 사용한 것을 보면서 꽤 흐뭇해 하고 있었습니다. 그런데 더 나아가 오픈 소스로 전환하여 공개하고, 글까지 쓰자니 부끄럽기도 합니다. 그래도 이 프레임워크로 많은 사람이, 수 많은 프로젝트에, C++로 손쉽게 데몬을 만들고 시스템을 구성할 수 있길 바랍니다.

소스는 https://github.com/skplanet/libpw 에서 받을 수 있습니다.

임유빈 SVP개발팀

SK Planet Consumer Product본부, SVP개발팀 소속
시럽과 함께하는 다양한 서비스를 개발하고 있습니다.

공유하기