2014년 12월 20일 토요일

Windows에서 Vagrant, Ansible로 개발환경 구축하기.

지난 번에 포스팅한 '자바 서버 어플리케이션 개발 기반 코드(Boilerplate code) 작성하기'에서 공개한 코드를 테스트하기 위해서는 Couchbase, Redis, MySQL이 설치된 개발환경이 준비되어 있어야 하는데 하나하나 설치하기가 매우 귀찮고 시간이 걸리는 일이다. Vagrant와 배포 자동화 도구인 Ansible을 사용하여 개발환경 운영체제 뿐만 아니라 Couchbase, Redis, MySQL 설치를 자동화해서 개발환경을 손쉽게 구축해보자.

다음과 같은 구성요소를 포함한 개발환경 구축을 목표로 한다.

- Ubuntu 12.04 LTS (Precise Pangolin) 64bits
- Couchbase Server 3.0.1 Community Edition
- Redis 2.8.19 (현재 기준)
- MySQL 5.5.40 (현재 기준)

Vagrant는 https://www.vagrantup.com/downloads.html에서 받아서 설치하면 Oracle VM VIrtualBox까지 한 번에 설치가 된다. Vagrant를 통해서 개발환경 운영체제가 VirtualBox에 가상화되어 설치, 동작하게된다.

https://github.com/lekdw/book-dev-env에 Vagrant와 Ansible 설정파일을 공개해 놓았으니 그대로 이용하자. 실무에서도 배포와 관련된 설정파일은 git과 같은 SCM을 통해 관리하고 공유하면 매우 편리하다. 여기서는 git을 사용하므로 https://msysgit.github.io에서 Windows 콘솔에서 사용할 수 있는 msysgit를 받아서 설치한다. msysgit를 설치하면 따라오는 MinGW와 MSYS는 덤으로 Windows 콘솔에서 ssh 같은 POSIX 도구들을 사용할 수 있다.

적당한 위치에서 콘솔창을 열어 다음과 같이 book-dev-env 프로젝트를 clone 한다.

git clone https://github.com/lekdw/book-dev-env

위의 결과로 다음과 같은 book-dev-env 프로젝트 디렉토리와 설정 파일들이 생성된다.


vagrant 디렉토리는 Vagrantfile 설정파일과 ansible.sh 파일을 포함하는데 ansible.sh 파일은 Vagrant가 Vagrantfile에 지정된 운영체제(Ubuntu) 설치가 완료되면 바로 실행되는 쉘 스크립트이다.

Vagrantfile에서 box 즉, Guest 운영체제는 Ubuntu 12.04 LTS 64bits로 지정하고 내부 네트워크 IP 192.168.33.10를 할당하였다. 운영체제 설치가 완료되면 ansible.sh 쉘 스크립트를 Ubuntu 상에서 실행된다. 참고로 box가 실행되는 Host 운영체제에는 네트워크 IP 192.168.33.1이 할당되어 Guest 운영체제와 네트워크를 이룬다. 이제 vagrant 디렉토리에서 다음과 같은 명령어를 실행하여 설치를 시작한다.


Guest 운영체제 설치가 완료되면 ansible.sh가 실행되어 Ubuntu 패키지 저장소에서 git와 ansible을 설치하고 위에서 언급한 book-dev-env git 프로젝트를 Guest 운영체제에서도 clone하여 최종적으로 ansible.sh의 13라인과 같이 site.yml playbook이 실행된다.

ansigle-playbook -i hosts site.yml

playbook 내에는 couchbase, mysql, redis role을 차례대로 실행하도록 기술되어있고 각각의 role에서는 대상의 설치 및 초기화 task를 수행한다. 모든 설치가 완료되면 다음과 같이 Host 운영체제에서(192.168.33.1) Guest 운영체제(192.168.33.10)에 설치된 각각의 대상이 동작하고 있는지 확인이 가능하다.

Couchbase 접속 확인

MySQL 접속확인

Redis 접속확인

2014년 12월 9일 화요일

Cocos2d-x에서 게임 라이브 업데이트 기능을 구현하기

요즘 모바일 게임도 PC 게임과 같이 라이브 업데이트 기능을 가지고 있는 경우를 흔히 볼 수 있다. 게임 내에서 사용하는 각종 테이블, 이미지, 사운드, 스크립트 등의 리소스 파일 수정이 필요할 경우 라이브 업데이트 기능을 구현하지 않았다면 이를 수정해서 사용자에게 새로운 빌드를 배포하는데 1주일 정도까지 시간이 걸릴 수 있기 때문에 상용 게임에서 라이브 업데이트 기능은 필수라고 할 수 있다.

Cocos2d-x에는 AssetManager라는 라이브 업데이트에 사용할 수 있는 클래스를 Extension으로 제공한다. 하지만 문서화가 잘 되어있지 않고 라이브 업데이트는 코드가 잘못 동작했을 경우 발생할 수 있는 부작용이 크기 때문에 AssetManager를 분석하여 사용하는 대신에 이해하기 쉽게 간단히 라이브 업데이트 기능을 구현해 보았다.(압축 해제 루틴은 AssetManager 클래스를 그대로 참조하였다.) https://github.com/lekdw/cocos2dx-live-update에 작성한 코드를 공개하였으니 참고하기 바란다.

프로젝트에서 cocos2d 폴더와 Android, iOS 프로젝트 폴더가 생략되었으므로 Cocos2d-x 프로젝트에서 복사하여 사용하도록 한다. Update 폴더에 테스트로 만들어 놓은 업데이트 파일과 버전 파일을 라이브 업데이트를 위한 Http 서버에 복사해 놓은 후 AssetPanel.cpp의 VERSION_FILE_URL에 버전 파일 위치로 변경하고 Win32 프로젝트를 빌드하여 실행하면 다음과 같이 새로운 버전 업데이트 파일을 다운로드하여 압축을 해제하는 것을 확인할 수 있다.



다운로드한 업데이트 파일이 저장되고 압축이 해제되는 위치는 코드 상에서 FileUtils::getInstance()->getWritablePath() + "asset/"이고 번들의 Resources 디렉토리보다 파일 찾기 우선순위가 높기 설정되어 있기 때문에 업데이트된 파일과 버전 파일을 asset 디렉토리에서 읽어들이게 된다. 파일 찾기 우선순위는 AppDelegate.cpp에서 아래과 같은 코드로 변경된다.

AppDelegate.cpp
bool AppDelegate::applicationDidFinishLaunching() {
  (생략)

  // 캐쉬 디렉토리에 에셋 디렉토리가 없으면 생성한다.
  std::string assetPath = FileUtils::getInstance()->getWritablePath() + "asset/";

  if (!FileUtils::getInstance()->isDirectoryExist(assetPath))
    FileUtils::getInstance()->createDirectory(assetPath);

  // 캐쉬 디렉토리의 에셋 디렉토리에서 먼저 리소스 파일을 찾을 수 있도록 설정한다.
  // 요청하는 리소스 파일이 캐쉬 디렉토리의 에셋 디렉토리에 없을 경우는 번들 리소스 디렉토리에서 찾는다.
  std::vector searchPath;
  searchPath.push_back(assetPath);

  for (std::string path : FileUtils::getInstance()->getSearchPaths())
    searchPath.push_back(path);

  FileUtils::getInstance()->setSearchPaths(searchPath);

  (생략)
}

구현된 라이브 업데이트 기능의 실행 플로우를 그려보면 아래와 같다.


파일 다운로드는 Cocos2d-x의 network::HttpClient 클래스를 사용하는데 이 클래스 구현은 파일 다운로드 진행 상황을 알 수가 없다.다운로드 진행 상황을 Polling 방식으로 알 수 있도록 network::HttpClient 클래스가 수정되어 적용되었다.

프로세스 항목 중에서 파일 다운로드와 압축 해제 과정은 별도의 Thread에서 처리되므로 Cocos2d-x Thread가 Block되지 않도록 AssetPanel.cpp에 구현되어있다. 모든 처리가 완료된 후 프로그램을 다시 실행하면 다음과 같이 라이브 업데이트가 적용된 것을 확인할 수 있으며 Android, iOS 모두 동일하게 동작한다.





2014년 12월 6일 토요일

자바 서버 어플리케이션 개발 기반 코드(Boilerplate code) 작성하기

게임 서비스를 개발하다보면 비슷한 구조의 서버 어플리케이션 또는 데몬을 여러개 만들어야할 필요가 있다. 서비스 초기에는 몇 개 안되겠지만 시간이 지날수록 관리해야할 수가 기하급수적으로 늘어나 있을 것이다. 어느 회사나 서버 어플리케이션이라면 자주 사용하는 구성요소들이 대게 정해져 있기 때문에 이런 것들의 사용법을 규격화하여 어플리케이션 개발 기반 코드를 작성해 놓으면 일관성 있는 구조의 어플리케이션 개발이 가능하고 유지보수에도 큰 도움이 될 것이다. 우리 회사의 경우 모든 서버 어플리케이션은 환경설정과 로그설정 파일을 기본으로 사용할 수 있어야 하며 Netty(범용 네트워크 라이브러리), Couchbase(NoSQL), Redis(Cache), MySQL(RDBMS), Quartz(Job Scheduler), Sigar(System Infomatioin) 같이 자주 사용되는 것들의 Wrapper를 작성하고 이를 모든 프로젝트에서 공유하여 필요에 따라서 취사선택하여 사용하고 있다. https://github.com/lekdw/book-boilerplate는 간단하게 구현하여 공개한 자바 서버 어플리케이션 개발 기반 코드와 이를 이용한 게임서버 및 테스트 클라이언트 샘플 프로젝트의 소스 저장소이다.


위의 그림은 소스를 얻어와 Eclipse에서 common, gc, gs 프로젝트를 Import하여 Workspace에 추가한 모습이다. common은 어플리케이션 개발 기반 코드와 기타 공유 코드를 포함한다. 이를 공유해서 작성된 어플리케이션이 테스트 게임 클라이언트 gc와 게임서버 gs 프로젝트이다.

common 프로젝트는 다음과 같은 기능을 어플리케이션에 제공한다.
  • 어플리케이션 시작 코드 (AppImpl.java) - 기본사용
  • 환경설정 (AppConfig.java) - 기본사용
  • Netty 범용 네트워크 라이브러리 Wrapper 코드 (AppNettyXXX.java) - 선택사용
  • Couchbase NoSQL 클라이언트 라이브러리 Wrapper 코드 (AppCouchbaseXXX.java) - 선택사용
  • Hibernate ORM Wrapper 코드 (AppMySQLXXX.java) - 선택사용
  • Redis 클라이언트 라이브러리 Wrapper 코드 (AppRedisXXX.java) - 선택사용
  • 공유 객체 (common.model 패키지) - 선택사용

common 프로젝트 내 ant 프로젝트 build.xml을 실행하면 소스를 빌드하여 lib 디렉토리 내의 jar 라이브러리 파일들을 하나의 external.jar로 묶어서 gs, gc 프로젝트내 lib 디렉토리에 복사한다. 이에 대한 자세한 설명은 지난 글 자바 코드와 외부 jar 라이브러리를 하나로 합치기를 참조한다. 어플리케이션이 common 프로젝트를 공유해서 작성되면 어플리케이션 시작 코드와 환경설정은 기본으로 사용되며 나머지는 어플리케이션의 기능에 따라서 취사선택이 가능하다.

common.model 패키지 내의 객체들은 common.storage 내의 DAO 코드를 통해서 다루어지게되는 Mapping된 POJO 객체이다. 이에 대한 자세한 설명은 지난 글 클라이언트, 서버 자바 어플리케이션에서 DTO 없이 계층 간 데이터 교환하기를 참조한다.

이제 common 프로젝트를 공유하여 게임서버 gs 프로젝트를 만들어보자. 게임서버는 다음과 같은 기능이 필요하다고 가정하고 어플리케이션 시작 코드를 계승받아 다음과 같이 시작한다.
  • Http 서버
  • Couchbase 연결
  • MySQL 연결
  • Redis 연결

App.java

App 클래스는 AppImpl 어플리케이션 시작 코드를 계승하고 AppNettyServerHandler, AppCouchbaseHander, AppMySQLHandler, AppRedisHandler 인터페이스를 구현하기만 하면 어플리케이션은 설정파일을 이용할 수 있고 Couchbase, MySQL, Redis를 사용할 수 있다.


gs 실행 시에는 conf 디렉토리를 classpath에 추가해야하며 conf 디렉토리에는 gameserver.json 환경설정 파일, log4j.properties 로그설정 파일, mysql_01.xml Hibernate 설정 파일이 존재한다. lib 디렉토리에는 위의 common 프로젝트에서 묶어서 복사된 external.jar 파일이 위치하게된다.

gameserver.json

gameserver.json에는 App에서 구현한 인터페이스에 대응하는 설정 내용이 기술되며 작성된 어플리케이션은 기술된 설정대로 초기화되어 동작된다.

위와 같이 기반 코드(Boilerplate Code)를 잘 작성해서 사용한다면 짧은 코드로 복잡한 요구를 만족하는 작업이 가능하며 어플리케이션이 표준화된 형상을 유지할 수 있으므로 유지보수성이 뛰어난 서버 어플리케이션 개발이 가능할 것이다.



2014년 11월 28일 금요일

자바 코드와 외부 jar 라이브러리를 하나로 합치기

자바 어플이케이션을 작성하다보면 외부 jar 라이브러리가 수 십개가 넘어가는 일이 다반사이다. 배포 및 관리의 편의를 위해서 어플리케이션에서 사용하는 공통 코드와 외부 jar 라이브러리를 하나로 합쳐보자.


두 개의 프로젝트 common과 gs가 있다. gs는 개발하려는 최종 어플리케이션 프로젝트이고 common은 gs와 기타 다른 프로젝트에서 공통으로 쓰일 코드와 외부 jar 라이브러리로 구성된 프로젝트이다. common 프로젝트에서 코드는 빌드하여 lib/common.jar로 생성하고 이후 lib 폴더에 모든 jar를 external.jar 파일 하나로 합쳐서 gs 프로젝트의 lib 디렉토리에 복사하는 ant 스크립트를 아래와 같이 작성하였다.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="package-external-jar" name="common">
  <target depends="pacakge-common-jar" description="" name="package-external-jar">
    <zip destfile="external.jar">
      <zipgroupfileset dir="lib" includes="*.jar">
    </zipgroupfileset></zip>
    <copy file="external.jar" todir="../gs/lib">
  </copy></target>
  <target name="pacakge-common-jar">
    <jar destfile="lib/common.jar">
      <fileset dir="bin">
      <fileset dir="src" includes="**/*.java">
    </fileset></fileset></jar>
  </target>
</project>

디버깅 편의와 GWT 프로젝트와 같이 소스를 필요로 하는 경우를 위해 common 프로젝트 코드를 빌드할 때 소스를 추가하였다. 이제 gs를 배포할 때 깔끔하게 external.jar 하나만 있으면 된다.

2014년 11월 26일 수요일

Cocos2d-x Scene 노드를 Lua 스크립트로 실시간 제어하기

Cocos2d-x의 경우 Unity 처럼 Scene 에디터와 통합된 개발환경이 아니기 때문에 Scene을 구성하는 각각의 자식 노드를 생성하기 위해 직접 코드를 작성해야 한다. 물론 CocoStudio 같은 에디터를 사용하면 Scene 구성을 좀 더 편하게 할 수 있지만 이미 한참 진행된 프로젝트의 경우 적용하기가 쉽지 않다. UI의 경우 프로젝트 진행 중 Look and feel이 여러번 바뀌는 경우가 많은데 UI 수정 작업 시간의 대부분은 노드의 위치를 새롭게 변경하기 위해 반복적으로 코드를 컴파일하고 실행, 확인하느라 소모되는 경우가 많다. 다음은 Scene 노드를 Lua 스크립트로 제어할 수 있도록 작성된 코드로 특정 노드의 상태(위치 등)를 Lua 스크립트를 통해 변경, 확인이 가능하다.

DebugNode.h
#ifndef __DEBUG_NODE_H__
#define __DEBUG_NODE_H__

#include "cocos2d.h"

USING_NS_CC;

class DebugNode : public Node {
public:
  DebugNode();
  virtual ~DebugNode();

  CREATE_FUNC(DebugNode);

public:
  virtual bool init();
  void apply(const std::string &script);

private:
  std::string m_script;
};

#endif

DebugNode.cpp
#include 
#include 

#include "DebugNode.h"

DebugNode::DebugNode() {
  m_script = "";
}

DebugNode::~DebugNode() {
  stopAllActions();
}

bool DebugNode::init() {
  Node::init();
  return true;
}

void DebugNode::apply(const std::string &script) {
  m_script = script;

  LuaEngine *luaEngine = LuaEngine::getInstance();
  ScriptEngineManager::getInstance()->setScriptEngine(luaEngine);

  auto L = luaEngine->getLuaStack()->getLuaState();

  // DebugNode의 부모 Node를 루아 스크립트 전역 변수 rootNode로 지정한다.
  luaEngine->getLuaStack()->pushObject(getParent(), "cc.Node");
  lua_setglobal(L, "rootNode");

  // 지정된 스크립트 파일을 실행한다.
  luaEngine->executeScriptFile(m_script.c_str());

  // 이후 1초 마다 스트립트를 재실행한다.
  auto action = RepeatForever::create(
    Sequence::createWithTwoActions(
      DelayTime::create(1.0f),
      CallFunc::create(
        [&]() {
          LuaEngine::getInstance()->reload(m_script.c_str());
        })));
  runAction(action);
}

제어할 Scene 노드의 자식으로 DebugNode 객체를 생성해서 추가하고 apply 함수를 호출하면 1초에 한 번씩 지정된 Lua 스트립트 파일을 실행하는데 제어할 Scene 자식 노드는 setName 함수를 사용하여 유일한 이름을 지정해 주어야 한다. 다음의 샘플은 Cocos2d-x에 포함된 cpp-empty-test 프로젝트에 DebugNode를 적용한 부분의 코드 조각이다.

HelloWorldScene.cpp
bool HelloWorld::init()
{
  (생략)
    
  /////////////////////////////
  // 3. add your codes below...

  // add a label shows "Hello World"
  // create and initialize a label
    
  auto label = LabelTTF::create("Hello World", "Arial", TITLE_FONT_SIZE);

  // 스크립트에서 참조할 이름을 지정한다.
  label->setName("label");
    
  (생략)

  auto debugNode = DebugNode::create();
  this->addChild(debugNode);
  debugNode->apply("HelloWorldScene.lua");
    
  return true;
}

HelloWorldScene.lua
label = rootNode:getChildByName("label")
label:setPosition(100, 200)

HelloWorldScene.lua 파일을 텍스트 에디터에서 열고 setPosition의 인자를 변경한 후에 저장하면 바로 위치 값이 적용되는 것을 확인할 수 있다.


2014년 11월 25일 화요일

클라이언트, 서버 자바 어플리케이션에서 DTO 없이 계층 간 데이터 교환하기

현재 개발 중인 게임 서버에서 사용자의 정보를 담고 있는 Game이라는 이름의 클래스의 내용을 MySQL, Couchbase, Redis에 저장하고 게다가 클라이언트로 전송해야 하는 요구 사항이 있다. 현재는 MySQL은 Hibernate ORM을 이용하여 저장하고 Couchbase, Redis는 Jackson Json Processor를 이용해서 객체를 Json 형태로 변환하여 저장하고 있다. 클라이언트로 전송하는 패킷의 포맷은 이전에는 Json 형태였으나 현재는 MessagePack Serializer을 이용하여 압축된 바이너리 포맷을 사용하여 패킷을 생성할 수 있도록 하였다. 이 각각의 라이브러리들은 공통적으로 Object Mapping을 지원하는데 Java에서는 Reflection과 Annotation의 언어적인 특징을 이용하여 POJO라 불리는 일반적인 클래스의 객체를 필요한 형태로 매핑하여 용도에 따라 처리한다.

@javax.persistence.Entity
@javax.persistence.Table(name = "game")
@org.msgpack.annotation.Message
public class Game implements Serializable {
  private static final long serialVersionUID = -6305522860872314804L;

  // 채널 아이디 (패킷에서 제외)
  @javax.persistence.Id
  @org.msgpack.annotation.Ignore
  public String channelId = "";
 
  // 이름 (패킷에서 제외)
  @org.msgpack.annotation.Ignore
  public String nickName = "";
 
  // 마켓 (패킷에서 제외)
  @org.msgpack.annotation.Ignore
  public int marketId = 0;
 
  // 푸쉬 아이디 (패킷에서 제외)
  @org.msgpack.annotation.Ignore
  public String pushId = "";

  // 로그인 블록 상태
  public int loginBlock = 0;

  // 메시지 블록 상태
  public int mailBlock = 0;

  // 푸쉬 블록 상태
  public int pushBlock = 0;

  // 운영 보상 아이디
  public int lastRewardId = 0;

  (생략)

  // 쪽지 (MySQL, Couchbase, Redis, 패킷에서 제외) 
  @javax.persistence.Transient
  @org.msgpack.annotation.Ignore
  @com.fasterxml.jackson.annotation.JsonIgnore
  public Map mails = new HashMap();
 
  public Game() {
  }
}

위와 같이 게임 서버에서 사용 중인 각각의 라이브러리에서 제공하는 Annotation을 Game 클래스에 적절히 적용하면 Game 클래스 하나를 가지고 다양한 용도에 사용이 가능하다.


위의 그림은 현재 진행 중인 프로젝트의 구성도 중 일부이다. 각각의 블록을 잇는 모든 선은 다양한 형태의 데이터로 변환된 동일한 Game 클래스 객체 데이터의 흐름이다. 이와 같은 설계의 가장 큰 장점은 MySQL, Couchbase, Redis와 같은 Persistence 계층과 Game Server와 같은 Controller 계층, 게임 사용자와 같은 Present 계층이 모두 같은 클래스를 공유할 수 있어서 각 계층 간 데이터 전환을 위한 DTO가 필요하지 않아 간결한 코드를 유지할 수 있었다. 예외적으로 GWT로 만들어진 운영툴의 경우 GWT 서버-클라이언트 코드 간에 MySQL에서 읽어들인 Game 클래스 객체 교환이 불가능하기 때문에 (Hibernate가 Game 클래스 객체를 POJO가 아닌 객체로 동적 변경을 하므로 변경된 부분을 클라이언트 Java Script 코드로 생성이 불가능하기 때문에) 반드시 DTO를 생성해야만 했다.

2014년 11월 24일 월요일

Google breakpad를 이용해서 Android용 Cocos2d-x 게임 크래쉬 리포트를 HockeyApp에 전송하기

HockeyApp에 Cocos2d-x로 개발된 게임의 크래쉬 리포트를 전송하기 위해서 제공되는 HockeyApp SDK는 NDK로 빌드된 C, C++ 코드의 크래쉬 정보 덤프할 수 없기 때문에 Google에서 공개한 크래쉬 리포트 라이브러리인 breakpad를 이용하여 HockeyApp에 전송한다.


현재 3.x 대 Cocos2d-x 소스는 NDK r9d 이하에서 빌드가 가능한데 breakpad 저장소의 최신 리비전 소스는 NDK r10c 이상을 지원한다. 이전 chrome_38 브랜치 소스를 가지고 라이브러리를 빌드하여 문제를 해결한다.


breakpad 라이브러리 빌드를 위해서 (path_to_breakpad)/android/sample_app 디렉토리에 있는 샘플 프로젝트에서 Application.mk의 내용 중 APP_STL 설정을 stlport_static에서 gnustl_static으로 변경하고 jni 폴더에서 ndk-build를 실행하면 breakpad 라이브러리와 테스트 프로그램 빌드가 시작된다. 빌드가 완료된 후 obj/local/armeabi 디렉토리에 libbreakpad_client.a가 생성되어있음을 확인할 수 있다.


자 이제 Cocos2d-x Android 프로젝트에 앞에서 만들어진 breakpad 라이브러리를 추가하자. 게임 프로젝트에서 위의 그림과 같이 Application.mk의 내용중 APP_STL 설정을 c++_static에서 gnustl_static으로 APP_CPPFLAGS의 -std=c++11에서 -std=gnu++11로 변경한다.


그리고 Android.mk 파일에 breakpad 라이브러리의 헤더 경로를 지정해주고 prebuilt 라이브러리로 등록하여 링크될 수 있도록 위와 같이 설정을 추가하여 빌드한다. 생성된 apk 파일과 심볼 파일을 HockeyApp 사이트에 전송하면 아래와 같은 크래쉬 리포트를 받을 수 있다.


HockeyApp SDK 연동에 대한 자세한 설명은 여기를 참조한다.

2014년 11월 23일 일요일

IP 주소로 Windows용 Boot2Docker 외부에서 Container로 접속하기

Windows 환경에서 Docker는 Boot2Docker 커맨드라인 툴을 통해 가상화된 Linux에서 실행된다.


Boot2Docker Start 아이콘을 실행하면 위와 같은 화면이 나오고 이제 마음 것 docker 명령어를 이용할 수 있다.


실제로 Docker가 실행되는 가상화된 Linux는 Boot3Docker와 같이 설치되는 Oracle VIrtualBox 상에서 실행된다. ifconfig를 실행하면 다음과 같은 네트워크 정보를 알 수 있다.


docker0는 Docker 컨테이너들 간에 연결된 네트워크이고 eth0는 VirtualBox NAT에 연결되어 호스트(Windows)의 인터넷과 연결에 사용되고 eth1은 호스트에 설치된 가상 네트워트 아답터와 매핑되어 호스트에서 Boot2Docker에 연결할 때 사용된다.


위와 같은 호스트와 게스트의 네트워트 관계에서 호스트의 Host Only Network를 통해서 직접 게스트의 docker0 네트워크 IP에 접근할 수 없기 때문에 위의 점선과 같이 호스트의 라우팅 테이블을 수정해서 호스트의 어플리케이션이 docker 컨테이너에 접근할 수 있도록 한다.


관리자모드로 커멘드 창을 열고 route print하여 인터페이스 목록에서 위에서 확인한 VirtualBox Host-Only Ethernet Adapter #2의 인터페이스 아이디 32를 확인하고 아래와 같이 입력한다.

route -p add 172.17.0.0 mask 255.255.0.0 192.168.59.103 IF 32

간단히 설명하면 192.168.59.3이 할당된 인터페이스로 172.17.0.0으로 시작하는 IP 주소에 대한 모든 요청을 192.168.59.103 인터페이스로 돌아서 요청하라는 의미이다. -p 옵션을 주어 이 설정을 영구적으로 적용하게 하였다. 테스트로 아래와 같이 CouchBase 컨테이너를 하나 생성해본다.

docker run -d -p 11210:11210 -p 8091:7081 -p 8092:8092 dustin/couchbase

생성된 CouchBase 컨테이너는 IP 주소 172.17.0.2부터 차례로 할당되고 다음과 같이 호스트에서 CouchBase Console에 접속이 가능함을 확인할 수 있다.

2014년 11월 21일 금요일

Cocos2d-x Label에서 긴 문자열을 짧게 잘라 생략(Ellipse) 기호 붙이기

Cocos2d-x Label에서 긴 문자열을 제한된 폭으로 표시해야할 때가 종종있는데 생각보다 구현할 때 고려해야할 사항이 많다.


Cocos2d-x는 내부적으로 스트링 처리에 UTF8로 사용하기 때문에 유니코드에 대응해야하고 Label이 트루타입 폰트로 그려지는 폭을 Kerning을 고려하여 계산해야 하며, FreeType 폰트 캐쉬와 glyph 정보로 그리는 위치 또한 고려해야 한다.

아래의 코드는 Label의 폭을 limitWidth로 제한하고 생략 기호로 "..."를 붙이는 예제이다. 코드의 간략화를 위해서 함수 하나에 담았다.

void shortenLabelTTFString(Label *label, int limitWidth) {
  std::u16string utf16EllipseString;
  StringUtils::UTF8ToUTF16("...", utf16EllipseString);

  std::u16string utf16String;
  StringUtils::UTF8ToUTF16(label->getString(), utf16String);

  auto ttfConfig = label->getTTFConfig();
  auto ttfAtlas = FontAtlasCache::getFontAtlasTTF(ttfConfig);
  ttfAtlas->prepareLetterDefinitions(utf16EllipseString);

  int letterCount = 0;
  auto kerning = ttfAtlas->getFont()->getHorizontalKerningForTextUTF16(
    utf16EllipseString, letterCount);

  int ellipseWidth = 0;
  FontLetterDefinition letterDef = {0};

  for (int i = 0; i < utf16EllipseString.length(); i++) {
    ttfAtlas->getLetterDefinitionForChar(utf16EllipseString[i], letterDef);
    ellipseWidth += letterDef.xAdvance + kerning[i] + label->getAdditionalKerning();
  }

  if (kerning != nullptr)
    delete [] kerning;

  if (ellipseWidth > limitWidth) {
    label->setString("...");
    return;
  }

  letterCount = 0;
  kerning = ttfAtlas->getFont()->getHorizontalKerningForTextUTF16(
    utf16String, letterCount);

  std::string result;
  std::u16string utf16Result;
  int width = ellipseWidth;

  for (int i = 0; i < utf16String.length(); i++) {
    ttfAtlas->getLetterDefinitionForChar(utf16String[i], letterDef);

    if (width + letterDef.xAdvance > limitWidth) {
      StringUtils::UTF16ToUTF8(utf16Result, result);
      label->setString(result + "...");
      break;
    }

    width += letterDef.xAdvance + kerning[i] + label->getAdditionalKerning();

    utf16Result += utf16String[i];
  }

  if (kerning != nullptr)
    delete [] kerning;
}

2014년 11월 19일 수요일

모바일 스타트업 회사에 유용한 서비스들

스타트업 2년 차, 예전에 다녔던 회사에서는 업무 진행에 신경쓸 필요도 없던 사소한 것들을 하나씩 준비해야했다. 상황때문에 어쩔 수 없이 사용했던 서비스, 도구들이 많이 있었지만 현재 주옥같이 사용하는 것들을 정리해 보았다.

Microsoft BizSpark


등록 신청을 하면 3년인가 MS 운영체제, 오피스, 개발툴을 무료로 사용할 수 있는 라이센스 자격이 주어진다. 더우기 한달에 Azure 15만원 크레딧을 주어서 무료로 사용이 가능하다. 등록에 일주일 정도 걸린다고 했는데 오전에 신청해서 오후에 사용이 가능했다. 외부 접속용으로 사용했던 KT ucloud는 바로 해지했고 무료라서 사용 중이었던 Visual Studio Express, OpenOffice도 바로 교체, 이건 꼭 신청해야해~

Naver Works

예전에는 Google Apps를 무료(5인 이하)로 사용했었는데 사원 수가 많아져서 Naver Works로 바꾸었다. 네이버에 접속하거나 Outlook을 사용하여 메일 서비스를 이용할 수도 있고 모바일 앱으로도 제공되어 매우 편리하다. Google Apps의 경우 우리 LDAP 서버와 계정을 동기화해주는 툴을 제공해서 잘 사용했었는데 Naver Works에서는 이와 유사한 기능이 없는 것이 아쉽다. 하지만 네이버 캘린더, 메모, 주소록, NDrive 등을 모바일에서도 같이 사용할 수 있는 것은 참 좋다. 땡큐 네이버!

Google Docs

1년 정도 모든 기획서를 Atlassian Confluence를 사용해서 작업했었는데 어느 순간 사용하게 되었다. 단순한 기능만큼 간편하게 문서 공유 및 협업이 가능해서 별 불만 없이 사용 중이다. 공문서(아래아한글 파일 .hwp)을 볼 수 없는 것이 최대의 약점인데 이것은 Naver Office를 사용해서 해결 중이다. ^^;

Atlassian JIRA onDemand

프로젝트 관리 및 이슈트래킹을 위해 Confluence와 Jira를 설치해 사용해 오다가 관리의 압박에 못이겨 지금은 클라우드 OnDemand 서비스를 이용하고 있다. 회선 속도가 느린 것과 fisheye 연동 시 반드시 80포트를 사용해야한다는 것만 빼면 매우 만족한다. 한달에 $10...

HockeyApp


iOS, Android 앱을 사용자에게 배포하고 크래쉬 리포트와 피드백을 받을 수 있는 서비스이다. 앱에 SDK를 적용하면 간단하게 버전별로 취합된 크래쉬 리포트 관리가 가능하고 사용 중인 JIRA와도 잘 연계된다. 다만 NDK를 사용한 Android 앱의 경우 크래쉬 리포트를 얻기 위해 Google breakpad 라이브러리를 별도로 빌드해서 사용해야 하는데 앱에서 사용하는 NDK 버전과 breakpad에서 사용하는 버전이 안 맞아서 문제가 발생할 수 있다. 앱에서 사용하는 NDK 버전에 맞는 breakpad 소스 branch를 찾아서 빌드하면 된다. 한 달에 $10...

개발 중인 신규 게임 개발환경

내년 초 라인에 출시할 게임의 개발환경을 대충 그려보니 아래와 같은 모습이 되었다.


올 5월에 출시한 게임의 개발환경과의 비교해보면 메인 DB가 MySQL에서 CouchBase로 바뀌었고 테스트 배포 및 크래쉬 리포트 취합을 위해서 HockeyApp 서비스를 이용한 것을 제외하면 거의 유사하다. CouchBase로 바꾸면서 샤딩이나 캐슁을 위해 필요한 복잡한 구성이 단순화된게 가장 큰 이점이기는 하지만 RDBMS에 특화된 툴을 사용할 수 없어 운영툴에 많은 기능이 필요로했다. 현재 게임은 내부 테스트 중으로 HockeyApp을 통해서 배포하면 등록된 테스터에 알람이 가고 iOS, Android 단말기에서 바로 다운받아서 실행이 가능하다. 실행 중 크래쉬가 발생하면 다음 실행 시 리포트를 HockeyApp 사이트에 전송하며 별도의 Issue Tracker를 사용한다면 연동이 가능하다.


우리 같은 경우 사용 중인 Jira OnDemand로 크래쉬를 이슈로 등록해 준다. 이렇게 훌륭한 서비스를 매달 단돈 $10에 사용이 가능하니 정말로 좋은 세상이다. 아직 용도는 생각 못 해보았지만 시간이 허락된다면 CouchBase XDCR(Cross Data Center Replication)의 대상을 ElasticSearch로 하여 서비스에 활용하는 방안을 고민 중이다.