읽기 전에

Mono에서 웹개발을 하고 싶다면 OWIN 프로젝트를 활용하자. 차후 .NET mvc 프레임웍도 owin 기반에서 구동 가능할 예정이다.

tl;dr

  • Mono에서 MVC5 지금은 안됨
  • .Net 개발은 정신 건강을 위해 Windows 위에서 하자

요즘 닷넷 스터디를 한창 하고 있는데 요번에 새로 나온 MVC5를 기준으로 스터디가 진행되고 있다. 아직 윈도우 개발 환경이 준비 안된 탓에 이 MVC5 프로젝트를 Mono 환경에서 구동해보려고 했는데 결과적으로는 운용조차 해볼 수 없었다. 안된다고 딱 잘라 말하는 글이 하나도 없어서 에러 로그를 정리해 올려보려고 한다. 참고로 Mono의 호환 현황은 Mono 공식 사이트의 Compatibility에서 확인할 수 있다.1

웹으로 접속하면 다음의 에러가 발생한다.

Missing method System.Web.Hosting.HostingEnvironment::get_InClientBuildManager() in assembly /Library/Frameworks/Mono.framework/Versions/3.2.4/lib/mono/gac/System.Web/4.0.0.0__b03f5f7f11d50a3a/System.Web.dll, referenced in assembly /private/tmp/root-temp-aspnet-0/8717103c/assembly/shadow/d4ff52ca/402bd257_94d4809d_00000001/WebActivatorEx.dll

Application Exception System.TypeLoadException Could not load type 'System.Web.Http.WebHost.HttpControllerHandler' from assembly 'System.Web.Http.WebHost, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.

Description: HTTP 500.Error processing request. Details: Non-web exception. Exception origin (name of application or object): System.Web. Exception stack trace: at System.Web.Routing.RouteCollection.GetRouteData (System.Web.HttpContextBase httpContext)
[0x00000] in <filename unknown>:0 at System.Web.Routing.UrlRoutingModule.PostResolveRequestCache (System.Web.HttpContextBase context)
[0x00000] in <filename unknown>:0 at System.Web.Routing.UrlRoutingModule.PostResolveRequestCache (System.Object o, System.EventArgs e)
[0x00000] in <filename unknown>:0 at System.Web.HttpApplication+<RunHooks>c__Iterator5.MoveNext ()
[0x00000] in <filename unknown>:0 at System.Web.HttpApplication+<Pipeline>c__Iterator6.MoveNext ()
[0x00000] in <filename unknown>:0 at System.Web.HttpApplication.Tick ()
[0x00000] in <filename unknown>:0 Version Information: 3.2.4 ((no/294f999 Fri Oct 25 20:18:12 EDT 2013); ASP.NET Version: 4.0.30319.17020 Powered by Mono

위 에러는 get_InClientBuildManager 메소드가 없어 나타나는 문제로 Mono에서 구현된 System.Web.Hosting.HostingEnvironment에 해당 메소드가 구현되어 있지 않다. 그래서 MS에서 배포한 라이브러리 dll을 사용해 시도했다. System.Web.dll을 local copy 해서 다시 구동했다. 다음은 MVC5 프로젝트를 Mono의 xsp4로 구동했을 때 나오는 에러다.

Handling exception type TargetInvocationException Message is Exception has been thrown by the target of an invocation. IsTerminating is set to True System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.

Server stack trace: at System.Reflection.MonoCMethod.InternalInvoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 at System.Reflection.MonoCMethod.DoInvoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture)
[0x00000] in <filename unknown>:0 at System.Reflection.MonoCMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture)
[0x00000] in <filename unknown>:0 at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.ObjectRecord.LoadData (System.Runtime.Serialization.ObjectManager manager, ISurrogateSelector selector, StreamingContext context)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.ObjectManager.DoFixups ()
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadNextObject (System.IO.BinaryReader reader)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadObjectGraph (BinaryElement elem, System.IO.BinaryReader reader, Boolean readHeaders, System.Object& result, System.Runtime.Remoting.Messaging.Header[]& headers)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.NoCheckDeserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream)
[0x00000] in <filename unknown>:0 at System.Runtime.Remoting.RemotingServices.DeserializeCallData (System.Byte[] array)
[0x00000] in <filename unknown>:0 at (wrapper xdomain-dispatch) System.AppDomain:DoCallBack (object,byte[]&,byte[]&)

Exception rethrown at [0]: ---> System.ArgumentException: Couldn't bind to method 'SetHostingEnvironment'. at System.Delegate.GetCandidateMethod (System.Type type, System.Type target, System.String method, BindingFlags bflags, Boolean ignoreCase, Boolean throwOnBindFailure)
[0x00000] in <filename unknown>:0 at System.Delegate.CreateDelegate (System.Type type, System.Type target, System.String method, Boolean ignoreCase, Boolean throwOnBindFailure)
[0x00000] in <filename unknown>:0 at System.Delegate.CreateDelegate (System.Type type, System.Type target, System.String method)
[0x00000] in <filename unknown>:0 at System.DelegateSerializationHolder+DelegateEntry.DeserializeDelegate (System.Runtime.Serialization.SerializationInfo info)
[0x00000] in <filename unknown>:0 at System.DelegateSerializationHolder..ctor (System.Runtime.Serialization.SerializationInfo info, StreamingContext ctx)
[0x00000] in <filename unknown>:0 at (wrapper managed-to-native) System.Reflection.MonoCMethod:InternalInvoke (System.Reflection.MonoCMethod,object,object[],System.Exception&) at System.Reflection.MonoCMethod.InternalInvoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 --- End of inner exception stack trace --- at (wrapper xdomain-invoke) System.AppDomain:DoCallBack (System.CrossAppDomainDelegate) at (wrapper remoting-invoke-with-check) System.AppDomain:DoCallBack (System.CrossAppDomainDelegate) at System.Web.Hosting.ApplicationHost.CreateApplicationHost (System.Type hostType, System.String virtualDir, System.String physicalDir)
[0x00000] in <filename unknown>:0 at Mono.WebServer.VPathToHost.CreateHost (Mono.WebServer.ApplicationServer server, Mono.WebServer.WebSource webSource)
[0x00000] in <filename unknown>:0 at Mono.WebServer.XSP.Server.RealMain (System.String[] args, Boolean root, IApplicationHost ext_apphost, Boolean quiet)
[0x00000] in <filename unknown>:0 at Mono.WebServer.XSP.Server.Main (System.String[] args)
[0x00000] in <filename unknown>:0 [ERROR] FATAL UNHANDLED EXCEPTION: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.

Server stack trace: at System.Reflection.MonoCMethod.InternalInvoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 at System.Reflection.MonoCMethod.DoInvoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture)
[0x00000] in <filename unknown>:0 at System.Reflection.MonoCMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture)
[0x00000] in <filename unknown>:0 at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.ObjectRecord.LoadData (System.Runtime.Serialization.ObjectManager manager, ISurrogateSelector selector, StreamingContext context)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.ObjectManager.DoFixups ()
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadNextObject (System.IO.BinaryReader reader)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadObjectGraph (BinaryElement elem, System.IO.BinaryReader reader, Boolean readHeaders, System.Object& result, System.Runtime.Remoting.Messaging.Header[]& headers)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.NoCheckDeserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler)
[0x00000] in <filename unknown>:0 at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream)
[0x00000] in <filename unknown>:0 at System.Runtime.Remoting.RemotingServices.DeserializeCallData (System.Byte[] array)
[0x00000] in <filename unknown>:0 at (wrapper xdomain-dispatch) System.AppDomain:DoCallBack (object,byte[]&,byte[]&)

Exception rethrown at [0]: ---> System.ArgumentException: Couldn't bind to method 'SetHostingEnvironment'. at System.Delegate.GetCandidateMethod (System.Type type, System.Type target, System.String method, BindingFlags bflags, Boolean ignoreCase, Boolean throwOnBindFailure)
[0x00000] in <filename unknown>:0 at System.Delegate.CreateDelegate (System.Type type, System.Type target, System.String method, Boolean ignoreCase, Boolean throwOnBindFailure)
[0x00000] in <filename unknown>:0 at System.Delegate.CreateDelegate (System.Type type, System.Type target, System.String method)
[0x00000] in <filename unknown>:0 at System.DelegateSerializationHolder+DelegateEntry.DeserializeDelegate (System.Runtime.Serialization.SerializationInfo info)
[0x00000] in <filename unknown>:0 at System.DelegateSerializationHolder..ctor (System.Runtime.Serialization.SerializationInfo info, StreamingContext ctx)
[0x00000] in <filename unknown>:0 at (wrapper managed-to-native) System.Reflection.MonoCMethod:InternalInvoke (System.Reflection.MonoCMethod,object,object[],System.Exception&) at System.Reflection.MonoCMethod.InternalInvoke (System.Object obj, System.Object[] parameters)
[0x00000] in <filename unknown>:0 --- End of inner exception stack trace --- at (wrapper xdomain-invoke) System.AppDomain:DoCallBack (System.CrossAppDomainDelegate) at (wrapper remoting-invoke-with-check) System.AppDomain:DoCallBack (System.CrossAppDomainDelegate) at System.Web.Hosting.ApplicationHost.CreateApplicationHost (System.Type hostType, System.String virtualDir, System.String physicalDir)
[0x00000] in <filename unknown>:0 at Mono.WebServer.VPathToHost.CreateHost (Mono.WebServer.ApplicationServer server, Mono.WebServer.WebSource webSource)
[0x00000] in <filename unknown>:0 at Mono.WebServer.XSP.Server.RealMain (System.String[] args, Boolean root, IApplicationHost ext_apphost, Boolean quiet)
[0x00000] in <filename unknown>:0 at Mono.WebServer.XSP.Server.Main (System.String[] args)

위 기록을 보면 Mono에서 구현한 xsp 서버에 연결이 되지 않는데 SetHostingEnvironment 메소드가 제대로 바인딩 되지 않는다. 에러가 나는 두가지 모두 빌드는 제대로 진행 되는 것 보면, 추측컨데 xsp에서 구현되지 않은 부분이 문제일 뿐이지 본 프로젝트에 작성된 코드는 문제가 없는 것 같다. xsp 구현을 들여다보고 손을 댈려고 했지만, 간단한 문제라면 이미 수정이 되었을 것이고 그로 미루어 볼 때 내 작업으로 만들 수 있는 수준이 아닌듯 싶어서 코드를 좀 보다가 말았다. (게다가… C#은 전혀 모르는 쪽이니까.)

MVC5가 OWIN을 지원한다는 얘기를 들었는데 xsp 이외의 방법으로 이종 플랫폼에서 구동할 수 있는 방법이 있을 법도 하다. 하지만 문서도 잘 검색이 안되고 사용자도 너무 작아, 명목상의 존재 의의만 다하고 있는 것 같아 아쉽다.2 윈도우 환경을 위해 구입한 선더볼트 외장하드가 오면 mono를 쓸 일이 없어져 그렇게 큰 고민거리는 아니긴 하지만, 다른 플랫폼에서 닷넷을 오픈소스로 구현하기 위해 애쓰는 모습이 존경스럽다. 이 구현이 성숙해 닷넷 플랫폼을 온전한 오픈소스로 구동할 수 있다면 더 재미있는 프로젝트가 닷넷으로 작성되지 않을까 생각해본다.

  • 이 목록을 확인하지 않고서 무턱대고 시작한게 화근이었다. 아직 MVC4도 완전히 지원하는 상황이 아니다. 
  • 그리고 이 문제를 해결하기 위해 엄청 검색을 했었는데 비슷한 문제를 버전 대대로 겪은 사람들이 많이 나왔다. 물론 구현 자체가 없으니 아무도 해결하지 못했다. 
  • WordPress에 내장되어 있는 메뉴(Menu)는 이미 쓸만한 class명이 이미 다 붙어 있어 사실 딱히 수정이 필요가 없는 편이다. 예를 들면 현재 활성화 된 메뉴는 .current-menu-item 라든가, 해당 메뉴가 연결된 포스트의 타입을 .menu-item-object-page 식으로 이미 선언되어 있다. 하지만 디자인으로 인해 마크업을 추가하거나 변경해야 하는 경우가 간혹 있는데 그럴 때는 Walker 클래스를 사용해 변경할 수 있다.

    Walker는 WordPress에서 트리 형태와 같이 상속이 있는 데이터 구조를 다루기 위해 사용되는 클래스이며, 자세한 내용은 WordPress의 Walker 항목을 참고하도록 한다. 문서가 조금 조잡한 감이 있는데 구글에서 검색해보면 더 자세하게 풀어서 작성한 포스트도 꽤 많이 있으므로 참고하자.

    일반적으로 템플릿에서 메뉴를 불러오기 위해서 wp_nav_menu() 함수를 사용한다.

    <div class="nav-wrapper">
    <?php wp_nav_menu( array('menu' => 'Global Nav' )); ?>
    </div>
    

    이때 wp_nav_menu()를 통해 생성되는 메뉴는 walker 파라미터가 지정되어 있지 않기 떄문에 기본값인 Walker_Nav_Menu 클래스를 사용해 생성하게 된다. 즉, Walker_Nav_Menu 클래스를 상속하는 새 클래스를 작성한 후에 walker 파라미터에 지정해주면 원하는 형태의 마크업으로 변경할 수 있게 된다.

    다음의 예시는 다중 하위 메뉴에서 나타나는 ul.sub-menudiv.sub-menu-wrapper로 한번 더 감싸주기 위한 클래스다. 기존 Walker_Nav_Menu 클래스에서 정의된 메소드를 오버라이딩해서 div를 추가한다.

    <?php
    class Custom_Walker_Nav_Menu extends Walker_Nav_Menu {
    
            function start_lvl( &$output, $depth = 0, $args = array() ) {
                    $indent = str_repeat("\t", $depth);
                    $output .= "\n$indent<div class=\"sub-menu-wrapper\"><ul class=\"sub-menu\">\n";
            }
    
            function end_lvl( &$output, $depth = 0, $args = array() ) {
                    $indent = str_repeat("\t", $depth);
                    $output .= "$indent</ul></div>\n";
            }
    
    }
    

    Walker_Nav_Menuwp-includes/nav-menu-template.php에 선언되어 있으며, 오버라이딩 하고 싶은 메소드를 위와 같은 방식으로 본 클래스의 메소드 내용을 참고해 작성하면 된다.

    위와 같이 작성한 클래스를 현재 사용하고 있는 템플릿의 functions.php에 선언해주고 wp_nav_menu() 함수에서 해당 walker를 참조하도록 추가한다.

    <div class="nav-wrapper">
    <?php wp_nav_menu( array('menu' => 'Global Nav', 'walker' => new Custom_Walker_Nav_Menu )); ?>
    </div>
    

    그러면 앞서 작성한 클래스를 참고해 새로운 마크업 메뉴를 생성하게 된다.

    버전 2.1에서 제안된 Walker 클래스들은 앞서 이야기한 바와 같이 트리 구조의 데이터를 직접 파싱하는 형태로 구성되어 있다. 자료형과 마크업이 아직도 유기적인 상태라서, 부모 메소드를 실행한 후 결과를 처리하거나 하는 방식으로 작업할 수 없다. 그로 인해 다소 지저분하고 기존 내용을 반복적으로 작성해야 하는 번거로움이 여전히 있는데 최근 버전에서 Walker로 처리되는 데이터 구조가 많아지고 있어 조만간 리펙토링이 진행될 것으로 보인다.

    PyPy는 들을 때마다 호기심을 자극하는 프로젝트 중 하나인데 Python으로 Python을 작성한다는 간단히 이해하기 힘든 방식(?)의 프로젝트다. 최근들어 긴 인고의 노력 끝에 좋은 결실을 맺고 있다는 소식도 들려오고 있어서 관심을 가지고 찾아보게 되었다. 여러 글을 읽어봤지만 PyPy 공식 블로그에 올라와 있던 이 포스트가 왠지 와닿아 서둘러 번역했다.

    여전히 의역도, 엉터리도 많은 발번역이지만 PyPy가 어떤 이유로 CPython보다 빠르게 동작하는지에 대한 이해에 조금이나마 도움이 되었으면 좋겠다.

    For English, I translated the article to Korean. So you can see the original english article: http://morepypy.blogspot.com.au/2011/04/tutorial-writing-interpreter-with-pypy.html


    PyPy와 함께 인터프리터 작성하기

    Andrew Brown brownan@gmail.com가 작성했으며 pypy-dev 메일링 리스트의 PyPy 개발자들로부터 도움을 받았다.

    이 튜토리얼의 원본과 파일은 다음 리포지터리에서 확인할 수 있다: https://bitbucket.org/brownan/pypy-tutorial/

    내가 PyPy 프로젝트에 대해 처음으로 배웠을 때, 한동안은 정확히 어떤 프로젝트인지 살펴보는데 시간을 썼다. 그전까지 알지 못했던 것은 다음 두가지였다:

    • 인터프리트 될 언어를 위한 인터프린터 구현을 위한 도구 모음
    • 이 툴체인을 이용한 파이썬 구현

    대부분의 사람들이 두번째를 PyPy라고 생각한다. 하지만 이 튜토리얼은 파이썬 인터프리터에 대한 설명이 아니다. 이 글은 당신이 만들 언어를 위한 인터프리터를 작성하는 방법을 다루는 튜토리얼이다.

    다시 말해 이 글은 PyPy를 깊게 이해하기 위한 방법으로, PyPy가 무엇에 관한 것인지, 어떻게 구현되고 있는지를 살펴보는데 목적을 둔 튜토리얼이다.

    이 튜토리얼은 당신이 PyPy에 대해 어떻게 동작하는지 아주 조금 알고 있다고 가정하고 있다. (그게 PyPy의 전부 일지도 모른다.) 나 또한 초심자와 같은 시각으로 접근할 것이다.

    PyPy는 무엇을 하는가

    PyPy가 어떤 역할을 하는지에 관한 개론이다.지금 인터프리터 언어를 작성하고 싶다고 가정해보자. 이 과정은 일종의 소스 코드 파서와 바이트코드 통역 루프, 그리고 엄청나게 많은 양의 표준 라이브러리 코드를 작성해야 한다.

    적절하게 완전한 언어를 위한 약간의 작업이 필요하며 동시에 수많은 저수준의 일들이 뒤따르게 된다. 파서와 컴파일러 코드를 작성하는건 일반적으로 안재밌고, 이것 바로 파서와 컴파일을 만들다가 집어 치우게 되는 이유다.

    그런 후에도, 당신은 여전히 인터프리터를 위한 메모리 관리에 대해 반드시 걱정해야 하며, 임의 정밀도 정수, 좋고 편리한 해시 테이블 등의 데이터 타입 같은걸 원한다면 수많은 코드를 다시 구현해야 한다. 이와 같은 작업들은 그들 스스로의 언어를 구현하겠다는 아이디어를 그만 두기에 충분한 일이다.

    만약 당신의 언어를 이미 존재하는 고수준의 언어, 가령 파이썬을 사용해서 이 작업을 한다면 어떨까? 메모리 관리나 풍부한 데이터 타입을 원하는대로 자유롭게 쓸 수 있는 등, 고수준 언어의 모든 장점을 얻을 수 있기 때문에 이상적인 선택일 수 있다. 아, 물론 인터프리터 언어를 다른 인터프리터 언어로 구현한다면 분명 느릴 것이다. 코드를 이용할 때 통역(interpreting)이 두번 필요하기 때문이다.

    당신이 추측할 수 있는 것과 같이, PyPy는 위와 같은 문제를 해결했다. PyPy는 똑똑한 툴체인으로 인터프리터 코드를 분석하고 C코드(또는 JVM, CLI)로 번역한다. 이 과정을 “번역(translation)”이라 하며 이 과정은 수많은 파이썬 문법과 표준 라이브러리를 (전부는 아니지만) 번역하는 방법을 의미한다. 당신이 해야 할 일은 만들고자 하는 인터프리터를 RPython으로 작성하는 것이다. RPython은 파이썬의 위에서 이야기한 분석과 번역을 위해 주의깊게 작성된, 파이썬의 하위 언어이며, PyPy는 아주 유능한 인터프리터다. 유능한 인터프리터는 코드를 작성하는데 있어서 어렵지 않게 도와준다.

    언어

    내가 구현하고자 하는 언어는 초 단순하다. 언어 런타임은 테잎의 숫자, 초기화를 위한 0, 싱글 포인터를 위한 하나의 테잎 셀들로 구성되어 있다. 언어는 8개의 커맨드를 지원한다 :

    : 테잎의 포인터를 오른쪽으로 한 칸 이동

    < : 테잎의 포인터를 왼쪽으로 한 칸 이동

    : 포인터가 가리키는 셀의 값을 증가

    – : 포인터가 가리키는 셀의 값을 감소

    [ : 만약 현재 포인터의 셀이 0이면 ]를 만나기 전까지의 명령을 건너뜀

    ] : 이 이전부터 [ 까지의 내용을 건너 뜀 (이것의 상태를 평가)

    . : 포인트가 가리키고 있는 셀의 싱글 바이트를 stdout으로 출력

    , : 싱글 바이트를 stdin에서 입력받아 포인트가 가리키는 셀에 저장

    인지할 수 없는 모든 바이트들은 무시한다.

    위 내용을 통해 이 언어를 알 수 있을 것이다. 이것을 BF 언어라고 명명하자.

    내가 알아차린 하나는 이 언어는 그 스스로의 바이트코드이기 때문에 소스코드로부터 바이트코드로 번역하는 과정이 없다. 그 의미는 이 언어가 직접적으로 통역될 수 있다는 뜻이며, 우리 인터프리터의 메인 실행 루프가 소스코드를 바로 실행하게 된다. 이런 방법은 구현을 좀 더 쉽게 만든다.

    첫걸음

    BF 인터프리터를 평범하고 오래된 파이썬 언어로 작성해보자. 첫걸음은 실행 루프를 작성해본다:

    def mainloop(program):
        tape = Tape()
        pc = 0
        while pc < len(program):
            code = program[pc]
    
            if code == ">":
                tape.advance()
            elif code == "<":
                tape.devance()
            elif code == "+":
                tape.inc()
            elif code == "-":
                tape.dec()
            elif code == ".":
                sys.stdout.write(chr(tape.get()))
            elif code == ",":
                tape.set(ord(sys.stdin.read(1)))
            elif code == "[" and value() == 0:
                # Skip forward to the matching ]
            elif code == "]" and value() != 0:
                # Skip back to the matching [
    
            pc += 1
    

    위에서 볼 수 있는 것처럼, 프로그램 카운터(pc)가 현재 명령 인덱스를 담고 있다. 위 루프에서 명령문을 실행하기 위해 명령문에서 첫 명령을 얻은 후, 명령문을 어떻게 실행할지 결정하게 되면 실행하게 된다.

    []의 구현은 아직 남겨져 있는 상태다. 이 구현은 프로그램 카운터로 대괄호에 맞는지 값을 비교하는 것으로 변경되어야 한다. (그리고 pc는 증가하게 된다. 그래서 루프에 진입할 때 한번 평가하고 각 루프의 종료에서 평가한다.)

    다음은 Tape 클래스의 구현으로, 테잎의 값을 테잎 포인터처럼 담고 있다:

    class Tape(object):
        def __init__(self):
            self.thetape = [0]
            self.position = 0
    
        def get(self):
            return self.thetape[self.position]
        def set(self, val):
            self.thetape[self.position] = val
        def inc(self):
            self.thetape[self.position] += 1
        def dec(self):
            self.thetape[self.position] -= 1
        def advance(self):
            self.position += 1
            if len(self.thetape) <= self.position:
                self.thetape.append(0)
        def devance(self):
            self.position -= 1
    

    위에서 보듯, 테잎은 필요한 만큼 오른쪽으로 확장하게 된다. 하지만 이런 방식은 다소 불명확하므로 포인터가 잘못된 값을 가리키지 않게 확신할 수 있도록 에러를 확인하는 코드를 추가해줘야 한다. 지금은 걱정하지 말고 그냥 두자.

    [] 구현을 제외하고서 이 코드는 정상적으로 동작한다. 만약 프로그램에 주석이 많이 있다면, 그 주석을 실행하는 동안에 하나씩 건너 뛰어야만 한다. 그러므로 먼저 이 모든 주석을 한번에 제거하도록 하자.

    그와 동시에 대괄호 사이를 딕셔너리로 만들어, 대괄호 짝을 찾는 작업 대신 딕셔너리 하나를 살펴보는 작업으로 처리하게 만든다. 다음과 같은 방법으로 한다:

    def parse(program):
        parsed = []
        bracket_map = {}
        leftstack = []
    
        pc = 0
        for char in program:
            if char in ('[', ']', '<', '>', '+', '-', ',', '.'):
                parsed.append(char)
    
                if char == '[':
                    leftstack.append(pc)
                elif char == ']':
                    left = leftstack.pop()
                    right = pc
                    bracket_map[left] = right
                    bracket_map[right] = left
                pc += 1
    
        return "".join(parsed), bracket_map
    

    이 함수는 실행에 필요 없는 코드를 제거한 문자열을 반환하고, 또한 대괄호의 열고 닫는 위치를 저장한 딕셔너리를 반환한다.

    이제 우리에게 필요한 것은 위의 내용을 연결하는 코드이다. 이제 동작하는 BF 인터프리터를 가지게 되었다:

    def run(input):
        program, map = parse(input.read())
        mainloop(program, map)
    
    if __name__ == "__main__":
        import sys
        run(open(sys.argv[1], 'r'))
    

    혼자 집에서 따라하고 있다면, mainloop()의 서명을 변경해야 하며 if 명령문을 위한 대괄호 브랜치 구현이 필요하다. 이 구현은 다음 예에서 확인할 수 있다: example1.py

    이 시점에서 파이썬 아래에서 이 인터프리터를 구동하는게 정상적으로 동작하는지 실행해볼 수 있다. 그러나 미리 경고하는데, 이것은 엄청 느리게 동작하는 것을 다음 예에서 확인할 수 있다:

    $ python example1.py 99bottles.b
    

    mandel.b와 여러 예제 프로그램들을 내 리포지터리에서 확인 할 수 있다. (내가 작성하지는 않았다.)

    PyPy 번역

    하지만 이 글은 BF 인터프리터를 작성하는 것에 관한 이야기가 아니라 PyPy에 대한 글이다. 그러니까, 어떻게 PyPy로 번역이 되는 것은 엄청 빠르게 실행이 되는 것일까?

    참고삼아 이야기하면, PyPy 소스 트리에서 pypy/translator/goal 디렉토리에 도움이 될 만한 간단한 예제들이 있다. 학습을 위한 첫 시작점은 targetnopstandalone.py 예제이며 이 코드는 PyPy를 위한 간단한 hello world 코드다.

    예를 들어, 모듈은 필수적으로 target이라는 함수를 정의해 시작점을 반환하도록 해야 한다. 번역은 모듈을 불러오고 target이라는 이름을 확인하고, 호출하며, 함수 객체가 번역의 시작점이 어디인지를 반환하는 과정을 통해 진행된다.

    def run(fp):
        program_contents = ""
        while True:
            read = os.read(fp, 4096)
            if len(read) == 0:
                break
            program_contents += read
        os.close(fp)
        program, bm = parse(program_contents)
        mainloop(program, bm)
    
    def entry_point(argv):
        try:
            filename = argv[1]
        except IndexError:
            print "You must supply a filename"
            return 1
    
        run(os.open(filename, os.O_RDONLY, 0777))
        return 0
    
    def target(*args):
        return entry_point, None
    
    if __name__ == "__main__":
        entry_point(sys.argv)
    

    entry_point 함수는 최종 결과물을 실행할 때 커맨드 라인의 아규먼트를 넘겨준다.

    여기서 몇가지 내용을 더 변경해야 하는데 다음 섹션을 살펴보자.

    RPython에 대하여

    이 시점에서 RPython에 대해 이야기해보자. PyPy는 아무 파이썬 코드나 번역할 수는 없다. 파이썬은 동적 타입 언어이기 때문이다. 그래서 표준 라이브러리 함수와 문법 구조에 대한 제약을 통해야만 사용할 수 있다. 여기서 모든 제약 사항을 다루진 않을 것이며 더 많은 정보를 알고 싶다면 다음 페이지를 확인하도록 하자. http://readthedocs.org/docs/pypy/en/latest/coding-guide.html#restricted-python

    위에서 본 예에서 몇가지 변경된 점을 확인할 수 있을 것이다. 이제 파일 객체 대신에 os.open과 os.read를 활용한 저레벨의 파일 디스크립터(descriptor)를 사용하려고 한다. .,의 구현은 위에서 살펴본 방식과 다르게 약간 꼬아야 한다. 이 부분이 코드에서 변경해야 하는 유일한 부분이며 나머지는 PyPy를 소화하기 위해 살펴 볼 간단한 부분들이다.

    그렇게 어렵진 않다. 그러지 않나? 난 여전히 딕셔너리와 확장 가능한 리스트, 몇 클래스와 객체를 사용할 뿐이다. 또 로우 레벨 파일 디스크립터가 너무 저수준이라 생각되면 PyPy의 _RPython 표준 라이브러리_에 포함되어 있는 rlib.streamio 라는 유용한 추상 클래스가 도움이 된다.

    위 내용을 진행한 예는 example2.py 에서 확인할 수 있다.

    번역하기

    PyPy를 가지고 있지 않다면, bitbucket.org 리포지터리에서 PyPy 최신 버전을 받기 바란다:

    $ hg clone https://bitbucket.org/pypy/pypy
    

    (최근 리비전이 필요한데 몇 버그픽스가 있어야만 예제가 동작하기 때문이다)

    “pypy/translator/goal/translate.py” 스크립트를 실행한다. 이 스크립트를 실행하면 예제 모듈을 아규먼트로 넣어 실행하면 된다.

    $ python ./pypy/rpython/bin/rpython example2.py
    

    (엄청난 속도가 필요하다면 역시 PyPy의 파이썬 인터프리터를 사용하면 되지만 이 일에는 딱히 필요 없다)

    PyPy는 맷돌처럼 소스를 갈아내고, 갈아낼 동안 멋져보이는 프랙탈을 사용자의 콘솔에 보여준다. 이 작업은 내 컴퓨터에서 20초 정도 걸렸다.

    이 작업의 결과로 BF 인터프리터 프로그램이 실행 가능한 바이너리로 나왔다. 내 리포지터리에 포함된 몇 BF 프로그램 예제를 구동해보면, 예를 들면 mandelbrot 프랙탈 생성기는 내 컴퓨터에서 실행하는데 45초가 걸렸다. 한번 직접 해보자:

    $ ./example2-c mandel.b
    

    비교를 위해 번역되지 않은 생 파이썬 인터프리터를 실행해보자:

    $ python example2.py mandel.b
    

    직접 해보면 영원히 걸릴 것만 같다.

    결국 당신은 해냈다. 우리는 성공적으로 RPython으로 작성된 우리 언어의 인터프리터를 가지게 되었고 PyPy 툴체인을 이용해 번역한 결과물을 얻게 되었다.

    JIT 추가하기

    RPython에서 C로 번역하는건 정말 쿨하지 않나? 그것 말고도 PyPy의 뛰어난 기능 중 하나는 _지금 만든 인터프리터를 위한 just-in-time 컴파일러를 생성_하는 능력이다. PyPy는 단지 인터프리터가 어떤 구조를 가지고 있는가에 대한 몇가지 힌트를 통해 JIT 컴파일러를 포함해서 생성한다. 이 기능은 실행 시점에 번역될 코드인 BF 언어를 기계어로 번역해주는 일을 해준다.

    그래서 이런 일이 제대로 동작하도록 PyPy에게 무엇을 알려줘야 하는걸까? 먼저 바이트코드 실행 루프의 시작점이 어디인지 가르쳐줘야 한다. 이 작업은 목표 언어(여기에서는 BF)에서 명령이 실행되는 동안 계속 추적해갈 수 있도록 돕는다.

    또한 개개의 실행 프레임을 정의해 알려줘야 한다. 여기서 만든 언어가 실제로 쌓이는 프레임을 가지고 있지 않지만, 무엇이 각각의 명령어를 실행하는데 변하거나 변하지 않는지에 대해 정리해줘야 한다. 각각 이 역할을 하는 변수를 green, red 변수라고 부른다.

    example2.py 코드를 참조하며 다음 이야기를 계속 보자.

    주요 루프를 살펴보면, 4개의 변수를 사용하고 있다: pc, program, bracket_map, 그리고 tape. 물론 pc, program, 그리고 bracket_map은 모두 green 변수다. 이 변수들은 개개의 명령을 실행하기 위해 _정의_되어 있다. JIT의 루틴에서 green 변수로서 이전에 확인했던 동일 조합이라면, 건너 뛰어야 하는 부분인지 필수적으로 루프를 실행해야 하는지 알게 된다. 변수 tape은 red 변수인데 실행하게 될 때 처리가 되는 변수다.

    PyPy에게 이런 정보를 알려줘보자. JitDriver 클래스를 불러오고 인스턴스를 생성한다:

    from rpython.rlib.jit import JitDriver
    jitdriver = JitDriver(greens=['pc', 'program', 'bracket_map'],
            reds=['tape'])
    

    그리고 우리는 메인 루프 함수의 최상단에 있는 while 루프에 다음 줄을 추가한다:

    jitdriver.jit_merge_point(pc=pc, tape=tape, program=program,
            bracket_map=bracket_map)
    

    또한 JitPolicy를 선언해야 한다. 이 부분에서 특별한 점은 없어서 다음 내용을 파일 어딘가에 넣어주기만 하면 된다:

    def jitpolicy(driver):
        from rpython.jit.codewriter.policy import JitPolicy
        return JitPolicy()
    

    example3.py 예제를 확인하자.

    이제 번역을 다시 하는데 --opt=jit 옵션을 포함하고서 번역하자:

    $ python ./pypy/rpython/bin/rpython --opt=jit example3.py
    

    이 명령은 JIT을 활성화한 번역이기 때문에 엄청나게 긴 시간이 걸리는데 내 컴퓨터에서는 거의 8분이 걸렸고 이전보다 좀 더 큰 결과 바이너리가 나올 것이다. 이 작업이 끝나면, mandelbrot 프로그램을 다시 돌려보자. 이전에 45초가 걸렸던 작업이 12초로 줄어들었다!

    충분히 흥미롭게도, 기계어로 번역하는 인터프리터가 JIT 컴파일러를 켜는 순간 얼마나 빠르게 mandelbrot 예제를 처리하는지 직접 확인했다. 첫 몇 줄의 출력은 그럭저럭 빠르고, 그 이후 프로그램은 가속이 붙어서 더욱 빠른 결과를 얻게 된다.

    JIT 컴파일러 추적에 대해 조금 더 알아보기

    이 시점에서 JIT 컴파일러가 추적을 어떻게 하는지를 더 알아두는 것이 좋다. 이것이 더 명확한 설명이다: 인터프리터는 일반적으로 작성한 바와 같이 인터프리터 코드로 동작한다. 목표 언어(BF)가 실행 될 때, 반복적으로 동작하는 코드를 인지하게 되면 그 코드는 “뜨거운” 것으로 간주되고 추적을 위해 표시를 해둔다. 다음에 해당 루프에 진입을 하면, 인터프리터는 추적 모드로 바꿔 실행했던 모든 명령 기록에서 해당 루프를 찾아낸다.

    루프가 종료될 때, 추적도 종료한다. 추적한 루프는 최적화 도구(optimizer)로 보내지게 되며 어셈블러 즉, 기계어로 출력물을 만든다. 기계어는 연달아 일어나는 루프의 반복에서 사용된다.

    이 기계어는 종종 가장 일반적인 상황을 위해 최적화 되며, 또 코드에 관한 몇가지 요건을 갖춰야 한다. 그러므로, 기계어는 보호자를 필요로 하며, 이 몇가지 요건에 대한 검증이 필요하다. 만약 보호자가 있는지, 그리고 요건을 충족하는지 확인하는 검증에 실패한다면, 런타임은 일반적인 인터프리터 모드로 돌아가 동작하게 된다.

    더 자세한 정보는 다음 페이지에서 제공된다. http://en.wikipedia.org/wiki/Just-in-time_compilation

    디버깅과 추적 로그

    더 나은 방식이 있을까? JIT이 무엇을 하는지 볼 수 있을까? 다음 두가지를 하면 된다.

    먼저, get_printable_location 함수를 추가한다. 이 함수는 디버그 추적을 위한 로깅에서 사용된다:

    def get_location(pc, program, bracket_map):
        return "%s_%s_%s" % (
                program[:pc], program[pc], program[pc+1:]
                )
    jitdriver = JitDriver(greens=['pc', 'program', 'bracket_map'], reds=['tape'],
            get_printable_location=get_location)
    

    이 함수는 green 변수를 통과하며, 문자열을 반환해야 한다. 여기서 우리가 추가한 코드는, 현재 실행되는 BF코드의 앞과 뒤의 담긴 값도 밑줄과 함께 확인할 수 있도록 작성했다.

    example4.py 를 내려받고 example3.py와 동일하게 번역 작업을 실행하자.

    이제 테스트 프로그램을 추적 로그와 함께 구동한다. (test.b는 단순히 “A” 문자를 15번 또는 여러번 출력한다.):

    $ PYPYLOG=jit-log-opt:logfile ./example4-c test.b
    

    이제 “logfile”을 확인한다. 이 파일은 살짝 읽기 힘들기 때문에 가장 중요한 부분에 대해서만 설명할 것이다.

    파일은 실행된 모든 추적 로그가 담겨있고 또 어떤 명령이 기계어로 컴파일 되었는지 확인할 수 있다. 이 로그를 통해서 필요 없는 명령이 무엇인지, 최적화를 위한 공간 등을 확인할 수 있다.

    각각의 추적은 다음과 같은 모습을 하고 있다:

    [3c091099e7a4a7] {jit-log-opt-loop
    

    그리고 파일 끝은 다음과 같다:

    [3c091099eae17d jit-log-opt-loop}
    

    그 다음 행은 해당 명령의 루프 번호를 알려주며 얼마나 많은 실행(ops)이 있는지 확인할 수 있다. 내 경우에는 다음과 같은 첫 추적 로그를 얻을 수 있었다:

    1  [3c167c92b9118f] {jit-log-opt-loop
    2  # Loop 0 : loop with 26 ops
    3  [p0, p1, i2, i3]
    4  debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0)
    5  debug_merge_point('+<[>[>_+_<-]>.[<+>-]<<-]++++++++++.', 0)
    6  i4 = getarrayitem_gc(p1, i2, descr=<SignedArrayDescr>)
    7  i6 = int_add(i4, 1)
    8  setarrayitem_gc(p1, i2, i6, descr=<SignedArrayDescr>)
    9  debug_merge_point('+<[>[>+_<_-]>.[<+>-]<<-]++++++++++.', 0)
    10 debug_merge_point('+<[>[>+<_-_]>.[<+>-]<<-]++++++++++.', 0)
    11 i7 = getarrayitem_gc(p1, i3, descr=<SignedArrayDescr>)
    12 i9 = int_sub(i7, 1)
    13 setarrayitem_gc(p1, i3, i9, descr=<SignedArrayDescr>)
    14 debug_merge_point('+<[>[>+<-_]_>.[<+>-]<<-]++++++++++.', 0)
    15 i10 = int_is_true(i9)
    16 guard_true(i10, descr=<Guard2>) [p0]
    17 i14 = call(ConstClass(ll_dict_lookup__dicttablePtr_Signed_Signed), ConstPtr(ptr12), 90, 90, descr=<SignedCallDescr>)
    18 guard_no_exception(, descr=<Guard3>) [i14, p0]
    19 i16 = int_and(i14, -9223372036854775808)
    20 i17 = int_is_true(i16)
    21 guard_false(i17, descr=<Guard4>) [i14, p0]
    22 i19 = call(ConstClass(ll_get_value__dicttablePtr_Signed), ConstPtr(ptr12), i14, descr=<SignedCallDescr>)
    23 guard_no_exception(, descr=<Guard5>) [i19, p0]
    24 i21 = int_add(i19, 1)
    25 i23 = int_lt(i21, 114)
    26 guard_true(i23, descr=<Guard6>) [i21, p0]
    27 guard_value(i21, 86, descr=<Guard7>) [i21, p0]
    28 debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0)
    29 jump(p0, p1, i2, i3, descr=<Loop0>)
    30 [3c167c92bc6a15] jit-log-opt-loop}
    

    debug_merge_point 라인은 정말 길어서, 임의로 잘랐다.

    자 이제 살펴보도록 하자. 이 추적은 4개의 파라미터를 받았다. 2개의 객체 포인터(p0과 p1) 그리고 2개의 정수(i2 and i3)를 받았다. 디버그 행을 살펴보면, 이 루프의 한 반복이 추적되고 있음을 확인할 수 있다: “[>+<-]”

    이 부분은 실행의 첫 명령인 “>”에서 실행되었지만 (4행) 즉시 다음 명령이 실행되었다. “>”는 실행되지 않았고 완전히 최적화 된 것으로 보인다. 이 루프는 반드시 항상 같은 부분의 테잎에서 동작해야 하며, 테잎 포인터는 이 추적에서 일정해야 한다. 명시적인 전진 동작은 불필요하다.

    5행부터 8행까지는 “+” 동작을 위한 실행이다. 먼저 배열 포인터인 p1에서 인덱스인 i2(6행)을 이용해 배열에 담긴 값을 가져오고, 1을 추가한 후 i6에 저장(7행), 그리고 다시 배열로 저장(8행)한 것을 확인할 수 있다.

    9번째 행은 “<” 명령이지만 동작하지 않는다. 이 동작은 루틴 안으로 통과된 i2와 i3은 두 포인터를 통해 이미 계산된 값으로서 사용하게 된다. 또한 p1을 테잎 배열을 통해 추측한다. p0는 무엇인지 명확하지 않다.

    10부터 13행까지는 “-” 동작에 대한 기록으로 배열의 값을 얻고(11행), 추출해(12행) 배열 값으로 저장 (13행)한다.

    다음 14행은 “]” 동작에 해당하는 내용이다. 15행과 16행에서 i9가 참인지(0이 아닌지) 확인한다. 확인할 때, i9는 배열값으로 감소한 후 저장되며 루프의 상태가 확인된다. (“]”의 위치를 기억한다.) 16번 행은 보호자로, 해당 실행 요건이 아니라면 실행은 다른 어딘가로 뛰어 넘어가게 된다. 이 경우에는 루틴은 를 호출하고 p0라는 파라미터를 넘겨준다.

    17행부터 23행까지는 보호자를 통과한 후 프로그램 카운터가 어디로 넘어갈지를 찾기 위해 bracket_map을 살펴보는 딕셔너리 검색을 하는 부분이다. 사실 내가 이 명령이 실제로 어떻게 동작하는지 친숙하지 않지만 이 모습은 두번의 외부 호출과 보호자 셋으로 이루어져 있다. 이 명령은 비싼 것으로 보인다. 사실 우리는 이미 bracket_map은 앞으로 절대 변경되지 않는다는 사실을 알고 있다. (PyPy는 모르는 부분이다.) 다음 챕터에서는 이 부분을 어떻게 최적화 하는지 살펴본다.

    24행은 명령 포인터가 새로 증가되었다는 것을 확인할 수 있다. 25, 26행은 프로그램의 길이보다 작은 것을 확신할 수 있다.

    덧붙여, 27행은 i21을 보호하는데 명령 포인터가 증가하는 부분으로, 이 부분은 정확히 86이다. 왜냐하면 시작 부분으로 돌아가는 부분(29행)에 관한 내용이기 때문이고 명령 포인터가 86인 이유는 이 블럭에서 미리 전제되었기 때문이다.

    마지막으로 루프는 28행에서 종료된다. JIT은 이 경우를 반복적으로 다루기 위해서 로 다시 넘어가 (29행) 루프의 처음부터 다시 실행을 한다. 이때 4개의 파라미터도 같이 넘어간다. (p0, p1, i2, i3)

    최적화

    미리 언급했듯, 루프의 모든 반복에는 최종 결과를 위해 맞는 대괄호를 찾아야 하며 그로 인해 딕셔너리 검색을 하게 된다. 이런 반복은 최악에 가까운 비효율이다. 목표로 넘기는 것은 한 루프에서 다음으로 간다고 해서 변경되는 부분이 아니다. 이 정보는 변하지 않으며 다른 것들과 같이 컴파일이 되어야 한다.

    이 프로그램은 딕셔너리에서 찾기 시작하는데 PyPy는 이 부분을 불투명한 것처럼 다룬다. 그 이유는 딕셔너리가 변경되지 않고 다른 쿼리를 통해 다른 결과물을 돌려줄 가능성이 전혀 없다는 사실을 모르고 있기 때문이다.

    우리가 해야 할 일은 번역기에게 다른 힌트를 주는 일이다. 이 딕셔너리 쿼리는 순수한 함수이고 이 함수는 같은 입력을 넣으면 항상 같은 출력을 낸다는 사실을 알려줘야 한다는 것이다.

    이를 위해 우리는 함수 데코레이터인 rpython.rlib.jit.purefunction을 사용해, 해당 딕셔너리를 호출하는 함수를 포장해준다:

    @purefunction
    def get_matching_bracket(bracket_map, pc):
        return bracket_map[pc]
    

    위와 같은 처리가 된 코드는 example5.py 파일에서 확인할 수 있다.

    JIT옵션과 함께 다시 번역해보고 속도가 상승했는지 확인해본다. Mandelbrot은 이제 6초 밖에 걸리지 않는다! (이번 최적화 이전에는 12초가 걸렸다.)

    동일한 함수의 추적 로그를 살펴보자:

    1  [3c29fad7b792b0] {jit-log-opt-loop
    2  # Loop 0 : loop with 15 ops
    3  [p0, p1, i2, i3]
    4  debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0)
    5  debug_merge_point('+<[>[>_+_<-]>.[<+>-]<<-]++++++++++.', 0)
    6  i4 = getarrayitem_gc(p1, i2, descr=<SignedArrayDescr>)
    7  i6 = int_add(i4, 1)
    8  setarrayitem_gc(p1, i2, i6, descr=<SignedArrayDescr>)
    9  debug_merge_point('+<[>[>+_<_-]>.[<+>-]<<-]++++++++++.', 0)
    10 debug_merge_point('+<[>[>+<_-_]>.[<+>-]<<-]++++++++++.', 0)
    11 i7 = getarrayitem_gc(p1, i3, descr=<SignedArrayDescr>)
    12 i9 = int_sub(i7, 1)
    13 setarrayitem_gc(p1, i3, i9, descr=<SignedArrayDescr>)
    14 debug_merge_point('+<[>[>+<-_]_>.[<+>-]<<-]++++++++++.', 0)
    15 i10 = int_is_true(i9)
    16 guard_true(i10, descr=<Guard2>) [p0]
    17 debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0)
    18 jump(p0, p1, i2, i3, descr=<Loop0>)
    19 [3c29fad7ba32ec] jit-log-opt-loop}
    

    훨씬 나아졌다! 각각의 루프 반복은 추가, 제거, 두 배열 불러오기, 두 배열 저장하기, 그리고 상태를 종료하는 상황에서의 보호자 확인 정도가 있다. 이게 전부다. 이 코드는 더이상 어떤 프로그램 카운터 계산도 요구하지 않는다.

    난 최적화에 관해서는 전문가가 아니다. 이 팁은 Armin Rigo가 pypy-dev 메일링 리스트에서 추천해준 방법이다. Carl Friedrich는 인터프리터를 어떻게 최적화 하는가에 관해서 아주 유용한 시리즈 포스트를 작성했으니 관심이 있다면 참고하기 바란다: http://bit.ly/bundles/cfbolz/1

    마무리

    이 글이 PyPy가 어떻게 동작하는지, 어떻게 기존의 Python 구현보다 빠르게 동작하는지 이해하는데 도움이 되었으면 한다.

    어떻게 프로세스가 동작하는가와 같은 세부적인 과정들을 더 자세히 알고 싶다면, 이 프로세스의 자세한 내용을 설명하는 몇 개의 논문, Tracing the Meta-Level: PyPy’s Tracing JIT Compiler와 같은 논문을 살펴보기를 권장한다.

    더 궁금하면 다음 링크를 참조하자. http://readthedocs.org/docs/pypy/en/latest/extradoc.html

    PHP에서의 DateTime은 늘 문자열로 처리되어 strtotime()를 엄청나게 사용하게 되고, 기간 비교를 위해 timestamp를 직접 다뤄야 하는 번거로움 등 불편함을 다 적기에 시간이 부족할 정도다. 5.2.0 이후 지원되는 DateTime은 다른 언어들과 비교하면 아직 모자란 부분이 많이 있지만 그래도 쓸만한 구석은 거의 다 갖추고 있다. 모든 시간을 문자열로 다뤄 데이터의 의미를 제대로 살리지 못했던 과거에 비해, 더 읽기 쉽고 다루기 편한 코드를 작성하는데 도움이 된다.

    PHP 5.2.0 이후 DateTime Class가 지원되기 시작했으며 버전이 계속 올라가면서 다양한 DateTime 관련 Class가 추가되었다.

    • DateTime (PHP 5 >= 5.2.0)
    • DateTimeImmutable (PHP 5 >= 5.5.0)
    • DateTimeInterface (PHP 5 >= 5.5.0)
    • DateTimeZone (PHP 5 >= 5.2.0)
    • DateInterval (PHP 5 >= 5.3.0)
    • DatePeriod (PHP 5 >= 5.3.0)

    짧은 요약

    • 버전이 5.3.0 이상은 되야
    • 문자열로 DateTime을 사용하는 것에 비해 훨씬 편리
    • 시간 비교가 편리해짐
    • 타임존 변경이 비교적 편리해짐

    살펴보기

    DateTime 클래스는 문자열로 바로 선언해 사용할 수 있으며 setDate(), setTime() 같은 메소드도 지원한다. 1

    $now = new DateTime();
    $yesterday = new DateTime('yesterday');
    $birthday = new DateTime('1988-06-08');
    

    출력은 기존에 date() 함수를 사용하던 것과 유사하다. format() 메소드를 통해 문자열 format을 지정해 출력할 수 있다.

    print $now->format('Y-m-d H:i:s');
    

    선언한 DateTime에 시간을 더하거나 뺄 때는 DateInterval 클래스를 활용할 수 있다. 2

    $date = new DateTime('2013-12-24');
    
    $term = new DateInterval('P100D');
    $date->sub($term);
    print $date->format('Y-m-d'); // 2013-09-15
    
    $date->add(new DateInterval('P1M10D'));
    print $date->format('Y-m-d'); // 2013-10-25
    

    또한 기간끼리의 비교도 간편하게 지원한다. diff()를 활용해 기간을 비교할 수 있다. 3diff()DateInterval 객체를 반환한다.

    $now = new DateTime();
    $birthday = new DateTime("1988-06-08");
    $diff = $now->diff($birthday);
    print $diff->days;
    

    DateTime으로 생성되는 시간은 시스템에 설정된 시간대를 기준으로 생성된다. 4DateTimeZone 클래스를 이용해 시간대를 지정할 수 있으며 지정할 때는 setTimezone() 메소드를 이용하면 된다.

    $date = new DateTime("now");
    print $date->format("c");
    $date->setTimezone(new DateTimeZone("Asia/Seoul"));
    print $date->format("c");
    $us_date = new DateTime("now", new DateTimeZone("US/Eastern"));
    print $date->format("c");
    

    주의해야 할 부분은 일광절약시간을 사용하는 지역이다. 일광절약시간(Daylight saving time)을 사용하는 경우 1년에 특정 시간이 2번 나타나거나 아예 안나타나는 시간이 존재한다. 예를 들면 호주 멜번에서는 2014년 4월 6일 일광절약시간이 종료되는데 그로 인해 새벽 2시가 2번 나타나야 한다.

    $term = new DateInterval("PT1H");
    $au_date = new DateTime("2014-04-06 00:00:00",
                            new DateTimeZone("Australia/Melbourne"));
    
    print $au_date->format("c");  // 2014-04-06T00:00:00+11:00
    $au_date->add($term);
    print $au_date->format("c");  // 2014-04-06T01:00:00+11:00
    $au_date->add($term);
    print $au_date->format("c");  // 2014-04-06T02:00:00+10:00
    

    일광절약시간 해제는 정상적으로 되지만 2시는 두번 나타나지 않는다. 이런 문제는 시간을 UTC로 다뤄야 해결할 수 있다.

    $term = new DateInterval("PT1H");
    $utc = new DateTimeZone("UTC");
    $melb = new DateTimeZone("Australia/Melbourne");
    
    $utc_date_before = new DateTime("2014-04-05 15:00:00", $utc);
    $utc_date_before->setTimezone($melb);
    
    $utc_date_after = new DateTime("2014-04-05 16:00:00", $utc);
    $utc_date_after->setTimezone($melb);
    
    print $utc_date_before->format("c");  // 2014-04-06T02:00:00+11:00
    print $utc_date_after->format("c");   // 2014-04-06T02:00:00+10:00
    

    이런 경우 DateTimeImmutable 클래스를 사용하면 좀 더 편리하다. 이 클래스는 객체를 변경하지 않는 대신 새 객체를 반환한다.

    $term = new DateInterval("PT1H");
    $utc = new DateTimeZone("UTC");
    $melb = new DateTimeZone("Australia/Melbourne");
    
    $utc_date = new DateTimeImmutable("2014-04-05 15:00:00", $utc);
    $utc_date_after = $utc_date->add($term);
    
    print $utc_date->setTimezone($melb)->format("c");
                                            // 2014-04-06T02:00:00+11:00
    print $utc_date_after->setTimezone($melb)->format("c");
                                            // 2014-04-06T02:00:00+10:00
    

    마지막으로 DatePeriod 클래스는 반복문에서 유용하게 사용되는 클래스다. 일정 기간동안 특정 주기를 반복적으로 처리할 때 유용한 클래스며 5.0.0 에서 추가된 Traversable 내장 클래스를 상속하는 클래스다. 특히 주기를 상대적인 포맷(Relative formats)을 사용해 작성할 수 있다.

    $begin = new DateTime('2013-01-01 00:00:00');
    $end = new DateTime('2013-12-31 23:59:59');
    $interval = DateInterval::createFromDateString('next monday');
    
    $period = new DatePeriod($begin, $interval, $end, DatePeriod::EXCLUDE_START_DATE);
    
    foreach ($period as $dt){
        echo $dt->format("l Y-m-d") . "\n";
    }
    

    시간을 다루는 편리한 방법, 단 버전이 된다면

    이상으로 PHP에서 쉽게 시간을 다룰 수 있도록 하는 클래스인 DateTime을 살펴봤다. 몇 클래스는 5.5.0 이상에서만 지원하고 있기 때문에 바로 사용하긴 어려울 수 있을지도 모르겠다. 하지만 PHP에서 시간을 문자열로 다루는 것은 여전히 번거로운 작업이므로 적극적으로 도입해야 하지 않나 생각이 든다. 환경이 5.3.0 이상이라면 꼭 사용해보도록 하자.

    이 글은 PHP 레퍼런스의 datetime 항목을 토대로 정리했다. 자세한 내용은 해당 메뉴얼을 살펴보면 확인할 수 있다.

    Footnotes

    1. DateTime class http://www.php.net/manual/en/class.datetime.php

    2. DateInterval class http://www.php.net/manual/en/class.dateinterval.php

    3. DateTime diff method http://www.php.net/manual/en/datetime.diff.php

    4. date_default_timezone_get() 함수를 통해 현재 설정된 타임존을 확인할 수 있다.

    근래 들어서는 공개적으로 하는 작업은 아니지만 잔잔하게 프로토타이핑은 꾸준히 하고 있는데 flasksqlalchemy 조합으로 진행하고 있었다.

    • flask는 micro web framework이며 micro 답게 간단하게 작성 가능해 생각나는 대로 작성하기 편리
    • sqlalchemy는 class 정의 만으로 db를 쉽게 구성하고 코드와 스키마를 두번 작성하는 수고를 줄여주는, 짱짱 좋은 ORM

    호기심으로 로컬의 php를 이번달에 공개한 5.5.5으로 업데이트 하면서 micro framework이 없을까 찾아봤더니 비슷한 컨셉의 프레임워크가 많이 보여 간단하게 정리해봤다. (sqlalchemy를 대안으로 사용할 php orm은 제대로 찾아보지 못했다.)

    찾아보니 생각보다 많은 편이었고 flask에서 이용한 파이썬의 delegate과 같은 feature는 php에 존재하지 않기 때문에 다양한 방식으로 구현되어 있었다. 특히 php에서 익명함수(Anonymous function)는 5.3.0 이후 제공되고 있기 때문에 이를 기준으로 지원 여부를 살펴보는 것도 도움이 된다. 1

    대다수 micro framework는 Composer라는 의존성 관리도구를 설치하길 권장한다. Composer 시작하기 문서를 살펴보면 도움이 된다.

    Slim

    Slim은 composer를 통해 간편하게 설치할 수 있으며 PHP 5.3.0 이상을 요구한다.

    $app = new \Slim\Slim();
    $app->get('/hello/:name', function($name){
        echo 'Hello, ' . $name;
    });
    $app->run();
    

    $app에 인스턴스를 할당할 때 mode, debug 등 다양한 옵션을 넣을 수 있으며 use2를 이용한 스코핑 인젝션 등이 가능하다. 익명함수, 일반함수 두가지 모두 가능하다. Route가 명시적이라 코드 리딩이 더 쉬운 편이다. 자세한 내용은 Slim의 문서 http://www.slimframework.com/ 참고.

    Limonade

    Limonade http://limonade-php.github.io/는 루비의 Sinatra와 Camping, Lua의 Orbit의 영감으로 만들어진 PHP micro framework이다.

    require_once 'vendors/limonade.php';
    dispatch('/', 'hello');
      function hello()
      {
          return 'Hello world!';
      }
    run();
    

    각 기능을 함수로 선언해 dispatch를 이용해 각 URL에 라우팅 하는 형태로, 직관적인 코드 작성이 가능하지만 동일한 함수명을 사용하지 않도록 주의해야 한다.

    Flight

    Flight라는 Public class를 이용해 작성해나가는 형태의 framework이다.

    require 'flight/Flight.php';
    
    Flight::route('/', function(){
        echo 'hello world!';
    });
    
    Flight::start();
    

    사용자가 선언한 인스턴스가 아닌 Flight를 반복적으로 입력해야 해서 별로 좋은 것 같진 않다. Flight http://flightphp.com/

    Silex

    Silex http://silex.sensiolabs.org/는 앞서 살펴본 Slim과 상당히 유사하며 Symfony2 컴포넌트를 기반으로 한 특징을 가지고 있다.

    require_once __DIR__.'/../vendor/autoload.php'; 
    
    $app = new Silex\Application(); 
    
    $app->get('/hello/{name}', function($name) use($app) { 
        return 'Hello '.$app->escape($name); 
    }); 
    
    $app->run(); 
    

    Bullet

    PHP 5.3+을 요구, 5.4 이상을 추천하는 framework이다. Composer로 패키지 관리가 가능하다.

    $app = new Bullet\App();
    $app->path('/', function($request) {
        return "Hello World!";
    });
    
    echo $app->run(new Bullet\Request());
    

    확장성을 고려해 구현해둔 부분이 많이 보인다. Bullet 문서 참고.

    GluePHP

    그냥 받아서 압축을 해제하면 바로 사용할 수 있는 micro framework이다. 다른 것들에 비해 가장 생각없이(?) 쉽게 사용할 수 있다.

    <?php
        require_once('glue.php');
        $urls = array(
            '/' => 'index'
        );
        class index {
            function GET() {
                echo "Hello, World!";
            }
        }
        glue::stick($urls);
    ?>
    

    url에 대해 정규표현식으로 지정할 수 있는 특징이 있다. 각각의 메소드를 바인딩 하는 것이 아니라 클래스를 바인딩하며 각 클래스마다 GET(), POST() 등의 메소드를 호출하는 형태다. 3

    Footnotes

    1. 사실 대다수의 micro framework는 5.3 이상을 요구한다. 그래도 예전에 비해 php 최근 버전을 지원하는 호스팅이 많이 늘었다.

    2. 익명함수 내부에서 외부의 변수를 이용하기 위해 사용하는 메소드다.

    3. 클래스의 단위를 잘 고려하지 않으면 쉽게 지저분해질 것 같다.

    AngularJS{{}} 방식의 인터폴레이션 마크업을 사용하는데 flask(jinja)django 등에서 {{ foo }} 형태의 템플릿 마크업을 이미 사용하고 있어서 문제가 된다. 이런 경우를 위해 AngularJS에서 $interpolateProvider를 지원하는데 이를 이용해 문제를 회피할 수 있다.

    var customInterpolationApp = angular.module('customInterpolationApp', []);
    
    customInterpolationApp.config(function($interpolateProvider) {
        $interpolateProvider.startSymbol('{@');
        $interpolateProvider.endSymbol('@}');
    });
    

    자세한 내용은 AngularJS의 $interpolateProvider 문서에서 확인할 수 있다.

    요즘 한참 핫(!)한 빅데이터 스터디에 참여하게 되었다. AWS에서는 사실 EMR을 지원하는 등 직접 설치할 일이 없다고 하는데 1EC2 Micro 인스턴스에 Hadoop을 실습을 위해 설치했다. 예/복습 차원에서 간략하게 스터디 내용을 정리해보려고 한다.


    Apache Hadoop

    **Apache Hadoop(High-Availability Distributed Object-Oriented Platform)**은 Apache 재단에서 진행하는 오픈소스 프로젝트로 대량의 자료(빅데이터)를 처리할 수 있는 분산 시스템을 구축할 수 있게 돕는 소프트웨어 라이브러리다. 이 프로젝트는 다음의 모듈을 포함하고 있다.

    • Hadoop Common: Hadoop 모듈을 지원하기 위한 일반적인 유틸리티들
    • Hadoop Distributed File System (HDFS): 어플리케이션 데이터를 대량으로 처리할 수 있게 접근이 가능한 분산 파일 시스템
    • Hadoop YARN: job 스케쥴링과 클러스터 리소스 관리를 위한 프레임워크
    • Hadoop MapReduce: YARN을 기반으로 한 대형 데이터 셋 병렬처리 시스템

    Hadoop HDFS 간단히 알기

    HDFS는 namenodedatanode로 구성되어 있다. Namenode는 HDFS의 파일 구조를 저장하는 영역이고 datanode는 실제 파일을 저장하는 영역이다. 일반적으로 사용되는 NTFS나 FAT같은 파일 시스템은 파일 구조의 위치에 실제 파일이 저장되지만 2HDFS는 그 파일 경로와 실제 파일이 분리되어 각각의 노드에 저장되는 형태이다. Namenode에는 datanode가 어디에 존재하는지 저장되어 있으며, 클라이언트가 해당 파일을 namenode에 요청하면 저장되어 있는 datanode를 알려줘 파일을 내려받을 수 있도록 돕는다. 이와 같이 HDFS는 구조(namenode)와 데이터(datanode)를 분리했기 때문에 데이터의 분산 처리가 가능하다.

    Datanode는 자료를 분산해서 저장함과 동시에 복제본을 담아둬 데이터를 유실했을 때를 대비한다.3

    Namenode는 HDFS의 모든 파일 명령 수행 목록을 Edits에 저장을 한다. FsImage는 Edits를 병합해 기록해둔 파일이다. Secondary namenode는 Namenode를 대체하거나 하진 않고 Edits 파일을 불러다가 FsImage로 병합해 namenode에게 돌려주는 역할을 한다.

    Hadoop MapReduce 간단히 알기

    구조와 데이터를 분산해서 처리한 것과 같이 데이터를 다루는 프로그램도 분산해서 각각의 데이터를 처리할 수 있도록 도와주는 것이 MapReduce이다.

    MapReduce는 Job trackerTask tracker로 구성이 되어 있는데 Job tracker는 Task tracker의 할 일을 관리하는 역할이고 Task tracker는 각 분산된 datanode에서 연산과정을 처리하는 역할을 한다.

    AWS에 Hadoop 설치하기

    설치 준비하기

    AWS에서 인스턴스 생성해서 콘솔로 접속한다. AWS 인스턴스를 시작하는 것은 검색하면 많이 나온다. ubuntu 12.04 LTS를 선택했다.

    $ ssh ubuntu@instance-address
    

    jdk를 설치한다.

    $ sudo apt-get update
    $ sudo apt-get install openjdk-6-jdk
    

    Hadoop을 아파치 다운로드 사이트에서 찾아 내려받고 압축을 해제한다.

    $ wget http://apache.mirror.uber.com.au/hadoop/common/hadoop-1.2.1/hadoop-1.2.1.tar.gz
    $ tar xvfz hadoop-1.2.1
    $ cd hadoop-1.2.1
    

    Hadoop 환경 설정

    Hadoop은 Java 기반이므로 사용할 환경의 경로를 지정해야 한다. conf/hadoop-env.sh을 열어 경로를 지정한다.

    $ vim conf/hadoop-env.sh
    

    JAVA_HOME의 주석을 지우고, jdk의 경로를 정해준다.

    # Set Hadoop-specific environment variables here.
    
    # The only required environment variable is JAVA_HOME.  All others are
    # optional.  When running a distributed configuration it is best to
    # set JAVA_HOME in this file, so that it is correctly defined on
    # remote nodes.
    
    # The java implementation to use.  Required.
    export JAVA_HOME=/usr/lib/jvm/java-6-openjdk-amd64/jre
    
    # Extra Java CLASSPATH elements.  Optional.
    # export HADOOP_CLASSPATH=
    

    Pseudo-Distibuted로 동작할 것이므로 각각의 configuration을 작성해준다.

    core-site.xml에서 fs.default.name 프로퍼티는 namenode가 동작하는 서버를 적어준다. secondary namenode, datanode, task tracker는 이 프로퍼티를 참고한다.

    $ vim conf/core-site.xml
    

    파일에서 아래 부분을 추가한다.

    <configuration>
        <property>
            <name>fs.default.name</name>
            <value>hdfs://localhost:9000</value>
        </property>
    </configuration>
    

    hdfs-site.xml에서 dfs.replication 프로퍼티는 복제 개수를 의미한다. 이 숫자가 3이면 동일한 데이터를 3개로 중복으로 저장해 유실을 방지할 수 있다.

    $ vim conf/hdfs-site.xml
    

    파일에서 아래 부분을 추가한다.

    <configuration>
        <property>
            <name>dfs.replication</name>
            <value>1</value>
        </property>
    </configuration>
    

    mapred-site.xml에서 mapred.job.tracker 프로퍼티는 task tracker를 위해 job tracker가 동작하는 서버를 적어준다.

    $ vim conf/mapred-site.xml
    

    파일에서 아래 부분을 추가한다.

    <configuration>
        <property>
            <name>mapred.job.tracker</name>
            <value>localhost:9001</value>
        </property>
    </configuration>
    

    SSH 설정하기

    각각의 node와 tracker끼리 데이터를 주고 받을 때를 위해 ssh key를 등록해준다.

    $ ssh-keygen -t rsa -P ""
    # 경로는 기존에 생성한 키가 없다면 기본 경로로 해도 된다.
    $ cat /home/ubuntu/.ssh/id_rsa.pub >> /home/ubuntu/.ssh/authorized_keys
    # 추가한 ssh key를 허용된 키로 등록해준다.
    

    키가 제대로 설정되었는지는 다음의 명령어로 확인해볼 수 있다.

    $ ssh localhost
    # 이러면 localhost에 다시 접속된다.
    $ exit
    

    Hadoop 실행하기

    $ pwd
    /home/ubuntu/hadoop-1.2.1
    

    Hadoop을 실행하기 이전에 namenode를 초기화한다.

    $ bin/hadoop namenode -format
    

    그다음 start-all.sh를 실행한다.

    $ bin/start-all.sh
    starting namenode, logging to /home/ubuntu/hadoop-1.2.1/libexec/../logs/hadoop-ubuntu-namenode-ip-10-240-106-45.out
    localhost: starting datanode, logging to /home/ubuntu/hadoop-1.2.1/libexec/../logs/hadoop-ubuntu-datanode-ip-10-240-106-45.out
    localhost: starting secondarynamenode, logging to /home/ubuntu/hadoop-1.2.1/libexec/../logs/hadoop-ubuntu-secondarynamenode-ip-10-240-106-45.out
    starting jobtracker, logging to /home/ubuntu/hadoop-1.2.1/libexec/../logs/hadoop-ubuntu-jobtracker-ip-10-240-106-45.out
    localhost: starting tasktracker, logging to /home/ubuntu/hadoop-1.2.1/libexec/../logs/hadoop-ubuntu-tasktracker-ip-10-240-106-45.out
    

    namenode, datanode, jobtracker, tasktracker가 순서대로 실행되는 것을 확인할 수 있다. 현재 실행되고 있는지 확인하려면 jps로 jvm에서 구동되고 있는 프로세스 항목을 확인할 수 있다.

    $ jps
    10379 NameNode
    11044 TaskTracker
    10852 JobTracker
    11192 Jps
    10562 DataNode
    10753 SecondaryNameNode
    

    종료는 stop-all.sh를 실행하면 된다.

    $ bin/stop-all.sh
    

    Hadoop Mode

    Hadoop은 아래 세가지의 모드로 설치할 수 있다.

    • Local (Standalone) Mode
    • Pseudo-Distributed Mode
    • Fully-Distributed Mode

    위에서 진행한 방식은 Pseudo-Distributed Mode로 하나의 서버에서 namenode, datanode, job tracker, task tracker를 모두 운용하는 모드다. 진짜(?) hadoop은 3번째 모드라고 하니 다음 글에선 3번째 모드로 진행하는 방법을 알아볼 것이다.

    Footnotes

    1. 잘 모른다;;

    2. 내부적으로 어떻게 동작하는지는 잘 모름;

    3. 이 복제 수를 hdfs-site.xml에서의 dfs.replication 프로퍼티로 제어한다.

    .Net 스터디를 대비해 개발 환경을 설치한 과정을 기록해둔 포스트. 이전 MonoDevelop에 비해 훨씬 뛰어난 모습의 Xamarin Studio와 각종 add-in package로 mac OSX에서의 .Net 개발 환경을 구축할 수 있었다. 스터디는 Visual Studio 기준이라서 아마 가상 환경을 구축하게 될 것 같지만 그러기엔 에어 용량이 많이 허덕여서 일단 급한대로 mono 환경을 다시 세팅했다.

    Mono란?

    Mono는 개발자들이 쉽게 크로스 플랫폼 어플리케이션을 만들 수 있도록 고안된 소프트웨어 플랫폼이다. Xamarin사에서 지원하며, Mono는 Microsoft .NET 프레임워크를 구현한 오픈소스이며 ECMA 표준을 따르는 C#과 공용언어 런타임을 기반으로 하고 있다. Mono 환경과 Xamarin Studio와 함께 .Net 개발을 시작할 수 있다. 1현재 안드로이드, iOS 등 광범위한 영역의 크로스 플랫폼을 구현하고 있다.

    Mono와 Xamarin Studio 설치

    1. Mono 웹사이트에서 Mono SDK인 MDK를 받는다. JRE, JDK처럼 MRE, MDK가 있는데 MDK를 설치하면 된다.

    Xamarin Studio

    1. Xamarin 웹사이트에서 Xamarin Studio를 내려받는다.

    둘다 받아 설치하면 일단 끝난다.

    최신의 MSBuild를 사용하기 위해서는 Xamarin Studio > Preferences... 로 들어가서 Project > Load/Save 항목을 눌러 최신 버전의 MSBuild를 선택한다.

    NuGet을 Xamarin Studio에 설치하기

    NuGet은 .Net을 포함한, Microsoft 개발 환경 플랫폼을 위한 패키지 매니저다. PyPI, npm 같이 편리하게 리포지터리에서 받아 사용할 수 있다.

    Mac에서 사용하고자 하면 이전엔 CLI를 이용해 설치하는 방법이 있었지만 Xamarin Studio에서 바로 사용할 수 있도록 add-in으로 만들어뒀다. 덕분에 편리하게 설치하고 사용할 수 있다.

    1. Xamarin Studio > Add-in Manager로 들어간다.
    2. Gallery 탭에서 Repository를 선택해 Manage Repositories에 들어간다.
    3. Add를 누른 후 자신의 Xamarin Studio 버전에 맞는 NuGet Add-inNuGet GitHub 페이지에서 찾아 추가한다.
    4. Refresh 버튼을 눌러 갱신한 후 Nuget Package Management를 검색해 설치한다.
    5. 이제 각 Project에서 오른쪽 클릭하면 Manage NuGet Packages를 볼 수 있으며 눌러 설정할 수 있다.

    NuGet 실행 화면

    기존의 project가 load failed 되는 경우

    solution을 불러오면 몇 project에서 불러와지지 않는 문제가 나타나는데 해당 프로젝트의 csproj를 열어 <ProjectTypeGuids> 항목을 지워주면 정상적으로 불러온다.


    기본적인 설치를 마치고 .Net 스터디에서 작성했던 코드를 받아 구동해봤는데 정상적으로 잘 동작한다. Xamarin Studio는 예전 MonoDevelop을 생각하면 엄청나게 좋아졌다는 것을 느낄 수 있다.

    Footnotes

    1. MS에서 제공하는 것에 비해 모자란 점이 있긴 하지만 꾸준히 성장하고 있다.

    다음의 파이썬 코드에서 Bus 클래스가 Base를 상속을 받을 수 있는가. 다시 말해 어떻게 변수를, 또는 인스턴스를 class가 상속을 받을 수 있는가? 라는 질문을 받았다.

    Base = declarative_base()
    class Bus(Base):
        __tablename__ = 'bus' 
        ....
    

    듣고 나도 혹해서 어떻게 이게 가능하지? 이러고서 모듈을 들여다봐도 이해가 안되게 너무 커서 이해가 전혀 되질 않았다. 잠도 안오고 그래서 메일링 리스트에 올리니 바로 답변이 왔다.

    >>> def make_human(): 
    ...    class Human(object): 
    ...       def say(self): 
    ...          print "I'm a human"
    ...    return Human
    ... 
    >>> dude = make_human() 
    >>> edward = dude() 
    >>> edward.say() 
    I'm a human
    >>> 
    

    파이썬에선 클래스도 반환이 가능하다.

    근래 간단한 서비스를 만들고 있는데 시작부터 시간대로 인한 문제가 있어 이 기회에 제대로 살펴보게 되었다. 한국에서 개발할 때는 단 한번도 생각해본 적이 없던 시간대 문제에 대해서 찾아볼 수 있게 되어 참 좋았고, 국가가 시간대를 변경함에 따라 역사적으로 사라진 시간들이 존재한다는 점, 동부표준시(EST)와 미동부 시간대(US/Eastern)가 어떻게 다른가 등 상당히 재미있는 (다른 의미로 일관성 없는) 부분들이 있다는 것을 알게 되었다.

    pytz는 Olson 시간대 데이터베이스를 기준으로 한, 역사적인 시간대와 현대적인 시간대를 모두 망라하고 있는 라이브러리다. 이 라이브러리 문서를 통해 시간대로 인해 발생할 수 있는 여러 경우를 살펴볼 수 있으므로 꼭 Python 개발자가 아니더라도 시간대 문제에 대해 관심이 있다면 살펴볼만한 이야기가 담겨져있다.

    특히 처음에 번역할 때 동부표준시와 미동부 시간대에 대해 정확한 이해가 없어서 대충 옮겼다가 전체적으로 다시 살펴보긴 했는데 여전히 오류가 있는 것 같아 앞서 그 차이를 밝혀두면, 미동부 시간대(US/Eastern)는 동부표준시인 EST와 동부일광절약시인 EDT를 교차로 사용한다. EDT 없이 EST만 사용하는 곳도 존재한다.

    결론적인 부분을 먼저 적어보면, UTC로 모든 시간을 관리하고 사용자에 따라 각 시간대에 맞춰 출력해주는 방식이 시간을 다루는 가장 좋은 방법이다. (UTC 만세!)


    pytz – 세계 시간대 정의를 위한 Python 라이브러리

    Stuart Bishop (stuart@stuartbishop.net)

    원문 https://pypi.python.org/pypi/pytz/

    소개

    pytz는 Olson tz databse를 Python으로 옮겨온 라이브러리다. 이 라이브러리는 정확하게, 크로스 플랫폼을 지원하는 시간대 계산도구로 Python 2.4 이상에서 사용할 수 있다. 또한 일광 절약 시간이 끝날 때 발생하는 시간의 모호한 문제를 해결해주는데 이에 대한 자세한 내용은 Python 표준 라이브러리에서 더 찾아볼 수 있다. (datetime.tzinfo)

    거의 대부분의 Olson 시간대 데이터베이스를 지원한다.

    덧붙여, 이 라이브러리는 Python API의 tzinfo 구현과는 다르다. 만약 지역의 벽시계를 만들고 싶다면 이 라이브러리의 localize() 메소드를 사용해야 한다. 추가적으로, 시간을 산술적으로 계산하는데 일광절약시간의 영역을 넘나든다면 그 결과물은 다른 시간대가 되어야 한다. (예를 들면 2002-10-27 1:00 동부표준시에서 1분을 빼면 2002-10-27 1:59 동부일광절약시가 아닌 2002-10-27 0:59 동부표준시를 반환할 것이다.) 이런 경우 이 라이브러리의 normalize() 메소드가 도움이 된다. 이러한 문제는 Python의 datetime 구현을 수정하지 않는 이상 해결하기 어려운 문제다.

    설치

    이 패키지는 설치도구를 이용해 .egg로 설치할 수도 있고 Python 표준 distutill로 tarball로부터 설치도 가능하다.

    만약 tabll로 설치한다면 관리자 권한으로 아래 명령어를 실행한다::

    python setup.py install
    

    만약 설치도구로 설치한다면 Python 패키지 인덱스에서 알아서 최신 버전을 받아 설치해준다::

    easy_install --upgrade pytz
    

    .egg파일을 이미 가지고 있다면 아래와 같이 설치가능하다::

    easy_install pytz-2008g-py2.6.egg
    

    예제와 사용법

    현지 시간과 일자의 계산

    >>> from datetime import datetime, timedelta
    >>> from pytz import timezone
    >>> import pytz
    >>> utc = pytz.utc
    >>> utc.zone
    'UTC'
    >>> eastern = timezone('US/Eastern')
    >>> eastern.zone
    'US/Eastern'
    >>> amsterdam = timezone('Europe/Amsterdam')
    >>> fmt = '%Y-%m-%d %H:%M:%S %Z%z'
    

    이 라이브러리는 지역 시간을 생성하기 위한 두가지 방법을 지원한다. 첫째는 pytz 라이브러리에서 제공하는 localize() 메소드를 이용하는 방법이다. 이 메소드는 시간대 보정이 없는, 순수한 datetime을 지역화하는데 사용한다:

    >>> loc_dt = eastern.localize(datetime(2002, 10, 27, 6, 0, 0))
    >>> print(loc_dt.strftime(fmt))
    2002-10-27 06:00:00 EST-0500
    

    둘째로 astimezone()메소드를 이용해 이미 만들어 지역화된 시간을 변경하여 사용하는 방법이 있다:

    >>> ams_dt = loc_dt.astimezone(amsterdam)
    >>> ams_dt.strftime(fmt)
    '2002-10-27 12:00:00 CET+0100'
    

    안타깝게도 표준 datetime 생성자에서 사용하는 tzinfo 아규먼트는 pytz의 많은 시간대에서 정상적으로 ”동작하지 않는다”.

    >>> datetime(2002, 10, 27, 12, 0, 0, tzinfo=amsterdam).strftime(fmt)
    '2002-10-27 12:00:00 AMT+0020'
    

    일광절약시간으로 변경하지 않더라도 UTC와 같은 시간대를 사용하는 것이 안전하다.

    >>> datetime(2002, 10, 27, 12, 0, 0, tzinfo=pytz.utc).strftime(fmt)
    '2002-10-27 12:00:00 UTC+0000'
    

    시간을 다루는 좋은 방법은 항상 UTC로 시간을 다루고 사람이 보기 위해 출력할 때만 해당 지역 시간으로 변환해 보여주는 것이다.

    >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
    >>> loc_dt = utc_dt.astimezone(eastern)
    >>> loc_dt.strftime(fmt)
    '2002-10-27 01:00:00 EST-0500'
    

    이 라이브러리는 지역 시간을 이용해 날짜를 산술 계산할 수 있다. UTC에서 계산하고 normalize() 메소드를 이용해 일광절약시간과 다른 시간대로 변환하는 것을 조정하는 것보다는 조금 복잡하지만 말이다. 예를 들면 loc_dt는 미국 동부(US/Eastern) 시간대의 일광 절약 시간이 종료될 때의 시간으로 값을 받는다.

    >>> before = loc_dt - timedelta(minutes=10)
    >>> before.strftime(fmt)
    '2002-10-27 00:50:00 EST-0500'
    >>> eastern.normalize(before).strftime(fmt)
    '2002-10-27 01:50:00 EDT-0400'
    >>> after = eastern.normalize(before + timedelta(minutes=20))
    >>> after.strftime(fmt)
    '2002-10-27 01:10:00 EST-0500'
    

    지역 시간을 생성하는건 좀 까다롭기 때문에 지역 시간으로 작업하는 것을 권장하지 않는다. 안타깝게도 datetime을 생성할 때 tzinfo 아규먼트를 사용해서는 해결될 수 없다. (다음 섹션에서 더 자세하게 다룬다)

    >>> dt = datetime(2002, 10, 27, 1, 30, 0)
    >>> dt1 = eastern.localize(dt, is_dst=True)
    >>> dt1.strftime(fmt)
    '2002-10-27 01:30:00 EDT-0400'
    >>> dt2 = eastern.localize(dt, is_dst=False)
    >>> dt2.strftime(fmt)
    '2002-10-27 01:30:00 EST-0500'
    

    시간대 간 변환을 할 때도 특별한 주의를 요구한다. 여기서도 normalize() 메소드를 활용해 이 변환이 올바르게 되도록 한다.

    >>> utc_dt = utc.localize(datetime.utcfromtimestamp(1143408899))
    >>> utc_dt.strftime(fmt)
    '2006-03-26 21:34:59 UTC+0000'
    >>> au_tz = timezone('Australia/Sydney')
    >>> au_dt = au_tz.normalize(utc_dt.astimezone(au_tz))
    >>> au_dt.strftime(fmt)
    '2006-03-27 08:34:59 EST+1100'
    >>> utc_dt2 = utc.normalize(au_dt.astimezone(utc))
    >>> utc_dt2.strftime(fmt)
    '2006-03-26 21:34:59 UTC+0000'
    

    또한 UTC로 된 시간대 변환이 필요할 때 아래와 같은 지름길을 이용할 수 있다. normalize()localize()는 일광절약시간의 문제가 없다면 꼭 필요한 것은 아니다.

    >>> utc_dt = datetime.utcfromtimestamp(1143408899).replace(tzinfo=utc)
    >>> utc_dt.strftime(fmt)
    '2006-03-26 21:34:59 UTC+0000'
    >>> au_tz = timezone('Australia/Sydney')
    >>> au_dt = au_tz.normalize(utc_dt.astimezone(au_tz))
    >>> au_dt.strftime(fmt)
    '2006-03-27 08:34:59 EST+1100'
    >>> utc_dt2 = au_dt.astimezone(utc)
    >>> utc_dt2.strftime(fmt)
    '2006-03-26 21:34:59 UTC+0000'
    

    tzinfo API

    tzinfo 인스턴스는 timezone()함수에 의해 반환되는데 이 함수는 모호한 시간대에 대응하기 위한 is_dst 파라미터를 utcoffset(), dst(), tzname() 와 같은 메소드를 확장한 것이다.

    >>> tz = timezone('America/St_Johns')
    
    >>> normal = datetime(2009, 9, 1)
    >>> ambiguous = datetime(2009, 10, 31, 23, 30)
    

    is_dst파라미터는 많은 타임스템프들에서 무시된다. 단지 DST 전환에 의해 나타나는 모호한 시간을 해결하기 위해 사용된다.

    >>> tz.utcoffset(normal, is_dst=True)
    datetime.timedelta(-1, 77400)
    >>> tz.dst(normal, is_dst=True)
    datetime.timedelta(0, 3600)
    >>> tz.tzname(normal, is_dst=True)
    'NDT'
    
    >>> tz.utcoffset(ambiguous, is_dst=True)
    datetime.timedelta(-1, 77400)
    >>> tz.dst(ambiguous, is_dst=True)
    datetime.timedelta(0, 3600)
    >>> tz.tzname(ambiguous, is_dst=True)
    'NDT'
    
    >>> tz.utcoffset(normal, is_dst=False)
    datetime.timedelta(-1, 77400)
    >>> tz.dst(normal, is_dst=False)
    datetime.timedelta(0, 3600)
    >>> tz.tzname(normal, is_dst=False)
    'NDT'
    
    >>> tz.utcoffset(ambiguous, is_dst=False)
    datetime.timedelta(-1, 73800)
    >>> tz.dst(ambiguous, is_dst=False)
    datetime.timedelta(0)
    >>> tz.tzname(ambiguous, is_dst=False)
    'NST'
    

    만약 is_dst값이 지정되지 않으면, 모호한 타임스탬프에서 pytz.exceptions.AmbiguousTimeError 예외가 발생한다.

    >>> tz.utcoffset(normal)
    datetime.timedelta(-1, 77400)
    >>> tz.dst(normal)
    datetime.timedelta(0, 3600)
    >>> tz.tzname(normal)
    'NDT'
    
    >>> import pytz.exceptions
    >>> try:
    ...     tz.utcoffset(ambiguous)
    ... except pytz.exceptions.AmbiguousTimeError:
    ...     print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous)
    pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00
    >>> try:
    ...     tz.dst(ambiguous)
    ... except pytz.exceptions.AmbiguousTimeError:
    ...     print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous)
    pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00
    >>> try:
    ...     tz.tzname(ambiguous)
    ... except pytz.exceptions.AmbiguousTimeError:
    ...     print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous)
    pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00
    

    지역시간으로 인한 문제들

    시간으로 인해 발생하는 가장 중요한 문제는 특정 일시가 1년에 두 번 나타날 수 있다는 부분이다. 예를 들면 미 동부 시간대에서 10월 마지막 일요일 아침에 아래와 같은 일련의 사건이 나타났다고 가정해보자.

    - 01:00am 동부 일광 절약 표준시가 됨
    - 1시간 후, 2:00am 시계를 1시간 뒤로 돌리면 또 01:00am가 됨
      (이 시간은 01:00 동부표준시)
    

    사실 모든 인스턴스는 01:00부터 02:00 사이에 두번씩 나타난다. 이 의미는 미동부 시간대에서 표준 datetime 문법을 따르면 일광절약시간이 끝난 시간보다 전의 시간을 정의할 수 있는 방법이 없다는 뜻이다.

    >>> loc_dt = datetime(2002, 10, 27, 1, 30, 00, tzinfo=eastern)
    >>> loc_dt.strftime(fmt)
    '2002-10-27 01:30:00 EST-0500'
    

    위에서 보듯, 시스템은 하나를 골라야만 하고, 이 한시간 이내에 제대로 시간이 표기될 확률은 50%가 된다. 몇 어플리케이션에서는 이런건 문제가 되지 않는다. 하지만 다양한 시간대에 살고 있는 사람들의 미팅 스케쥴을 잡아야 하거나, 로그 파일을 분석해야 한다면 이건 문제가 된다.

    최고의 방법이자 가장 단순한 해결책은 UTC를 사용하는 것이다. pytz 패키지는 내부적으로 시간대를 표현하는데 UTC를 사용하기를 권장하며, 특히 Python에서 표준 레퍼런스를 기반으로 구현된 특별한 UTC 구현을 활용하는 것을 권장한다.

    UTC 시간대는 같은 인스턴스가 되는 문제가 없지만 다른 pytz tzinfo 인스턴스보다는 큰 사이즈라는 문제가 있다. UTC 구현은 pytz.utc, pytz.UTC 또는 pytz.timezone(‘UTC’)에 포함된다.

    >>> import pickle, pytz
    >>> dt = datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc)
    >>> naive = dt.replace(tzinfo=None)
    >>> p = pickle.dumps(dt, 1)
    >>> naive_p = pickle.dumps(naive, 1)
    >>> len(p) - len(naive_p)
    17
    >>> new = pickle.loads(p)
    >>> new == dt
    True
    >>> new is dt
    False
    >>> new.tzinfo is dt.tzinfo
    True
    >>> pytz.utc is pytz.UTC is pytz.timezone('UTC')
    True
    

    덧붙여, 이 UTC 인스턴스는 다른 이름에 같은 의미를 가진 시간대(GMT, 그리니치, 유니버셜 등)와 같은 인스턴스 (또는 같은 구현)이 아니다.

    >>> utc is pytz.timezone('GMT')
    False
    

    지역 시간으로 표기하고 싶을 때, 이 라이브러리는 시간대들이 모호하지 않도록 편의를 제공할 것이다:

    >>> loc_dt = datetime(2002, 10, 27, 1, 30, 00)
    >>> est_dt = eastern.localize(loc_dt, is_dst=True)
    >>> edt_dt = eastern.localize(loc_dt, is_dst=False)
    >>> print(est_dt.strftime(fmt) + ' / ' + edt_dt.strftime(fmt))
    2002-10-27 01:30:00 EDT-0400 / 2002-10-27 01:30:00 EST-0500
    

    is_dst 플래그를 None으로 둔 채 localize()를 사용하면, pytz는 결과값을 예측하지 못하게 되고 그로 인해 모호하거나 존재하지 않는 시간을 생성하게 되어 예외가 발생한다.

    예를 들면 미국동부시에서 일광절약시간이 종료되어 시계를 한시간 뒤로 돌려 2002년 10월 27일 1:30am이 두번 나타나게 되는 경우에 아래와 같은 예외가 발생하는 것을 확인할 수 있다:

    >>> dt = datetime(2002, 10, 27, 1, 30, 00)
    >>> try:
    ...     eastern.localize(dt, is_dst=None)
    ... except pytz.exceptions.AmbiguousTimeError:
    ...     print('pytz.exceptions.AmbiguousTimeError: %s' % dt)
    pytz.exceptions.AmbiguousTimeError: 2002-10-27 01:30:00
    

    유사한 이유로, 2002년 4월 7일 2:30am은 모든 미국동부 시간대에서 절대 발생하지 않는데 모든 시계가 1시간을 앞당겨 2:00am은 존재하지 않기 떄문이다:

    >>> dt = datetime(2002, 4, 7, 2, 30, 00)
    >>> try:
    ...     eastern.localize(dt, is_dst=None)
    ... except pytz.exceptions.NonExistentTimeError:
    ...     print('pytz.exceptions.NonExistentTimeError: %s' % dt)
    pytz.exceptions.NonExistentTimeError: 2002-04-07 02:30:00
    

    두 예외는 공통적인 기반 클래스를 공유하고 있기 때문에 에러를 다루는데는 큰 문제가 없다:

    >>> isinstance(pytz.AmbiguousTimeError(), pytz.InvalidTimeError)
    True
    >>> isinstance(pytz.NonExistentTimeError(), pytz.InvalidTimeError)
    True
    

    localize()로 대다수의 경우를 다룰 수 있지만, 아직까지 모든 경우를 다루지는 못한다. 국가가 시간대 정의를 변경하는 경우, 일광절약시간 종료일 같은 문제들은 어떠한 방법으로도 그 모호성을 없엘 수 없다. 그 예로 1915년 바르샤바(주. 폴란드의 수도)는 바르샤바시에서 중앙유럽시로 변경했다. 1915년 8월 5일 자정을 기해 24분을 뒤로 돌렸는데 이로 인해 정의할 수 없는 모호한 시간 기간이 생겨나게 되었고 그 기간은 축약 시간대나 실제 UTC 표준시 이외에는 표기할 방법이 없게 되었다. 이와 같이 자정이 두번 발생하는 경우는, 일광절약시간으로 발생하는 문제와도 다른 경우다:

    >>> warsaw = pytz.timezone('Europe/Warsaw')
    >>> loc_dt1 = warsaw.localize(datetime(1915, 8, 4, 23, 59, 59), is_dst=False)
    >>> loc_dt1.strftime(fmt)
    '1915-08-04 23:59:59 WMT+0124'
    >>> loc_dt2 = warsaw.localize(datetime(1915, 8, 5, 00, 00, 00), is_dst=False)
    >>> loc_dt2.strftime(fmt)
    '1915-08-05 00:00:00 CET+0100'
    >>> str(loc_dt2 - loc_dt1)
    '0:24:01'
    

    이 잃어버린 24분 사이의 시간을 생성하는 방법은 다른 시간대로부터 변환하는 방법 밖에 없는데 어떤 시간대를 사용한다 하더라도 일광 절약 모드의 API를 활용한다 해도 단순하게 나타낼 방법이 없기 때문이다:

    >>> utc_dt = datetime(1915, 8, 4, 22, 36, tzinfo=pytz.utc)
    >>> utc_dt.astimezone(warsaw).strftime(fmt)
    '1915-08-04 23:36:00 CET+0100'
    

    표준 Python에서 이와 같은 모호함을 처리하는 방법은 다뤄지지 않는데 Python 문서에 나온 미동부 시간대의 예제를 보면 확인할 수 있다. (이 구현은 1987년과 2006년 사이에서만 동작하는데 단지 테스트를 위해 포함되었다):

    >>> from pytz.reference import Eastern # pytz.reference only for tests
    >>> dt = datetime(2002, 10, 27, 0, 30, tzinfo=Eastern)
    >>> str(dt)
    '2002-10-27 00:30:00-04:00'
    >>> str(dt + timedelta(hours=1))
    '2002-10-27 01:30:00-05:00'
    >>> str(dt + timedelta(hours=2))
    '2002-10-27 02:30:00-05:00'
    >>> str(dt + timedelta(hours=3))
    '2002-10-27 03:30:00-05:00'
    

    첫 두 결과를 확인해보면, 처음에 슬쩍 봐서는 옳은 결과값이라 생각이 들겠지만 UTC를 기준으로 편차 계산해보면 사실 우리가 요청한 1시간이 아닌 실제로 2시간임을 확인할 수 있다.

    >>> from pytz.reference import UTC # pytz.reference only for tests
    >>> str(dt.astimezone(UTC))
    '2002-10-27 04:30:00+00:00'
    >>> str((dt + timedelta(hours=1)).astimezone(UTC))
    '2002-10-27 06:30:00+00:00'
    

    국가 정보

    ISO 3166 국가 코드를 사용해 개별 국가들이 사용하는 일반적인 시간대를 접근할 수 있도록 지원한다. pytz.timezone()을 이용하면 문자열 리스트를 반환하는데 이 문자열을 관련된 tzinfo 인스턴스를 가져오는데 사용할 수 있다:

    >>> print(' '.join(pytz.country_timezones['nz']))
    Pacific/Auckland Pacific/Chatham
    

    Olson 데이터베이스는 ISO 3166 국가 코드를 영문 국가명과 맵핑해뒀기 때문에 pytz를 딕셔너리와 같이 사용할 수 있다:

    >>> print(pytz.country_names['nz'])
    New Zealand
    

    UTC란 무엇인가

    ‘UTC’는 협정 시간으로, 그리니치 표준시나 영국의 GMT로 많이 알려져 있다. 다른 모든 시간대는 UTC를 기준으로 편차 계산하는 방식이다. UTC에서는 일광절약시간이 존재하지 않기 때문에 산술적으로 계산하는데 아무런 문제가 없어서, 일광절약시간 변환, 국가가 시간대를 변경하는 경우, 또는 이동형 컴퓨터가 다른 여러 시간대로 이동해야 하는 경우에도 아무런 문제를 만들지 않는다.

    헬퍼

    헬퍼는 두가지 목록의 시간대를 제공한다.

    all_timezones는 명확한 시간대명 목록으로 활용 가능하다.

    >>> from pytz import all_timezones
    >>> len(all_timezones) >= 500
    True
    >>> 'Etc/Greenwich' in all_timezones
    True
    

    common_timezones는 현재의 시간대 목록으로 유용하게 사용할 수 있다. 이 목록은 몇가지 일반적으로 필요한 경우를 제외하고, 더이상 존재하지 않는 시간대나 역사적인 시간대를 포함시키지 않았다. 예를 들면 미국동부시의 경우는 포함되어 있다. (만약 생각하기에 여기에 포함되어야 한다고 생각하는 시간대가 있다면 버그리포트를 만들어주기 바란다.) 이 또한 문자열 목록으로 제공된다. (주. 미국동부시의 경우 동부표준시 EST와 동부일광절약시 EDT를 둘 다 사용한다. 같은 시간대에 있는 국가 중 EDT의 적용 없이 EST만 적용하는 경우도 있다.)

    >>> from pytz import common_timezones
    >>> len(common_timezones) < len(all_timezones)
    True
    >>> 'Etc/Greenwich' in common_timezones
    False
    >>> 'Australia/Melbourne' in common_timezones
    True
    >>> 'US/Eastern' in common_timezones
    True
    >>> 'Canada/Eastern' in common_timezones
    True
    >>> 'US/Pacific-New' in all_timezones
    True
    >>> 'US/Pacific-New' in common_timezones
    False
    

    common_timezonesall_timezones 두 목록은 알파벳 순으로 정렬되어 있다:

    >>> common_timezones_dupe = common_timezones[:]
    >>> common_timezones_dupe.sort()
    >>> common_timezones == common_timezones_dupe
    True
    >>> all_timezones_dupe = all_timezones[:]
    >>> all_timezones_dupe.sort()
    >>> all_timezones == all_timezones_dupe
    True
    

    all_timezonescommon_timezones 두 목록은 set으로도 사용 가능하다:

    >>> from pytz import all_timezones_set, common_timezones_set
    >>> 'US/Eastern' in all_timezones_set
    True
    >>> 'US/Eastern' in common_timezones_set
    True
    >>> 'Australia/Victoria' in common_timezones_set
    False
    

    또한 시간대 목록에서 개별 국가를 이용해 사용할 때 country_timezones() 함수를 활용할 수 있다. 이 함수는 ISO-3166 2글자 국가코드를 사용한다.

    >>> from pytz import country_timezones
    >>> print(' '.join(country_timezones('ch')))
    Europe/Zurich
    >>> print(' '.join(country_timezones('CH')))
    Europe/Zurich
    

    라이센스

    MIT license.

    This code is also available as part of Zope 3 under the Zope Public License, Version 2.1 (ZPL).

    I’m happy to relicense this code if necessary for inclusion in other open source projects.

    최신 버전

    이 패키지는 Olson 시간대 데이터베이스가 갱신될 때마다 업데이트 될 것이다. 최신 버전은 Python Package Index http://pypi.python.org/pypi/pytz/ 에서 받을 수 있다. 이 배포판을 생성하기 위해 launchpad.net에서 호스트 되고 있으며 Bazaar<br /> 버전 컨트롤 시스템 http://bazaar-vcs.org 에서는 아래와 같이 사용할 수 있다:

    bzr branch lp:pytz
    

    버그, 기능 요청과 패치

    버그는 다음 경로로 제보 바란다. Launchpad https://bugs.launchpad.net/pytz

    이슈와 한계점

    • UTC로부터의 편차계산은 가장 가까운 분을 기준으로 반올림 되는데 그로 인해 1937년 이전 유럽/암스테르담과 같은 시간대들은 30초씩 잃어버리게 된다. 이런 한계는 Python datatime 라이브러리의 한계다.

    • 만약 보기에 시간대 정의가 잘못되었다면, 아마 고칠 수 없으리라 본다. pytz는 Olson 시간대 데이터베이스를 그대로 번역한 것이라 시간대 정의를 변경하고 싶다면 이 데이터베이스를 수정해야 한다. 만약 시간대와 관련된 문제를 찾는다면 다음 링크의 메일링 리스트를 통해 리포트하기 바란다. http://www.iana.org/time-zones

    더 읽어보기

    시간대에 대한 이해가 더 필요하다면 다음 글이 도움이 될 것이다: http://www.twinsun.com/tz/tz-link.htm

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.