프로그래밍과 글쓰기

지난 프로그래밍과 수학에 이어 오늘은 프로그래밍과 글쓰기에 대한 이야기를 하고자 합니다.

게임을 포함한 모든 소프트웨어는 요구사항 및 기획, 설계, 코드 작성, 테스트, 출시 ,유지 보수 등의 과정을 거쳐야만 하고, 다른 공학 분야와 마찬가지로 해당 분야에 대한 전문 지식, 고도의 사고력, 제품 경험 등 종합적인 능력이 요구됩니다. 따라서 소프트웨어를 잘하기 위해 어떤 능력이 필요하냐는 질문에는 보는 관점에 따라 “해당 분야의 전문 지식”을 말하는 사람도 있고, “수학이나 논리적 사고력”을 말하는 사람도 있고, 더 나아가 “인문학적 소양”을 말하는 사람도 있게 됩니다.

저는 프로그래밍이란 행위는 글쓰기와 가장 유사하다고 생각합니다. 주제를 선정하고 개요를 작성하고 글을 쓰고 퇴고하는 일련의 행위는, 어떤 소프트웨어를 만들지 정하고 중요 모듈을 설계하고 코드를 작성하고 리팩토링하는 과정과 닮아 있습니다. 좋은 코드를 작성하기 위한 팁을 담고 있는 Clean CodeCode Complete과 같은 책들은 내용과 구성 면에서 글쓰기 책과 다를 바가 없고, Refactoring은 글을 다 쓴 후에 퇴고하는 과정을 설명합니다.

대부분의 소프트웨어는 무척 복잡합니다. 소프트웨어 작성의 핵심은 결국 복잡한 문제를 얼마나 사람이 이해하기 쉽게 풀어쓸 것인가에 대한 문제입니다. 운영체제나 웹브라우제, 프로그래밍 언어 런타임 같은 소프트웨어가 아니더라도 웬만큼 성공한 소프트웨어는 1명의 인간이 이해할 수 있는 한계를 넘어서 있고, 이러한 복잡한 소프트웨어를 계속 유지 보수하고 개선해 나가기 위해서는 코드가 얼마나 이해하기 쉽게 작성되었는지가 가장 중요합니다. 새로운 기능을 추가하거나 성능 최적화를 하려고 해도 코드 이해가 선행되어야만 합니다.

제가 지난 1년 2개월간 현업의 게임 개발자들과 같이 이야기하면서 알게된 것은 게임을 잘 만들기 위한 도메인의 전문 지식에 비해, 프로그래밍을 잘 작성하기 위해 필요한 “글쓰기”로서의 프로그래밍 능력이 현저히 부족하다는 사실입니다.

여기에는 이유는 여러 가지가 있을 수 있습니다만, 가장 큰 문제는 도메인에 대한 전문 지식과 프로그래밍 능력을 구분하지 않는 풍조에 있습니다. 예를 들어, 머신 러닝 관련 프로젝트를 진행한다고 하면 당연히 해당 분야를 전공한 전문 지식이 필요합니다. 하지만 머싱 러닝 알고리즘을 오랜 시간 연구하였다고 해서 반드시 프로그래밍을 잘하는 것은 아닙니다. 프로그래밍은 글쓰기와 비슷해서 잘 쓴 글을 많이 보고, 많이 작성해 보고, 작성한 코드를 고쳐보고 고민하면서 조금씩 발전하기 때문입니다. 오히려 전문 분야 연구에 시간을 많이 쓴 개발자일수록 프로그래밍 경험이 부족해서 코드 작성을 잘 못하는 경우가 더 빈번합니다.

물론 현업 개발자라면 어쨌거나 하루 최소 8시간은 코드 작성에 시간을 쓰고 있고(물론 회의에 불려가거나 잡무를 하는 경우는 제외하고), 코드 작성 경험 부족이 프로그래밍을 못 하는 이유라고 보기는 어렵습니다. 실제로 개발에 많은 시간을 쓰는 데도 실력이 늘지 않는 또 다른 이유도 글쓰기 비유에서 답을 찾 수 있습니다. 유명 작가가 되려면 글을 많이 써보는 것도 중요하지만, 이른바 고전이라고 불리는 잘 쓴 글을 많이 읽는 것이 무척 중요합니다. 코드 작성도 마찬가지라서 잘 쓴 코드를 읽는 것이 코딩을 배울 수 있는 가장 좋은 방법입니다.

대부분의 개발자는 현업에서 선배 개발자들이 작성한 코드를 유지보수하면서 코드 작성 방법을 배우게 됩니다. 회사에 따라 다르긴 하겠지만, 이렇게 접한 대부분의 코드는 겨우 겨우 동작만 하지, 코드의 가독성이나 유지보수를 거의 생각하지 않고 작성된 코드일 확률이 높습니다. 이런 코드를 읽고, 어떻게든 돌아가게 수정하는 훈련만 하다 보면 애시당초 코드를 잘 짠다는 게 무엇인지 알기가 어렵게 됩니다.

방법이 없지는 않습니다. 잘 짠 코드는 인터넷에 이미 오픈소스로 많이 공개되어 있기 때문입니다. 관심 있는 분야의 오픈소스 프로젝트를 골라서 코드를 읽고 공부하는 것은 고전을 읽고 공부하는 것과 마찬가지라 정말 많은 것을 배울 수 있습니다. 물론 진입 장벽이 있습니다. 고전이란 모두가 읽고 싶어하지만 아무도 안 읽은 책인 것처럼 이런 성공한 오픈소스 프로젝트들도 사람들의 관심에 비해 실제로 코드를 읽고 참여하는 사람이 지극히 소수이기 때문입니다. 하지만 해답도 고전 읽기에서 찾을 수 있습니다. 원전을 두고 해설서만 찾아 헤맬 것이 아니라 원전을 사서 한 줄이라도 읽기 시작하는 것입니다.

게임 서버: node.js의 장점과 단점

그간 Unity, C# 등 클라이언트 기술에 대한 이야기만 했는데, 오늘은 서버에 대한 이야기도 해볼까 합니다.

제가 게임 개발을 맡았을 때 두 팀 중 한 팀은 이미 1년 가까이 프로젝트를 진행 중이었고, 다른 MMORPG에 사용했던 C++로 작성된 게임 서버를 이용해 서버 로직을 작성하고 있었습니다. 당시 서버/서버팀에는 다음과 같은 몇 가지 문제점이 있었습니다.

  • 개발자 생산성 저하 (C++로 서버 로직 작성)
  • 서버 엔진에 대한 경험 부족
  • 서버 자체의 안정성 문제

사실 더 큰 문제는 프로젝트 성격에 맞지 않는 게임 서버 엔진을 사용하고 있다는 점였습니다. MMORPG 게임 서버는 보통 DB 접근의 오버헤드를 줄이기 위해 로직 서버가 게임 진행에 대한 상태 정보를 메모리에 가지고 있는 구조이고, 여러 서버 간의 상태를 동기화를 위한 방법도 제공합니다. 하지만 우리가 제작하는 게임은 네트워크 플레이를 지원하지 않는 모바일 RPG게임이고 서버가 하는 일은 단순히 요청을 처리하고 DB에 기록하고 응답을 주는 일뿐인데, 불필요하게 복잡한 서버 아키텍처를 사용하고 있는 상황이었습니다.

아직 개발 기간도 충분히 남은 상태이고, 앞으로 추가해야 할 서버-클라이언트 간의 프로토콜도 많이 남아있던 상황이라 결국 출시까지 이 문제를 계속 끌고 가는 부담을 지는 대신에 서버를 교체하기로 결정내렸습니다. 개발자의 생산성, 서버의 규모 가변성(scalability) 등 여러 면을 검토한 결과 내린 결론은 DB만 증설하면 게임 로직 서버 자체의 규모 가변성을 쉽게 확보할 수 있는 웹서버를 채택하는 것이었습니다.

웹서버의 장점은 명확합니다. 일단 규모가변성 면에서는 DB 외에는 상태를 저장하지 않기 때문에 로직 서버에 부하가 걸리면 로직 서버만 증설해주면 됩니다. (물론 자주 쓰는 데이터는 RedisMemcached로 캐싱하는 옵션이 있습니다.) node.js, Django, Ruby on Rails 등 이미 수많은 웹서버 프레임워크가 존재하기 때문에 이 중 하나를 골라서 쉽게 로직 코드를 작성할 수 있습니다. Unity는 HTTP 요청을 쉽게 하기 위한 WWW 클래스를 제공합니다.

node.js, Django, Ruby on Rails 등은 모두 검증된 웹서버 프레임워크지만 저의 선택은 node.js였습니다. 일단 게임 로직 서버는 REST API 서버와 유사한 구조이고, Django나 Ruby on Rails가 제공하는 템플릿 엔진이나 ORM 등이 크게 필요하지 않았기 때문입니다. node.js에 REST API를 쉽게 작성할 수 있는 모듈인 restify와 MySQL에 접근하기 위한 드라이버인 node-mysql 정도면 충분히 필요한 기능을 다 제공한다고 판단하였습니다.

node.js를 선택한 또 다른 이유는 성능입니다. node.js 엔진인 V8의 성능이 Python이나 Ruby 보다 뛰어나다는 점도 고려 대상이지만, 더 중요한 이유는 node.js가 제공하는 비동기 IO 방식에 있었습니다. C10K 문제로 알려져 있는 서버 성능 이슈의 결론은 요청 하나 하나를 별도의 쓰레드를 만들어서 동기 IO를 하는 방식으로 서버 성능을 확보할 수가 없고, 운영체제가 제공하는 epoll이나 kqueue 같은 시스템을 콜을 사용해서 비동기 IO를 해야한다는 것입니다. node.js는 운영체제에 상관 없이 비동기 IO 메커니즘을 제공하는 libuv라는 라이브러리를 이용해 작성되어 있고, 내부적으로 epoll, kqueue, IOCP 등을 사용하고 있습니다.

물론 세상에 공짜는 없습니다. node.js가 비동기 I/O를 통해 확보한 성능 이점은 불편한 비동기 프로그래밍 모델로 그대로 노출되기 때문입니다. 다음 할 일을 계속 콜백(callback) 함수로 넘기는 스타일로 코딩을 하다 보니 Pyramid of Doom이라고 알려져 있는 다음과 같은 스타일의 코드가 나오게 됩니다. 이 문제를 해결하기 위해 비동기 제어 흐름을 좀 더 쉽게 표현하기 위한 async 모듈, underscore 등도 사용했지만, 이러한 프로그래밍 스타일은 Django나 Ruby on Rails에 비교해서 코드의 가독성을 현저히 떨어뜨리고 있는 것이 사실입니다.

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

다음 할 일을 계속 콜백으로 넘기는 방식은 프로그래밍 언어에서 Continuation Passing Style(CPS)로 알려져 있습니다. CPS는 제너레이터(generator), 코루틴(coroutine), 예외(exception) 등 다양한 프로그래밍의 제어 흐름을 표현할 수 있는 강력한 개념이지만, 사실 컴파일러가 아닌 사람이 사용하기에 편리한 방식이 아닙니다. node.js가 제공하는 프로그래밍 모델은 따지고 보면 비동기 IO를 통해 성능 이점을 제공할 테니, CPS 스타일의 불편함을 감수하라는 얘기가 됩니다.

다행히 이 문제는 여러 차원에서 해결책이 논의 중입니다. 자바스크립트 Promise 라이브러리와 EcmaScript 6의 제너레이터(Generator)의 결합으로 이 문제를 해결하기 위한 방법들이 논의되고 있고, EcmaScript 7은 아에 프로그래밍 언어 자체에 비동기 프로그래밍 지원하기 위한 기능을 추가할 움직임을 보이고 있습니다. (이 부분에 대해서는 다음에 조금 더 자세히 다루겠습니다.)

결론은 모바일 RPG 게임 서버로 node.js를 선택한 것에 대해서는 나름대로 만족하고 있습니다. 다만, 비동기 프로그래밍이 프로그램을 복잡하게 만들고 가독성을 떨어뜨리는 문제에 대해서는 좀 더 고민이 필요하다는 교훈도 얻었습니다. 서버 프로그래밍의 본질은 결국 성능 혹은 규모 가변성이라는 걱정거리(concern)를 어떻게 코드 내에서 분리(seperation)할 수 있는지에 대한 고민인 것 같습니다.

프로그래밍과 수학

모든 공학은 기초 과학에 뿌리를 두고 있습니다. 따라서 해당 학문의 뼈대를 구성하는 기초 과학을 잘 이해하는 것이 엔지니어링을 잘하는 가장 빠른 지름길이기도 합니다. 프로그래밍도 예외는 아니어서 프로그래밍을 잘하기 위해서는 컴퓨터 과학, 더 나아가서 논리학과 수학에 대한 이해가 필수입니다.

저는 게임 개발자를 뽑을 때 주로 컴퓨터공학과(혹은 컴퓨터과학과) 학부에서 배우는 전공 지식에 대해 주로 질문하였습니다. 게임 프로그래밍 경험이 아무리 출중해도 알고리즘의 시간/공간 복잡성(time/space complexity)에 대한 개념도 이해를 못 하고 있다면 개발자로서 앞으로의 발전을 기대하기는 힘들다고 생각하기 때문입니다.

컴퓨터 과학이 수학과 논리학에서 출발학 학문임에도 불구하고 개발자들은 수학과 프로그래밍의 상관 관계를 피부로 체감하지 못하는 경우가 많은 것 같습니다. 저 또한 대학을 수학과로 입학했음에도 불구하고, 대수학 같은 추상적인 수학을 공부하면서 지루해 했고 결국 컴퓨터공학과로 전과 후에는 수학 공부를 멀리 했었습니다. 오히려, 졸업 후에 프로그래밍을 더 공부하다 보니 다시 수학의 필요성을 절감하고 뒤늦게 수학 공부하느라 진땀을 빼고 있는 중입니다.

이번 글에서는 수학 공부에 대한 흥미를 유발하기 위해 수학과 프로그래밍의 재미있는 상관 관계 하나를 보여드리고자 합니다. 일단 우리가 알고 있는 간단한 덧셈부터 시작해 보겠습니다.

1 + 2 = 3

이런 수식의 구성 요소는 숫자(1, 2, 3 등)과 연산자(+, – 등)이 있고, 숫자 대신 변수(x, y, z 등)을 사용할 수도 있습니다. 또한 다음과 같은 법칙들도 존재합니다.

0 + x = x

1 * x = x

위 예에서 알 수 있듯이 대수학이란 크게 3가지 요소로 구성됩니다.

  • 오브젝트(object) – 대수학이 다루는 것.
  • 연산자(operation) – 오브젝트로 새로운 오브젝트를 만드는 것.
  • 법칙(law) – 오브젝트와 연산자들의 상관 관계

여기까지 복습을 했으면 이제 프로그래밍으로 돌아가 봅시다. 1+1=2라는 사실과 프로그래밍이 어떤 관계가 있을까요? C# 타입 시스템을 예로 들어 설명해 보겠습니다.

일단 C# 타입 시스템으로 숫자 0, 1, 2를 만들어 보겠습니다. 타입 시스템과 숫자를 연결시키기 위해 어떤 타입이 가지는 인스턴스의 수를 세는 방법을 사용하겠습니다.

우리가 가장 사랑하는 디자인 패턴인 싱글톤(singletone)은 하나의 인스턴스만 만들 수 있으므로 숫자 1이라고 생각할 수 있습니다. 다음과 같이 Unit이라는 싱글톤 클래스를 선언하였습니다.

class Unit
{
    public static Unit Instance = new Unit();

    private Unit()
    {
    }
}

숫자 0은 어떻게 만들 수 있을까요? 클래스는 있지만 인스턴스를 전혀 만들 수 없는 클래스를 만들면 됩니다.

class Void
{
    private Void()
    {
    }
}

숫자 2는 어떻게 만들 수 있을까요? 해당 타입의 인스턴스를 단 2개만 만들 수 있어야 하는데, 우리는 이런 타입을 이미 알고 있습니다. 바로 bool이죠. 아니면 Unit과 유사한 방식이지만 인스턴스가 2개 있는 타입을 만들 수도 있습니다.

class Bool
{
    public static Bool True = new Bool();
    public static Bool False = new False();

    private Bool()
    {
    }
}

자 이제 숫자는 충분히 있으니 덧셈을 만들어 보겠습니다.

abstract class Sum
{
}

sealed class SumLeft : Sum
{
    private readonly TL value;

    public SumLeft(TL left)
    {
        this.value = left;
    }
}

sealed class SumRight : Sum
{
    private readonly TR value;

    public SumRight(TR right)
    {
        this.value = right;
    }
}

</tl,></tl,></tl,></tl,></tl,>

Sum이라는 클래스는 SumLeftSumRight라는 두 개의 서브클래스만이 존재하는 추상 클래스입니다. 바꿔 말해 Sum은 반드시 SumLeft이거나 SumRight입니다.

이게 왜 덧셈이 되는 걸까요? Sum 타입의 인스턴스를 나열해 보면 알 수 있습니다. 예를 들어, Sum[bool, Unit] 타입의 인스턴스는 SumLeft(true), SumLeft(false), SumRight(Unit) 세 개의 인스턴스를 만들 수 있습니다. bool은 2, Unit은 1이므로 Sum[bool, Unit]은 2+1을 뜻하고 실제로 인스턴스의 수도 3인 것을 확인할 수 있습니다.

곱셈은 더 간단합니다. C#의 클래스 자체가 곱셈을 표현하는 타입이기 때문입니다.

class Product
{
    private readonly TL left;
    private readonly TR right;

    public TL Left { get { return left; } }
    public TR Right { get { return right; } }

    public Product(TL left, TR right)
    {
        this.left = left;
        this.right = right;
    }
}

</tl,>

Product[TL, TR]leftright 2개의 필드를 가지고 있습니다. 이게 곱셈이 되는 이유는 Product[bool, Unit]의 인스턴스 수를 세어 보면 알 수 있습니다. 이 타입은 Product(true, Unit)Product(false, Unit) 두 개의 값을 가집니다. left 값 하나에 대해서 모든 right 값이 반복되므로 Cartesian product이라고 생각할 수도 있습니다.

이제 오브젝트(Void, Unit, bool 등)와 연산자(Sum, Product)를 모두 정의했으므로 우리가 알고 있는 대수학의 법칙이 그대로 적용되는지도 확인해 보겠습니다.

0 + x = x

위 식을 C#의 타입으로 풀어보면 Sum[Void, X] = X가 됩니다. Void는 인스턴스가 존재하지 않는 타입으므로 SumLeft[Void]의 인스턴스는 존재할 수가 없습니다. 따라서 모든 인스턴스는 SumRight[X] 타입이 되고, 전체 인스턴스의 수는 X 타입의 인스턴스 수와 일치하게 됩니다. 0은 덧셈에 대한 항등원이고, VoidSum 연산에 대한 항등원임을 확인할 수 있습니다.

x+y=y+x

위 식도 C# 타입으로 풀어보면 Sum[X, Y] = Sum[Y, X]가 됩니다. Sum[X, Y]의 의미는 인스턴스가 SumLeft[X]이거나 SumRight[Y]라는 뜻이므로 X 타입의 인스턴스 수 + Y 타입의 인스턴스의 수가 됩니다. 반대로 Sum[Y, X]는 인스턴스가 SumLeft[Y]이거나 SumRight[X]라는 뜻이므로 마찬가지로 X 타입의 인스턴스 수 + Y 타입의 인스턴스의 수가 됩니다. C# 타입 시스템에서 Sum[X,Y]Sum[Y,X]는 엄밀히 말해 다른 타입이긴 하지만, 덧셈으로서 의미는 일치함을 알 수 있습니다.

곱셈도 마찬가지 방식으로 법칙을 확인할 수 있습니다.

0 * x = 0

Product[Void, X] 타입의 인스턴스는 존재할 수 없습니다. Void의 인스턴스가 존재하지 않기 때문입니다. 인스턴스의 수가 0이므로 이 타입은 0을 뜻하는 Void와 의미가 일치함을 알 수 있습니다.

1 * x = x

Product[Unit, X] 타입의 인스턴스는 Product(Unit, X)가 됩니다. Unit은 인스턴스가 1개밖에 없으므로 이 타입의 인스턴스 수는 X 타입의 인스턴스 수와 동일합니다.

x * y = y * x

Product[X, Y]Product[Y, X]는 모두 X 타입의 인스턴스 수 * Y 타입의 인스턴스 수를 가지므로 곱셈으로서 의미가 일치한다는 사실도 쉽게 확인할 수 있습니다.

같은 방법으로 배분 법칙(distributive law)과 같은 좀 더 복잡한 법칙도 성립함을 알 수 있습니다.

x * (y + z) = x * y + x * z

이번에 반대 방향으로 C#의 함수 타입이 우리가 알고 있는 대수학과 어떤 관계가 있는지 살펴봅시다. bool -> bool 함수 타입은 C# 타입으로 Func[bool, bool]입니다. 이 타입의 인스턴스 수는 몇 개일까요? booltrue 혹은 false 두 개의 값을 가지므로 Func[bool, bool]은 2 * 2, 즉 4개의 값을 가집니다. 이렇게 보면 곱셈과 비슷한 것 같기도 한데, 확인을 위해 인스턴스가 3개인 타입을 정의해서 다시 한 번 확인해 봅시다. (enum 타입은 임의의 int로 만들어 낼 수 있으므로 엄밀하 말해 인스턴스가 3개는 아니지만, 여기서는 편의를 위해 인스턴스가 3개라고 가정합니다.)

enum Num
{
   One,
   Two,
   Three
}

Func[Num, bool] 타입의 인스턴스 수는 Num의 인스턴스 각각에 대해 true, false 두 개의 값을 가질 수 있으므로 2 * 2 * 2 = 8개임을 알 수 있습니다. 곱셈이 여러 번 반복되는 것은 지수이므로 2^3 = 8로 표현할 수도 있습니다. 바꿔 말해, 인스턴스의 수가 각각 a, b인 Func[A,B]의 값은 b^a이 됩니다.

함수 타입에서도 몇 가지 법칙을 확인할 수 있습니다. 예를 들어, Func[Unit, X]X와 같습니다. Unit 값 하나에 대해 X 타입 인스턴스 수만큼 인스턴스가 존재하기 때문입니다. 반대로 Func[X, Unit]Unit과 같습니다. X의 값에 상관 없이 Unit이 나오기 때문입니다. 이는 우리가 아는 대수학으로 표현하면 각각 a^1 = a, 1^a = 1이 됩니다. 이처럼 함수와 지수는 그 수학적 성질이 같기 때문에 타입 이론에서는 함수 타입을 지수 타입(exponential type)이라고도 부릅니다.

이런 상관 관계를 이용하면 재미있는 사실을 하나 발견할 수 있습니다. 우리가 함수 언어에서 말하는 currying은 인자 여러 개의 함수를 인자 하나짜리 함수로 표현하는 것을 말합니다. 예를 들어, (A, B) => C 타입의 함수는 A => B => C 타입으로 표현될 수 있습니다. C#의 람다 표현식(lamba expression)으로 적어보면 다음과 같습니다.

Func add = (x, y) => x + y;
int a = add(2, 3);  // a = 5

Func<int, func> curriedAdd = x => y => x + y;
int b = curriedAdd(2)(3); // b = 5

</int,></int,>

add 함수는 인자 x, y를 받아서 x+y를 계산하는 함수입니다. curriedAdd 함수는 인자 ‘x’를 받아서 인자 y를 받아 x+y를 리턴하는 함수를 리턴합니다. 여기에 인자 2,3을 차례대로 호출하면 add와 마찬가지로 5가 리턴되는 것을 확인할 수 있습니다.

참고로 (int, int)가 인자인 함수는 Product[int, int] 타입을 받는 인자 하나짜리 함수로 생각할 수 있습니다. 따라서 (A, B) => C 타입의 함수를 지수로 쓰면 c^(ab)가 됩니다. 이는 c^(ba)와 같고 (c^b)^a와 같습니다. 여기서 다시 함수 타입으로 돌아가면 A => (B => C)가 나옵니다. 인자 여러 개를 받는 함수를 인자 하나를 받는 함수를 반복해서 표현할 수 있다는 currying을 지수로 표현하면 이미 우리가 알고 있는 간단한 연산만으로 유도할 수 있는 셈입니다.

한 발 더 나가서 이번에는 재귀적 데이터 타입인 리스트(List)를 정의해 보겠습니다. C#에서 리스트는 여러 방식으로 구현할 수 있는데, 여기서는 함수 언어에서 사용하는 전통적인 방식의 리스트를 정의하겠습니다. List[T]는 두 가지 경우의 수가 있는데, 빈 리스트를 뜻하는 Nil이거나 T 타입의 tail과 List[T] 타입의 tail로 구성된 Cons가 있습니다.

이를 우리가 앞서 정의한 SumProduct 타입으로 표현해 보면 List[T]Sum[Unit, Product[T, List[T]]]가 됩니다. 풀어서 설명하면 리스트는 빈 리스트를 뜻하는 SumLeft[Unit]이거나 TList[T]로 이루어진 SumRight[Product[T, List[T]]]입니다.

SumProduct는 각각 덧셈과 곱셈, Unit은 1로 대체할 수 있으므로 위 리스트 타입을 대수학으로 표현하면 다음과 같이 됩니다.

L(T) = 1 + T * L(T)

수식을 풀어보면 다음과 같습니다.

L(T) – T * L(T) = 1

(1 – T) * L(T) = 1

L(T) = 1 / (1 – T)

여기까지는 간단한 계산입니다. 1 / (1 – T)를 테일러 시리즈(Taylor Series)로 풀어보면 다음과 같습니다.

L(T) = 1 + T + T^2 + T^3 + T^4 + …

이 수식을 다시 타입으로 들고 와서 해석해 보면 List[T]Unit이거나 T이거나 Product[T, T]이거나 Product[T, T, T]이거나… 라는 식으로 해석이 되고 이는 정확히 리스트 타입이 의미하는 바를 나타냅니다. Tint를 대입해보면 각각의 타입이 [], [1], [1,2], [1,2,3]와 같은 리스트의 값을 표현하고 있기 때문입니다.

여기서 가장 재미있는 점은 우리가 타입에 대해서는 뺄셈이나 나눗셈 연산을 전혀 정의하지 않았음에도 불구하고, 타입을 대수로 옮긴 다음에 뺄셈, 나눗셈 및 테일러 시리즈까지 수행한 결과를 다시 타입으로 들고 와도 여전히 유효하다는 사실입니다. 단순히 우연일까요? 이게 가능한 이유는 타입과 대수가 모두 semiring이라는 수학적 구조를 따르고 있기 때문입니다.

설명이 길어졌는데, 우리가 매일 매일 사용하는 프로그래밍 언어에도 우리가 생각치 못한 수많은 수학적 원리들이 숨어 있습니다. 앞서 살펴본 것처럼 우리가 늘 사용하는 간단한 사칙 연산조차도 프로그래밍 언어의 타입 시스템과 깊은 관계가 있음을 알 수 있습니다. 따라서 프로그래밍을 좀 더 깊이 있게 공부하려면 이런 수학, 논리학과 같은 기초에 관심을 가지고 조금 더 공부해 볼 것을 권합니다.

참고로 이 내용은 헤스켈로 설명한 The Algebra of Algebraic Data Types 강연 내용을 이해하기 쉽게 C#으로 정리하였습니다. 좀 더 깊이 있는 내용을 원하시면 해당 강연을 들어보시기 바랍니다.

Unity가 직면한 기술적 문제들

UPDATE: 이 글에 대한 피드백을 정리한 Unity가 직면한 기술적 문제들 2를 작성했습니다.

자체 게임 엔진을 개발할 여력이 안 되는 중소 모바일 게임 개발사가 선택할 수 있는 게임 엔진은 크게 Unity3DUnreal Engine으로 양분되는 것 같습니다. 제가 맡았던 두 팀은 이미 Unity로 게임을 제작하고 있었고, 팀원들도 모두 Unity 에디터가 손에 익었기 때문에 별다른 고민 없이 Unity를 사용해 프로젝트를 계속 진행하게 되었습니다.

Unity가 모바일 게임 개발에 기여한 바는 이론의 여지가 없을 정도로 큽니다. .NET을 이용한 크로스 플랫폼 개발 환경 제공, C#과 같은 고수준 프로그래밍 언어의 사용, 편리한 에디터 등 Unity의 장점은 매력적이었습니다. C, C++, J2ME로 피처폰 게임을 개발하던 방식과 비교하면 격세지감을 느낄 수 있는 수준입니다.

하지만 시간이 지나면 혁신의 속도도 늦어지는 법인지, 제가 지난 1년 2개월간 경험한 Unity는 다음과 같이 몇 가지 기술적인 문제점을 노출하고 있었습니다.

  • 정체된 .NET 버전

Unity는 .NET을 오픈소스로 구현한 Mono 엔진을 사용하고 있습니다. 문제는 굉장히 오래된 Mono 엔진을 사용하고 있다는 점입니다. Unity 4.5.1f3의 Mono 버전은 2.0.50727.1433인데, Mono 2.0이 2008년에 릴리즈되었다는 점을 생각하면 골동품 수준의 엔진을 사용하고 있습니다. 최근에 릴리즈된 Unity 5에서도 기대와는 달리 Mono 버전의 업그레이드는 없었습니다.

Mono 버전이 오래되었다는 건 최신의 .NET 기술을 전혀 사용할 수 없다는 뜻입니다. 일례로, Unity는 비동기 프로그래밍에 상당한 발전을 가져온 .NET의 Task Parallel Library나 C#의 async, await 키워드 등을 사용할 수 없습니다.

Unity가 사용하는 Mono는 .NET 버전 기준으로 3.5 + 일부 4.0 API가 포함되어 있는 수준입니다. 요즘 대부분의 .NET 개발자들은 .NET 최소 버전을 4.0으로 잡고 있기 때문에 최신의 .NET 라이브러리는 수정 없이 Unity에서 사용하기가 어려워지고 있습니다. 어떤 라이브러리를 Unity에 사용하려면 별도의 수정 작업이 필요하다는 건, 더 이상 Unity를 .NET 개발 환경이라고 부르기 힘들어졌다는 뜻입니다. 일례로, Reactive Programming의 유행을 가져온 Rx는 Unity에서 지원되지 않습니다. 대신 비슷한 개념을 Unity에서 사용할 수 있게 새로 만든 UniRx라는 별도의 라이브러리가 나와 있습니다.

  • 안정성과 성능

Unity가 직면한 또 다른 문제점은 안정성과 성능입니다. 특히, iOS 빌드에 사용하는 AOT 컴파일러는 심각한 수준의 문제가 있습니다. 일례로, LINQ를 사용한 코드는 iOS 빌드 시에 크래시를 일으킵니다. LINQ가 아에 안 되는 것도 아니고 LINQ의 특정한 조합에서만 발생하는 문제이기 때문에 디버깅도 쉽지 않습니다. 이 문제를 피하기 위해 아에 LINQ 사용을 금지하는 것이 가장 현실적인 조언이 될 정도입니다.

이 문제도 노후한 Mono 엔진과 연결이 되어 있습니다. iOS 빌드 시 발생하는 크래시 문제도 Mono 2.0에 포함된 AOT 컴파일러의 안정성에 문제가 있기 때문에 발생합니다.

Mono 엔진 자체는 2.0 버전 이후 이후 안정성과 성능에 상당한 개선이 있었습니다. 또한, 최근 마이크로소프트가 coreclrcorefx라는 이름으로 .NET 코드를 오픈소스로 공개하였는데, Mono 4.0부터는 필요한 부분에 대해서는 마이크로소프트 코드를 일부 차용하기 시작하여 안정성이나 호환성이 크게 개선되기 시작하였습니다.

문제는 Unity는 이런 Mono 엔진의 성능 및 안정성 개선 혜택을 전혀 누리지 못하고 있다는 점입니다. Unity 엔진은 계속해서 Mono 2.0에 머무르고 있고, 엔진 업그레이드를 못 하고 있는 이유가 단순히 인력 부족은 아닌 것으로 보입니다. 들리는 루머에 따르면 Mono 버전 업그레이드와 관련하여 Mono의 개발사인 Xamarin과 라이선스 협상에 문제가 있었고, 그 결과 Unity는 단순히 Mono 버전을 업그레이드하는 대신 이 문제를 전혀 다른 방식으로 문제를 풀기로 결정합니다.

  • IL2CPP 개발

Unity가 노후화된 Mono 엔진의 성능 및 안정성 문제를 한 번에 해결하기 위해 들고 나온 해결책은 IL2CPP라는 기술입니다. 이 기술을 간단히 말해 C#을 컴파일해서 나온 중간 언어인 IL을 컴파일해 C++를 출력한 다음 다시 각 플랫폼이 제공하는 C++ 컴파일러를 이용해 네이티브 코드를 만드는 기술입니다.

Unity가 IL2CPP를 통해 목표하는 바는 제가 앞서 제기한 Unity의 문제점과 정확히 일치합니다. 노후화된 Mono 런타임의 성능 및 안정성 문제를 해결할 뿐만 아니라, .NET 버전도 업그레이드하겠다는 것입니다. 하지만 IL2CPP 자체는 특별한 게 없습니다. Mono의 AOT 컴파일러를 대체하는 또 다른 AOT 컴파일러이기 때문입니다. 다만, 그동안은 Mono를 주력으로 사용하고, iOS 등 라이선스 정책 때문에 어쩔 수 없이 AOT 컴파일을 해야하는 상황에서만 AOT 컴파일러를 사용한 것과 달리 될 수 있으면 모든 플랫폼에 AOT를 적용하겠다는 게 달라진 부분입니다. 물론, 이는 성능이나 안정성 측면에서 IL2CPP가 제공하는 AOT 컴파일러 및 런타임이 Mono 런타임을 뛰어넘었을 경우에만 가능한 옵션이기도 합니다.

  • 노출된 문제점

IL2CPP은 크게 AOT 컴파일러와 VM으로 구성되어 있습니다. 바꿔 말해 Mono를 완전히 대체하는 대신 .NET API 구현 등 기존 Mono 인프라는 최대한 살려서 이용하고 성능 및 안정성, 이식성 등의 핵심이 되는 엔진만 교체하겠다는 구상입니다. 이 계획 자체는 상당히 합리적이라고 생각하지만, 문제는 시간입니다. .NET이라는 게 작은 플랫폼이 아니고, 다양한 플랫폼에서 새로운 엔진의 성능을 개선하고 안정성을 확보하는 것도 상당한 노력이 들어가는 작업입니다.

이 과정에서 가장 큰 변수는 애플의 64비트 지원 강제 정책입니다. iOS에 신규로 등록되는 모든 앱에 대해서 64비트 지원을 강제하고, 약간의 유예 기간이 있지만 기존 앱에 대해서도 64비트 지원을 요구하면서 Unity는 발등에 불이 떨어지게 됩니다. 왜냐하면, iOS 빌드에 사용해온 기존 Mono AOT 컴파일러가 64비트 빌드를 지원하지 않고 있기 때문에, 64비트 빌드를 지원하려면 반드시 IL2CPP를 사용해야 하는 상황이 되었기 때문입니다.

결국 Unity는 울며겨자 먹기 식으로 아직 안정성이 확보되지 않은 IL2CPP를 Unity 4와 5에 탑재하게 됩니다. UNITY 4.6.2 IOS 64-BIT SUPPORT 릴리즈 노트를 보면 BeginInvoke/EndInvoke등 .NET의 기본적인 API도 동작 안 하는 상태에서 iOS 64 비트 지원을 발표합니다. 이후 Unity는 거의 매주 업데이트 및 패치를 진행하면서 IL2CPP 버그에 대응하고 있는 상황입니다.

정리하면, 지금 Unity가 직면한 기술적인 문제들은 Unity의 미래를 흔들 수도 있을 만큼 심각합니다. IL2CPP가 이 문제에 대한 궁극적인 해결책이 되기도 어렵습니다. 단순한 안정성이나 성능 확보에도 어려움을 겪고 있는 상황에서 빠른 속도로 발전하는 .NET 기술을 따라가는 것이 게임 엔진 개발사의 Unity 입장에서 쉬운 일이 아니기 때문입니다.

결국 .NET 기술을 주도하고 있는 마이크로소프트와 어떤 식으로는 협력을 모델을 끝어내지 않으면 이런 한계를 넘어서기는 어려워 보입니다. 더군다나 마이크로소프트가 .NET 기술을 오픈소소화하면서 여러 가지 변화를 꾀하고 있는 상황이라 Unity가 직면한 불확싱성이 어느 때보다 커보입니다. 게임 개발사 입장에서 Unity는 여전히 좋은 도구이지만, 앞으로 계속 투자할 수 있는 기술인지에 대해서는 의문이 남는 대목입니다.

코루틴(Coroutine) 이해하기

Unity 프로그래밍에서 중요한 개념 중 하나는 코루틴(Coroutine)입니다. 게임 프로그래밍은 수많은 게임 오브젝트들의 상호 작용으로 표현될수 밖에 없기 때문에 어떤 식으로는 동시성(concurrency)을 표현하는 것이 중요합니다. Unity는 .NET을 사용함에도 불구하고 멀티쓰레드가 아닌 코루틴으로 동시성을 표현하는데, 이는 멀티쓰레드 프로그래밍이 버그 없는 코드를 작성하기 어렵기로 악명 높기 때문입니다.

일단 코루틴이 무엇인지 용어 정의부터 시작하겠습니다. 코루틴은 서브루틴(C#의 메소드에 해당)을 다음 두 축으로 확장한 강력한 서브루틴입니다.

  • 여러 개의 입구(entry point)를 허용하여 실행 재개가 가능
  • 멈춤 시 돌아갈 위치 지정

일반적인 서브루틴은 호출될 때마다 메소드의 처음부터 실행을 시작하고 리턴될 때 항상 호출자(caller)로 돌아가는 반면, 코루틴은 이전 상태를 기억하고 있어서 호출 시 이전 호출에서 멈춘 위치에서부터 다시 실행을 재개하게 되고, 멈출 때는 호출자로 돌아가는 대신 돌아갈 위치를 지정할 수 있습니다.

Unity 프로그래밍에서 코루틴이 중요한 이유는 Unity는 코루틴을 협력형 멀티태스킹(cooperative multitasking)을 구현하는 용도로 사용하기 때문입니다. 협력형 멀티태스킹은 일종의 시분할(time-sharing) 방식으로 여러 task가 하나의 CPU를 나눠쓰는 방식인데, 선점형 멀티태스킹(preemptive multitasking)과 달리 운영체제의 개입 없이 각 task가 독점적으로 CPU를 사용하고 사용이 끝나면 자발적으로 양보하는 방식입니다. 이 방식의 장점은 리소스를 사용하는 도중에 강제로 CPU를 뺏기는 일이 없으므로, 크리티컬 섹션(critical section)을 보호하기 위한 락(lock)이나 세마포어(semaphore) 등 동기화 수단이 필요 없다는 점입니다.

그럼 코루틴으로 협력형 멀티태스킹을 어떻게 구현하는 걸까요? 이해를 돕기 위해 큐에 아이템을 생성하여 집어넣는 produce task와 큐에서 아이템을 꺼내와서 사용하는 consume task를 의사코드(pseudo code)로 구현해 보겠습니다. 이 예제는 위키피디아의 Coroutine 페이지를 참고하였습니다.

var q := new queue
coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume

produce task는 q가 가득 차지 않았으면 새로운 아이템을 생성하여 q에 집어 넣습니다. q가 가득 찼으면 더 이상 할 수 있는 일이 없으므로 consume task에 양보합니다.

coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce

consume task는 q가 가득 차 있으면 q에서 아이템을 꺼내서 사용합니다. 꺼낼 아이템이 없으면 더 이상 할 수 있는 일이 없으므로 produce task에 양보합니다.

위 코드에서 주목할 부분은 별도의 스케쥴러가 없다는 점입니다. produce task와 consume task 모두 다음 task를 명시적으로 지정하고 있기 때문에 제3자가 개입하여 다음 task를 지정할 필요가 없습니다. “협력형 멀티태스킹”이란 말도 이런 특징에서 기인하였습니다.

여기까지 이해했으면 의문점이 생기게 됩니다. 코루틴으로 협력형 멀티태스킹을 구현할 수 있다는 건 알겠는데, 정작 Unity게임 프로그래밍에 사용하는 C#이란 언어는 코루틴을 제공하지 않기 때문입니다. C#이 코루틴이 없는데, 어떻게 Unity는 코루틴으로 협력형 멀티태스팅을 구현하는 걸까요? 정답은 C#의 이터레이터(iterator)를 사용하는 것입니다.

C# 개발자라면 yield 키워드로 대표되는 C#의 이터레이터를 사용해 본 경험이 있을 겁니다. C# 이터레이터는 프로그래머가 IEnumerableIEnumerator를 쉽게 구현할 수 있게 프로그래밍 언어가 제공하는 기능입니다. 파이썬, EcmaScript 6 등 다른 프로그래밍 언어에서는 제너레이터(generator)로 알려져 있는 기능이기도 합니다. 이해를 돕기 위해 다시 위키피디아 Generator 페이지에 나와있는 C# 이터레이터 예를 하나 살펴보겠습니다.

public class CityCollection : IEnumerable {
    public IEnumerator GetEnumerator() {
        yield return "New York";
        yield return "Paris";
        yield return "London";
    }
}

위 예제의 GetEnumerator 함수는 IEnumerator 타입을 리턴하는데, 이 IEnumeratorMoveNext() 메소드는 호출될 때마다 “New York”, “Paris”, “London”을 차례대로 리턴하게 됩니다. 다시 말해, 일반적인 서브루틴과 달리 위 메소드는 yield return이 값을 리턴하고 이 위치를 기억해 두었다가 다음 호출 시에는 그 다음 위치부터 실행을 재게합니다.

입구가 여러 개이고 실행이 멈춘 지점을 기억했다가 다시 재개한다는 점은 앞서 설명한 코루틴에 대한 설명과 무척 닮았습니다. 물론 코루틴은 리턴될 때 다음 실행할 위치를 명시적으로 지정할 수 있고, 제너레이터는 반드시 호출자로 돌아간다는 차이점도 있습니다. 이런 유사점 때문에 제너레이터를 세미 코루틴(semi coroutine)이라고 부르기도 합니다. 한국말로는 반쪽 코루틴 혹은 약한 코루틴으로 표현할 수도 있겠네요.

여기서 제너레이터-코루틴-협력형 멀티태스킹의 연결 고리가 생기게 됩니다. 제너레이터가 있으면 코루틴을 만들어낼 수 있기 때문입니다. 앞서 살펴본 produce, consume 코루틴의 예를 제너레이터로 다시 구현해 보겠습니다.

generator produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield consume
generator consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield produce

코루틴 예와 달라진 부분은 yield to consumeyield to produce가 각각 yield consumeyield produce로 바뀌었다는 점입니다. 고작 to라는 단어 하나가 빠진 것 같지만, 실제로는 큰 차이가 있습니다. 코루틴의 yield to consumeconsume 코루틴 실행을 재개하라는 뜻이고, yield consume는 이 제너레이터를 호출한 호출자로 리턴되는 값이 consume이라는 뜻입니다.

yield consume의 의미는 함수가 first-class인 자바스크립트나 함수형 언어를 생각하면 쉽게 이해됩니다. consume이라는 제너레이터를 함수 값으로 생각하고 단순히 리턴하는 것입니다. 따라서 consume을 리턴한다고 해서 자동으로 consume 제너레이터가 실행되는 것이 아닙니다. 제너레이터를 코루틴처럼 사용하기 위해서는 다음과 같이 다음으로 실행할 task를 지정해 줄 스케쥴러가 필요하게 됩니다.

subroutine dispatcher
    var d := new dictionary(generator → iterator)
    d[produce] := start produce
    d[consume] := start consume
    var current := produce
    loop
        current := next d[current]

위 코드의 dispatcher는 스케쥴러 역할을 합니다. 일단 dispatcher가 먼저 실행되어 produceconsume 제너레이터를 만들고 루프를 돌면서 제너레이터가 리턴하는 값을 이용해 다음 task를 정하는 방식입니다. 위 예제의 경우 current의 값이 produceconsume로 계속 바뀌면서 번갈아 실행되게 되므로 앞서 살펴본 코루틴의 예제와 마찬가지로 협력형 멀티태스킹이 구현됩니다.

꼭 각 제너레이터가 다음 task를 지정하는 방식이 아니어도 괜찮습니다. 스케쥴러가 전체 task 목록을 가지고 스케쥴링 하는 방식이 되면 일반적인 운영체제의 스케쥴러와 마찬가지로 다양한 스케쥴링 알고리즘을 적용할 수도 있습니다. 예를 들어 yield 문에 다음으로 실행할 제너레이터를 지정하는 대신에 해당 task가 다시 스케쥴링될 시간을 지정할 수도 있습니다. 그리고 이 방식이 바로 Unity가 협력형 멀티태스킹을 구현하는 방법입니다.

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

위 코드는 yield 문이 있는 C#의 이터레이터이고 따라서 리턴 타입도 IEnumerator입니다. 하지만 이 코드가 실제로 의미하는 바는 여러 개의 입구가 존재하는 제너레이터이고 yield를 통해 해당 task가 다음에 스케쥴링될 시간을 지정하고 있습니다. Unity 내부에는 이런 제너레이터의 목록을 가지고 있는 스케쥴러가 있어서 앞서 살펴본 방식과 동일한 방식으로 스케쥴링을 해주고 있습니다. MonoBehaviour.StartCoroutine 메소드가 Unity 스케쥴러에 코루틴을 추가하는 방법입니다.

정리하면

  • C#의 이터레이터는 제너레이터이다.
  • 제너레이터는 세미 코루틴이라고도 불리며 스케쥴러의 도움을 받으면 코루틴을 구현할 수 있다.
  • 코루틴을 이용하면 협력형 멀티태스킹이 가능하다.

제너레이터로 코루틴 및 협력형 멀티태스킹을 구현하는 방식은 Unity뿐만 아니라 여러 프레임워크나 라이브러가 사용해 온 일반적인 방법입니다. C++ 대신 쉬운 C#으로 코딩하라는 모토를 가지고 나온 Unity 입장에서 프로그래머가 실수하기 쉬운 멀티쓰레드 프로그래밍보다는 코루틴을 이용한 협력형 멀티태스킹을 강조한 것도 이해가 되는 부분입니다.

하지만 제너레이터를 이용한 코루틴 구현은 어느 정도 “해킹”스러운 면이 있고, IEnumerator를 본래 의미인 여러 collection 타입에 대한 단일 인터페이스가 아닌 전혀 다른 의미로 사용하면서 프로그래머들에게 혼란을 초래하는 면이 있습니다. 게다가 이후 .NET의 Task Parallel Library, C# 5의 async 키워드 등이 나오고 .NET의 멀티쓰레드 프로그래밍 모델이 상당한 발전을 하면서 Unity의 협력형 멀티태스킹 방식의 장점이 상당 부분 희석되었다고 생각합니다. 이 부분은 다음에 다시 한 번 다루도록 하겠습니다.

코드 스멜

코드 스멜이란 문제 있는 코드에서 보이는 현상을 지칭하는데, 게임 코드를 리뷰하면서 가장 많이 발견한 코드 스멜은 Primitive Obsession입니다.

Primitive Obsession은 쉽게 말해 class나 struct를 선언하는 대신 int, double, string 같은 기본적인 타입만 사용해서 프로그램 내 모든 데이터를 표현하는 것을 의미합니다. 특히, 여러 필드와 메소드를 가지는 class를 선언해야 하는 상황에도 string에 모든 데이터를 인코딩해서 넣고, 메소드는 갈 곳 없이 코드 여기저기 흩어져 있는 경우를 자주 발견하였습니다.

Primitive Obsession을 포함하여 여러 코드 스멜이 나오는 과정을 보면 흥미롭습니다. 처음부터 문제 있는 코드를 작성하는 경우는 생각보다 드뭅니다. 프로젝트 초기에는 시간도 충분히 있고, 요구사항도 복잡하지 않기 때문에 나름의 설계도 하고 class도 적절하게 만들게 됩니다.

문제는 게임 기획 변경으로 새로운 요구사항이 나오면 이 요구사항을 수용할 수 있게 기존 코드를 어느 정도 리팩토링하는 과정이 필요한데, 대게의 개발자들은 구조 변경 없이 최소한의 변경만으로 해당 요구사항만 어떻게든 구현하고 싶어합니다. 당연히 새로운 클래스를 만드는 일도 꺼리기 마련이고, 적당히 string 타입으로 현재 존재하는 클래스 어딘가에 필드로 추가하게 됩니다.

코드에 문제가 있다고 생각되면 그때 그때 정리하는 작업이 그래서 중요합니다. 새로운 요구사항이 나오면 현재 코드가 이 요구사항을 구현하기에 적당한지 먼저 판단하고 기존 코드를 유연하게 바꾸는 작업이 선행되어야 합니다. 코드 정리가 시간 있어야만 할 수 있는 사치가 아니라, 앞으로 남은 프로젝트 기간을 단축시켜 주는 유일한 방법임도 깨달아야 합니다.

Creating a ServerSentEventServer with ReactiveWebServer

Recently, I am working on a toy project named ReactiveWebServer. It is an event-driven web server built with Rx.

Here’s a simple web server returning “Hello World”.

using (var ws = new WebServer("http://*:8080/"))
{
    const string responseBody = "Hello World";

    ws.GET("/")
        .Subscribe(ctx => ctx.Respond(responseBody));

    Console.ReadLine();
}

The code is simple, but there is nothing new compared to other web servers such as NancyFx.

The real power of ReactiveWebServer comes from its ability to handle streaming data. For example, here’s a ServerSentEvent server implementation streaming integers every second.

using (var ws = new WebServer("http://*:8000/"))
{
    ws.GET("/events").Subscribe(ctx =>
    {
        var obs = Observable.Interval(TimeSpan.FromSeconds(1))
            .Select(t => new ServerSentEvent(t.ToString()));

        ctx.Respond(new ServerSentEventsResponse(obs));
    });

    Console.ReadLine();
}

Data streaming is made simple and easy. You just need to create an instance of IObservable<ServerSentEvents>, wrap it with ServerSentEventsResponse, and pass the result to ctx.Respond method.