Egloos | Log-in


모바일 게임을 웹 브라우저 게임으로 포팅하기 - HTML5 + WebGL 실전 사례

모바일 게임이 게임 시장의 주도권을 갖고 있는 것은 사실이지만, 게임 사용 환경으로서 웹 브라우저 역시 아래와 같은 몇 가지 이유로 충분히 매력적인 환경이다.
  • 아직도 많은 국가에서는 모바일 기기를 이용하는 게이머보다 PC를 통해 게임을 즐기는 인구수가 더 많다.
  • 모바일 대비 넓은 화면과 빠른 성능이 제공되므로 좀 더 좋은 품질의 게임 플레이가 가능하다.
  • 페이스북 웹 게임 형식으로 제공한다면 글로벌하게 매우 많은 유저에게 직접 서비스 가능하며, 소셜 기능을 통한 사용자 확산의 잇점을 누릴 수 있다.
  • 유저들이 모바일 기기와 데스크탑에서 동시에 게임을 플레이 할 수 있으므로 어떤 상황에서도 끊임없이 게임에 연결할 수 있다.
  • 게임의 코드와 리소스를 사용자에게 매우 빠르게 업데이트하고 테스트 할 수 있다.
따라서, 기존에 모바일로 제공되던 게임을 웹 브라우저 환경에서도 서비스 할 수 있다면 게임 타이틀이 훨씬 많은 유저에게 더욱 쉽게 노출될 수 있으므로, 추가 사용자 확보라는 비즈니스 측면은 물론 모든 디바이스에서 연결가능한 잇점으로 인해 게임 플레이 몰입성에도 큰 도움이 된다.

게임이 유니티나 언리얼엔진과 같이 웹 브라우저 환경을 지원하는 엔진을 이용하여 개발되었다면 웹 브라우저용으로 포팅하는 것이 어려운 일은 아니겠으나, 게임이 C++ 코드로 직접 구현되어 있다면 웹 브라우저 게임으로 포팅 또는 개발하는 것은 과거에는 사실상 게임을 새로 개발하는 것과 다름이 없었다. 그러나 이제는 이미 업계 표준이 되어있는 HTML5, JavaScript(Emscripten), WebGL 기술들을 활용하면 그다지 어렵지 않게 웹 브라우저 게임으로 포팅할 수 있다.

본 글에서는 '드래곤프렌즈' (이하 드프) 게임의 웹 포팅 사례를 중심으로, C++로 개발된 모바일 게임을 웹 브라우저 게임으로 포팅하는 방법과 웹 환경에서 발생하는 다양한 문제점들에 대해 다루고자 한다.


C++ 코드를 JavaScript로 변환 (Emscripten)
이미 많은 사람들에게 알려져있다시피, C++ 코드를 JavaScript 코드로 컴파일 할 수 있는 매우 좋은 툴인 Emscipten 이 존재한다.
모질라 재단에 의해서 관리되며, 오픈소스이고, 충분히 안정화 되었고, 성능은 빠르고, 무료이다. 현재도 활발히 개발되고 있기 때문에 빠른 속도로 안정화 되고 있으며 신기능들이 계속 도입되고 있다.
Emscripten을 이용하면 C++을 JS로 변환하여 웹 페이지에서 작동되게 하는 목표 자체는 매우 쉽게 달성할 수 있다. 게다가 VisualStudio상에서 C++ 솔루션을 JS로 컴파일 할 수 있는 플러그인도 제공된다.
그러나 브라우저 상에서 작동하는 JS 코드이기 때문에 상용게임을 컴파일 했을 때 다양한 문제점을 보게 된다.
  1. JS 코드 크기문제
    웬만한 상용 게임 프로젝트라면 디버그 가능한 JS 코드로 컴파일 했을때 JS 파일의 크기가 쉽게 100MB에 다다른다.
    이정도 크기의 JS 코드는 대부분의 웹 브라우저에서 실행이 불가능하다. 따라서, 코드 크기를 최소로 줄일 수 있는 옵션을 모두 사용해야만 구동이 가능하나, 대신에 모든 소스코드 심볼의 이름이 축약화되므로 웹브라우저 내에서 JS 디버깅은 매우 어려워진다.
    최소한 크래쉬 발생시 JS 콜스택을 통해 C++ 코드의 콜스택을 유추할 수 있어야 하는데, 다행히도 Emscripten으로 빌드시 자동 생성되는 .symbols 파일을 이용하면 JS 콜스택에 해당하는 C++ 코드 위치를 알 수 있다. JS 콜스택 텍스트를 C++ 콜스택으로 변환해주는 스크립트를 한 번 작성해두면 이러한 경우에 두고두고 도움이 될 것이다.

  2. JS 코드의 직접 코딩
    JS의 window, document 등 최상위 개체에 접근하거나 Http request를 전송하거나 웹 페이지의 HTML element를 게임코드에서 조작하는 등, C++ 코드에서는 구현에 어렵고 JS 코드로 직접 작성하는 것이 유리할 때가 많다.
    이런 경우에는 Emscripten 라이브러리의 EM_ASM 매크로를 사용하면 C++ 코드 안에서 JS 코드가 함께 작동하게 할 수 있다. C++ 코드의 값을 EM_ASM에 파라메터로 전달하거나 반환 받을 수 있고, Module.Heap 접근을 통해 EM_ASM의 JS 코드 내에서 C++ heap을 직접 조작하는 것도 쉽다.

  3. 비동기 처리 - JS Callback 문제
    C 코드는 대부분의 경우 작업 호출의 결과가 반환될때까지 코드의 실행이 대기하는 동기적 방식으로 구현되나, JS는 작업의 결과를 비동기적인 callback에서 처리하는 것이 일반적이다. 작업의 결과를 처리하는 접근법이 전혀 다르기 때문에 일부 기능들은 일반적인 C 라이브러리의 방식으로 웹 브라우저 상에서 작동하게 하는 것이 쉽지 않다.
    예를 들어, 웹서버에서 파일을 다운로드하여 이를 메모리에 로드하는 과정이 JS 상에서는 callback으로 처리되어야 하기 때문에, Emscripten에서는 앱에 필요한 리소스 파일 전체를 웹브라우저의 메모리에 미리 적재한 후에 C의 동기적 파일 시스템을 에뮬레이션 한다. 이 때문에 리소스 파일 로드시 메모리 문제가 심각한데, 이에 대해서는 이 글의 아래쪽에서 자세히 언급될 것이다.
    또 다른 예로, Http request의 경우에는 Emscripten에서 제공되는 C의 동기적 호출 에뮬레이션이 없기 때문에 JS의 XMLHttpRequest를 직접 사용하고, C 코드에서는 비동기 방식으로 구현하여 JS의 callback에서 완료된 작업을 C 코드에 전달해야 한다.
    최근에는 Emscripten 차원에서 이 문제를 해결하기 위해 Emterpreter 라는 간접 실행환경 기능이 지원된다.

  4. 메모리 정렬 (Alignment)
    Emscripten으로 컴파일 된 코드는 heap 메모리 접근시 데이터크기에 맞추어 정렬되어 있어야 한다. 즉, int/float와 같은 4 bytes 크기의 값에 접근하기 위해서는 해당 데이터의 메모리 주소가 4의 배수에 정렬되어 있어야 한다.
    모바일 기기용 ARM CPU에서도 비슷한 특성이 있으므로, 이미 여기에 맞춰진 코드라면 Emscripten에서의 정렬 문제도 쉽게 해결될 것이다.

  5. 외부 라이브러리 빌드
    Emscripten은 C/C++ 소스코드만을 JS로 컴파일 할 수 있다. 따라서, .lib 등의 바이너리 형태의 라이브러리 파일들은 사용할 수 없고, 모두 소스코드로부터 빌드되어야 한다.
    일반적으로 많이 사용되는 오픈소스 라이브러리들은 이미 Emscripten 라이브러리 내에 컴파일 가능한 버전의 C 소스코드 또는 JS 버전으로 포함되어 있으나, 그렇지 않은 경우 직접 소스코드로부터 빌드하거나 JS로 직접 구현해야 한다. JS로 구현해야만 하는 경우, 이미 다양한 기능들이 JS 공개 라이브러리로 존재하므로 그들을 검색해 보는것이 빠를수도 있다.

  6. 쓰레드
    웹의 JS 환경에서는 쓰레드와 유사하게 작동하는 Web Worker 라는 기능이 제공되나, 메인 쓰레드와 메모리 공유가 되지 않기 때문에(PS3의 SPU와 유사한 구현 문제) 일반적인 쓰레드 코드를 그대로 Web Worker로 변환할 수가 없다. 압축 해제와 같은 매우 독립적인 단위 기능에 대해서는 해당 데이터만 Web Worker에 전달하여 쓰레드화 할 수 있을 것이나, 게임 리소스 로드와 같이 메인 쓰레드의 메모리에 대용량 접근해야 하는 작업이라면 Web Worker의 사용이 어렵다.
    그렇다고 모든 작업을 메인 쓰레드에서 동기식으로 처리한다면 JS의 각 프레임 호출이 너무 길어져서 웹브라우저가 페이지다운으로 간주하여 강제 종료시킬 가능성이 높다.
    드래곤프렌즈는 웹 환경인 경우 각 쓰레드 구현이 시간분할 모드로 작동되도록 하여, 각 논리 쓰레드 개체가 매 프레임당 일정 시간 내의 연산만을 수행하고 다음 프레임에서 작업을 이어하는 방법으로 이 문제를 해결하였다.

파일 시스템
대부분의 게임은 저장소의 파일에서 텍스처 등의 리소스를 로드해야 하는데, 웹 브라우저 환경이기 때문에 서버로부터 리소스를 다운받은 후에 로드해야 한다.
Emscripten은 C 소스코드의 파일 억세스 코드를 그대로 사용할 수 있도록 하기 위해 앱 시작시 C의 표준 라이브러리의 동기적 파일 억세스를 시뮬레이션 하는 기능이 지원된다. 세부적으로는 여러 종류의 서버 또는 웹 브라우저의 소스로부터의 파일 접근을 지원하나, 대체로 아래와 같은 방식으로 작동한다.
  1. 개발자가 앱의 리소스들을 Emscrpten의 file packer를 사용하여 Emscripten으로 빌드된 JS 코드가 사용할 수 있는 리소스 파일로 묶음
  2. 사용자가 웹 페이지를 로드하면, 웹 페이지에서 JS 코드 다운로드 직후 실제 앱 코드 실행 이전에 서버상의 파일 또는 웹 브라우저가 보관한 로컬 리소스 파일을 다운로드
  3. 다운로드된 리소스 파일 내의 단위 파일들이 웹 브라우저의 메모리로 모두 로드됨
  4. C 소스코드의 fopen, fread 등의 파일 접근 API는 모두 JS 코드에 의해 웹 브라우저 메모리상의 파일들에 접근함
Emscripten이 기존 C 코드에서 웹 리소스를 로드할 수 있도록 훌륭히 호환성을 구현하나, 실제 게임에 적용하게 되면 역시나 다양한 문제가 발생한다.
  1. 과도한 메모리 사용량
    모든 파일을 우선 메모리에 로드해야 하는데, 32비트 웹 브라우저의 가용 메모리는 수 백 MB에 불과하다. 앱의 구동을 위해 동적으로 할당될 메모리를 제외하면 파일이 로드될 메모리는 200MB 미만으로 봐야한다.
    때문에, 그래픽 리소스 등을 다량으로 포함하는 게임이라면 PC에서조차도 메모리 때문에 리소스를 로드할 수 없는 상황이 발생한다.
    이 문제를 해결하기 위해서는 리소스 데이터를 압축된 형태로 메모리에 로드하거나, 게임의 각 스테이지에 필요한 데이터만 웹서버에 요청하여 다운로드 받도록 구현해야 한다.
    드래곤프렌즈의 경우에는 모든 리소스를 zip 압축하여 패킹하고, 웹 브라우저도 zip 압축된 상태로 메모리에 로드한다. 게임 코드가 데이터를 필요로 하는 최종 순간에만 일시적으로 압축 해제한다.

  2. 리소스 다운로드 용량
    게임의 리소스 파일을 최초 한 번 다운로드 받는 것은 피할 수 없겠으나, 매번 게임 페이지에 접속시마다 대용량의 다운로드가 일어난다면 게임의 로드 속도와 서비스 비용에 큰 문제가 발생한다.
    즉, 한 번 다운로드된 리소스는 재접속시 다시 다운로드 받지 않도록 해줘야 한다.
    다행히도 모든 웹 브라우저는 브라우저 수준의 캐쉬를 구현하기 때문에 많은 경우에는 웹 어플리케이션 개발자가 아무런 작업을 하지 않고 웹브라우저 캐쉬에 의존해도 된다. 그러나 웹 브라우저의 캐쉬 기능이 언제 어떻게 작동하느냐는 각 웹 브라우저의 정책에 달려있기 때문에 의도와 다르게 캐쉬가 작동하지 않는 경우도 많다. 대표적인 예로, 서버에서 다운받아야 하는 파일의 크기가 20MB를 넘어가면 대부분의 웹브라우저에서는 서버의 파일을 로컬에 캐쉬하지 않는다. 또는 PC의 하드디스크 여유공간 상태에 따라 자동으로 캐쉬 기능이 꺼지기도 한다.
    따라서 웹브라우저의 기본 캐쉬 기능에 의존하기 보다는, 리소스 파일들을 20MB 이하로 유지하고 HTML5 Application Cache 등의 로컬 캐쉬 기능의 사용을 추천한다.
    또한, 대부분의 웹서버는 네트웍 전송시 zip 압축을 지원하므로 파일 다운로드에 대한 HTTP request와 response에서 gzip이 작동하도록 설정해야 네트웍 전송량을 최소화 할 수 있다.

  3. 로컬에 파일 저장
    유저의 설정 파일과 같이 PC의 하드디스크에 파일을 저장해야 하는 경우가 있다.
    다행히 대부분의 웹 브라우저들은 로컬에 파일을 저장하기 위한 목적으로 Web Browser Local StorageIndexed DB 기능을 지원한다.
    게다가 Emscripten은 C 코드의 표준 파일 API를 이용하여 웹 브라우저의 Indexed DB에 파일을 읽고 쓸 수 있도록 지원한다. (File system API 문서 참고)
    그러나 브라우저 환경에 따라 로컬 저장수에 수 십 메가 이상의 용량은 저장 안 될 수 있다는 점에 유의해야 한다.

OpenGL → WebGL

대부분의 PC용 웹브라우저들은 WebGL 1.0을 지원하며, 다행히도 WebGL 1.0은 현재 모바일 기기에서 가장 널리 지원되는 OpenGL ES 2.0과 거의 유사하다. 그러므로 OpenGL ES 2.0에서 작동하는 코드라면, 단 한가지의 용법을 제외하고는 Emscripten을 이용한 컴파일만으로 별 문제없이 작동한다.
WebGL에서는 Client side data (메인 메모리상에 있는 렌더링 데이터로부터 직접 렌더링 하는 경우) 기능을 지원하지 않는데, 이것을 사용하는 부분에 대해서만 Server side data 를 생성하도록 약간의 수정을 해 주면 되므로 큰 제약사항은 아니다.
WebGL을 사용해도 PC의 GPU에 의한 하드웨어 렌더링 가속이 작동하므로 모바일 환경보다 강력한 렌더링 성능의 잇점을 누릴 수 있다.


Audio 구현

현재의 웹브라우저 환경에서는 HTML5의 audio 태그 또는 Web Audio API 를 이용하여 사운드 구현을 할 수 있으나, 인터넷 익스플로러는 11 버전에서 조차 Web Audio API를 지원하지 않기 때문에 사실상 audio 태그만을 유일한 동적 사운드 구현 방안으로 볼 수 있다.
Emscripten에 포함된 SDL 라이브러리 호환 포팅도 내부적으로 audio 태그를 사용하도록 구현되어 있다.
audio 태그는 웹 페이지에서 배경음이나 간단한 사운드 효과음 플레이만을 지원하기 위한 기능이기 때문에 그 이상의 효과는 웹 환경에서 사실상 구현이 어렵다고 봐야 한다.
게다가 웹브라우저마다 동적으로 생성되는 audio 태그에 대한 작동 방식이 약간씩 다르기 때문에 Emscripten 1.1x 버전도 사운드 기능의 브라우저 호환성이 매우 안좋다. 호환성 문제때문에 드프의 경우 Emscripten 내부의 SDL 구현에 다수의 수정을 가해야 했다.


브라우저 호환성의 고려

전세계에서 사용되는 PC용 주요 웹브라우저들을 점유율 순서대로 나열하면 크롬, 인터넷익스플로러, 파이어폭스, 사파리로 볼 수 있다. 각 조사 기관마다 각 브라우저별 점유율과 순위가 다소 차이가 있지만 추세는 크게 다르지 않다.
이들 중에서 내부 엔진이 전혀 다른 인터넷익스플로러만 나머지 브라우저들과 큰 호환성 차이가 있고, 그 외 4개의 브라우저들은 매우 유사한 특징을 가진다.
그러므로 인터넷 익스플로러의 경우가 가장 호환성 문제가 심각하다고 볼 수 있으며, 그나마 인터넷 익스플로러 11 이상 버전만이 위에서 언급한 대부분의 기능들을 지원하고, 그 이전 버전에서는 WebGL을 비롯한 다수의 기능이 사용 불가능하다. 때문에 드래곤프렌즈도 IE는 11이상만을 지원한다.
본인이 웹 환경으로 게임을 포팅중에 발견한 브라우저별 주요 문제점들을 정리하면 아래와 같다.

  • 크롬
    - 수 십 메가 이상의 JS 파일이 로드되면 JS 디버거에서 소스코드를 제대로 볼 수 없다.
    - 원인을 알 수 없으나, 정상 작동하던 일부 PC에서 WebGL 기능이 갑자기 영구적으로 사용 불가능해 지는 경우가 있다. chrome://gpu 페이지에 들어가면 렌더링 가속이 모두 사용 불가능한 것으로 표시된다. 
    - 페이지 재로드 시에 JS 코드의 실행속도가 매우 느려지는 경우가 간혹 발생한다. 크롬 프로파일러로 확인하면 브라우저의 내부 기능에서 매우 느려짐을 볼 수 있다. 다행히 페이지를 다시 한 번 재로드하면 해결된다.

  • 인터넷 익스플로러 11
    - 브라우저의 버그인 것으로 보이는데, 개발자용 창들이 켜지면 메모리를 무제한 할당하는 현상이 있어, JS나 HTML 페이지의 디버깅은 사실상 불가능하다.
    - 페이지 재로드 시에 기존에 페이지에서 할당된 메모리가 먼저 해제되지 않기 때문에 메모리 부족 현상이 발생하기 쉽다.
    - 불과 몇 달 전까지만 해도 facebook.com에 접속시 구버전 익스플로러버전 호환 모드로 자동 전환되어 페이스북 사이트 내에서 WebGL 기능을 사용할 수 없기 때문에 사용자가 호환성 설정을 바꿔줘야만 페이스북 사이트와의 문제를 해결할 수 있었다. (본인이 페이스북의 기술 담당자에게 이 문제를 계속 제기한 때문인지는 모르겠으나) 최근에는 IE11 기본 설정에서도 facebook.com에 접속시 기존버전 모드로 전환되지 않는다.
    - audio 태그를 동적으로 삭제하면 내부적으로는 즉시 사라지지 않아, 다수의 사운드를 연속으로 재생시 동시 플레이 채널 수 부족 오류가 발생한다.

  • 파이어폭스
    - 대체로 크롬과 매우 유사한 특징을 지니나, 크롬에 비해 실행 속도가 느리다.

  • 사파리
    - 7 버전에서는 WebGL 기능이 기본 설정에서는 막혀 있고, 사용자가 직접 이 기능을 켜줘야 한다.
    - JS 코드가 매 프레임 실행될 떄 사용자 입력이 전혀 안 먹히는 경우가 간혹 발생한다.
    - facebook.com과 같이 https로 접속되는 사이트 내에 임베딩 되는 페이지의 경우(일반적인 페이스북 게임의 형태), local storage를 사용할 수 없다.

마치며

이상으로 모바일 게임을 PC 웹 브라우저 게임으로 포팅하는데 발생하는 여러 이슈들을 간략이 정리하였다.
웹에서의 HTML5와 WebGL 기술은 아직 완벽하지는 않지만, 다양한 PC 환경의 유저들에게 상용 게임을 서비스 할 수 있는 충분한 여건은 이미 조성되어 있다.
이 기술들은 게임 뿐만이 아니라 매우 광범위한 애플리케이션들에서 활용 가능하다. MS 오피스나 3DSMAX와 같이 거대한 클라이언트 앱을 웹으로 옮기는 것도 충분히 실현 가능한 일이다.
불과 2년 전만 해도 미래를 기대하는 비전 제시에 불과해 보였던 관련 기술들이 최근에 여러 참여 업체들에 의해 빠르게 표준으로 자리잡았음을 느낀다.

게임 서비스에 필요한 모든 것을 웹 환경에서 구현해 내는 것에는 많은 노력이 필요한 것이 사실이나, 웹브라우저에서 거의 모든 것을 할 수 있는 환경을 구현한다면 고생한 것 이상의 가치가 있다고 생각한다.



by 김성균 | 2014/11/25 17:20 | 일(Work) | 트랙백 | 덧글(0)

[KGC2014] 두 마리 토끼를 잡기 위한 C++-C# 혼합 멀티플랫폼 게임 아키텍처 설계


2014년 11월 7일, KGC 2014에서 주제 발표했던 C++-C# 혼합 아키텍처에 대한 자료입니다.

by 김성균 | 2014/11/10 19:57 | 일(Work) | 트랙백 | 덧글(0)

.NET 어셈블리 동적 로드 기법

.NET 또는 Mono 환경으로 툴을 개발할 때 유연한 기능 확장성을 확보하기 위해서는 외부의 .NET 코드를 플러그인 방식으로 로드하여 실행하는 기능이 필요하다.
예를 들어, C#으로 작성된 게임 코드와 연동되는 툴이라면 게임 코드에 해당하는 DLL을 툴에서 로드하여 그 안에 구현된 기능을 활용할 수 있을 것인데, 게임코드 DLL이 새로 컴파일되어 변경 되었을 때 이를 다시 로드하기 위해 툴을 매번 재시작해야 한다면 개발 프로세스에 지속적인 비효율이 발생한다.
Unity3D와 같은 툴에서 볼 수 있듯이, 로드된 어셈블리(DLL)가 변경되었을 때 툴에서 자동으로 로드되어 그 내용이 즉시 반영되는 기능이 구현된다면 생산성 증가에 매우 큰 도움이 된다.
다행히도 .NET(Mono) 환경에는 이러한 목적을 위해 어셈블리의 동적로드를 지원하는 몇 가지 방법이 존재한다.
프레임웍 수준에서 기본으로 지원하는 기능이기 때문에 웹상에 많은 관련 자료가 존재하기는 하지만, 실제로 적용할 때에 발생하는 여러 문제점들에 대해 정리된 자료는 부족한 것 같아 이 글을 작성하게 되었다.

어셈블리 동적로드 기능의 구현을 위해 필자가 테스트 해 본 바로는 크게 두 가지의 접근 방법을 발견할 수 있었으며, 그 구체적인 방법과 특징에 대해서 아래에서 구체적으로 설명하고자 한다.


단일 AppDomain 내에서의 재로드 방법
Assembly클래스의 Load 메소드를 사용하면 현재의 AppDomain에 동적으로 어셈블리를 로드할 수 있다. 그러나 한 번 로드된 어셈블리는 메모리에서 언로드 할 수 없다는 큰 단점이 있다.
한 가지 다행인점은, 동일한 이름의 어셈블리라고 해도 이미 로드된 것과 변경된 어셈블리 파일의 버전이 다르면 앱에 중복 로드할 수 있다.
로드될 어셈블리 파일의 AssemblyInfo.cs에서 Assembly Version을 기존에 로드된 것과 다르게 수동으로 바꿔서 빌드하거나, 1.0.*와 같이 * 기호를 사용함에 의해 컴파일 될 때 마다 어셈블리의 버전번호가 바뀌에 할 수 있다. 이렇게 빌드된 어셈블리의 파일명을 Assembly.Load에 전달하면 .NET 프레임웍은 새 어셈블리를 추가로 로드할 것이다. 새로 빌드 되었더라도 버전 번호가 같다면 Assembly.Load 호출은 기존에 로드된 Assembly 객체를 반환한다.
단, 기존에 로드된 어셈블리 파일은 어플리케이션에서 사용중인 상태이므로 동일한 파일에 덮어 씌우기가 불가능하다. 따라서 어플리케이션이 실행중이라면 새로 빌드될 때 마다 다른 파일명으로 어셈블리가 복사되어 Load 메소드에서 새 파일명을 이용하여 로드해야 한다. Assembly.Load에서 원본 어셈블리 파일을 직접로드하지 않고, 매번 임의의 복사본 파일을 생성한 후에 Load를 호출하는 방법이 유리할 것이다. 편리하게도 Assembly.LoadFile 메소드는 프레임웍의 어셈블리 로드 경로와 무관하게 절대경로로 파일명을 지정하여 로드할 수 있다.


독립 AppDomain의 사용 방법
동적으로 생성된 AppDomain은 어셈블리를 동적으로 로드할 수 있고, AppDomain의 파괴에 의해 AppDomain내의 모든 어셈블리를 메모리에서 해제할 수 있다.
그러나 아래와 같은 많은 제약사항들이 있다.
  • Assembly.Load 또는 AppDomain.LoadAssembly로 새 AppDomain에 어셈블리를 직접 로드할 수는 없고, AppDomain.CreateInstance 시리즈를 통해 어셈블리 로드와 동시에 어셈블리 내 클래스의 인스턴스를 생성해야 한다.
  • 서로 다른 AppDomain에 있는 객체에 대해 메소드 호출과 반환이 이루질 때에는 원격의 객체와 통신하는 것과 같은 remoting 호출이 일어난다. 심지어 원격객체 timeout 특성도 동일하다. 호출 대상 객체는 MarshalByRef에서 상속받아야 하고, 인자와 반환 값은 모두 serialization을 지원해야 한다. 따라서 다른 AppDomain 간에 호출이 일어나는 시점과 전달되는 타입에 대해서 세심한 주의가 필요하다.
  • 다른 AppDomain으로 메소드 호출시에 Type이나 Assembly 등의 객체가 전달되거나 반환된다면 상대방 AppDomain 측에서 해당 어셈블리의 로드가 자동으로 일어난다. 따라서 어셈블리의 동적 로드와 언로드의 목적 달성을 위해서는 이런 종류의 호출은 피해야 한다.
  • AppDomain이 Unload 되더라도 로드된 어셈블리 파일에 대한 점유가 즉시 해제되지는 않는다. (파일이 해제되는 정확한 시점은 필자도 못 찾았다) 대신, AppDomain 생성시에 AppDomainSetup.ShadowCopyFiles를 "true"로 설정해주면 로드하는 원본 어셈블리 파일이 직접 점유되지 않으므로, 어플리케이션의 실행 도중에 로드된 어셈블리 파일의 갱신이 가능하다.
구체적인 어셈블리 로드는 다음과 같은 절차에 의해 수행된다.
  1. 새 AppDomain 인스턴스 생성
  2. AppDomain.CreateInstanceAndUnwrap 호출을 통해 지정된 어셈블리 내의 클래스 인스턴스 생성. 따라서 로드할 어셈블리 내의 MarshalByRef 에서 상속된 클래스명을 알고 있어야 한다. 또는, 어셈블리를 로드할 대리자 객체를 새 AppDomain내에 CreateInstanceAndUnwrap으로 생성한 후에, 이 대리자 객체에서 Assembly.Load / LoadFile을 수행해도 된다. 이 방법은 아래의 샘플코드에서 사용된다.
  3. 반환된 원격 객체를 통해 필요한 작업 수행
  4. 로드된 어셈블리가 더 이상 불필요하거나 새로운 버전의 어셈블리를 로드해야 한다면 AppDomain.Unload 호출을 통해 AppDomain 해제와 함께 AppDomain에 로드된 모든 어셈블리 해제.

독립 AppDomain을 통한 어셈블리 동적 로드/언로드 샘플코드

// Assembly 파일을 원격 AppDomain에서 로드할 대리자 객체
public class AssemblyLoader : MarshalByRefObject
{
public void LoadAssembly( string filename )
{
// 이 객체가 속한 AppDomain내에 어셈블리 파일 로드
Assembly.LoadFile( filename );
}

public override object InitializeLifetimeService()
{
// 이 객체가 원격 도메인 유휴 시간에 의해 자동으로 삭제되지 않게 함
return null;
}
}

...

// 원격 AppDomain 생성
AppDomainSetup setup = new AppDomainSetup();
setup.ShadowCopyFiles = "true";
AppDomain newAppDomain = AppDomain.CreateDomain( "newDomain", null, setup );

AssemblyLoader proxy = newAppDomain.CreateInstanceAndUnwrap( typeof(AssemblyLoader).Assembly.FullName, "AssemblyLoader" ) as AssemblyLoader;

// 원격 AppDomain에 Assembly 로드
proxy.LoadAssembly( assembly_filename );

// 원격 AppDomain의 코드 실행:
// CreateInstanceAndUnwrap을 사용하여 인스턴스를 만들고 반환된 객체를 통해 호출 가능. 단, 모든 인자들과 반환 타입은serializable해야 한다.
...

// 원격 AppDomain 파괴 및 Assembly들 언로드
AppDomain.Unload( newAppDomain );


* 동적로드 대상 어셈블리가 외부에서 변경되었을 때 자동으로 재로드 하는 기능을 구현하기 위해서는 FileSystemWatcher를 사용하면 된다.

by 김성균 | 2014/07/09 18:58 | 일(Work) | 트랙백 | 덧글(0)

한국에서 다국적 게임 개발팀 꾸리기

IT 업계 전반은 물론 게임업계에서도 글로벌라이제이션(globalization)에 대한 관심이 어느 때보다 뜨겁다. 기술과 교육수준의 발전으로 인해 국가간 장벽이 점점 낮아지고 있는 만큼, 게임 개발에 종사하고 있는 우리들 역시 이에 대한 고민과 준비가 필요하다.
필자는 지난 몇 년 간 다국적 출신의 프로그래머들로 구성된 국내의 게임 개발팀을 관리할 경험을 쌓을 수 있었기에, 이 글에서는 한국내 다국적 개발팀의 관리자로서 그리고 동료 개발자로서 쌓은 경험과 느낀 점 들을 정리해 보고자 한다.
인사(Human Resource Management) 관점의 다국적 팀에 대한 자료는 차고도 넘치지만 국내의 게임 개발팀에 특화된 내용은 흔치 않으므로 내 경험을 공유하는 것이 누군가에겐 도움이 될 수도 있겠다는 생각이 들어 이 글을 쓰게 되었다. 그간 필자가 느꼈던 교훈에 대한 서술과 함께 다국적 팀 내에서 발생할 수 있는 현실적인 일상의 문제들에 대해서도 몇 가지 언급하고자 한다.
필자의 경험이 매우 성공적이었다고 자찬할 수는 없으나, 흔치 않은 환경을 겪으며 프로세스와 문화를 다듬어 본 경험은 국내의 다른 이들에게도 참고할 만 한 자료가 될 것으로 생각한다. 이 내용이 한국 내에서 장기적으로 승리하는 진정한 글로벌 개발팀을 만드는데에 조금이라도 도움이 되었으면 좋겠다.


1. 왜 다국적 개발팀인가?

국내의 전반적인 개발 수준이 많이 향상되었다고는 하나, 국내에서 진행되는 개발 프로젝트의 숫자에 비해 그에 필요한 뛰어난 개발자들의 숫자는 턱없이 부족하다. 따라서 뛰어난 개발자의 저변이 넓은 해외에서 인력을 보충하는 것이 국내 프로젝트의 인력난을 해소할 수 있는 하나의 방편이 될 수 있다. 특히 게임엔진 프로그래머나 technical artists 등 높은 전문성이 요구되는 분야에서는 국내에서 인력을 찾는 것 보다 해외에서 채용하는 것이 쉬울 수 있다.
또한, 선진 개발팀 출신의 인력 채용을 통해 그들의 접근 방식을 쉽게 체득할 수 있고, 해외 시장에 대한 직접적인 정보를 쉽게 얻을 수 있다는 점에서 회사 전체에 다양한 이득이 될 수 있다.


2. 왜 쉽지 않은가?

우선, 누구나 예상할 수 있듯이 언어의 장벽으로 인한 비효율성은 분명히 존재한다. 회의 및 국내 생활지원을 위해 다른 내국인 직원과 회사 모두에게 유무형적인 추가 비용의 발생할 수 밖에 없다. 회의의 예를 들면, 영어학원에서 비즈니스 영어 수업을 듣는 정도의 영어실력을 가진 내국인과 영어가 모국어인 외국인 간에 1:1로 업무 대화를 할 때의 효율성은 내국인간 대화 효율성에 비해 절반 이하라고 생각한다. (회의 효율성 문제에 대해서는 아래에서 더욱 자세히 언급할 것이다)
언어 문제는 업무 수행을 위한 직접적인 비용 외에도 여러 종류의 간접 비용을 발생시키는데, 외국인 직원이 한국내 생활을 하면서 다양한 이슈들이 발생하기 때문에 다른 내국인 직원들이 도와줄 수 밖에 없다는 점이 가장 큰 요인이다. (가령, 함께 식사하는 자리에서 한글을 전혀 모르는 외국인 직원의 음식 주문을 매번 도와줘야 하는 상황을 상상해 보라) 회사 및 개인의 생활 지원때문에 추가로 발생하는 비용은 의외로 크며, 내 경험에는 외국인 직원의 입사 후 첫 3개월 동안은 내국인 직원 한 두 명이 본인 업무시간의 30% 이상 소모해야 한다.
내국인 직원에 비해 인건비 및 기타 지원 비용이 많이 필요되는 것이 사실이나, 정부의 해외 전문인력 채용 지원사업(인건비 지원 부분은 2014년에는 존재하지 않는 것으로 보임) 등을 활용하면 비용과 절차면에서 다소 도움을 받을수도 있다.
그러나 가장 큰 어려움은 문화와 상식의 차이에서 온다. 언어의 문제는 기계적으로 시간과 노력을 투자하면 극복이 충분이 가능한 문제이지만, 문화의 차이는 뭐가 잘못되었는지 이해하지 못하면 관계자들에게 상처만을 남기게 마련이다. 본인 역시 이전 직장에서 팀에 외국인 직원을 처음으로 채용했을 때, 한국 기업문화에서 상식적으로 요구하는 사항과 외국인 직원의 상식 차이를 잘 몰랐기 때문에 실패를 했던 기억이 있다. 
또한, 국적을 불문하고 모든 사람에게는 업무와 생활의 기복이 있는데, 특히 낯선 타국에 와서 생활하는 외국인에게는 그 기복이 매우 클 수 있다. 뭔가 문제가 있다는 신호를 관리자가 미리 파악하여 대처하지 않으면 미처 눈치채기도 전에 나쁜 결과(퇴사 등)를 보게되는 상황이 올 수도 있다. 
이러한 관리의 눈치게임은 모든 관계자에게 수치로 표현할 수 없는 큰 심리적인 비용을 요구한다.
그럼에도 불구하고 외국인 팀원들을 받아들이는 것이 내국인 채용에 비해 유리한 상황 역시 분명이 존재하므로, 다국적 개발 팀을 꾸리는 것이 무조건 손해라고 볼 수는 없다.

아래에서는 외국인 개발자 채용 과정의 시간 순서에 따라 관련 이슈들을 정리하고자 한다.


3. 채용

모든 회사들이 어떤 방식으로든 채용 프로세스를 갖고 있을 것이나, 외국에서 개발자를 채용하고자 하는 경우 지리적 거리와 문화의 차이 때문에 내국인의 경우와 다소 다른 절차를 밟아야 할 것이다. 정기 채용공고를 내면 많은 개발자들이 이력서를 보내오고, 모든 지원자들이 동일한 일정에 1,2,3차 면접을 보는 (회사입장의) 이상적인 절차는 불가능할 것이다.

대상자 탐색

해외에서 개발자를 채용해야겠다고 결정했다면 어디에서부터 개발자들을 찾아서 접촉을 시작해야 할까? 크게 아래와 같은 탐색 방법들을 생각할 수 있다.
  • 광고
해외의 개발자들이 자주 접속하는 커뮤니티나 잡지 등의 매체에 광고를 게재하여, 관심있는 개발자가 접촉해 오게 할 수 있다.
대표적인 예로는, Gamasutra, Stack Overflow, Game Developer Magazine (폐간됨), Game Developer Conference 등을 들 수 있다. 또한, 지역별로(아시아,유럽,북미) 효과가 높은 매체가 다르므로, 여유가 있다면 지역별로 다른 매체를 활용하는 것이 좋을 것이다.
광고는 광범위한 개발자 풀에 노출하는 방식이므로, 의외로 다양한 사람들로부터 연락을 받을 수 있을 것이다. 단, 해외 개발자의 입장에서 채용을 원하는 회사가 어떤 점에서 매력적인지 충분히 어필할 수 있어야 하므로, 다음과 같은 사항들에 유의해야 한다.
* 어떤 일(roles and responsibilities)을 하게 될 것인지 매우 구체적으로 기술 - 이것에 충분한 노력을 기울이지 않는다면 채용 후에 많은 문제가 생길 수 있음에 유념해야 한다.
* 회사의 장점 어필 - 국내의 개발사들은 극소수의 몇 개 회사를 제외하고는 그들에게 낯설다. 회사에서 그동안 출시했던 타이틀들에 대한 상세한 정보와 그 성과를 설명해 줌이 좋다.
* 한국에서의 생활에 대해 어필 - 잘나가는 게임 산업, 친절하고 능력있는 동료들, 역동적인 도시 ...
이러한 사항들은 광고에 게제할 Job Description에 뿐만이 아니라, 회사의 홈페이지에도 매력있게 게시함이 좋다. 
  • 헤드 헌터
국내에서도 이미 헤드헌터를 이용한 채용이 많이 활성화 되었지만, 해외 개발자 채용의 경우에도 효과적인 경우가 많다.
헤드헌터를 통한 채용시의 비용 구조는 국내와 비슷한 편이며, 어떤 헤드헌터사를 선택하느냐가 가장 중요하다고 할 수 있다.
각 헤드헌터사별로 주로 영업활동을 하는 지역이 다르므로, 각 대륙별로 다른 업체에 접촉하는 것이 효과적일 것이다. 헤드헌터 업체는 통해 개발자 개인의 이력서를 받는 역할 외에도 각 지역의 게임산업 현황이나 개발자들의 움직임에 대해 정보를 얻는 채널로도 유용하다.
  • SNS를 이용한 직접 탐색
국내에도 많이 알려진 LinkedIn 서비스를 통해 키워드 검색만으로도 많은 뛰어난 개발자들에게 직접 접촉할 수 있다.
사실 위에서 언급한 헤드헌터들도 이 방법을 많이 사용하기 때문에, 어찌보면 외주용역으로 접촉하느냐 직접 접촉하느냐의 차이라 할 수 있다. 단, 직접 접촉하게 되는 경우에는 회사의 장점에 대해 설명함에 있어 헤드헌터를 통하는 것보다 좀 더 효과적일 수 있다는 장점이 있겠다.
  • 지인의 소개
지인이나 직원중에 해외의 개발자들과 친분을 갖고 있는 사람들이 있다면 그들을 통해 수소문 하는 방법이 있다.
국내에서의 경우와 마찬가지로 개발자와 회사 상호간에 좀 더 깊은 정보를 미리 알 수 있다는 점에서 매우 훌륭한 방법이나, 외국에 인맥을 갖고 있는 지인 자체가 귀하므로 현실성은 그다지 높지 않다.


회사에 입사지원 의사가 있는 대상자가 판별되어 이력서를 받은 후에는 실제로 회사가 원하는 인재인지 판별하기 위한 절차들이 필요하다. 
필자가 생각하기에, 한국에서 장기적으로 성과를 낼 수 있는 개발자들은 업무능력 측면 외에 다음과 같은 성향을 갖고 있어야 한다.
  • 문화적 포용성/도전정신 - 자국의 문화 뿐만이 아닌 다양한 문화에 대한 호기심이 많으며, 아시아 문화에 대한 이해가 깊어야 함
  • 유연한 사고 - 자신과 의견이 다른 사람을 만났을 때 스트레스 받지 않고 해결점을 쉽게 찾을 수 있어야 함
  • 사교성 - 타인들과 어울리는 것을 즐길 수 있다면 큰 도움이 됨
개발자를 채용할 때에는 업무적인 능력을 가장 중요시하여 평가하는 것이 당연하나, 한국이라는 낯선 환경에서 오래 근무할 수 있는 개발자를 찾기 위해서는 위와 같은 사항들에 대해서도 충분히 평가해 볼 필요가 있다. 


원격 인터뷰

해외에 거주중인 지원자를 1차로 인터뷰하기 가장 좋은 방법은 전화 인터뷰 또는 이메일 인터뷰이다.
지원자와 면접관 양측에게 가장 적합한 시각을 미리 이메일을 통해 약속한 후에 면접관(회사) 측에서 전화를 거는 것이 일반적이다. 지원자는 전화 통화가 약속된 시각에 집 등 개인적이고 조용한 장소에서 회사측의 전화를 기다리는 것이 일반적이므로, 약속된 정확한 시각에 통화를 시도하는 것이 중요하다.
경력, 기술, 한국에서의 근무 가능 여부 등 가장 중요한 사항 위주로 간단하게 확인할 수 있는 좋은 기회이지만, 길게 진행하기는 어려우므로 질문해야 할 목록을 미리 준비해 두고 실제 통화시에는 미리 계획한 흐름대로 질문하여 확인한 후에 통화 종료 후 종합적인 판단을 내리는 것이 좋다고 생각한다. 
통화에 사용된 언어가 지원자의 모국어가 아닌 경우에는 통화 품질과 주변 상황에 따라 지원자의 언어 유창함이 제대로 반영되지 않을 수 있다는 점을 염두해 두는 것이 좋다. 면접관이 통화에 사용된 언어에 유창하지 않은 경우에는 침착하게 천천히 대화하려 노력하는 것이 중요하다. 혹시라도 통화 당시에 잘못 알아듣는 실수에 대비하기 위해 통화를 녹음하는 방법도 좋을 것이다.


국내 초청 인터뷰

전화 인터뷰 결과 여전히 채용을 검토하는 것이 좋겠다는 판단이 든다면, 다음 단계로는 직접 대면 인터뷰를 진행하고 지원자가 실제로 근무하게 될 환경을 보여주는 단계를 거치는 것이 좋다.
따라서 지원자를 국내로 초청하여 대면 인터뷰를 비롯한 집중적인 채용 검토 절차를 진행해야 한다. 이 시기에는 회사가 생각하는 지원자의 적합성 뿐만이 아니라 지원자가 한국 환경에서 장기간 근무할 수 있을는지 스스로 확인하는 기회가 되어야 하므로, 다양한 활동을 고려함이 좋다.
국내 입국 및 출국에 1박 2일, 2차와 3차 인터뷰를 위한 하루 이상의 시간, 그리고 지원자가 한국에서의 생활을 엿볼 수 있는 활동 등을 생각하면 최소 2박3일, 길게는 5일 정도의 체류 기간을 제공해야 한다. 출입국에 필요한 비행기편 등 교통비와 숙박/숙식을 모두 회사측에서 부담하는 것이 기본이기 때문에 회사측에서 비행기편과 호텔 등을 모두 예약해주는 것이 일반적이다. 비행기편은 이코노미, 숙소는 비즈니스텔 정도의 등급이면 무난하다.
채용하는 회사 입장에서는 국내 초청시 많은 비용이 들기 때문에 최대한 빠르고 저렴하게 처리하고자 하는 욕심이 들게 마련이다. 그러나 이 단계에서 지원자의 능력/태도/문화적 적합성 들을 충분히 검토하지 못하여 잘못된 인력을 채용하는 경우, 결국에는 채용비용보다 훨씬 많은 자원이 낭비되는 결과를 가져올 것이며, 뛰어난 지원자가 미리 실망하여 입사지원을 취소한다면 해외 개발자 채용의 목적을 달성하지 못하게 된다는 점을 명심해야 한다.


인터뷰 진행

인터뷰의 형식 자체는 회사에서 내국인 개발자 채용시 진행하는 방법과 같은 방식으로 처리하면 된다. 인터뷰시 전문성에 대한 필기 시험을 포함하는 것도 해외에서도 보편적으로 사용되는 방법이므로, 해외의 지원자에 대해서도 동일하게 적용할 수 있다. 다만 국가별로 회사별로 그 방법들이 매우 다양하기 때문에 지원자를 국내에 초청하기 이전에 어떠한 방식의 인터뷰들이 이루어질지 미리 알려주는 것이 좋다.
우리나라 사람들도 이제 많이 인지하고 있다시피 일부 문화권에서는 일과 무관한 개인적인 사항을 언급하는 것에 매우 거부감을 느끼고 또 어떤 문화권에서는 한국 이상으로 개인사에 관심이 많기 때문에, 나이/부모형제 등 개인사는 차라리 언급을 안하는 것이 낫다. 단, 비자 발급 및 국내 거주환경의 지원을 위해서는 국내에 데려올 가족의 수 등이 중요할 수 있으므로 그러한 목적에 한해 언급하는 것은 자연스럽다.
인터뷰 절차 중 채용 후 관리자가 될 사람과 직접 대화하는 단계가 가장 중요한데, 이 때 지원자가 실제로 맡게 될 업무와 업무책임의 내역, 업무 지휘의 상하관계에 대해 상세히 설명해야 한다. 처음 채용공고 게시에 이미 안내된 사항이겠지만, 서로 이해하고 있는 바가 동일한지 재차 확인할 필요가 있다.
특히, 문화권별로 직책에 따른 책임과 권한이 다르므로, 직책 이름만으로 설명해서는 안되고 지원자와 관리자 그리고 동료들이 맡은 roles & responsibilities를 확실하게 이해시켜야 한다. 가령, 회사에서 프로그램 팀장의 직급이 과장이라는 이유로 General Manager로만 설명하고 넘어간다면, 외국인은 그 직책에 대해 전혀 다른식으로 이해하기 쉽다. Technical Director와 같이 구체적인 직급명이라고 해도, 스케쥴과 인사평사에 대한 책임을 맡고 있는 것인지 등의 구체적인 업무 서술이 필요하다.
이러한 정확한 설명에 실패하는 경우 채용 후 단시일 내에 채용실수라는 결론을 내릴 가능성이 매우 높다. 개발자라면 지원자가 단기적으로는 어떤 부분을 책임지고 구현해야 하고, 최후의 결정권은 누구에게 있고, 누구와 함께 토론해서 작업을 진행해야 하는 등의 상황을 설명함이 좋다. 공식 인터뷰의 절차는 아니더라도 함께 일하게 될 팀원 일부와 대화하는 자리를 마련함이 큰 도움이 될 수 있다.
내국인 채용의 경우 대면 인터뷰를 2차, 3차에 걸쳐서 다른 날짜에 단계별로 진행하는 것이 일반적이지만, 해외거주 지원자의 경우 국내에 다수 방문하는 것이 어려우므로 한 번 입국시에 이에 해당하는 모든 절차를 진행함이 좋다. 최종 승인자(임원) 면접, 연봉 협상 등을 방문기간 동안 일단 모두 진행한 후에 종합적으로 검토하여 지원자가 돌아간 이후에 최종 통보하는 것이 효율적인데, 만약 국내에서의 첫 면접시에 이미 채용을 안하는 것이 낫겠다는 확신이 선다면 2차, 3차의 인터뷰는 여러 사람의 시간낭비이므로 그 이후의 절차는 취소하는 것이 좋을 것이다. 이 경우, 국내 체류기간과 비행기편을 갑작스럽게 변경하는데에는 어려움이 있어 결과적으로는 해당 지원자에 대한 많은 비용 낭비가 불가피한 만큼, 최초 전화 인터뷰시에 이런 상황을 피할 수 있을 만큼의 충분한 지원자 확인 절차가 중요하다 할 수 있다.
외국인 지원자가 국내에서 장기적으로 성과를 내기 위해서는 업무 적합성 이외에도 다른 다양한 요소들이 영향을 주게 되므로, 한국내에서 인터뷰시에 업무 외적인 성향도 최대한 파악해야만 한다. 예를 들어, 외국인 지원자가 국내에서 성공적으로 적응할 수 있게하는 의외의 요인들로 다음과 같은 예를 들 수 있다.
  • 한국(동양) 문화에 대한 적응 - 업무 외적으로 가장 중요한 요소라고 할 수 있다. 동양 사회의 상하 관계인식, 동료간의 존중 방법, 공공질서 인식 등에 대해 미리 어느정도 이해를 하고 있거나, 쉽게 적응할 수 있는 타입이 아니라면 국내생활 초기에 많은 스트레스를 받을 수 밖에 없다. 미리 한국과 동양문화에 대해 이해를 하고 있는 경우라면 쉽게 풀릴 것이나, 그렇지 않은 경우라면 다른 문화에 대한 빠른 적응력이 가장 중요해진다.
  • 음식 - 그다지 중요하지 않은 요소하고 생각할 수도 있으나, 개인의 취향에 의해 한국 음식에 적응이 어려운 경우라면 음식 문제 때문에 향수병이 생기기 쉽다. 또한 일상에서 내국인들과 함께하는 식사가 어렵다면 국내 직장문화에 적응하기도 어려울 테므로, 한국의 음식에 적응할 수 있거나, 국내 생활환경 주변에 외국인 고국의 음식을 제공하는 식당이 있는 것이 좋다.
  • 이성관계 - 매우 민감하고 논란의 소지가 있는 주제이긴하나, 외국인의 국내 정착에 매우 큰 영향을 미치는 것도 사실이다. 국내에서 이성을 만나는 것에 관심이 많은 외국인이라면 국내에 쉽게 정착할 가능성이 높다. (한국인 이성과 결혼하여 영구 정착하게 되는 경우도 종종 있다)
또한, 이 기간동안 지원자가 한국에서의 거주지, 유흥문화 등에 대해 미리 볼 수 있는 것도 중요하다. 지원자가 http://www.korea4expats.com과 같은 외국인을 위한 한국 생활안내 웹사이틑 통해 정보를 얻을 수 있도록 안내하는 것도 좋을것이다.


4. 초기 정착

위의 모든 과정에 상호 만족스러운 결과를 얻어, 외국인 지원자에 대한 채용이 결정되었다면 국내에 입국하여, 장기체류를 위해 정착하기 위한 절차들이 필요한데, 이 때 회사에서 많은 도움을 주어야 한다. 
  • 입국 허가 - 골드카드, 워킹비자 등의 발급에는 지원자와 회사로부터 많은 증빙 자료를 필요로 하며, 각 서류의 발급 기간이 얼마나 걸릴지 예상하기 어려우므로 채용 확정으로부터 지원자의 실제 입국일까지는 1달 이상의 승인 기간을 미리 고려해 두어야 한다. 그간 겪어본 실례로는 2주 ~ 3개월 정도로 소요 기간의 편차가 매우 커서 정확한 기간을 미리 예측하기가 어렵다.
  • 집 - 채용계약에 수습기간 조건이 있다면 수습기간 종료후 다시 고국으로 돌아가게 될 가능성이 있으므로 일단 단기 거주가 가능한 주택의 형태가 좋을 것인데, 비즈니스텔의 장기숙박이나 단기 임대 오피스텔 등이 유리할 것이다. 정직원 계약시 장기 거주를 위해서는 오피스텔의 월세나 전세 형태가 가장 무난할 것이며, 월룸텔 등도 고려해 볼 수 있다. 각 나라별로 상식적으로 생각하는 월세 지출의 규모는 큰 차이가 있으므로, 외국인 직원이 선호하는 예산을 미리 확인하여 주택을 알아봄이 좋다. 월세나 전세 모두 큰 금액의 보증금이 필요한데 외국인 직원이 큰 돈을 저축하고 있지 않을 가능성이 높으므로, 이를 회사에서 어떤 형태로 보조할 것인지의 결정을 미리 해두어야 한다. 회사에서 보조하는 경우, 전입신고가 가능해야 하고 전세권 설정 등이 필수일 수 있으므로 부동산에 이러한 조건들을 미리 알려주어야 한다. 또한, 외국인 직원이 부담해야 할 관리비가 어느 정도 될 것인지에 대한 정보도 알려주어야 한다. 현실적으로는 외국인의 한국내 생활에 있어 가장 어렵고 많은 도움이 필요한 것이 거주의 문제인 만큼, 부동산 업자의 선택, 주택의 선택, 하자보수 등에 있어 많은 주의를 기울여야 할 것이다. 특히, 좋은 부동산 업자를 만나는 것이 스트레스를 줄이는 가장 빠른 방법이 될 것이다.
  • 핸드폰 - 외국인의 경우 내국인과 같은 핸드폰 요금제를 사용하는 것이 어려울 수 있다. 가장 쉬운 방법은 선불폰을 사용하는 것이며, 스마트폰의 경우 KT에서 외국인에게 내국인과 동일한 요금제로 판매하고 있다. (KT 강남점과 같은 외국인 개통이 가능한 전문 매장을 방문해야 한다)


5. 성과내기

업무 지시와 피드백

흔히들 한국은 high context 문화이고, 서구권은 low context 문화라고 말한다. 내국인끼리 회의를 하거나 업무지시를 할 때에는 암묵적으로 상호 이해하는 뉘앙스가 포함되어 있기 때문에 구체적으로 언급하지 않는 사항들이 종종 있으나, 외국인에게 업무지시를 할 때에는 각 업무의 모든 디테일에 대해서 언급해야만 하지 않으면 업무의 목표에 대해 오해하기 쉽다. 특히, 일의 진행 순서에 따른 담당자와 각 개인의 담당 범위, 기한, 크런치의 필요 여부 등을 가능한 자세히 설명해 주어야 한다. 돌이켜보면, 필자가 처음으로 외국인 팀원과 업무를 수행했을 때에 이 부분에서 큰 실수를 했음을 반성한다.
외국인 직원과 관리자가 서로의 업무 방식에 익숙해지기 전까지는 진행중인 사항에 대해서 최소 하루에 한 번 이상 가볍게 의사소통 할 필요가 있다. 하루 10분 이내의 간단한 대화만으로도 업무 스펙이 불명확한 것인지, 회사의 시스템에 익숙하지 않은 문제인지, 개발 장비들에 문제가 있는 것인지 충분히 파악할 수 있으므로, 업무 초기일수록 잦은 접촉이 굉장히 중요하다. 외국인 팀원 역시 이러한 접촉을 통해 본인이 회사와 국내의 시스템에 맞춰가야 할 부분들을 파악하고 변화해 가게 될 것이다.

팀원들간의 협업

내국인 팀원들과 외국인 팀원들이 효율적으로 협업하여 좋은 성과를 내기 위해 가장 중요한 과제는 업무적 커뮤니케이션과 문화적 차이의 이해라고 생각한다.
모두가 한국어 또는 영어에 유창하다면 업무 내용의 커뮤니케이션에 큰 장애는 없을 것이나, 현실적으로 이걸 기대할 수 없는 환경이라면 언어적 두려움에 의한 커뮤니케이션 장벽을 없애기 위해서 의무적으로라도 서로 소통하는 시간을 만들어야 한다. 특히 프로그래머들의 경우 며칠간 의사소통 없이도 개인별로 작업을 진행할 수 있기 때문에 의사소통에 소극적이 되기 쉽다. 그러나 이런 분위기를 방치해두면 팀 전체의 효율성이 계속 저하될 수 밖에 없으므로 작은 규모의 데일리 미팅이라도 유지해야 한다.
직접적인 커뮤니케이션 문제 외에도 문화적인 차이에 의한 대인관계 방법의 차이는 모든 구성원들에게 경험이 쌓이는 것 외에는 왕도가 없는 것 같다. 예를 들어, 서구권 개발자들의 경우 본인의 직급/책임과 무관하게 본인이 느낀 프로젝트의 문제점들에 대해 국내에서는 프로젝트의 리더가 아니면 함부로 말하기 어려운 민감한 부분들에 대해 직설적이고 공개적으로 표현하는 경우를 쉽게 볼 수 있다. 처음에는 관계자 모두가 당황스러워 할 것이나, 문제점을 인정하고 함께 개선하거나 모두가 공감할 수 있는 방법으로 상황설명이 된다면 오히려 건설적인 팀 문화를 만들어낼 기회가 될 수도 있다. 쿨한 방법으로 대처가 어렵다면 차라리 직설적으로 국내 문화에서 어떤 것들이 금기시 되는지 말해주는것이 당사자가 납득할 수 있는 방법이 될 수도 있다. 거꾸로, 내국인 직원들이 직설적으로 표현하지 않아서 외국인 직원이 알아차리지 못하고 있는 점이 있다면 개인적인 대화 시간에 제 삼자의 관점에서 이를 설명해주는 편이 외국인 직원이 국내 개발팀의 문화에 적응하기 쉬울 것이다.

회의

모든 직원들이 영어로 회의하는 것에 익숙하지 않다면 전체 인원을 소규모로 분리하여 진행하거나, 한국어를 주요 언어로 사용하되 한 두 명이 동시통역으로 도와주는 방법이 가능하다. 필자의 경험에 의하면 참석 인원이 많은 회의에서 외국인 직원에게 동시통역으로 도와주는 경우에는 모두의 의견이 서로 오고가는 상황이 되기는 어렵고, 외국인 직원이 일방적으로 듣기에만 바쁜 상황이 되기 쉽다. 역으로, 회의에서 일단 영어가 사용되기 시작하면 말을 꺼내기 자제하는 내국인 직원들이 있게 마련이다. 따라서 여러 언어가 동시에 사용되어야만 하는 회의라면 5명 이하의 규모로 분리하는 것을 추천한다.
회의가 끝난 후에는 서로 이해한 바의 차이를 없애기 위해 영문으로도 정리된 이메일로 회의록을 회람하여 상호 확인해야 한다. 수신자들 중에 영어에 익숙하지 않은 직원들이 있다면 한글 회의록에 영문 요약으로 첨언하는 형식이 효율적이다. 이렇게 하면 이메일 중간에 영어로 회신하더라도 한글로 된 내용과의 맥락 파악이 용이하다.

회식

업무시간 이후 회식 참석의 의무성에 대해 이해하지 못하는 외국인들이 분명히 많기는 하나, 변방의 한국까지 일을 하러 온 외국인이라면 대체로 문화적 도전정신이 높은 편이므로 한국식 회식을 즐기는 경우도 많다. 그러므로 회식의 취지와 의무성 여부에 대해 분명히 설명해 준 이후에 당사자의 취향을 존중해 주어야 할 것이다.
간혹 일부 한국인들이 회식자리에서 외국인에게 술을 강요하거나 그들에겐 혐오스런 음식을 권하는 경우가 종종 있는데, 바람직한 팀의 분위기를 만들기 위해서는 이러한 모습은 자제하는 것이 좋다. "Do you know PSY?"를 남발하는 것 보다도 홍어를 억지로 권하는 것이 훨씬 무례해 보인다.

(코딩)

프로그래머들에게만 해당되는 사항인데, 코딩 문화에 있어서도 국가별, 회사별로 분명한 차이가 있다.
분명한 코딩 원칙의 설정과 프로그래머들의 엄격한 준수가 국내의 회사들에서는 중요하게 여겨지지 않는 경우도 많지만, 해외 개발자들 중에서는 이러한 원칙에 대해 매우 민감하게 반응할 수도 있고, 간혹 명명법이나 띄어쓰기에 대한 종교적인 논쟁도 필요할 수 있다.
외국인 팀원에 의한 문제제기가 관리자 관점에서 고려해 본 적이 없는 원칙에 대한 논의라면 함께 분명한 원칙을 세우는 기회가 될 수도 있을 것이나, 이미 팀 내에서 결정된 사항에 대한 지속적인 문제제기라면 서로 스트레스가 될 수 있다. 애초에 유연한 사고의 개발자를 채용했다면 쉽게 넘어갈 수 있는 문제이겠으나, 불필요한 논쟁이 지속된다면 결단을 내려야 할 시점이 있다. 논쟁을 피하지는 않되, 소모적이 되지 않도록 분명한 선을 그어야 한다.


6. 정리

이상으로 다국적 개발팀을 구성하기 위한 다양한 측면 이슈들을 간단히 설명했지만, 동료 개발자로서 필자가 그들과 함께 일하면서 느낀 가장 중요한 교훈들은 아래와 같이 요약 할 수 있다.
  • 언어와 문화차이의 두려움을 극복하기 위해 가장 좋은 방법은 지속적인 대화이다.
  • 외국인 직원들과 사적으로도 가까워지려 노력하는 것이 나중에 큰 도움이 된다.
  • 중요한 대화가 있었다면 함께 확인할 수 있는 글로 남겨라.
  • 늘 착한 한국인이 될 필요는 없다. 내국인 직원에게 요구되는 만큼 당당히 요구해야 할 필요가 있을 때에는 직설적으로 표현해야 한다.
  • 역지사지로 생각하여 본인이 타국의 근로자가 되었을 때를 가정해서 감정이입 해 보라.
위와 같은 모든 어려움을 극복하고 성과높은 다국적 팀의 문화를 만들어 낸다면 관련 구성원들 모두가 개발자로서 그리고 국제 비즈니스의 일원으로서 비약적인 성장을 이룰 수 있을 것이다.

by 김성균 | 2014/04/28 19:12 | 일(Work) | 트랙백 | 덧글(2)

모바일 게임 최적화의 정석 - 메모리 편

이전 글

모바일 게임의 성공적인 출시를 위해서는 매우 한정된 자원 내에서 최대한의 앱 성능과 멀티태스킹을 지원하기 위한 기기와 OS의 특성을 이해하고, 그에 맞추어 메모리 관리 정책을 구현하는 것이 반드시 필요하다.
본 글에서는 용량, 속도, 안정성이라는 3개의 카테고리에 관계된 메모리 최적화 전략들을 몇 가지 정리하고자 한다.
  1. 메모리 사용량 최적화
  2. 메모리 사용 속도 최적화
  3. 메모리 접근 안정성 최적화
다행히 iOS나 안드로이드 등의 주요 모바일 OS들이 제공하는 메모리 관리 기법이 데스크탑 OS와 크게 다르지 않기 때문에 대체로 사용되는 최적화 기법들 역시 PC 등의 플랫폼과 유사하나, 메모리의 크기/CPU의 성능/다양한 하드웨어 스펙트럼 등의 이유로 인해 특별히 신경써야 할 사항들이 다수 있다.

    메모리 사용량 최적화
    게임 개발자 관점에서 모든 게이밍 하드웨어의 가용 메모리는 늘 충분하지 않다. 사양이 낮은 모바일 기기들 역시 고유한 문제점들이 있는데, 특히나 매우 다양한 모바일 기기들이 시장에 존재하기 때문에 게임 프로그래머가 예상치 못했던 다양한 암초들이 잠재되어 있다.
    현재 일반 사용자들이 주로 사용하는 모바일 기기들은 256MB ~ 2G 정도의 메인 메모리를 장착하고 있으며, OS가 실제로 허용하는 단일 앱의 가용 메모리는 100MB ~ 300MB 수준이다. (기기별로, OS 버전별로, 사용 상황별로 천차만별) OS가 많은 메모리를 사용하도록 허용한다 해도 앱이 적은 양의 메모리를 사용할수록 사용자가 멀티태스킹을 수행해도 게임과 다른 앱들이 강제 종료되지 않고 살아있을 확률이 높아진다.
    다양한 기기에서 최상의 경험을 제공하기 위한 메모리 사용량 최적화의 원칙은 아래와 같이 몇 가지 방법으로 정리될 수 있다.
    • 꼭 필요한 것들만 메모리에 적재
    필요한 것만 로드하는 정책은 너무나 당연한 이야기이지만, 게임의 요구사항이 자주 바뀌는 환경에서는 전체 게임 데이터에서 작은 일부만을 로드하도록 제어하는것이 쉽지 않은 경우도 많다. 또한, 게임이 데이터를 자주 로드함에 의해 유저의 게임플레이가 방해된다면 가능한 많은 데이터를 메모리에 적재하고자 하는 유혹을 떨치기 어렵다.
    필요한 것만 게임플레이 방해없이 로드하기 위한 기술적인 필수 요소로는 필요한 데이터의 판별과 적절한 로드 방법 등이 있다. 게임의 각 순간에 필요한 최소한의 데이터 집합(working set)을 분류하여, 어느 시점에 어느 데이터를 로드할지 계획해 두어야 한다. 그런데 다행히도 게임에서 메모리 사용량의 대부분을 사용하는 것은 일반적으로 이미지 등의 그래픽 데이터이다. 따라서 이러한 특수한 종류의 데이터에 대한 on-demand 로드 시스템을 구현해 둔다면 게임의 로드 단위를 제어하기 쉬워진다. 로드 단위의 제어 형태는 게임마다 매우 다를 수 있지만, 게임에서 가장 큰 용량을 차지하는 이미지가 필요로 하는 순간에 로드하는 시스템만으로도 대부분의 메모리 사용량 제어 문제는 해결 가능하다. 좀 더 나아간다면, 메쉬/텍스처/애니메이션/사운드 등 "단위 리소스"를 추상화하고, 각 단위 리소스가 필요로 하는 순간에 존재 여부를 체크해서 로드할 수 있는 중앙 집중화된 이른바 리소스 관리자(resource manager)를 구현해 둔다면, 여기에 추후에 다양한 메모리/로드 단계 기능을 붙일 수 있다. 단, 게임의 흐름을 끊지 않는 로딩을 추가로 고려한다면 스테이지 단위로 필요할 것으로 예상되는 리소스들을 로딩화면 단게에서 미리 리소스 관리자에 요청하거나 on-demand로 스트리밍 로딩을 구현할 수 있다. 그런데 다행히도 대부분 모바일 기기에서는 플래쉬 메모리로부터 데이터들을 읽어들이므로 과거의 PC보다는 로딩 속도가 체감상 빠르기 때문에 일부 리소스를 필요로 하는 그 순간에 로드하더라도 큰 문제가 되지 않을 수 있다. (단, 일부 저사양 기기의 경우 플래쉬 메모리의 전송속도가 10MB/s 정도로 HDD보다 느림)
    일단 메모리로 로드된 리소스가 더 이상 필요하지 않을 때 메모리에서 삭제하기 위해서는 필요없는 리소스를 판별하는 절차가 필요하다. 단순하면서도 강력한 방법으로는 리소스 관리자가 각 리소스에 대한 요청/해제의 reference count를 관리하여, 각 리소스의 reference count가 0이 될 때 자동으로 삭제하는 방법을 생각할 수 있으며, 좀 더 리소스 관리자 주도적인 방법으로는 주기적으로 게임 내에서 사용되는 모든 객체를 순회하면서 현재 사용중인 리소스와 로드되어 있는 리소스를 비교하는 방법(garbage collection) 등을 생각할 수 있다.
    reference count나 garbage collection의 세부적인 구현방법에는 다양한 접근 방법이 있는데, 그 분량이 매우 방대하므로 여기에서는 구체적으로 설명하지는 않는다. reference count의 구현에 대해서는 smart pointer, intrusive pointer 등의 자료를 참고하되, 멀티쓰레딩이 사용되는 환경에서는 counter와 smart pointer 객체에 대한 동기화(synchronization) 전략이 매우 중요하다는 점을 유의해야 한다. 
    • 메모리 내 데이터의 압축
    메모리 내에 상주하고 있는 데이터라 할지라도 매우 빠른 속도로 압축을 해제할 수만 있다면 압축기법의 사용으로 한번에 로드되는 데이터의 양을 몇 배는 증가시킬 수 있다.
    가장 쉬운 부분은 GPU에 의해 직접 지원되는 텍스처 압축일 것이다. 텍스처 압축은 1/4 ~ 1/8 정도의 압축률을 보이며, 압축의 사용에 의해 렌더링이 오히려 빨라지는 경우도 많기 때문에 반드시 사용해야 할 기법이라고 할 수 있다. 텍스처 압축의 구체적인 이슈들에 대해서는 이전에 본인이 작성한 글인 모바일 게임 최적화의 정석 - 텍스처 압축 편 을 참고하면 된다.
    그 외에 대부분의 게임에서 많은 메모리를 차지하는 부분은 애니메이션이다. 이미 다양한 애니메이션 압축 기법들이 보편적으로 사용되고 있으며, 그 예를 들자면 데이터 양자화(quantization)/bit수 줄이기, 중요하지 않은 key 삭제하기(trivial key removal)과 같은 직관적인 방법부터, wavelet 압축과 같이 수학적인 모델을 사용하는 방법 등이 있다.
    직관적인 방법들은 적은 CPU 부담을 주면서도 쉽게 구현할 수 있기 때문에 우선적으로 고려되어야 한다.
    데이터의 정밀도를 낮춰서 양자화 시키면 각 애니메이션 정보의 bit수를 줄일 수 있으므로 전체 애니메이션 데이터의 크기가 작아진다. 데이터를 사용할 때에는 사칙연산과 bit 연산만으로 본래의 값을 복원할 수 있기 때문에 속도도 매우 빠르다. 중요하지 않은 key 삭제 기법의 경우에는 애니메이션의 수행에 거의 눈에 띄지 않는 key들을 삭제하는 간단한 전처리 과정을 도입하게 되며, 게임 내에서 애니메이션을 재생하는 방법은 전혀 달라지는 점이 없다는 잇점이 있다. 안 중요한 key를 판별하는 방법에도 다양한 접근법이 있으나, 간단한 예로는 모든 인접한 3개의 key A,B,C를 대상으로 하여 만약 key B가 A~C를 보간한 결과와 거의 유사하다면(threshold 이내) key B를 삭제하는 것을 생각할 수 있다.
    이러한 압축 기법들은 애니메이션 외에도 다양한 종류의 데이터에 적용될 수 있으므로, 게임 내에서 큰 용량을 차지하는 데이터부터 적용을 고려함이 좋을 것이다.
    • OS에 의해 메모리 부족이 보고될 때의 불필요한 메모리 사용의 해제 또는 최악의 경우를 위한 비상대책
    게임이 100MB 이상의 메모리를 사용한다면 간혹 발생하는 기기의 메모리 부족 상황은 피할 수 없다. iOS, 안드로이드 모두 이러한 상황에는 각 앱이 메모리의 일부를 해제하도록 요구하며, 그 이후에도 메모리가 부족하다면 앱들이 강제 종료된다. 따라서, 메모리 사용량이 적을수록, 그리고 기기의 메모리가 부족할 때 최대한 많은 메모리를 해제할수록 게임 앱이 살아남을 가능성이 높아진다. 유저가 다른 앱들을 사용하다가 게임으로 돌아왔을 때 게임이 종료되어 처음부터 다시 시작된다면 게임의 흥미도가 떨어질 수 있으므로, 게임이 제대로 실행되기 위해서 그리고 멀티태스킹 환경에서 훌륭한 게임플레이를 제공하기 위해서는 메모리 부족 상황에 대한 대책이 필요하다.
    쉽게 다시 로드될 수 있거나 다시 계산될 수 있는 데이터를 해제하는 것이 원칙인데, 이를 좀 더 세분화하여 Level-Of-Detail을 고려한 부분적인 unload도 대안이 될 수 있다. 텍스처의 경우라면 메모리가 부족할 때 일단 고해상도 텍스처를 해제하고 추후에 저해상도 버전을 로드하거나, 중요하지 않은 텍스처와 사운드 데이터 등을 해제하고 게임에서 아예 표현하지 않는 방법을 생각할 수 있다.
    특히, 유저가 게임을 플레이하고 있는 상황에서 메모리 부족 경고가 발생한다면 현재 플레이 중인 게임이 강제 종료될 수 있는 급박한 상황이므로, 게임 플레이에 중요치 않은 모든 데이터를 해제할 수 있는 설계가 필요하다. 즉, 게임의 리소스 관리자 수준에서 심각도에 따라 단계적으로 해제할 데이터를 분류해야 하며, 일부 데이터가 메모리에 존재하지 않더라도 게임 플레이가 가능하고 리소스가 필요할 때에 해당 리소스만 다시 로드할 수 있는 시스템의 구현이 필요하다.


    메모리 사용 속도 최적화
    메모리 접근에 대한 속도를 높이기 위한 방법에는 다양한 접근법이 있으나, 여기에서는 주로 사용되는 몇 가지를 소개한다.
    • 캐쉬의 고려 - 데이터에 대한 선형 접근
    CPU에서 메모리의 데이터를 접근할 때, 데이터가 L1 cache에 의해 즉시 사용 가능한 경우와 L2 cache에 의해 접근 가능한 경우, 그리고 cache에서 벗어나 있어 메모리 칩으로부터 가져와야 하는 경우들은 각각 몇 배의 성능 차이를 보인다. L1 cache로 부터 가져오는 가장 효율적인 경우에 비교하여 메모리 칩으로부터 가져오는 경우는 100배 정도의 성능 차이가 날 수도 있다.
    프로그래머의 관점에서 cache 기능이 효율적으로 작동하게 할 수 있는 기본적인 기법은 가능하면 작은 크기의 데이터를 사용하고, 데이터가 선형으로(순차적으로) 접근되게 하는 것이다.
    작은 크기의 데이터를 사용하기 위해서는 위의 메모리 사용량 최적화 부분에서 언급된 내용들과 더불어, 각 데이터의 실제 용도에 맞는 정밀도의 bit 수를 사용하고 data packing등의 기법을 사용할 필요가 있다. 예를 들어, 4개의 byte를 데이터가 필요하다면 32bit 단일 항목 안에 합쳐서 사용하고, bool 타입의 데이터들은 bitfield를 사용하는 등의 기법을 통해 데이터의 크기를 비약적으로 줄일 수 있다.
    데이터가 순차적으로 접근되게 하기 위해서는 자료구조를 설계하는 시점에 이를 고려해야 한다. 예를 들어, tree 구조의 경우 코드에서 각 노드를 억세스하는 순서대로 노드들을 메모리상에 순차적으로 배치하고, 2D 배열이라면 메모리상의 순서와 코드에서의 순환(iteration) 순서가 동일하게 하고, 불가피한 경우가 아니라면 linked list 대신에 배열을 사용하는 것을 생각할 수 있다.
    본인이 작성한 코드의 cache 사용 호율성을 확인하고 싶다면, ARM CPU에서 작동하는 툴인 NVidia의 Tegra System Profiler나 ARM의 Streamline Analyzer 등이 있다.
    • 불필요한 메모리 복사 회피
    저사양 CPU에서 메모리 복사는 매우 비싸다. 그런데 게임의 구조적인 문제로 인해 메모리 복사가 다수 일어나는 경우들도 많지만, 대부분의 경우 메모리 복사를 고려하지 않은 비효율적인 코딩에 의해 발생하곤 한다.
    예를 들어, 다음의 예제 함수들은 그 선언 자체가 많은 양의 메모리 복사가 일어날 것임을 암시한다.
    void SetMatrix( Matrix4x4 matrix ); // 호출시 최소 64 bytes의 메모리 복사
    void SetName( std::string name ); // name의 길이에 따라 수 십 bytes의 메모리 복사
    BigObject GetObject(); // BigObject의 크기만큼 메모리 복사

    최초 코딩 당시에는 별 생각없이 작성되었겠으나 프로파일러 상에서 위와 같은 함수들이 유난히 느린 것을 발견하게 될 것이다.
    이러한 문제들은 object 자체를 전달하는 대신에 포인터 혹은 참조 포인터(reference pointer) 형식으로 전달하는 것만으로도 대부분 해결되니, 이 문제가 발견만 된다면 쉽게 해결할 수 있다.
    눈에는 쉽게 띄지 않으나, 프로그래머들이 자주 실수하는 메모리 복사 병목의 대표적인 예에는 vector(array)의 사용이 있다. 예를 들어 아래의 코드는 매우 느릴 소지가 있다.
    std::vector<BigObject> objects;
    ...
    objects.push_back( aObject );
    만약에 위의 push_back이 자주 호출된다면 vector인 objects 요소들의 크기가 증가되어야 할 때마다 메모리 할당->복사->해제가 이루어지므로 예상치 못한 성능문제가 발생한다. BigObject의 크기가 클수록 문제는 더욱 심각해 질 것이다. vector를 생성할 때에 예상되는 크기를 미리 지정하거나, std::vector<BigObject*> 형식으로 사용하는 것이 쉬운 해결책이 될 수 있다. (그러나 포인터 타입에 대한 vector는 cache 효율성이 낮다)
    • 메모리 할당/해제 횟수 최소화
    C로 작성된 코드의 경우 new/delete 구문 자체는 다른 시스템 호출에 비해 많이 느린편은 아니다. 그러나 여전히 OS 내부적으로 상당히 많은 연산을 필요로 하므로 new/delete의 사용을 최소화 하는 것이 좋다.
    극단적인 경우라면 게임의 로딩시에 필요로 되는 모든 메모리를 한번에 new 한 후에 자체 메모리 관리자를 통해 각 용도별로 쪼개서 쓰는 기법들도 다수 존재한다. 이 접근법의 경우에는 게임의 모드 코드에 대해서 자체 메모리 관리자를 경유하도록 해야하는 등 코드의 복잡도가 크게 증가하지만, 멀티쓰레딩과 잦은 할당이 필요한 경우에에도 고성능 튜닝이 가능하다는 점에서는 많은 잇점이 있다.
    자체 메모리 관리자를 사용하지 않는 경우에도 몇가지 간단한 주의만으로도 메모리 할당/해제의 병목들을 피할 수 있다.
    * 임시로 사용하는 작은 버퍼의 경우에는 new를 사용하기 보다는 지역변수인 array로 선언하여 stack에 할당되게 할 것
    * vector 등 동적으로 크기 변경 가능한 오브젝트 사용시 예상되는 크기 힌트를 지정해 줄 것
    * std::string과 같이 내부적으로 메모리 할당이 필요한 객제 사용시 객체간 복사를 최소화 할 것. 아래와 같은 코드는 피하고, 차라리 sprintf를 사용하는게 낫다.
    std::string newString = std::string("hello ") + "2014";

    추가로, 안드로이드용 Java 코드의 경우 모든 메모리는 garbage collector에 의해 관리된다. 메모리 할당이 많이 일어나서(총 사용량 기준) garbage collector가 자주 작동할수록 전체적인 게임의 성능은 느려지게 된다. Logcat에서 "GC freed ..." 메세지가 자주 보인다면 문제가 있는 것으로 생각하면 된다. 특히, 이미지 관련 객체들은 내부적으로 큰 메모리 할당을 수행하므로, garbage collector가 자주 작동하는 것을 방지하기 위해서는 삭제하지 않고 계속 사용할 버퍼 용도의 객체를 남겨두는 것이 좋을 때가 있다.
    • 메모리 위치 정렬 (alignment)
    CPU의 종류에 따라 데이터가 메모리 어느 주소에 위치하느냐에 따라 접근 성능이 달라진다. 최신의 ARM CPU들은 데이터(변수 타입)의 메모리상 위치가 그 크기와 동일한 alignment를 가져야만 최적의 성능을 발휘한다. 예를 들어, int/float 등 32bit 값들은 메모리 주소가 4의 배수이어야 하며, double/long long등 64bit 값들은 메모리 주소가 8의 배수이어야 한다.
    iOS나 안드로이드의 컴파일 모두 C코드에서 struct의 멤버로 각 데이터 타입을 정의하여 사용하면 기본으로 위의 규칙에 맞추어 선언이 되므로 대부분의 경우는 크게 신경 쓸 필요가 없다.
    그러나 텍스처 데이터 로드의 경우처럼 메모리에 큰 버퍼를 일괄 할당한 후에 이 주소로부터 단위 데이터들을 읽고 쓰는 경우라면, 정렬 규칙을 한 번 되새겨서 그에 의한 페널티가 있는지 생각해 볼 필요가 있다.


    메모리 접근 안정성 최적화
    C언어와 같이 게임이 할당하는 메모리의 해제를 직접 관리해야 하는 환경이라면 메모리 할당과 해제의 실수로 인한 안정성 문제는 어려우면서도 반드시 해결되어야만 한다.
    버그가 발견될 때마다 추적하여 그 원인을 수정하는 것에 더불어, 시스템적으로 그러한 버그를 방지하거나 문제를 쉽게 발견할 수 있는 방법들을 제공한다면 게임 코드의 품질 향상에 큰 도움이 된다. (크래쉬가 더 이상 두렵지 않을지도 모른다)
    • 메모리 할당/해제의 자동화
    위에서 이미 언급된 리소스 관리자의 개념을 좀 더 일반화하여 모든 메모리 할당에 적용한다면 Java나 Objective-C와 같이 관리된 환경과 유사한 메모리 관리의 자동화를 구현할 수 있다.
    모든 class/struct 타입에 reference count를 도입하거나, 모든 new 구문을 위한 new operator overload를 정의한다면 할당된 메모리/객체의 추적이 가능해진다. 직접 구현한 메모리/리소스 관리 시스템 하에서는 메모리 해제가 필요한 시점을 직접 제어할 수 있고, 실수에 의한 메모리 누수를 방지할 수 있다. 또한, 아래에서 언급할 오류 검출용 정보 삽입이 쉬워진다.
    Garbage collector를 직접 구현하고자 한다면 reference들을 추적하는 문제가 핵심이 될 것이다. 각 struct/class 내에 reference(pointer) 멤버의 주소들을 Garbage collector가 모두 알고 있어야 하므로 어느 정도의 reflection 시스템이 필요하고, reflection 정보의 관리 문제가 Gargbage collection 자체보다 어려울 수 있으므로 이 접근법은 신중히 택해야 한다. 혹은 Boehm GC와 같은 오픈소스 라이브러리의 사용을 고려할수도 있다.

    • 메모리 검증용 툴의 사용
    만약 게임 코드가 윈도우 환경에서도 테스트 가능하다면 Microsoft의 무료 툴인 Application Verifier 등을 통해 메모리 접근의 문제점을 쉽게 검증할 수 있다.
    이 툴은 OS에서 제공되는 검증 기능들을 제어하여 애플리케이션을 실행하므로, 실행 속도가 매우 빠르고 비주얼스튜디오의 디버그 창에 상세한 정보들을 출력해 준다. 다만 작은 메모리 할당이 매우 많이 일어나는 경우에는 Application Verifier 자체의 메모리 부담이 커져서 실행에 어려움이 있을 수 있다.
    그 외의 상용 툴들은 대부분 윈도우 플랫폼에서만 작동하며, iOS를 위해서는 XCode의 instrument 툴이 아주 간단한 정보를 제공할 수 있다. 안드로이드의 경우에는 native 코드에 대한 검증이 가능한 툴은 아직 발견하지 못했다.

    • 오류의 빠른 발견
    게임 코드에 실수가 있을 때 문제가 있다는 사실을 가급적 빨리 알아챌 수 있다면 추후에 추적이 매우 어려운 상황을 맞게 되는 것보다 훨씬 싸게 고칠 수 있다.
    비단 메모리 관리에만 해당되는 내용은 아니지만, 코드에서 assert 구문을 적극적으로 사용할수록 오류의 존재를 빨리 확인할 수 있다. 주요 함수마다 시작과 끝 부분에 함수 작성 의도상 당연히 만족해야 할것으로 생각되는 pre-condition과 post-condition을 assert 구문으로 삽입해두면 코드에 의도치 않은 버그가 생겼을 때 빨리 잡을 수 있다. 예를 들어 Visual C의 std 컨테이너들은 내부적으로 상당히 많은 양의 assert를 구현하고 있어, 쓰레드 동기화 문제로 인한 데이터 오류가 생겼을 때 쉽게 찾아낼 수 있다. 게임 코드도 같은 전략에 의해 오류들을 자동으로 검증할 수 있을 것이다. assert에 의해 검증되지 않은 버그가 발견될 때마다 이에 해당하는 assert 구문을 추가한다면 점차 자기 방어형으로 진화하는 코드를 구축할 수 있다.
    다른 오류 검출 기법의 예로는 메모리가 할당된 직후 또는 해제하기 직전에 식별 가능한 데이터 시퀀스로 채워서 이 값을 검증하는 assert를 삽입해 두거나, 나중에 크래쉬가 발생한 상황에서 해당 메모리가 어떤 상태인지 판별하는 것이다. new operator를 override 했다면, new 안에서 할당된 메모리를 반환하기 직전에 0xbaadf00d와 같은 특수한 값으로 채워둘 수 있다. delete의 경우 해제하기 직전에 0xdeadbeef와 같은 값으로 채워 두고, 메모리를 실제로 사용할 코드에서 접근한 메모리의 값을 위의 비정상 상태 값들과 비교하는 assert를 삽입해 둘 수 있을 것이다. Windows의 경우 디버깅 환경에서 이와 유사한 기능이 기본 제공되나, 디버그 모드를 위한 특별한 기능이 없는 OS를 위해서는 자체적으로 간단한 구현을 고려할만 하다.


    프로파일러!
    메모리 사용의 문제는 게임 플레이 테스트만으로는 쉽게 눈에 띄지 않으며, 문제를 한 번 수정했다 해도 개발이 추가로 이루어짐에 따라 유사한 문제가 다시 발생하기 쉽다.
    메모리 문제의 수정만큼이나 중요한 것은 메모리 문제를 쉽게 인지할 수 있는 시스템을 게임 내에 넣어두고 개발이 이루어지는 동안에 주기적으로 검증하는 것이다. 게임 내 프로파일러에 의해 메모리 사용 현황을 쉽게 파악할수록 메모리 문제로 씨름하는 시간은 줄어든다. 좋은 프로파일러를 구현할수록 프로파일러 구현 시간은 오래 걸리겠지만 그에 의해 줄어드는 디버깅 시간의 이득은 훨씬 크다.

    게임 내에 구현할 수 있는 메모리 프로파일러 기능의 예시를 아래와 같이 몇가지 들 수 있다.
    * 카테고리별 메모리 사용량
    * 메모리를 많이 사용중인 항목들(리소스) 목록
    * 메모리 단편화 현황
    * 시간에 따른 메모리 사용량 그래프
    * 모든 메모리 할당에 대한 히스토리 로그
    * 시간당 메모리 할당량/횟수

    웹페이지나 GDC 등에서 위의 기능들에 대해 설명된 자료들이 이미 많이 존재하므로 그것들을 참고하면 더욱 구체적인 정보를 얻을 수 있다.

    by 김성균 | 2014/01/04 00:35 | 일(Work) | 트랙백 | 핑백(1) | 덧글(0)

    ◀ 이전 페이지          다음 페이지 ▶