OWIN은 Open Web Interface for .NET의 약어로 요즘 MS 진영에서 핫한(?) 오픈소스 프로젝트다. 다음은 OWIN 공식 사이트에 나와 있는 프로젝트의 목표.

The goal of the OWIN interface is to decouple server and application, encourage the development of simple modules for .NET web development, and, by being an open standard, stimulate the open source ecosystem of .NET web development tools.

MS에서 제공하는 ASP.NET MVC framework는 System.Web에, 특히 HttpContext에 깊은 의존성이 있어서 IIS 이외에는 구동이 불가능하다. 하지만 OWIN 구현을 통해 웹서버와 어플리케이션의 의존성이 분리1되어 IIS 이외의 웹서버에서도 C#으로 작성된 웹 어플리케이션을 구동할 수 있게 되었다.2 OWIN은 인스턴스로 처리하기 간편한 구조라 횡적으로 확장이 쉬운데다 서버의 유연성까지 보장하고 있다. OWIN을 이용해 개발한다면 ASP.NET MVC을 기반으로 작업할 수 없지만3 Nancy나 Simple.Web 등 다양한 프레임워크/라이브러리를 활용할 수 있다.

OWIN의 구조

OWIN 명세에서는 각각 서버, 웹프레임워크, 웹 어플리케이션, 미들웨어, 호스트로 정의했다. 발번역(…)하면 다음과 같다.

  • 서버: HTTP 서버로 클라이언트와 직접 소통. 요청을 처리하는데 OWIN 문맥을 사용. 서버는 OWIN 문맥을 이해할 수 있도록 돕는 어뎁터 레이어가 필요.
  • 웹 프레임워크: 요청을 처리하는데 사용하는 어플리케이션을 OWIN에서 원활하게 구동할 수 있도록 돕는 독립적 컴포넌트. OWIN 문맥을 이해할 수 있는 어뎁터 레이어 필요.
  • 웹 어플리케이션: 웹프레임워크 위에서 개발 가능하며, OWIN 호환 서버에서 구동이 가능한 특정 어플리케이션.
  • 미들웨어: 서버와 어플리케이션 사이의 파이프라인을 통과해 구동되는 컴포넌트로 특정의 목적에 따라 응답과 요청을 점검, 라우팅 또는 수정할 수 있음.
  • 호스트: 서버 내에서 어플리케이션을 실행하기 위해 우선적으로 응답하는 어플리케이션 실행부. 특정 서버는 호스트의 역할도 수행함.

OWIN의 전체적인 흐름은 다음과 같다.

owin

서버를 통해 요청이 들어오면 호스트는 Startup 클래스에 정렬된 파이프라인을 따라 미들웨어를 실행한 후 어플리케이션을 실행한다. 어플리케이션 실행 후 응답 또한 미들웨어를 거쳐 빠져나간다. 미들웨어는 Task를 이용해 다음 파이프라인으로 컨텍스트를 넘겨준다. 각각의 미들웨어는 앞서의 정의처럼 응답과 요청을 조작할 수 있다.

OWIN 명세에서는 AppFunc라고 불리는 어플리케이션 대리자(delegate)를 통해 환경을 주고 받는다. 명세에서는 다음과 같이 환경과 Task로 구성되어 있다.

using AppFunc = Func<
    IDictionary<string, object>, // Environment
    Task>; // Done

환경은 크게 요청, 응답, 기타 데이터로 구분되는데 자세한 내용은 OWIN 스펙에서 찾아볼 수 있다. MS에서는 OWIN 명세를 기반으로 Katana라는 이름으로 프로젝트를 진행하고 있는데(Microsoft.Owin) 여기에서는 IOwinContext 인터페이스를 활용할 수 있다. 이 포스트의 예제에서도 IOwinContext를 사용하고 있다.

맛보기 코드

OWIN 어플리케이션

이 포스트에서는 누구나 따라해볼 수 있도록 Mono 환경을 기준으로 작성했다. IDE로는 Xamarin Studio를 사용했다.

Xamarin Studio을 실행해 새 솔루션을 생성한다. Console에서 self-host로 구동하는 예제이므로 Console Project를 선택한다. (Visual Studio를 이용하는 예제에서는 Web Empty Project를 사용해도 된다.4)

create solution

먼저 최신 패키지 설치를 위해 target framework를 Mono / .NET 4.5로 변경해야 한다. 해당 솔루션의 옵션에서 Build > General에서 변경할 수 있다.

options

project options

솔루션에 패키지를 추가한다. 솔루션에 오른쪽 클릭해서 Add > Add Packages... 를 클릭한다.

add packages menu

Show pre-release packages를 체크하고 Owin을 검색한다. 검색 결과에서 OWIN, Microsoft.Owin, Microsoft.Owin.Hosting, Microsoft.Owin.Host.HttpListener를 체크하고 Add Packages 버튼을 누른다.

add packages

설치가 완료되었으면 OWIN에서 엔트리 포인트가 될 Startup.cs 파일을 추가한다.

create file

이 파일의 내용은 다음과 같다. 5

using System;
using Owin;
using Microsoft.Owin;

[assembly: OwinStartup(typeof(OwinHelloWorld.Startup))]
namespace OwinHelloWorld
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Run (context => {
                context.Response.ContentType = "text/html";
                return context.Response.WriteAsync("<h1>It Works</h1>");
            });
        }

    }
}

Startup 클래스의 Configuration(IappBuilder app)메소드를 통해서 어떤 미들웨어에 어떤 순서로 접근하며(app.Use()) 최종적으로 어떤 어플리케이션이 실행되는지(app.Run()) 작성하게 된다.

[assembly: OwinStartup(typeof(OwinHelloWorld.Startup))] 부분은 이 Owin 코드가 컨테이너 형태로 실행될 때 엔트리 포인트를 지정해주는 부분이다. (이외에도 appsettings에 추가해주거나 owinhost.exe를 실행할 때 미리 선언한 identifier를 사용하는 방법이 있다.)

이제 이 인터페이스를 실질적으로 접근 가능하게 만들어줄 콘솔 어플리케이션을 작성한다. Program.cs를 열어 다음의 코드를 입력한다.

using System;
using Microsoft.Owin.Hosting;

namespace OwinHelloWorld
{
    class MainClass
    {
        public static void Main (string[] args)
        {
            var url = "http://localhost:9000";
            using (WebApp.Start<Startup> (url)) {
                Console.WriteLine (url);
                Console.WriteLine ("Press enter to quit.");
                Console.ReadLine ();
            }
        }
    }
}

Microsoft.Owin.Hosting.WebApp으로 Startup 클래스를 구동한다. 이 과정에서 내부적으로 Microsoft.Owin.Host.HttpListener를 사용한다.

이제 프로젝트를 빌드하고 실행하면 다음과 같은 콘솔 화면이 나타난다.

console

그리고 해당 주소를 브라우저에서 열면 페이지를 확인할 수 있다.

helloworld

미들웨어 Middleware

미들웨어 간의 소통은 앞에서 말한 appFunc를 사용해 환경과 컨텍스트를 넘겨주는데 여기 예제에서는 Katana에서 제공하는 OwinMiddleware 클래스와 IOwinContext 인터페이스를 활용해 미들웨어를 작성할 것이다.

다음은 MyFirstMiddleware.cs의 코드다.

using System;
using System.Threading.Tasks;
using Owin;
using Microsoft.Owin;

namespace HelloWorldOwin
{
  public class MyFirstMiddleware : OwinMiddleware
  {
    public MyFirstMiddleware (OwinMiddleware next) : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context){
      context.Response.Write ("<!doctype html><html><body>");
      await Next.Invoke (context);
      context.Response.Write ("</body></html>");
    }
  }
}

OwinMiddleware 클래스를 상속받는 MyFirstMiddlewareInvoke(IOwinContext) 메소드를 사용해 해당 미들웨어에서 데이터를 핸들링 하거나 응답/요청을 변경하는 등의 코드를 작성할 수 있다.

다음은 MySecondMiddleware.cs의 코드다.

using System;
using System.Threading.Tasks;
using Owin;
using Microsoft.Owin;

namespace HelloWorldOwin
{
  public class MySecondMiddleware : OwinMiddleware
  {
    public MySecondMiddleware (OwinMiddleware next) : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context){
      context.Response.Write ("<header><h1>It works</h1></header>");
      await Next.Invoke (context);
      context.Response.Write ("<footer><a href=\"http://localhost:9000\">http://localhost:9000</a></footer>");
    }
  }
}

파이프라인의 흐름을 보여주기 위해서 그냥 예시 코드를 넣은 두번째 미들웨어다. 마지막으로 Startup.cs로 돌아가서 해당 미들웨어를 파이프라인으로 집어 넣는다.

using System;
using Owin;
using Microsoft.Owin;

[assembly: OwinStartup(typeof(HelloWorldOwin.Startup))]

namespace HelloWorldOwin
{
  public class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      app.Use (typeof(HelloWorldOwin.MyFirstMiddleware));
      app.Use (typeof(HelloWorldOwin.MySecondMiddleware));

      app.Run (context => {
        context.Response.ContentType = "text/html";
        return context.Response.WriteAsync("<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>");
      });
    }
  }
}

app.Use()로 각각의 미들웨어를 연결했다. 앞서 예제와 동일하게 구동해보면 다음 결과를 볼 수 있다.

Owin with MiddlewareMiddleware Result

특별한 예제가 딱히 떠오르지 않아서 그냥 html 코드를 넣었는데 GitHub이나 nuget 리포지터리에 OAuth Autentication 같은 멋진 미들웨어가 많이 있어 위와 같이 간편하게 추가만 하면 사용이 가능하다.

여기까지 OWIN에 대해 살펴봤다. 아직 C#는 초보라서 깊이있게 글을 쓰지 못하는게 아쉽다. 하지만 앞으로의 발전이 더 기대되고 꾸준히 Follow-up 할 닷넷 프로젝트가 될 것 같다.

예제 코드

예제는 모두 Xamarin Studio로 작성했고 GitHub 리포지터리에서 내려받을 수 있다.

  • HelloWorldOwin : 위에서 작성한 예제 코드
  • HelloWorldOwinHost : owinhost.exe로 구동하는 예제 코드로 web.config등 Configuration 하는 방식이 조금 다르다.

더 읽을 거리

  • Python에서의 uWSGI와 유사한 접근 방식이다. 
  • OWIN에서 ASP.NET MVC를 구동할 수는 없지만 MVC에서는 Helios라는 프로젝트를 통해 OWIN을 사용할 수 있다. IIS의 파이프라인 중간에서 OWIN을 구동하게 돕는다. 
  • ASP.NET MVC에서 OWIN은 구동 가능하지만(Helios) 아직까지는 OWIN에서 ASP.NET MVC를 쓸 수는 없다. WebAPI는 OWIN 기반이라 현재도 활용 가능. MVC vNext에서는 OWIN도 지원될 예정이라고. 
  • Mono에 내장된 웹서버인 xsp는 아직 OWIN Startup을 인식하지 못해 Web Project로 시작하면 mscorlib 에러를 만나게 된다. 
  • 사실 Flask처럼 Python uWSGI를 기반으로 한 라이브러리/모듈을 사용해봤다면 익숙한 양식이다. 
  • 읽기 전에

    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도 완전히 지원하는 상황이 아니다. 
  • 그리고 이 문제를 해결하기 위해 엄청 검색을 했었는데 비슷한 문제를 버전 대대로 겪은 사람들이 많이 나왔다. 물론 구현 자체가 없으니 아무도 해결하지 못했다. 
  • .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에서 제공하는 것에 비해 모자란 점이 있긴 하지만 꾸준히 성장하고 있다.

    색상을 바꿔요

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

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