Thrift# – 특성 기반 .NET Thrift 클라이언트 라이브러리

개요

Apache Thrift는 Facebook에서 개발되어 Apache에서 오픈 소스화된 통신 프레임워크로 다양한 프로그래밍 언어로 개발된 구성요소 사이의 인터페이스를 제공하는 역할을 합니다. 며칠 전 회사 팀에서 Thrift를 위한 .NET 클라이언트 라이브러리인 Thrift#에 대한 조사 업무를 할당받아 간단한 테스트를 진행했습니다. 이 과정과 결과를 정리합니다.

Apache Thrift

이 포스트의 주제는 .NET 라이브러리인 Thrift#이기 때문에 Thrift 개념과 사용법에 대해 자세히 다루지는 않지만 기반 기술에 대한 얘기를 전혀 하지않고 진행할 수는 없으니 꼭 필요하다고 생각되는 내용만 간략히 정리 하겠습니다.

정의

Apache Thrift는 서로 다른 플랫폼 사이에 메시지를 주고 받는 프로토콜과 이를 지원하는 번역기(코드 생성기)를 포함한 프레임워크입니다. 다음은 공식 사이트의 Thrift에 대한 정의입니다.

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages.

인터페이스 명세 언어(IDL)

메시징에 사용되는 형식과 서비스 인터페이스는 인터페이스 명세 언어(Interface Description Language, IDL)를 사용해 정의합니다. 데이터와 서비스를 *.thrift 파일에 작성하고 Thrift 컴파일러를 사용해 특정 플랫폼에 사용할 수 있는 언어로 번역한 뒤 제공되는 라이브러리를 사용해 서비스를 공급하거나 소비합니다.

이 포스트에서 사용할 my-service.thrift 파일의 내용은 아래와 같습니다.

struct Contact {
  1: i32 id,
  2: required string firstName,
  3: required string lastName,
  4: required string email
}

service ContactsService {
  list<Contact> getContacts(),
  list<Contact> addContacts(1: list<Contact> contacts)
}

ContactsService는 연락처를 관리하는 서비스입니다. getContacts() 서비스 함수는 모든 연락처를 반환하고 addContacts() 서비스 함수는 연락처 목록을 입력받아 저장하고 작업이 완료되면 저장된 연락처 목록을 반환합니다. 이때 반환된 연락처는 서버에서 부여받은 식별자를 가집니다.

설치

*.thrift 파일을 번역하려면 컴파일러를 설치해야 합니다. 저는 개인용 개발 장비로 MacBook을 사용하기 때문에 Homebrew를 사용해 Thrift를 설치했습니다.

$ brew install thrift

플랫폼 별 설치 방법은 여기에 있습니다.

Thrift#

Thrift#은 .NET 플랫폼을 위한 Thrift 클라이언트 라이브러리입니다. IDL 번역 파일을 사용하는 대신 특성(attributes)를 사용해 통신 계약을 준수합니다. 현재 Thrift#는 이진 프로토콜과  HTTP 전송만을 지원합니다.

Thrift 컴파일러를 통해 번역된 코드와 Thrift#을 사용할 때 작성하는 코드를 비교해 보겠습니다. 먼저 Thrift 컴파일러가 만들어낸 코드를 살펴봅니다. 코드가 조금 길지만 비교를 위해 전체 코드를 포함…시키려 했지만 파일 2개에 900줄은 길어도 너무 기네요. 고작 서비스 함수 둘 뿐인데 말이죠. 일부만 발췌하고 전체 코드는 링크로 대신합니다.

코드 생성

$ thrift -r --gen csharp my-service.thrift

Contact.cs

/**
 * Autogenerated by Thrift Compiler (0.9.1)
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 *  @generated
 */
using System;
...

#if !SILVERLIGHT
[Serializable]
#endif
public partial class Contact : TBase
{
    private int _id;

    public int Id
    {
        get
        {
            return _id;
        }
        set
        {
            __isset.id = true;
            this._id = value;
        }
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    public Isset __isset;
#if !SILVERLIGHT
    [Serializable]
#endif
    public struct Isset
    {
        public bool id;
    }

    public Contact()
    {
    }

    public Contact(string firstName, string lastName, string email)
        : this()
    {
        this.FirstName = firstName;
        this.LastName = lastName;
        this.Email = email;
    }

    public void Read(TProtocol iprot)
    {
        ...
    }

    public void Write(TProtocol oprot)
    {
        ...
    }

    ...
}

ContactsService.cs

/**
 * Autogenerated by Thrift Compiler (0.9.1)
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 *  @generated
 */
using System;
...

public partial class ContactsService
{
    public interface Iface
    {
        List<Contact> getContacts();
#if SILVERLIGHT
    IAsyncResult Begin_getContacts(AsyncCallback callback, object state);
    List<Contact> End_getContacts(IAsyncResult asyncResult);
#endif
        List<Contact> addContacts(List<Contact> contacts);
#if SILVERLIGHT
    IAsyncResult Begin_addContacts(AsyncCallback callback, object state, List<Contact> contacts);
    List<Contact> End_addContacts(IAsyncResult asyncResult);
#endif
    }

    public class Client : IDisposable, Iface
    {
        ...
    }

    public class Processor : TProcessor
    {
        ...
    }

#if !SILVERLIGHT
    [Serializable]
#endif
    public partial class getContacts_args : TBase
    {
        ...
    }

#if !SILVERLIGHT
    [Serializable]
#endif
    public partial class getContacts_result : TBase
    {
        ...
    }

#if !SILVERLIGHT
    [Serializable]
#endif
    public partial class addContacts_args : TBase
    {
        ...
    }

#if !SILVERLIGHT
    [Serializable]
#endif
    public partial class addContacts_result : TBase
    {
        ...
    }
}

Silverlight까지 꼼꼼하게 고려된 코드입니다. 🙂

다음은 Thrift#을 이용할 때 필요한 코드입니다.

[ThriftStruct("Contact")]
public class Contact
{
    [ThriftField(1, false, "id")]
    public int Id { get; set; }
    [ThriftField(2, true, "firstName")]
    public string FirstName { get; set; }
    [ThriftField(3, true, "lastName")]
    public string LastName { get; set; }
    [ThriftField(4, true, "email")]
    public string Email { get; set; }

    public override string ToString()
    {
        return string.Format("Contact(Id: {0}, FirstName: {1}, LastName: {2}, Email: {3})", this.Id, this.FirstName, this.LastName, this.Email);
    }
}

[ThriftService("ContactsService")]
public interface IContactsService
{
    [ThriftMethod("getContacts")]
    List<Contact> GetContacts();
    [ThriftMethod("addContacts")]
    List<Contact> AddContacts([ThriftParameter(1, "contacts")]List<Contact> contacts);
}

직접 작성하는 것이 조금 귀찮기는 하지만 자동 생성된 코드에 비해 매우 간결합니다. 다양한 특성을 사용해 통신 계약과 .NET 코드를 연결합니다.

Thrift HTTP 서버

서비스 클라이언트를 만들어 테스트하기 전에 서버를 구축해야 합니다. 앞서 언급했듯이 현재 버전(1.0.8)의 Thrift#은 HTTP 전송만 지원하기 때문에 HTTP 서버를 만들어야죠.

다중 플랫폼을 지원하는 코드를 테스트 목적으로 빠르게 작성하고 싶을 때 Node.js는 아주 훌륭합니다. 이번에도 제가 가장 먼저 떠올린 도구는 Node.js입니다. Node.js용 Thrift 라이브러리를 뒤지다 보니 createHttpServer() 메서드를 제공합니다. 이걸 사용하면 쉽게 HTTP 서버를 만들 수 있을 것 같습니다. 하지만 저에게 돌아온 건 차가운 오류 메시지였습니다.

$ node server

/Users/.../thrift-server/server.js:12
var server = thrift.createHttpServer(ContactsService, {
                    ^
TypeError: Object #<Object> has no method 'createHttpServer'
    at Object.<anonymous> (/Users/.../thrift-server/server.js:12:21)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:901:3

이런 젠장, 말년에 버전이 안맞다니! 현재 npm에 마지막으로 배포된 버전은 0.9.1이고 createHttpServer() 메서드는 1.0.0-dev 버전에 포함되어 있습니다. 개발중인 버전을 직접 받아서 작업할까 잠깐 생각했지만 컴파일러 버전과 일치하지 않아 불길해서 OWIN 쪽으로 눈을 돌렸습니다.

OWIN(Open Web Interface for .NET)은 .NET 응용프로그램을 서버로부터 독립시키는 인터페이스이고 Katana 프로젝트OWIN을 지원하는 컴포넌트 집합입니다. 테스트용 서버를 구축하기 위해 IIS와 ASP.NET을 사용하는 것은 과하다고 생각되어 Microsoft.Owin.SelfHost를 사용해 간단히 HTTP Thrift 서버를 만듭니다.

콘솔 응용프로그램 프로젝트를 생성하고 Nuget 패키지 관리자에서 Microsoft.Owin.SelfHost 패키지와 Thrift 패키지를 설치합니다.

그 다음 서비스를 구현합니다.

public class ContactsServiceHandler : ContactsService.Iface
{
    private List<Contact> contacts = new List<Contact>
    {
        new Contact { Id = 1, FirstName = "Tony", LastName = "Stark", Email = "ironman@avenger.com" },
        new Contact { Id = 2, FirstName = "Bruce", LastName = "Banner", Email = "hulk@avenger.com" },
        new Contact { Id = 3, FirstName = "Thor", LastName = "Odinson", Email = "thor@avenger.com" }
    };

    List<Contact> ContactsService.Iface.getContacts()
    {
        return this.contacts;
    }

    List<Contact> ContactsService.Iface.addContacts(List<Contact> contacts)
    {
        int nextId = this.contacts.Select(e => e.Id).Max();
        foreach (var contact in contacts)
        {
            contact.Id = ++nextId;
            this.contacts.Add(contact);
        }
        return contacts;
    }
}

뭐 별로 특별할 것 없는 모의 서비스 구현입니다. 이제 서비스 호스트 코드를 작성합니다.

public class ThriftHttpHandler : THttpHandler
{
    public ThriftHttpHandler(Func<IDictionary<string, object>, Task> next)
        : base(new ContactsService.Processor(new ContactsServiceHandler()))
    {
    }

    public Task Invoke(IDictionary<string, dynamic> env)
    {
        Stream req = env["owin.RequestBody"];
        Stream res = env["owin.ResponseBody"];
        return Task.Factory.StartNew(() => this.ProcessRequest(req, res));
    }
}

public class Startup
{
    public void Configuration(IAppBuilder builder)
    {
        builder.Use(typeof(ThriftHttpHandler));
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (WebApp.Start<Startup>("http://localhost:3000"))
        {
            Console.WriteLine("Press [enter] to quit...");
            Console.ReadLine();
        }
    }
}

Microsoft.Owin.SelfHost를 사용해 Node.js 만큼은 아니지만 아주 짧은 코드로 서버 프로그램을 작성했습니다. 프로젝트를 빌드하고 실행하면 ContactsService 서비스를 제공하는 HTTP Thrift 서버가 구동됩니다.

Thrift# 클라이언트

서버가 준비되었으니 클라이언트를 만듭니다. 클라이언트 응용프로그램은 getContacts() 서비스 함수와 addContacts() 서비스 함수가 잘 작동하는지 검사하는 코드를 실행하고 종료됩니다. 콘솔 응용프로그램 프로젝트를 만들고 ThriftSharp 패키지와 ThriftSharp.Extensions 패키지를 설치합니다. ThriftSharp 패키지는 Thrift#의 핵심 코드를 제공하고 ThriftSharp.Extensions 패키지는 단순 반복적인 서비스 클라이언트 코드 구현을 런타임에 자동으로 해주는 역할을 합니다. 아래는 클라이언트 응용프로그램 전체 코드입니다.

using System;
using System.Collections.Generic;
using System.Linq;
using ThriftSharp;

namespace ThriftSharpClient
{
    [ThriftStruct("Contact")]
    public class Contact
    {
        [ThriftField(1, false, "id")]
        public int Id { get; set; }
        [ThriftField(2, true, "firstName")]
        public string FirstName { get; set; }
        [ThriftField(3, true, "lastName")]
        public string LastName { get; set; }
        [ThriftField(4, true, "email")]
        public string Email { get; set; }

        public override string ToString()
        {
            return string.Format("Contact(Id: {0}, FirstName: {1}, LastName: {2}, Email: {3})", this.Id, this.FirstName, this.LastName, this.Email);
        }
    }

    [ThriftService("ContactsService")]
    public interface IContactsService
    {
        [ThriftMethod("getContacts")]
        List<Contact> GetContacts();
        [ThriftMethod("addContacts")]
        List<Contact> AddContacts([ThriftParameter(1, "contacts")]List<Contact> contacts);
    }

    class Program
    {
        static void Main(string[] args)
        {
            var communication = ThriftCommunication.Binary().OverHttp("http://localhost:3000/");
            var service = ThriftProxy.Create<IContactsService>(communication);

            var contacts = service.GetContacts();
            foreach (var e in contacts)
            {
                Console.WriteLine(e);
            }

            contacts = service.AddContacts(new List<Contact>
            {
                new Contact { FirstName = "Bruce", LastName = "Wayne", Email = "batman@justiceleague.com" },
                new Contact { FirstName = "Clark", LastName = "Kent", Email = "superman@justiceleague.com" }
            });
            foreach (var e in contacts)
            {
                Console.WriteLine(e);
            }
        }
    }
}

클라이언트 프로그램을 실행시키면 다음의 출력 결과를 얻을 수 있습니다.

Contact(Id: 1, FirstName: Tony, LastName: Stark, Email: ironman@avenger.com)
Contact(Id: 2, FirstName: Bruce, LastName: Banner, Email: hulk@avenger.com)
Contact(Id: 3, FirstName: Thor, LastName: Odinson, Email: thor@avenger.com)
Contact(Id: 4, FirstName: Bruce, LastName: Wayne, Email: batman@justiceleague.com)
Contact(Id: 5, FirstName: Clark, LastName: Kent, Email: superman@justiceleague.com)

성능

그러면 Thrift#의 성능은 어떨까요? 프로젝트 사이트에 의하면 기존 방식에 비해 다소 성능이 떨어진다고 합니다. 개발 장비에서 1만개의 항목을 한 번에 추가하는 작업을 대상으로 TCP 전송, 기존 방식의 HTTP 전송, Thrift#을 사용한 HTTP 전송 3가지 경우에 대해 소요되는 시간을 측정했습니다.

구분 소요시간 TCP 대비 부하
1. TCP 10.479초
2. HTTP + Thrift 15.963초 +52.33%
3. HTTP + Thrift# 17.684초 +68.75%

예상했던 것처럼 TCP 전송이 가장 빨랐고 기존 방식의 HTTP 전송이 두 번째, Thrift#을 사용한 HTTP 전송이 가장 느렸습니다. HTTP 전송만을 고려했을 때에는 Thrift#이 기존 방식에 비해해 10.78% 더 소요되었습니다.

결론

Thrift#은 아직 시작 단계에 있는 프로젝트라는 느낌입니다. 현재 버전의 완성도를 평하는 것이 아니라 앞으로 추가될 기능과 발전 가능성의 관점에서 그렇습니다. 자동 생성 코드를 이용한 방식과 특성과 POCO를 이용한 방식은 장단점이 있으니 프로젝트 환경에 따라 대안을 제공한다는 부분은 긍정적으로 생각합니다. 성능적인 부분도 개선 가능성이 있다고 생각되고 TCP 전송도 곧 지원되지 않을까 예상해봅니다.

이 포스트에서 사용된 모든 코드는 여기에서 확인할 수 있습니다.

Advertisements

Thrift# – 특성 기반 .NET Thrift 클라이언트 라이브러리”에 대한 2개의 생각

  1. woosweb

    asp.net mvc 또는 asp.net webapi에서 thrift 프로토콜을 사용하려고 하는데 혹시 방법이 없을 까요? 물론 iis를 이용해서요..벌써 몇일째 삽질인데요 꼭 답변부탁드립니다.

    응답
    1. Gyuwon 글의 글쓴이

      제가 이 글을 작성하고 얼마 지나지 않아 회사를 옮겼고 이후로는 Thrift를 사용하지 않아서 저도 문의하신 내용에 대한 지식은 없습니다. https://www.facebook.com/groups/AspxKorea/ 이곳에 한 번 문의해보세요. 혹시 말씀하신 상황을 경험하신 분이 계실지 모르겠습니다. 질문을 보면 이미 어느 정도 시도를 해보신 것 같으니 어떤 시도를 해봤고 어떤 문제를 겪으시고 계신지 오류 메시지와 스택 추적, 작성하신 코드 등 최대한 구체적으로 질문해보시면 더 도움이 될 것 같습니다.

      응답

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중