근래 아이폰, 아이패드, 맥북 프로 등 통칭 레티나로 일컬어지는, 고밀도 디스플레이 기기가 늘고 있다. 그에 따라서 웹페이지도 레티나 해상도에 대응을 하기 시작했는데, 각 이미지를 2배의 해상도로 저장한 후 css 또는 js를 이용해 치환하는 형태 또는 svg, canvas 등을 이용하는 방식으로 대응하고 있다. 여러 방법 중 svg를 활용하는 방법을 살펴보려고 한다.

svg는 xml로 작성된 벡터 이미지 포맷으로 대다수의 최신 브라우저에서 지원하고 있어 이와 같은 문제를 해결하는데 도움이 된다. svg를 이용하는 장점은 다음과 같다.

  • 이미지를 2번 이상 생성하지 않아도 된다. 하나의 이미지로 여러 해상도를 지원할 수 있으며 단일 파일로 모두 제어할 수 있으므로 유지보수에 용이하다.
  • svg 엘리먼트를 이용해 인라인으로 사용하면 stylesheet나 js를 이용해 동적으로 활용할 수 있다.

물론 svg를 사용할 때 단점도 분명 존재한다.

  • 확대/축소에 따라서 의도와 다른 형태로 렌더링 될 수 있다. 예를 들면 비트맵에서는 쉽게 가능한 1px 선을 벡터 방식에선 면으로 표현해야 하기 때문에 그리기 어렵다.
  • IE 8 이하, Android 내장 브라우저 (2.1, 2.2, 2.3)에서는 지원하지 않는다.1

위에서 살펴본 장단점에 따라 svg를 사용할 수 있는지, 어떤 경우에 적용할 것인지 전략을 세워야 한다. 특히 렌더링에서 차이를 보이는 부분으로 인해 iOS에서 벡터 앱 아이콘을 사용할 수 있는 상황에도 애플앱들은 비트맵으로 작성한 아이콘을 쓰고 있는 예도 있다.2

svg 사용하기

svg를 웹에서 사용하는 방법은 img 엘리먼트와 svg 엘리먼트를 이용하거나 css를 통해 활용할 수 있다.

img를 이용해 일반 이미지처럼 사용할 수 있다.

<img src="logo.svg" alt="Weird Meetup" />

그리고 svg 엘리먼트를 통해 직접 넣을 수 있다. 이렇게 작성하면 다소 지저분해지는 경향이 있지만 svg 내부의 엘리먼트도 css로 제어할 수 있다는 장점도 있다. 아래는 이상한 모임 로고의 글자 부분이다.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="500px"
 height="500px" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve">
    <g id="이상한모임">
        <g>
            <path fill="#000000" d="M180.647,389.499c-2.698,0-5.852-1.786-5.852-6.649v-9.042c0-3.951,2.584-6.991,6.991-6.991h3.381
                c4.217,0,6.839,3.153,6.839,7.105v9.156c0,3.837-2.545,6.421-5.205,6.421H180.647z M184.447,385.586
                c1.899,0,2.773-1.824,2.773-3.268v-8.055c0-1.634-1.254-3.078-2.773-3.078h-1.9c-1.52,0-2.925,1.444-2.925,3.078v8.055
                c0,2.47,0.988,3.268,2.925,3.268H184.447z M200.291,364.309h-5.091v31.953h5.091V364.309z"/>
            <path fill="#000000" d="M216.556,365.639c0.342,4.521,4.597,11.702,7.485,15.273h-5.737c-1.52-1.823-3.153-5.015-4.407-7.94
                c-1.292,2.583-2.28,5.167-4.445,7.94h-6.421c5.433-4.559,8.207-13.374,8.435-15.273H216.556z M226.283,384.712
                c2.735,0,4.939,1.71,4.939,4.104v3.039c0,2.09-2.127,4.027-4.787,4.027h-12.918c-3.496,0-5.396-1.178-5.396-4.065v-2.887
                c0-2.812,2.128-4.218,5.281-4.218H226.283z M214.922,388.663c-1.444,0-1.976,0.38-1.976,1.292v1.102
                c0,1.103,1.064,1.103,2.052,1.103h9.727c0.912,0,2.052-0.381,2.052-1.103v-1.178c0-0.722-0.76-1.216-2.052-1.216H214.922z
                 M235.135,374.947h-4.255v7.979h-4.863v-18.655h4.863v6.231h4.255V374.947z"/>
            <path fill="#000000" d="M248.742,366.437h6.117v3.495h-2.735c0.874,0.912,1.405,2.166,1.405,3.496v3.951
                c0,3.191-2.355,5.471-5.053,5.471h-4.711c-2.698,0-5.091-2.279-5.091-5.091v-4.331c0-1.33,0.532-2.584,1.406-3.496h-2.66v-3.495
                h6.117v-2.166h5.205V366.437z M263.484,395.502h-22.721v-10.524h4.788v6.003h17.934V395.502z M249.122,376.847v-2.432
                c0-1.216-0.912-2.127-2.166-2.127h-1.596c-1.216,0-2.318,0.911-2.318,2.127v2.432c0,1.254,0.722,2.166,1.938,2.166h2.127
                C248.362,379.013,249.122,378.101,249.122,376.847z M262.42,372.819h4.483v4.56h-4.483v8.473h-4.901v-21.619h4.901V372.819z"/>
            <path fill="#000000" d="M298.063,392.767h-28.876v-4.445h11.893v-4.293h5.091v4.293h11.893V392.767z M272.493,367.12v15.616
                h22.455V367.12H272.493z M290.236,378.557h-12.538v-7.295h12.538V378.557z"/>
            <path fill="#000000" d="M315.621,370.806v5.243c0,2.926-2.317,5.243-5.091,5.243h-4.901c-2.773,0-5.281-2.317-5.281-5.243v-5.243
                c0-2.926,2.508-5.243,5.281-5.243h4.901C313.304,365.562,315.621,367.88,315.621,370.806z M326.031,384.598v11.664h-22.492
                v-11.664H326.031z M311.176,371.87c0-1.292-0.987-2.242-2.241-2.242h-1.71c-1.254,0-2.355,0.95-2.355,2.242v3.115
                c0,1.292,1.102,2.279,2.355,2.279h1.71c1.254,0,2.241-0.987,2.241-2.279V371.87z M322.422,388.587h-14.817v3.609h14.817V388.587z
                 M326.031,364.232h-4.407v18.124h4.407V364.232z"/>
        </g>
    </g>
</svg>

어떻게 지저분한지 보여주기 위해 위 예를 넣었다. 다섯글자일 뿐인데 이렇게 지저분하다.

modernizr와 css를 이용한 이미지 대체

위와 같이 직접 적용하면 svg를 지원하지 않는 브라우저에서는 이미지가 나타나지 않기 때문에 modernizr를 통해 svg 지원 여부를 확인하고 그에 따라 css로 이미지를 교체해주는 방법을 활용할 수 있다.

modernizr를 적용한 모습

Modernizr를 웹페이지에 적용하면 해당 브라우저에서 어떤 기능을 지원하는지 html의 클래스로 선언해준다. html에 적용된 클래스를 플래그로 이용해 css 배경을 대체/적용하는 방식으로 svg 미지원 브라우저 문제를 해결할 수 있다.

#logo { background: url("logo.svg"); }
.no-svg #logo { background: url("logo.png"); }

svg 적용 사례

  • Apple : apple 사이트의 메뉴 등에서 svg를 사용함. 벡터 이미지는 서체 표현에 용이함.
  • Mailchimp : 동보메일 서비스인 mailchimp도 이번 개편에서 svg를 곳곳에서 사용하고 있음. 특히 체크박스나 라디오 버튼을 svg로 대체하고 있는 점이 독특.
  • FontAwesome : 아이콘 폰트를 제공하는 서비스인데 svg로 된 포멧도 지원함.

이외에도 svg를 활용한 곳을 심심하지 않게 찾아볼 수 있다. svg도 이제 지원하는 브라우저가 많기도 하고 하위 호환을 고려하기도 큰 어려움이 없기 때문에 예전에 비해 많이 사용하는 추세다. 위의 예시에서 눈치챘을 수도 있지만 이상한모임의 로고도 svg를 사용했다.

읽어볼 만한 글

다음 두 포스트는 레티나 지원을 위한 다양한 방법에 대해 체계적으로 잘 정리된 글이다.

간단한 svg 적용 방법, 위에서 언급한 fontawesome를 이용하는 방법은 다음의 링크에서 확인해볼 수 있다.

Footnotes

  1. svg의 브라우저 지원 여부는 caniuse.com의 svg 페이지에서 확인해볼 수 있다.

  2. iOS7으로 오면서 애플앱도 벡터 방식 아이콘을 쓰는 것 같더라. (잘 모름)

읽기 전에

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도 완전히 지원하는 상황이 아니다. 
  • 그리고 이 문제를 해결하기 위해 엄청 검색을 했었는데 비슷한 문제를 버전 대대로 겪은 사람들이 많이 나왔다. 물론 구현 자체가 없으니 아무도 해결하지 못했다. 
  • 점심 먹으며 글을 읽다가 참 인상적인 내용의 포스트를 보게 되어 허락받고 글을 옮겨봤다. 꼭 이 글에서 얘기하는 특정적인 상황 뿐만 아니라, 우리가 일상적으로 자주 겪는 상황에도 충분히 적용될 수 있는 이야기다. 원문은 Why ask why? – Ned Batchelder에서 확인할 수 있다.


    나는 파이썬 질문을 하는 사람들에게 도움을 주고 있다. 보스턴 파이썬 모임의 지인이든, IRC 채널에서 만난 완전히 모르는 사람이든 말이다. 이 상황에서 간혹 뜬금없게 오해할 때가 있다. “왜”라고 질문하는걸 오해하는 경우도 여기에 포함된다.

    답을 찾기 위해 돕는 동안, 종종 질문자에게 “왜?” 라는 질문을 하게 된다. 예를 들면, 누군가가 두개의 파이썬을 랩탑에 설치해야 한다면서 도움을 요청했다. 그런 상황에서 나는 “왜 두번째 파이썬이 필요해요?” 라고 물어볼 것이다. 그럼 다른 사람이 그 글을 보고 웃게 되는데 내가 “너는 두번째 파이썬을 설치할 필요가 없다.” 라고 말한 것으로 생각하기 때문이다.

    이는 일반적인 반응이다. 왜냐고 되물어보는 것은 비난처럼 느껴질 수 있다. “왜 XYZ로 했나요?”라고 말을 하면 “이 멍청이, 넌 XYZ로 하지 말았어야해.” 라고 이해한다는 얘기다. 그러나 내가 왜냐고 되물어보는 것은 정말로 다음과 같은 의도에서 물어보는 것이다. “나는 당신이 XYZ를 한 이유에 대해서 이해하길 원합니다.”

    영어는 위와 같은 이유로 어려운 경우가 있다. 특히 IRC와 같이 비언어적 표현이 없는, 온전히 문자로만 전달되는 상황에서 특히 그렇다. 이런 질문을 누가 봐도 부드럽다 느낄 정도로 물어보기 위해 단어를 더 덧붙여 질문해야 한다. 예를 들면,

    왜 다른 파이썬을 설치해야 하나요?

    이보다 다음처럼 되물어보는 쪽이 낫다.

    왜 다른 파이썬을 설치해야 했는지 물어봐도 될까요? 이유를 이해하는 것은 중요한 단서를 제공하거든요.

    그러므로 내가 만약 왜냐고 되물어본다면, 이를 개인적인 의미로 듣지 말자. 난 정말로 무슨 이유에서 그런건지 알고 싶을 뿐이다. 만약 내가 정말 당신의 잘못을 꾸짖기 원했다면, “당신은 XYZ로 하지 말았어야 했어요.”, “XYZ는 좋은 아이디어가 아니에요.” 라고 말했을 것이다.

    때로는 사람들이 내가 왜냐고 되물어보는 이유를 알게 될 때, 오히려 그 질문에 발끈하는 경우가 있다. 그들은 이유는 중요하지 않다고 고집하고, 왜 단순한 질문에 간단한 답을 내놓지 못하는가에 대해 의아해한다.

    내가 되물어보는 경우의 75% 가량은 내가 큰 그림을 알게 되고 나서 질문의 답변이 달라진다. 일반적으로 사람들이 문제가 발생하고 해결책을 찾다가 막다른 길에 닿았을 때, 사람들은 막다른 길에 닿았다는 사실에 대해서만 질문을 한다. 당연한 일이다. 그러고 그들이 더이상 해결하지 못하는 상태에 빠지 이유는 문제의 처음부터 다시 출발해서 살펴보지 않기 때문에 더 나은 해결책을 찾지 못하는 것이다. 더 나은 길을 찾는걸 돕기 위해서는 그 질문의 이유를 이해해야 한다.

    왜냐고 되물어보는 과정을 통해서 문제가 나타나기 이전에 선택했던 부분에 대해 살펴보고, 이러한 방법으로 전체적인 문제를 살펴보는 과정은 문제 해결에 도움이 된다. 문제에 관한 모든 정보를 알게 된다는 것은 함께 더 좋은 해결책을 찾을 수 있다는 뜻이다.

    내가 더 큰 문제에 대해 설명해달라고 물어보면 개인적으로 받아들이지 말자. 복잡한 문제를 해결하는 것은 힘들다. 간단하게 선택한 첫번째 과정이 두번째 과정을 필요보다 더 어렵게 만드는 경우가 있기 때문이다. 도와주는 사람이 더 큰 그림에 대해 물어볼 때, 질문하는 사람을 면박 주려고 물어보는 것이 아니라, 최고의 해결책을 찾는데 도움을 주기 위해 물어보는 것이다.

    이런 현상에 대해 XY 문제라는 표현을 쓴다. X라는 문제에 대해 Y라는 해결책을 선택했는데, 이게 동작하지 않을 때, X라는 문제점을 물어보는 것이 아니라 Y라는 해결책이 왜 안되는가에 대해 물어본다는 말이다. 몇 사람들은 자신도 모르게 문제점과 해결책의 관계에 대해 중요하게 생각하지 않는다. 이는 아주 일반적인 상황이며 자신의 질문에 대해 고집을 부린다. 그래서 때때로 질문하는 사람은 대안을 빨리 알아채지 못한다. 또 때로는 질문하는 사람이 답하려는 사람보다 시간을 안쓰려고 명확하게 질문하지 않는 경우도 있다. 이유가 어찌되었든, 이런 일은 항상 일어나고, 답변하는 사람들이 XY 문제와 같은 이유로 화가 나면 다시는 질문에 답하는 일을 하지 않을 것이다.

    질문하는 사람들에게: 만약 누군가 왜냐고 물어보거나, 당신은 “XY 문제”를 가지고 있다고 얘기할 때, 그들은 당신에게 최적의 해결책을 찾는 것을 도와주려고 하는 것이다. 기분 나빠하지 말자. 우리는 힘겹게 학습하고 있고 복잡한 상황을 극복하려고 함께 노력하고 있기 때문이다.


    내 스스로도 알면서 잘 안되는 부분이라 글을 읽으면서 뜨끔뜨끔 했다.

    위에서 이야기한 XY 문제는 대다수의 커뮤니케이션에서 쉽게 발생한다. 단순하게 보면 별 이야기도 아닌데 의외로 직간접적으로 자주 겪는다. (내가 질문자 일 때도, 답변자 일 때도 있었던 것 보면 정말 흔한 상황이다.) 유대가 모자란 상황에서도 발생하지만 친밀한 경우에도 이와 같은 오해는 종종 일어난다. 그게 친구와도 나타나고, 직장 생활에서, 학교에서, 어디서든 쉽게 나타나는 일이다.

    배움에 있어서는 적극적인 어린이가 되어야 한다. 부끄러움 없이 지금 내가 아는 부분을 모두 보여주고 물어봐야 빠르게 답을 얻을 수 있다. 내가 무엇을 모르는지 정확히 아는 것이 현명한 사람이란 이야기가 있다. 아는 척 숨기고 있는다면 평생 배우지 못한 상태로 살게 되는 것과 같고 모르는 것보다 더 부끄러운 일이다.

    왜라는 질문에 솔직해지자. 타인에게도 나에게도.

    연초에 커다란 계획은 세우진 않았지만 자잘하게 꾸준히 해야 할 목록 정도는 적어 뒀었는데 연말에 돌아보니 더 명확하고 체계적으로 적었어야 하는 후회가 참 크다. 계획을 해야 측정이 가능하고 평가를 할 수 있다는, 누구나 쉽게 생각할 수 있는 사실을 다시 확인하는 멍청한 짓을 하고 말았다. 😛

    올해를 복기해 반면교사로 삼아 계획을 잘 세우고, 내년을 더 보람차게 보내자는 취지로 올해를 회고해본다.

    직장생활

    작년 6월부터 다니고 있는 회사에서 열심히 지내고 있다. 보스, 나, 그리고 신입까지 세명이 함께 일하고 있다. 아직까지는 인하우스 팀이 구성되지 않은 상태라서 디자인은 외주를 주는 상황인데 간혹 내가 디자인에, 프로그래밍에, 이것저것 다 하게 되는 경우가 많다. 호주에 와서는 개발자로만 커리어 패스를 만들어가야겠다고 생각했었다. 하지만 첫 단추를 잘 끼워야 한다는 말이 여기에서도 적용되더라. 내가 가지고 있는 기술과 지금까지 쌓아온 경력이 현재의 일을 좌우하게 되어 결과적으로 많은 노력과 약간의 희생이 있어야만 가능한 일이다. (못바꾼다는 고정관념도 그 관성에 일조하는 것 같다.)

    직장에서의 영어. 노출이 자주 되어서 늘고 있는 것인지 어쩐건지 정확히는 모르겠다. 다행인 것은 익숙해져서 그런지 동료가 하는 말은 귀에 쏙쏙 들린다. 예전엔 정확히 안들려서 되물어보는 경우가 많았는데, 지금은 생판 뜬금없는 이야기만 아니면 무슨 말 하는지 이해도 되고 제법 꿍짝에 맞춰 이야기도 할 정도가 되었다. 업무에 있어서는 의사소통 때문에 문제되는 부분은 많이 작아진 편인데, 물론 옆 회사 직원이나 클라이언트랑 얘기 해보면 아직 한참 멀었음을 느낀다. 특히 어려운 것은 잡담을 할 때랑 의견을 이야기해야 할 때인데 자리에 앉아서 “이렇게 얘기할껄” 하고 후회할 때가 많다. 갈 길이 참 멀다.

    이전에도 비슷한 내용으로 포스트하긴 했지만, 여기 와서는 한국에서는 경험하기 힘든 환경들, 예를 들어 MS Access를 Front-End로 사용하고, VBScript로 태스크를 만든다거나, WordPress, joomla 등의 CMS를 다루게 된다거나 하는 부분들에 대해서는 재미있게 참여하고 있다. 지금 하는 일들이 내가 가지고 싶은 스킬셋과는 다소 거리가 있어서 아쉬운 기분이 들 때가 많지만, 이 작은 경험들이 수많은 상황과 조건에 맞게 폭 넓고 유연한 선택지를 제시할 수 있는 통찰을 습득하도록 도와주리란 확신이 있고, 또 한 해를 돌아보면 그렇게 성장해가고 있는 기분이다.

    회사를 다니다보면 내가 잘 하고 있는지, 남들만큼 열심히 하고 있는지 확인하고 싶을 때가 있는데 1년 여 가까이 2인 회사 체제로 지내다보니 물어볼 수 있는 사람이 없었다. (다른 한 명이 보스이자 인사담당자이자 대표인데 물어보기가… 이걸 어려워 하는 것도 한국적인 정서에서 그런걸까 하는 생각도 든다.) 그래도 호주에서는 일반적으로 1년에 한번씩 퍼포먼스 리뷰를 통해 업무 평가 및 임금 조정을 진행한다. 나도 올해 6월 경에 퍼포먼스 리뷰를 했었다. 살짝은 불안한 마음에 리뷰에 임했는데 고맙게도 업무에 있어서는 좋은 평가를 잘 받았다. (끝나고 나서는 칭찬 받은 만큼 더 어필했으면 연봉을 좀 더 올릴 수 있지 않았을까 생각도… 하하;)

    회사 업무에 있어서 내 자신에게 아쉬웠던 점을 생각해보면, 10월 쯤 이후로 내 스스로의 퍼포먼스가 많이 떨어지고 있는 기분인데, 웹에이전시 특성상 잡무가 많아 컨텐츠 로딩이나 간단한 수정에 시간을 쓰다보면 좀 괴로워진다. 자동화 할 수 있는 배치 작업들은 코드를 만들어 해소하긴 하는데 어쩔 수 없이 손을 써야 하는 경우가 좀 많은 편이다. 뭔가 좋은 방법이 떠올라서 내년에는 이런 부분에서 스트레스 좀 덜 받았음 좋겠다.

    올해 회사 일에서 하고 싶었던 이른 VBScript로 작성된 태스크를 C#으로 작성해 유닛 테스트, 빌드 & 디플로이 환경을 만들어보고 싶었는데 12월까지 와서도 제대로 공부하지 못해서 시작조차 못했다. 지금 닷넷 스터디에 참여하고 있는데 부지런히 배워 실무에도 적용하는 착한 개발자(?)가 되도록 해야겠다.

    잡다하게 공부하고 있는 프로그래밍

    현재 PHP 개발자로 일하고 있지만 다른 언어를 배워두고 싶다는 욕심이 생겨서 이런 저런 언어를 공부했고 나름 성과가 있었다. 가장 많이 공부했던 것은 파이썬인데 아직 제대로 된 프로젝트는 진행 못해봤지만 요즘 혼자서 만들어보는 프로토타이핑은 파이썬으로 진행하고 있다. 회사에서도 가끔 필요한 노가다성 업무는 모두 파이썬 스크립트로 해결하고 있다. (이 편한 것을 이제서야…) 아직 함수형 프로그래밍에 익숙하지 않아 Java스럽거나 PHP스러운(뭔가 최악의 느낌) 코드를 작성하게 될 때가 많은 편인데 앞으로도 꾸준히 공부해서 오픈소스 쪽에도 기여하고 프로젝트도 진행했으면 좋겠다.

    업무상 필요로 인해 javascript를 사용하긴 하지만 jQuery 만으로도 충분히 잘 쓰고 있어서 다른걸 배우질 않았었는데 근래 AngularJS를 사용해보고 참 편리한 도구를 안쓰고 있었구나 하고 후회했다. 최근 간단한 프로젝트 하나를 AngularJS를 사용해 진행했고 모두가 대만족했다. 아직 수박 겉핥기 식으로만 봐서 근 시일 내에 다시 한번 찬찬히 살펴보려고 한다. 이외에도 backbone.js나 knockout이나 유명한 오픈소스도 많고, 서버 사이드에서의 node.js 등 살펴볼 것이 아주 쌓여 있어 어느걸 먼저 봐야 할지도 고민이다.

    PHP는 사실 따로 공부한 적이 없었는데 최신 버전에서 추가된 기능들을 최근에 살펴보고 있다. 5.3.0, 5.5.x 등에서 추가된 부분도 많은데다 PHP Framework Interop Group에서 진행하는 PSR 같은 표준 문서작업 등이 한참 진행중인데 한국어로 소개되질 않아 빨리 보고서 소개글을 쓰려는데 이것도 계획을 잘 세워서 진행해봐야겠다.

    Coursera에서 Startup Engineering 이라는 수업을 들었다. Startup에서 사용할 만한 기술들을 배우고 수많은 아티클을 읽으며 Startup으로 연결될 수 있는 프로젝트를 준비하는 수업이었는데 수업 자체에서 얻은 지식보다는 함께 들었던 #세러데이스벅 사람들을 만나게 되어 더 좋았다. (이게 Srinivasan 교수님의 깊은 뜻이었을까.) 수업이 끝난 이후에도 많은 자극을 받아 더 열심히 하게 되었었다…. 물론 수업은 완주를 못했다. 하하하.

    iTunesU에서 제공하는 [CS 193P iPhone Application Development] 강의도 들었는데 2011년 강의를 듣다가 iOS7를 갑자기 진행하시길래 멈췄다. (핑계도 좋아.) 연초에 이 강의를 제대로 들어보고 앱도 만들어보려고 한다.

    그리고 오프라인에서 스터디 두군데에 참여하고 있다. 하나는 매주 월요일에 하둡을 이용한 빅데이터 스터디, 수요일에 닷넷 C# 스터디에 참여하고 있다. 하둡 스터디는 한두번 한 이후 사정이 있어 쭉… 쉬다가 내년부터 다시 시작할 예정이고, 닷넷 스터디는 현재 진행중인데 윈도우 개발 환경이 아직 없어서 나 혼자만 부진한 진도를 내고 있다. 사야 하는 기기들을 얼른 사서 제대로 공부를 시작해야겠다.

    집중 없이 이것저것 산발적으로 살펴보고 있는 탓에 깊이가 없는 지식만 쌓이는 기분이 든다. 그리고 연초에 언어 위주의 학습보다 이론, 개념 위주의 학습을 해야겠다는 방향을 잡았었는데 여전히 언어 위주로만 보고 있어서 내년에는 iTunesU나 Coursera를 활용해 제대로 된 강의를 수강해 더 심도있는 공부를 할 계획이다.

    전혀 안하고 있는 운동

    매년 계획 중 가장 안지켜지는 것 중 하나인데, 역시 올해도 지키지 못했다. 다행히 이사오고 나서 운동하기가 예전에 비해 더 좋아졌기도 했고, 건강 상태의 심각성을 깨닫아 산책도 하고 운동도 하려고 애쓰고 있다. 내년에도 어김없이 실천 목록에 올려놓고 운동을 하려고 하는데 더 계획적으로 잘 세워 운동을 해야겠다.

    끝 없는 영어

    호주에서 사는데 있어 가장 중요한 부분이 영어인데 올 한해를 되돌아보면 가장 시간투자를 안한 부분이라 아쉽다. 매일 영어 공부 해야한다는 이야기를 입에 달고 살면서도 실제로는 책도 잘 안펴보고 있다. 작년에는 시험이라도 봤으니 한참 책도 들춰보고, 열심히 단어도 외우려고 노력하고 그랬는데 올해는 전혀 그런 적이 없었던 것 같다.

    그래도 한국에서 그저 있는 것 보다야 더 영어에 노출되고 간단한 소통이라도 영어로 하고 있으니 조금씩 나아지고 있다는 자기위안을 매일매일 하고 있는 상황에 와 버렸다. (이사하고 나서 정신 차리고 열심히 해야지 했는데 열심히 놀고있다. 하하…)

    나름 영어 문서를 한국어로 번역하는 소일거리를 공부 핑계로 하고 있는데 영어 실력도 깊지 않은데다 번역은 한국어 실력이 더 좋아야 한다는 얘기가 어떤 뜻인지 깊게 알 수 있었다.

    여튼, 얼른 시험도 신청하고 부지런히 공부해서 시험 점수도 만들고 해야겠다.

    항상 욕심내는 글쓰기

    블로그에 대한 욕심이 늘 많아서 일주일에 포스트 두개 쓰기라는 거창한 목표가 있었는데 현재 43개 포스트를 남겼다. 번역글 아니면 리뷰, 신변잡기 가득한 블로그로 나날이 진화중이다. 사실 쓰고 싶은 글은 그런 글이 아니었는데 이미 되돌리기 늦은 상황일까. 일기도 쓰겠다고 하고 연초에 좀 쓰다가 말았다. 대신 트위터는 참 많이 쓰고 있는데 12월 2일 현재까지 6256개의 트윗을 남겼다.

    페이스북도 간간히 하고 있다. 몰래 텀블러도 하고 있다. 짧은 글이나 생각들은 다 텀블러에 적고 있어서 텀블러를 사용하기 시작한 이후로 페이스북이나 블로그를 상대적으로 덜 하고 있다.

    글을 쉽게 쓰지 못하는 이유가 예전에 비해 책 읽는 양이 절대적으로 줄어서 그렇다고 생각이 들어 좋은 글을 찾아 읽겠다 마음먹고 연초에 rss나 블로그를 열심히 찾아다니면서 읽었었다. 특히 각각의 블로그에 있는 글을 모두 살펴보고, 좋은 자극을 주는 글을 목록으로 만들기도 했다. 또 오프라인 환경에서 쉽게 읽기 위해서 이메일로 수집해주는 북마클릿도 만들어 썼었다. (요번 서버 이전한 이후로는 동작하지 않고 있어 그냥 pocket을 활용하고 있다.) 도중에 너무 많은 시간을 쓰고 있다는 생각이 들어 클리핑한 글을 기준으로 rss 피드를 만들어 리더로 구독하기 시작했다.

    내년엔 책도 많이 읽고 생각도 꾸준히 정리하며 블로그를 꾸려나가야겠다는 막연한 목표를 세웠다.

    두번의 휴가, 한국과 미국

    한국은 올해 2월에, 미국은 올해 10월에 다녀왔다.

    오랜 기간 집떠나 살다가 1년만에 집을 다녀 온 것인데 오히려 호주에 돌아오는 비행기가 더 집으로 가는 느낌이 났으니, 진짜 집은 어디인가 싶었었다. 그만큼 잘 적응하고 있다는 이야기인지, 그런데 벌써 1년 가까이 지나서 길게 회고하기엔 기억이 너무 가물가물하다. (진작에 적어놓을 걸 그랬다)

    그리고 미국 동부지역 여행을 2주 가량 다녀왔는데 가고싶던 갤러리도 다녀오고 사람들도 만나고 좋은 시간을 가지고 돌아왔다. 정리해야지 하면서도 벌써 2달이나 지나가고 있으니 잊기 전에 빨리 정리를 해야 하는데 조만간 적어나가야겠다. (특히 동부라서 기대도 안했던 부분들인데 의외로 많이 마주하게 되어 인상적이었던 점도 많았다.)

    긴 비행을 동반한 휴가를 다녀올 때마다 그 다음의 휴가에 대해 고민하게 된다. (워낙 비행이 길어 별 생각을 다 하지만.) 휴가를 가면 항상 많이 배워오고 자극 받고 와야 한다는 생각에 말도 안되는 일정을 만들게 되고, 막상 가서 골골거려 제대로 일정을 따라가지 못하고서 돌아오고 있다. 내년 휴가는 아직 막연하지만 pycon 같은 컨퍼런스에 가보고 싶다.

    학업의 시작은 언제?

    아직도 학적이 유지되고 있다는 사실이 신기한데 현재에도 제주대학교 사회교육과에 이름이 있다. 호주에서 친구나 지인들과 연락하다 보면 빈번히 듣는 이야기가 언제 다시 학교를 가는가에 대한 질문인데 갈 때가 되면 가야죠 식의 애매한 답을 늘 해왔다. 지금 마음으로는 호주에서 학업을 시작하고 싶은데 재정적 여력이 아직 없기도 하고 영주권 이상을 취득하면 학비가 아주 저렴해지기 때문에 일단 학업은 영주권 이후에 생각하자고 결정해두고 있었다. (시간을 체우면 자연히 받게 될거라는 다소 수동적인 생각이 삶을 생산적으로 만드는데에는 별로 도움이 되지 않는 기분이지만.)

    일단은 당장에 해결할 수 없는 상황이니까 지금 바로 할 수 있는 일에 집중하려고 한다. 앞서 이야기했던, 좋은 질의 강의도 무료로 들을 수 있을 뿐만 아니라 수많은 책과 얻을 수 있는 경험이 도처에 있었고, 내년에는 제대로 계획 세워 하나씩 들어야겠다.

    할 말이 더 많지만, 2013년 안녕

    돌이켜 보면 아쉬운 점도 많고, 힘들었던 일도 많았다. 하지만 받은 복을 세다보면 감사해야 할 점이 한두가지가 아니었던 해였다. 어려운 일이 있을 때마다 붙잡을 말씀이 있었고, 좋은 관계 속에서 회복할 수 있었다. 내색하지 않지만 타지 생활에서 연고가 없어 힘들 때가 있는데 그럴 때마다 좋은 사람들을 통해 잘 회복하고 다시 힘낼 수 있어서 너무나도 감사하다. 좋은 비전을 품고, 내년을 더 기대하며, 하나씩 계획 준비해 2014년을 시작해야겠다.

    WSGI는 Web Server Gateway Interface의 약어로 웹서버와 웹어플리케이션이 어떤 방식으로 통신하는가에 관한 인터페이스를 의미한다. 웹서버와 웹어플리케이션 간의 소통을 정의해 어플리케이션과 서버가 독립적으로 운영될 수 있게 돕는다. WSGI는 파이썬 표준인 PEP333, PEP3333에 의해 제안되었고, 이 이후에 여러 언어로 구현된 프로젝트가 생겨나기 시작했다.

    WSGI 어플리케이션은 uWSGI라는 컨테이너에 담아 어플리케이션을 실행하게 되며, uWSGI가 각각의 웹서버와 소통하도록 설정하면 끝이다. Flask, django와 같은 프레임워크는 이미 WSGI 표준을 따르고 있기 때문에 바로 웹서버에 연결해 사용할 수 있다.

    이 글에서는 Flask를 nginx에 연결하는 방법을 설명한다. 이 글은 기록용이라 상당히 불친절하기 때문에 관련된 내용에 관심이 있다면 다음 페이지들을 참고하자.

    uWSGI 설치하기

    먼저 uwsgi가 설치되어 있는지 확인한다. 현재 데비안 기반(우분투 등)의 환경이라면 uwsgi가 이미 설치되어 있는데 고대의 버전이라 기존 설치본을 삭제하든 변경하든 해서 새 버전으로 설치해줘야 한다.

    $ mv /usr/bin/uwsgi /usr/bin/uwsgi-old
    

    그리고 uwsgi를 설치해준다.

    $ pip install uwsgi
    $ ln -s /usr/local/bin/uwsgi /usr/bin/uwsgi
    

    이제 uwsgi로 해당 어플리케이션을 실행한다.

    $ uwsgi -s /tmp/uwsgi.sock --module yourapplication --callable app --venv .venv
    

    --socket, -s는 통신을 위한 소켓, --venv, -H는 virtualenv 경로, --module은 어플리케이션, --callable은 WSGI의 시작점을 설정해주는 파라미터이다. 내 경우에는 파일명이 이상(?)해서인지 다음의 방식으로 실행했다.

    $ uwsgi -s /tmp/uwsgi.sock --wsgi-file app.py --callable app -H .venv
    

    파라미터를 매번 입력하면 번거로우므로 다음과 같이 ini 파일을 작성해 저장해놓고 실행해도 된다.

    [uwsgi]
    chdir=/home/ubuntu/helloWorld
    chmod-socket=666
    callable=app
    module=app
    socket=/tmp/uwsgi.sock
    virtualenv=/home/ubuntu/helloWorld/.venv
    

    이렇게 ini파일로 저장한 후 다음 명령어로 실행한다.

    $ uwsgi <filename> &
    

    nginx 설치, 설정하기

    nginx를 apt-get 등을 통해 설치한다.

    $ apt-get install nginx-full
    

    /etc/nginx/sites-available/default 파일을 열어 설정을 해준다.

    server {
            listen   8080;
    
            server_name helloworld.haruair.com;
    
            location / {
                    try_files $uri @helloworld;
            }
    
            location @helloworld {
                    include uwsgi_params;
                    uwsgi_pass unix:/tmp/uwsgi.sock;
            }
    }
    

    위 설정을 통해 nginx로 들어오는 모든 요청을 uWSGI로 보내고 또 돌려받아 nginx를 통해 클라이언트에 전달하게 된다. nginx를 재구동하면 적용된다.

    /etc/init.d/nginx restart
    

    사실 Flask에서의 nginx 설치 문서에 있는 글로도 충분한데 명령어가 계속 에러를 내는 탓에 한참 검색하게 되었다. 문제는 uwsgi의 낮은 버전이었고, 앞서 언급한 바와 같이 최신 버전으로 설치하면 해결된다.

    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. 클래스의 단위를 잘 고려하지 않으면 쉽게 지저분해질 것 같다.

    Composer라는 PHP 의존성 관리도구가 있다고 하길래 재빨리 찾아 Getting Started만 발번역했다. npm이나 apt, pip같은 것들과는 닮았지만 다른 부분이 많은데 그만큼 PHP라는 언어에 대한 고민의 흔적을 느낄 수 있다.


    Composer는 PHP를 위한 의존성 관리도구다. 이 도구를 사용해 해당 프로젝트에서 요구하는, 의존적인 라이브러리를 선언해 프로젝트에서 설치해 사용할 수 있도록 돕는다.

    의존성 관리도구

    Composer는 패키지 관리도구가 아니다. 물론 각 프로젝트 단위로 패키지나 라이브러리를 다룬다면 그런 역할을 할 수 있다. 하지만 이 패키지나 라이브러리는 프로젝트 내 디렉토리 단위로 설치된다. (예로 vender) 기본적으로 composer는 절대 전역적으로 사용하도록 설치하지 않는다. 그러므로 의존성 관리도구라고 부른다.

    이 아이디어는 새로운 것이 아니며 Composer는 nodejs의 npm이나 ruby의 bundler에 커다란 영감을 얻어 만들어졌다. 그러나 이러한 도구는 PHP에 적합하지 않았다.

    Composer가 해결한 문제는 다음과 같다:

    a) 프로젝트가 여러개의 라이브러리에 의존적이다 b) 몇 라이브러리가 다른 라이브러리에 의존성이 있다 c) 무엇에 의존성이 있는지 선언할 수 있다 d) Composer는 설치할 필요가 있는 패키지 버전을 찾아 설치한다. (프로젝트 안으로 설치한다는 뜻이다)

    의존성 선언

    프로젝트를 생성할 때 필요로 하는 라이브러리를 적어줘야 한다. 예를 들어 monolog를 프로젝트에서 사용하기로 결정했다고 치자. 그렇다면 필요로 하는 것은 composer.json 파일을 생성하고 프로젝트의 의존성을 명시적으로 작성해주면 된다.

    {
        "require": {
            "monolog/monolog": "1.2.*"
        }
    }
    

    시스템 요구사항

    Composer는 동작하기 위해 PHP 5.3.2 이상을 요구한다. 또한 몇가지의 php 세팅과 컴파일 플래그를 필수적으로 요구하며 설치할 때 적합하지 않은 부분에 대해 경고해줄 것이다.

    소스로부터 패키지를 설치할 때 단순히 zip 압축파일을 받는 대신 어떻게 패키지가 버전관리 되는지에 따라 git, svn 또는 hg가 필요할 것이다.

    Composer는 멀티플랫폼을 지원하며 Windows, Linux와 OSX에서 동일하게 동작하도록 만들기 위해 노력하고 있다.

    *nix 환경 설치

    실행 가능한 composer 다운로드하기

    지역 설치 (locally)

    Composer를 받기 위해서는 두가지가 필요하다. 첫째로 Composer를 설치하는 것이다. (프로젝트에 Composer를 내려받는다는 의미):

    $ curl -sS https://getcomposer.org/installer | php
    

    이 과정은 요구되는 PHP 세팅 몇가지를 확인한 후 composer.phar를 작업 디렉토리에 내려받는다. 이 파일은 Composer 바이너리이며 PHAR(PHP 아카이브)로 PHP를 커맨드 라인으로 실행할 수 있도록 해주는 아카이브 포맷이다.

    당신은 Composer를 --install-dir 옵션과 함께 경로 디렉토리를 입력해 특정 디렉토리에 설치가 가능하다 (절대경로와 상대경로 모두 가능):

    $ curl -sS https://getcomposer.org/installer | php -- --install-dir=bin
    

    전역 설치 (Globally)

    이 파일은 어디든 원하는 곳에 위치할 수 있다. 이 파일의 위치를 PATH 환경변수에 지정된 곳에 넣어두면 전역적으로 사용할 수 있다. unix와 같은 시스템에서 php 없이 실행할 수 있도록 만들 수도 있다.

    아래의 명령어는 composer로 시스템 어디에서든 쉽게 실행할 수 있도록 한다:

    $ curl -sS https://getcomposer.org/installer | php
    $ mv composer.phar /usr/local/bin/composer
    

    노트: 권한 문제가 있다면 mv 부분은 sudo를 이용해 다시 실행한다.

    그리고 php composer.phar로 실행하는 대신 composer로 실행하면 된다.

    전역 설치 (homebrew를 이용해 OSX에서 설치)

    Composer는 homebrew-php 프로젝트의 일부다.

    1. homebrew-php가 아직 설치되지 않았다면 brew를 통해 설치: brew tap josegonzalez/homebrew-php
    2. brew install josegonzalez/php/composer를 실행
    3. composer 명령어로 사용

    노트: PHP53 또는 그 이상의 버전이 존재하지 않는다는 에러가 나타나면 brew install php53-intl로 설치한다.

    Windows 환경 설치

    인스톨러 이용

    Composer를 설치하기 가장 쉬운 방법이다.

    Composer-Setup.exe 를 내려받아 실행한다. 이 인스톨러는 가장 최신 버전의 Composer를 PATH로 설정된 경로에 설치해 어느 경로에서든 composer 명령어를 사용할 수 있도록 해준다.

    수동 설치

    PATH 경로로 이동해 설치 스니핏을 실행하여 composer.phar를 내려받는다:

    C:\Users\username>cd C:\bin
    C:\bin>php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"
    

    노트: 위 내용 중 file_get_contents 함수가 동작하지 않는다면 http 주소로 내려받거나 php.ini에 php_openssl.dll를 활성화한다.

    composer.phar를 위한 composer.bat를 생성한다:

    C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat
    

    현재 터미널을 닫고 새 터미널에서 아래와 같이 테스트한다:

    C:\Users\username>composer -V
    Composer version 27d8904
    
    C:\Users\username>
    

    Composer 사용하기

    이제 Composer를 사용해 프로젝트에서 의존하고 있는 라이브러리를 내려받는다. composer.json 파일이 현재 디렉토리에 존재하지 않는다면 Basic Usage 챕터로 넘어가도 된다.

    의존적인 라이브러리를 내려받기 위해서, install 명령어를 실행한다:

    $ php composer.phar install
    

    전역 설치를 했다면 phar 없이 아래와 같이 실행한다:

    $ composer install
    

    위에서 예로 들었던 부분에 따라, 위 명령어를 통해 monolog를 vendor/monolog/monolog 디렉토리로 내려받게 된다.

    자동 불러오기

    라이브러리를 다운받는 것 이외에 Composer는 어떤 라이브러리든 자동으로 적합한 라이브러리를 불러와 사용하도록 돕는다. 자동 불러오기를 사용하려면 단지 아래의 코드를 넣어준다:

    require 'vendor/autoload.php';
    

    이제 monolog를 바로 사용할 수 있다. Composer에 대해 더 배우기 위해서는 Basic Usage 챕터를 참고한다.


    더 읽을 거리

    Xpressengine에서 Composer 문서를 전문 번역했다.

    색상을 바꿔요

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

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