Mono 웹 서버와 OWIN 웹 서버의 크래시 이슈 패치

OWIN 웹 서버 크래시

OWIN(Open Web Interface for .NET) 를 이용하여 mono 환경에서 웹 서버를 띄후 특정 명령으로 웹 서버 프로세스가 크래시가 발생한다.

간단한 아래의 OWIN 호스트를 mono 런타임으로 실행한 후 서버 크래시를 발생해 보자.

mono ./OwinConsoleApp1.exe

그리고 터미널을 열어 아래의 명령을 실행해보자.

curl -X POST http://localhost:8080

https://user-images.githubusercontent.com/1943755/80348607-01d7ec80-88a9-11ea-8795-be9f5c1d7726.gif

그러면 아래와 같이 서버 프로세스가 비정상 종료되고 아래와 같은 오류 메시지를 보여준다.

Unhandled Exception:
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'.
  at System.Net.HttpListenerResponse.set_StatusCode (System.Int32 value) [0x00013] in <b4473693dd3c4d45883c574a53529fbe>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerResponse.End () [0x0001c] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End () [0x00010] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End (System.Exception ex) [0x0001e] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestAsync (System.Net.HttpListenerContext context) [0x0019c] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestsAsync () [0x00125] in <68f7adf518f945aaa528fe9acf594456>:0 
  at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_1 (System.Object state) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x00007] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0 
[ERROR] FATAL UNHANDLED EXCEPTION: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'.
  at System.Net.HttpListenerResponse.set_StatusCode (System.Int32 value) [0x00013] in <b4473693dd3c4d45883c574a53529fbe>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerResponse.End () [0x0001c] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End () [0x00010] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerContext.End (System.Exception ex) [0x0001e] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestAsync (System.Net.HttpListenerContext context) [0x0019c] in <68f7adf518f945aaa528fe9acf594456>:0 
  at Microsoft.Owin.Host.HttpListener.OwinHttpListener.ProcessRequestsAsync () [0x00125] in <68f7adf518f945aaa528fe9acf594456>:0 
  at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_1 (System.Object state) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x00007] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in <f759957039b44a0190b1110fdfe3030f>:0 
  at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00000] in <f759957039b44a0190b1110fdfe3030f>:0

일반적으로 HTTP Header 의 Content-Length 속성은 반드시 포함되어야 하는 속성이다. (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) HTTP Body 의 Payload 가 있는 경우 무시할 수 있지만, Payload 가 없는 경우 웹 서버는 Length Required 응답코드로 연결을 끊어버린다.

Mono 런타임의 HttpListener 는 이 같은 예외에 대해 <h1>Length Required</h1> HTML 메시지를 전송한 후 Response 의 객체의 Disposed() 를 호출하고 HTTP 연결을 끊어버린다. 이 코드에서 HTTP 411 Error를 전송하고, 이 코드에서 Response 를 닫는 것을 알 수 있다. 그러나 OWIN 파이프라인에서 이 요청을 받아 사용자 코드를 실행한 후 Response.StatusCode 에 값을 설정하려고 하니 ObjectDisposedException 이 발생하고 프로세스는 죽어버린다. 이 코드에서 Mono 의 HttpListenerResponse.StatusCode 를 호출하는 이 코드에서 복구할 수 없는 예외가 발생한다.

OWIN 서버 패치

OWIN 서버의 구조를 살펴보면 다행스럽게도 요청을 처리하기 위해 아래의 코드의 생성자에서 대리자(Delegate)에 메서드를 할당한 것을 볼 수 있다. (코드) 그러므로 우리는 이 대리자에 내가 다시 구현한 메서드를 등록해 주면 된다.

internal OwinHttpListener()
{
    _listener = new System.Net.HttpListener();
    _startNextRequestAsync = new Action(ProcessRequestsAsync);
    _startNextRequestError = new Action<Task>(StartNextRequestError);
    SetRequestProcessingLimits(DefaultMaxAccepts, DefaultMaxRequests);
}

내부적으로 sealed class, internal, private 으로 정의된 것들이 많기 때문에 요청을 처리하는 구현에서 기존 동작과 일치하도록 처리해야 한다. 그리고 Mono 런타임에 의해 이미 클라이언트 연결에게 응답을 보냈다면 사용자 코드 및 더 이상 파이프라인을 실행하지 않고 중단해야 한다.

아래와 같이 IsEmptyPayloadAndContentLength 메서드에서 오류가 발생하는 경우인지 판단하고, 이미 클라이언트 연결에게 응답을 보냈다면 새로운 요청을 받을 준비를 시킨다.

if (IsEmptyPayloadAndContentLength(context))
{
    Interlocked.Decrement(ref currentOutstandingAccepts); // Decrement currentOutstandingAccepts counting.
    owinHttpListenerOffloadStartNextRequest(); // New request processing on Task if possible.
    continue;
}

아래는 OwinServerFactory 를 구현하여 OwinHttpListener 의 동작을 변경하는 전체 소스 코드이며, 더 이상 프로세스의 크래시가 발생하지 않는다.

이와 관련된 내용으로 OWIN 프로젝트의 구현체인 aspnet/AspNetKatana 공식 프로젝트에 이슈PR 을 요청하였다. 그러나 AspNetKatana 는 이 문제에 대해 '공식적으로 mono 를 지원하지 않는다' 고 하였고, 근본적으로 mono 의 문제가 패치되어야 한다고 한다.

일단 mono 는 이 이슈에 대해 해결되지 않았기 때문에 mono 런타임에서 OWIN 웹 서버를 구동할 경우 꾸준히 문제의 소지가 있다.

Mono 공식 저장소의 코드 패치

우선 이 문제의 원인은 Mono 런타임의 구현체의 버그가 맞다. Mono 런타임에서 HTTP 클라이언트에게 오류를 전송하고 응답 객체 메모리를 정리하고 연결을 끊었다면 Request Context 를 OWIN 에게 넘겨주면 안된다. 그러나 Mono 내부의 Request Context 를 다음 파이프라인으로 넘기면서 OWIN 웹 서버에까지 크래시가 발생하는 영향을 준다.

이와 관련하여 Mono 공식 저장소에 'POST/PUT request without Content-Length Header crashes the Process and HttpListener #10435' 이슈가 존재하는 것을 확인 하였다. 2018년 9월에 이슈가 등록되었지만 여전히 해결이 되지 않았다. 그래서 'Fix if already send error to http client, do not callback. #19664' 의 PR 를 요청하여 머지되었다.

우선 이 문제를 해결하기 위해 오류가 발생하는 코드를 아래와 같이 작성했다.

그리고 curl -X POST [http://localhost:8080/](http://localhost:8080/) 로 HTTP 요청을 보내면 아래와 같이 유시한 오류가 발생하는 것을 알 수 있다.

Unhandled Exception:System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.HttpListenerResponse'.  at System.Net.HttpListenerResponse.set_ContentLength64 (System.Int64 value) [0x00013] in <b44
73693dd3c4d45883c574a53529fbe>:0 
  at MonoConsoleApp1.Program.Main (System.String[] args) [0x000ff] in <60aed05f157f4c0c81f93d9221c9a2ec>:0 
  at MonoConsoleApp1.Program.<Main> (System.String[] args) [0x0000c] in <60aed05f157f4c0c81f93d9221c9a2ec>:0 
[ERROR] FATAL UNHANDLED EXCEPTION: System.ObjectDisposedException: Cannot access a disposed obj
ect.
Object name: 'System.Net.HttpListenerResponse'.
  at System.Net.HttpListenerResponse.set_ContentLength64 (System.Int64 value) [0x00013] in <b44
73693dd3c4d45883c574a53529fbe>:0 
  at MonoConsoleApp1.Program.Main (System.String[] args) [0x000ff] in <60aed05f157f4c0c81f93d92
21c9a2ec>:0 
  at MonoConsoleApp1.Program.<Main> (System.String[] args) [0x0000c] in <60aed05f157f4c0c81f93d9221c9a2ec>:0

Mono 런타임의 HttpListenerGetContextAsyncGetContext 메서드는 비동기로 동작하는 것을 알 수 있다. 그리고 EndGetContext 메서드는 스레드의 동기화 메서드를 통해 대기하는 것을 알 수 있다. 그럼 ares.AsyncWaitHandle 의 동기화 객체는 ListenerAsyncResult.AsyncWaitHandle 에서 ManualResetEvent 를 생성한다. 그리고 ListernerAsyncResult.Complete 메서드에서 ManualResetEvent.Set 을 호출하는 것을 알 수 있다.

HttpListener.BeginGetContext 에서 큐에서 컨텍스트를 정상적으로 가져오면 동기화 객체를 Set 하고, 그렇지 않으면 다시 wait_queue 에 넣는 동작이 반복된다.

그럼 이제 어디에서 연결을 맺는지 살펴보면 된다. HttpConnection.OnReadInternal 메서드에서 context 에 오류가 없으면 context.Request.FinishInitialization() 메서드를 호출하는데 이 메서드의 내용을 살펴보자. HttpRequest.FinishInitialize 메서드에서 올바른 연결에 대해 쿼리스트링을 생성하는 작업을 하는데 일부 조건에 만족하지 않는 경우 HTTP 클라이언트로 오류를 전송하는 코드를 발견할 수 있다. context.Connection.SendError (null, 411); 이 메서드는 HttpResponse 객체를 Dispose 를 수행하고 연결을 끊도록 내부 구현이 되어 있다.

그러나 HttpConnection.OnReadInternal 에서 HTTP 클라이언트에게 오류를 전송하고 연결이 끊어졌지면 파이프라인을 계속 실행하는 문제가 발생한다. 그래서 이 부분에서 HTTP 클라이언트로 오류 응답을 전송하였다면 더이상 파이프라인이 실행되지 않도록 PR 을 넣어 공식 Mono 저장소에 머지가 되었다.

그리고 Mono 저장소의 릴리즈는 빠른 편이 아니므로 OWIN 웹 서버를 패치하려면 위에 안내한 것처럼 임시방편으로 수정하면 되고, 차기 Mono 릴리즈 버전에서는 이 문제가 해결되니 기다리면 될 것 같다.

Mac Catalina 업그레이드 후 루트 디렉토리를 사용할 수 없다. Read-Only 상태의 파티션으로 나누어져 있어 기존 루트 디렉토리의 사용자 디렉토리는 "/Users/Shared/Relocated Items/" 디렉토리로 모두 옮겨진다. 이는 디스크의 논리 파티션이 운영체제를 위한 ReadOnly 전용 공간과 사용자 데이터의 파티션으로 나뉘어지기 때문이다.

만약 SVN 을 루트 디렉토리로 사용한 경우 문제가 발생하는데, 적당한 디렉토리로 옮긴 후에 다음의 SVN 명령을 통해 URL 주소를 수정해 주어야 한다.

아래와 같이 현재 SVN 저장소의 정보를 보자

cd <svn directory>
svn info

그렇다면 아래와 유사한 결과가 출력된다.

Path: .
Working Copy Root Path: /Users/powerumc/...생략...
URL: file:///Users/powerumc/...생략...
Relative URL: ^/
Repository Root: file:///Users/powerumc/...생략...
Repository UUID: fe1381c0-03a0-ad4a-96c3-71fb4ed8e9fb
Revision: 18046
Node Kind: directory
Schedule: normal
Last Changed Author: SYSTEM
Last Changed Rev: 18046
Last Changed Date: 2018-11-21 15:43:56 +0900 (수, 21 11 2018)

위의 결과에서 URL 정보를 참고하여 변경된 URL 정보를 변경해 주면 된다.

svn relocate file:///Users/powerumc/repo/svn

데이터 무결성이란

일반적으로 '데이터 무결성'이라고 함은 큰 범주에서 데이터베이스에서 데이터의 정확성과 일관성을 보증하는 것을 의미한다. 이런 데이터의 무결성을 보증할 수 없는 경우 우리는 '데이터가 변질되었다' 라고 할 수 있다. 이는 데이터가 우리가 기대하던 원본과 달라졌음을 의미한다.

일반적으로 파일이나 네트워크에서 무결성을 검증하기 위해 체크섬(checksum) 을 이용하고, 프로그래밍 언어에서는 해시값(hashvalue) 를 이용한다. 이 둘은 데이터의 무결성을 보장하기 위해 단 하나의 비트(bit) 의 데이터라도 수정이 되면 전체 해시값에 영향을 주어 원본과 일치하지 않는 해시값이 된다. 이 원본 해시값을 사본 해시값과 비교하면 데이터의 무결성이 보장되는지 쉽게 알 수 있다.

데이터와 관련된 소프트웨어 개발에서 무결성을 지키기란 쉽지 않다. 특히 여러 운영체제에서 동작하는 소프트웨어라면 운영체제의 특성과 관련된 부분으로 데이터 무결성이 쉽게 깨지곤 한다.

해시값을 통한 데이터 무결성

일반적으로 프로그래밍 언어에서의 데이터는 숫자형과 문자형이 있는데, 대부분 문자형의 데이터에서 데이터의 무결성이 깨지기 쉽다. 여러 운영체제에서 사용하는 개행 문자 값이 다르기 때문이다. 일반적 개발 툴에서는 개행 문자의 비트값은 우리 눈에 보이지 않는다.

  • 윈도우: CRLF (\r\n - &#A)
  • 맥: CR (\r - &#D)
  • 유닉스, 리눅스: LF (\n - &#A)

CR(Carriage Return) 은 0x0D 값이고,
LF(Line Feed) 는 0x0A 값이다.

이는 아주 간단한 실험으로 테스트해 볼 수 있다. 아래와 같이 일반적으로 "엔터키"를 누르면 추가되는 개행 문자 값은 모두 다른 것을 알 수 있다.

https://repl.it/@powerumc/string-carriage-return

일부 해시값을 계산하는 방법으로 공격하는 보안적인 취약점이 발견되어 일부 프로그래밍 언어의 특정 버전, 특정 플랫폼에서는 매번 해시값이 변한다.
예로 .NET Framework, .NET Core 와 Python 3.3 이상 버전부터는 새로운 프로세스가 실행되면 해시값도 항상 변하게 된다.

읽어볼거
Why is string.GetHashCode() different each time I run my program in .NET Core?https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/

using System;

class MainClass {
  public static void Main (string[] args) {
    var cr = "\r";
    var lf = "\n";
    var crlf = "\r\n";

    Console.WriteLine($"CR Hashcode={cr.GetHashCode()}");
    Console.WriteLine($"LF Hashcode={lf.GetHashCode()}");
    Console.WriteLine($"CRLF Hashcode={crlf.GetHashCode()}");

    Console.WriteLine($"cr = lf is {cr == lf}");
    Console.WriteLine($"cr = crlf is {cr == crlf}");
    Console.WriteLine($"lf = crlf is {cr == crlf}");
  }
}

// Results
// CR Hashcode=1948159545
// LF Hashcode=-1646523816
// CRLF Hashcode=-1196730459
// cr = lf is False
// cr = crlf is False
// lf = crlf is False

데이터 무결성이 깨지는 API

간단하게 테스트를 해볼 수 있는 다음의 XML 데이터를 다루는 소스코드를 준비했다. 원본 데이터의 개행 문자 값은 \r\n 이지만, 어떤 API 를 사용하느냐에 따라 반환되는 개행 문자 값은 달라진다. 만약 이런 API 들을 혼용해서 사용한다면 당연히 데이터의 무결성을 깨지게 된다.

  1. XmlTextReader 는 개행 문자 값 그대로 반환
  2. XmlReader\n 값으로 반환
  3. XmlDocument 는 개행 문자 값 그대로 반환
  4. XDocument \n 값으로 반환

https://repl.it/@powerumc/xml-carriage-return

데이터의 무결성이 깨지는 윈도우 클라이언트 프로그래밍

일반적으로 윈도우 클라이언트 프로그래밍을 할 여러 행의 문자열을 입력 받을 수 있는 컨트롤이 여기에 해당 된다. 이런 컨트롤은 내부적으로 Environment.NewLine 을 이용하는데, Environment.NewLine 자체가 운영체제에 해당하는 개행 문자 값을 반환한다.

예를 들어, 윈도우에서 구동되는 WPFTextBox 컨트롤이 개행 문자 값은 항상 \r\n 이 된다.

<TextBox AcceptsReturn="True"></TextBox>

데이터의 무결성이 깨지는 웹 프로그래밍

웹에서는 또 어떨까? 일반적으로 개행 문자를 입력 받을 수 있는 TextArea 의 개행 문자 값은 \n 이다. 이는 아래의 테스트 코드에서 확인해 볼 수 있다.

show 버튼을 클릭하면 자바스크립트로 개행 문자를 텍스트로 표시해 주도록 했고, submit 버튼을 클릭하면 서버로 폼의 데이터가 전송되도록 했다. 여기에서 눈여겨 보아야 할 것이 있는데 클라이언트의 결과와 서버로 전송된 데이터는 개행 문자가 달라진다.

  • HTML TextArea 컨트롤은 개행 문자를 \n 을 사용한다
  • Form 전송 시 기본 값인 enctype="application/x-www-form-urlencoded" 인 경우 개행문자는 \n 로 치환된다

Form 전송 시 Request Header 정보는 아래와 같다.

:method: POST
:path: /submit
:scheme: https
content-type: application/x-www-form-urlencoded

아래는 Form 전송 시 URL Encoded 된 Form Data 값이다. %0D%0A 값에서 알 수 있듯이 \n 개행 문자가 \r\n 로 치환된 것을 알 수 있다.

txt: Hello%0D%0AWorld

https://repl.it/@powerumc/html-textarea-carriage-return-by-node

클라이언트 HTML 코드 (index.html)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>html textarea carriage return test</title>
  </head>
  <body>
    <form action="/submit" method="POST">
      <textarea id="txt" name="txt" style="height: 200px; width: 200px;"></textarea>
      <button id="btn" type="button">show</button>
      <span id="span"></span>
      <button type="submit">submit</button>
    </form>
  </body>

  <script>
    document.querySelector("#btn").addEventListener("click",
      function() {
        var text = document.querySelector("#txt").value
          .replace(/\r/g, "\\r")
          .replace(/\n/g, "\\n");

        document.querySelector("#span").innerText = text;
      });
  </script>
  </body>
</html>

서버 자바스크립트 코드 (index.js)

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use(express.static('public'));

app.get('/', (req, res) => {
  res.sendFile('public/index.html');
});

app.post("/submit", (req, res) => {
  var txt = req.body.txt;
  res.send(txt.replace(/\r/g, "\\r")
          .replace(/\n/g, "\\n"));
});

app.listen(3000, () => console.log('server started'));

읽어볼거리
HTML textarea의 개행문자는 무엇일까? (LF vs CRLF vs 상황에 따라 다르다 vs 충격과 공포)
https://libsora.so/posts/what-is-textarea-newline/

마무리

일반적인 서비스/비즈니스 개발에서 개행 문자로 인해 해시값이 달라지는 문제는 크게 의미가 없을 수 있다. 그러나 다양한 운영체제를 지원하는 크로스 플랫폼에서는 문제가 될 수 있다. 사용자에게 보이는 화면의 텍스트의 한 줄의 빈 공백이 두 줄이 되는 경우가 있고, 데이터를 파일과 같은 저장소에 저장하는 경우 개행 문자가 달라지는 경우도 발생한다. 모바일 게임에서 이 개행 문자 하나로 해시 값이 달라져 데이터 파일을 1GB 를 다운로드 받는다고 생각하면 정말 끔찍한 일이다.

윈도우 클라이언트에서, 모바일 기기에서, 웹 페이지에서, 다양한 운영체제의 클라이언트에서 입력한 같은 데이터를 프로그래밍 언어는 다르다고 해석할 수 있다. 이것이 비즈니스에 영향을 줄 수 있다면 올바로 바로잡는 것도 좋을 것이다.

개요

mono-service 는 .NET Framework 로 작성된 윈도우 서비스(Windows Services)mono 환경에서 구동할 수 있는 도구이다. 윈도우 서비스는 일반적으로 GUI 가 없는 백그라운드로 동작하는 실행 바이너리로 윈도우 운영체제가 서비스를 안정적으로 동작하도록 지원해 준다.

mono-service 는 .NET Framework 로 컴파일된 바이너리 및 실행 파일을 AppDomain 을 생성한 후 로드한다. mono-service 가 하는 역할은 일반적으로 POSIX 가 정의하는 유닉스 시그널(Unix Signals) 를 받아 처리하기 위한 용도이다. 맥 또는 리눅스 운영체제는 윈도우 운영체제가 제공하는 서비스의 시작/중지 명령을 이해할 수 없기 때문에 SIGINT, SIGKILL 신호 등을 받아서 서비스를 중지하도록 해야 한다.

필자가 회사에서 담당하고 있는 오픈소스 프로젝트인 크레마(게임 데이터 개발 도구)는 전반적으로 플러그인 아키텍처로 여러 가지 기능을 제공한다. 크레마 서버는 다양한 운영체제에서 동작이 가능하도록 mono 에서 실행할 수 있는데, 맥의 launchctl 과 리눅스의 systemd 로 서비스를 제공하기 위해 mono-service 로 서비스 바이너리 파일을 호스팅하도록 한다.

문제 발생

일반적으로 .NET Framework 에서 AppDomain.CurrentDomain.BaseDirectory 속성의 반환되는 결과는 경로 마지막에 / 문자열 붙여준다. 반면 Environment.CurrentDirectory 속성은 마지막에 / 문자열을 붙이지 않는다.

간단한 아래의 콘솔 프로그램의 결과를 보면 쉽게 알 수 있다.

using System;

class MainClass {
  public static void Main (string[] args) {
    Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
    Console.WriteLine (Environment.CurrentDirectory);
  }
}

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug

Path.GetDirectoryName와 조합하면 기대하지 않은 결과가 나올 수 있다. Path.GetDirectoryName/ 가 없는 경로의 마지막은 파일로 인식하여 그 부모의 디렉토리 이름까지의 경로를 반환하는데에서 발생한다.

Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
Console.WriteLine (Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory));

Console.WriteLine (Environment.CurrentDirectory);
Console.WriteLine (Path.GetDirectoryName(Environment.CurrentDirectory));

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug
// ...생략.../bin/debug
// ...생략.../bin/

https://repl.it/@powerumc/BaseDirectory-vs-EnvironmentCurrentDirectory

mono-service 버그

처음 언급한 것처럼 mono-serviceAppDomain 을 생성하여 서비스를 실행한 바이너리를 로드한다. 여기에서 AppDomainApplicationBase 디렉토리를 Environment.CurrentDirectory 로 설정하는 바람에 서비스로 실행되는 컨텍스트에서 AppDomain.CurrentDomain.BaseDirectory 값이 기대한 값과 다르게 반환된다.

아래의 링크는 이런 문제를 해결하기 위해 Pull Request 를 보냈고, 정상적으로 메인 저장소에 머지가 되었다.

Fixed a bug in mono-service.cs by powerumc · Pull Request #17095 · mono/mono

아래의 코드는 간단하게 만든 mono-service 에서 구동할 예제 코드이다.

using System;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;

namespace MonoServiceTest
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceBase.Run(new MonoServiceTest());
        }
    }

    public class MonoServiceTest : ServiceBase
    {
        protected override void OnStart(string[] args)
        {
            // Debugger.Launch();
            var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            var directoryPath = Path.GetDirectoryName(baseDirectory);
            var logFilePath = Path.Combine(directoryPath, "log.txt");
            Console.WriteLine($"AppDomain.BaseDirectory = {baseDirectory}");
            Console.WriteLine($"DirectoryPath = {directoryPath}");
            Console.WriteLine($"LogFile Path = {logFilePath}");
        }
    }

    [RunInstaller(true)]
    public class MonoServiceInstaller : Installer
    {
        public MonoServiceInstaller()
        {
            this.Installers.AddRange(new Installer[]
            {
                new ServiceProcessInstaller
                {
                    Username = null,
                    Password = null,
                    Account = ServiceAccount.LocalSystem
                },
                new ServiceInstaller
                {
                    DisplayName = nameof(MonoServiceTest),
                    ServiceName = nameof(MonoServiceTest),
                    StartType = ServiceStartMode.Automatic
                }
            });
        }
    }
}

위의 코드를 컴파일 한 후 mono-service 로 실행하면 아래와 같이 잘못된 결과를 반환하는 것을 알 수 있다.

mono-service --no-daemon mono_service_test.exe

// Results
// AppDomain.BaseDirectory = ...생략.../bin/Debug
// DirectoryPath = ...생략.../bin
// LogFile Path = ...생략.../bin/log.txt

마무리

크로스 플랫폼을 지원하기 위해 기존 레거시를 .NET Core 로 전환하기엔 기술적인 부분과 운영적인 이슈도 있어서 mono-service 를 검토해 보았다. 그 중 기술적인 부분으로는 더 이상 WCF 서버는 .NET Core 에서 지원하지 않고, gRPC 사용을 권장하고 있다. 이를 위해 gRPC 를 WCF 와 대응되도록 호환 레이어를 만들어 전환하기 위해서 수 많은 테스트를 해야 하고 또 장애에 대응해야 한다.

기존 코드를 이식성이 좋은 mono 를 통해 여러 운영체제를 지원하고자 하였지만, mono-service 의 버그로 인해 버그를 조사하고 PR 를 보내기까지 많은 시간이 소요되었다.

위 버그 픽스 코드가 바로 릴리즈 되는 것은 아니기에 우선적으로 AppDomain.BaseDirectory 의 기대하지 않은 경로에 대해서도 올바르게 동작하도록 개발 중인 소스 코드를 수정해야 했다. 아마 차기 mono 릴리즈 버전에서는 이 문제가 수정될 것이니 가능하면 최신 버전의 mono 를 유지하는 것이 좋을 것 같다.

ASP.NET Core 2.1 또는 그 이상의 버전에서 dotnet new reactredux 템플릿에서 TypeScript 를 지원하지 않는다. 오직 Javascript 템플릿만 지원한다. 본 내용에서는 React+Javascript 를 React+TypeScript+Javascript 를 지원하는 환경으로 구성하는 방법을 알아본다.

1. 프로젝트 생성하기

다음의 명령을 입력하여 ASP.NET Core 의 React+Redux 프로젝트를 생성한다.

dotnet new reactredux -o aspnetcore-react-redux

프로젝트 생성이 성공하였다면 cd aspnetcore-react-redux 디렉토리로 이동한다.

2. package.json 파일 업데이트

ClientApp 디렉토리는 React+Redux 보일러플레이트가 설치된 경로이다. cd ClientApp 디렉토리로 이동한다.

ClientApp 디렉토리에 있는 package.json 파일에는 Spa 웹에서 사용할 npm 패키지를 설정할 수 있다. 이 파일을 열어 다음의 패키지를 추가한다.

package.json 파일

{
  "name": "aspnetcore_reactredux_typescript",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.7",
    "react": "^16.0.0",
    "react-bootstrap": "^0.31.5",
    "react-dom": "^16.0.0",
    "react-redux": "^5.0.6",
    "react-router-bootstrap": "^0.24.4",
    "react-router-dom": "^4.2.2",
    "react-router-redux": "^5.0.0-alpha.8",
+    "react-scripts": "1.0.17",
    "redux": "^3.7.2",
    "redux-thunk": "^2.2.0",
    "rimraf": "^2.6.2"
  },
+  "devDependencies": {
+    "react-app-rewired": "^1.6.2",
+    "react-scripts-ts": "^3.1.0",
+    "typescript": "^3.1.6",
+    "@types/jest": "^23.3.9",
+    "@types/node": "^10.12.2",
+    "@types/react": "^16.0.0",
+    "@types/react-dom": "^16.0.0",
+    "@types/react-router-bootstrap": "^0.24.4",
+    "@types/react-router-dom": "^4.2.2",
+    "@types/react-router-redux": "^5.0.0"
+  },
  "scripts": {
+    "start": "rimraf ./build && react-app-rewired start --scripts-version react-scripts-ts",
+    "build": "react-app-rewired build --scripts-version react-scripts-ts",
+    "test": "react-app-rewired test --env=jsdom --scripts-version react-scripts-ts",
+    "eject": "react-scripts eject"
  }
}

누락된 부분이 없는지 확인 후 다음의 명령으로 npm 패키지를 설치한다.

npm i

3. tsconfig.json 파일 생성

{
  "compilerOptions": {
    "baseUrl": ".",
    "module": "es2015",
    "moduleResolution": "node",
    "noUnusedParameters": false,
    "noUnusedLocals": true,
    "noImplicitAny": false,
    "target": "es6",
    "jsx": "react",
    "sourceMap": true,
    "skipDefaultLibCheck": true,
    "strict": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "strictNullChecks": true
  },
  "exclude": [
    "bin",
    "node_modules"
  ]
}

4. tslint.json 파일 생성

{
  "defaultSeverity": "error",
  "extends": [
    "tslint:recommended"
  ],
  "jsRules": {},
  "rules": {
    "interface-over-type-literal": false,
    "quotemark": false,
    "ordered-imports": false,
    "object-literal-sort-keys": false,
    "arrow-parens": false,
    "one-variable-per-declaration": false,
    "only-arrow-functions": false,
    "semicolon": [
      true,
      "ignore-interfaces"
    ],
    "no-console": false,
    "member-ordering": false,
    "variable-name": [
      true,
      "ban-keywords",
      "allow-leading-underscore"
    ],
    "member-access": false,
    "comment-format": false,
    "no-var-requires": false,
    "max-line-length": false,
    "jsx-alignment": false,
    "jsx-curly-spacing": [
      true,
      "never"
    ],
    "jsx-no-lambda": true,
    "jsx-no-multiline-js": true,
    "jsx-no-string-ref": true,
    "jsx-self-close": true
  },
  "rulesDirectory": []
}

5. config-overrides.js 파일 생성

/* config-overrides.js */

module.exports = function override(config, env) {
    //do stuff with the webpack config...
    return config;
}

6. 파일 확장자 변경

이제 Javascript 를 사용할지, TypeScript 를 사용할지 결정하여 파일 확장자를 다음과 같이 변경하면 된다. (단, 파일 확장자를 변경하지 않아도 무방하다)

  1. js -> jsx
  2. js -> tsx


자세한 소스 코드는 이 링크를 참고하기 바란다.


AOP 프레임워크 이해와 개발

AOP(Aspect Oriented Programming), 관점지향 프로그래밍은 OOP(Object Oriented Programming) 에게 ‘관심사’라는 관점을 더해 객체 지향 프로그래밍의 변경 없이 다른 관점의 구현을 추가할 수 있다. 더 쉽게 말하면 클래스나 메서드가 동작할 때 코드의 변경 없이 원하는 동작을 추가하는 기법이다.

흔히 AOP 의 예를 들때 ‘로깅(Logging)’ 을 든다. 기존 코드의 변경 없이 코드 본문이 실행 되기전 매개변수 값 등을 로깅하도록 하는 것이다. 물론 로깅 이외에 다양한 용도로 사용되는데, 비즈니스 로직의 검증이나 응용 프로그램 전역적으로 공통적인 관심사 분리에 사용된다.

AOP 프로그래밍의 활용 예

  • 로깅
  • 유효성 검사
  • 트랜잭션 처리
  • 인증/보안 등

AOP 구현 방법

AOP 프레임워크 개발은 비교적 고급 기술에 속한다. AOP 를 이해하고 쓰는 사람들은 많지만 내부 구현까지 이해하는 사람은 드문 것이 사실이다. 그리고 내부 구현을 이해해도 직접 만드는 것은 또 다른 이야기일 것이다.

필자는 어려운 이야기일 수 있는 이 부분에 대해 언급하고자 한다. AOP 프레임워크는 구현 방법이 매우 다양한데, 크게 두 가지 방법으로 요약할 수 있다. (더 자세한 분류는 이 링크를 참고)

AOP 프레임워크 구현 방안

  1. 런타임(Runtime) 구동 방식
    흔히 Dynamic Proxy 라고 하는데, 동적으로 프록시 패턴을 구현하는 객체를 생성해 내는 기법이다. 메모리에 직접 인스트럭션(Instruction) 을 쓰는데 언어마다, 그리고 컴파일 옵션에 따라 인스트럭션이 다를 수 있다. 따라서 최적화에 따라 성능을 좌지우지 한다.
    C# 에서는 MSIL(Microsoft Intermediate Language), 자바는 바이트코드(Bytecode), C/C++은 어셈블리(Assembly) 코드를 메모리에 생성하는 방식이다.
  2. 빌드 타임(Build-time) 구동 방식
    이 방식은 빌드 프로세스를 지원하는 언어에서 가능한 방식이다. 실제 작성한 코드를 컴파일 하기 전에 AOP 의 위빙(Weaving) 코드를 삽입하고 나중에 컴파일 하는 방식을 말한다. C# 에서는 PostSharp, 자바에는 AspectJ 가 대표적이고, 대체적으로 런타임 구동 방식에 비해 성능이 좋다.

AOP 구현

우선 필자가 간단하게 만든 SimpleAop 라이브러리를 참고해 보는 것이 좋겠다. 예전에 만든 이 링크 참고하면 많은 도움이 될 것 같다. 그리고 자바스크립트로 구현한 Javascript OOP-AOP-IoC 도 참고하면 좋다.

프로그래밍이란 무엇인가?

오로지 CPU 입장에서 본다면 프로그래밍은 이미 정의된 함수를 어떻게 호출할 것인가로 귀결된다. CPU 아키텍처에 따라 다르지만 대부분 함수 매개변수는 스택의 로드(Load)와 (Push) 로 구현된다. 그리고 매개변수가 적재되면 Call 인스트럭션을 보내 함수를 호출하는 것이다.

물론 모든 언어가 이에 해당하는 것은 아니다. 아름다움을 추구하는 오브젝티브-C 언어 1/ 2- 언어적 특성 은 조금은 다른 메커니즘으로 동작한다.

구현

첫 번째, 모든 함수는 return 인스트럭션을 가진다. 흔히 void 함수는 return 이 없어도 되지만 컴파일 된 코드(MSIL, Bytecode, Assembly 등)은 함수의 마지막은 항상 return 으로 종료된다. return 의 의미는 함수를 종료하는 것이 아니라 나를 호출한 caller 에게 되돌아 가라는 의미이다.

두 번째, 객체 지향 프로그래밍에서 상속한 클래스의 생성자는 항상 부모 객체를 먼저 생성한다. 다음의 코드를 보면 조금은 더 이해하기 쉬울 것이다.

foreach (var constructor in _implementationType.GetConstructors())
{
	var constructorTypes = constructor.GetParameters().Select(o => o.ParameterType).ToArray();
	var c = _typeBuilder.DefineConstructor(
		MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName,
		CallingConventions.Standard,
		constructorTypes);
	
	var il = c.GetILGenerator();
	il.Emit(OpCodes.Ldarg_0);

	for (var i = 0; i < constructorTypes.Length; i++)
	{
		il.Emit(OpCodes.Ldarg, i + 1);
	}

	il.Call(constructor);
	il.Emit(OpCodes.Nop);
	il.Emit(OpCodes.Ret);
}

세 번째, SimpleAop 에서는 런타임에 프록시 패턴을 구현하는 방식으로 AOP 를 구현하였다. 프록시 패턴을 런타임에 구현하기 위해서는 원본 대상이 필요한데, 이는 인터페이스(Interface) 정의를 사용한다. 대부분의 테스팅 프레임워크의 Mock 객체들은 인터페이스가 필요한데, 바로 프록시를 생성하기 위한 대상으로 사용되기 때문이다.

특히 자바에서는 기본적으로 virtual 메서드이기 때문에, 런타임에 클래스는 override 하기 용이하다. 반면 C# 언어는 virtual 메서드가 아니기 때문에, virtual 로 선언된 대상 클래스가 필요할 수도 있다. 이는 AOP 프레임워크 마다 구현 방법도 다르기 때문에 사용할 AOP 프레임워크가 어떤 방식인지 알아두면 좋을 것이다.

foreach (var method in _interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
    var methodTypes = method.GetParameters().Select(o => o.ParameterType).ToArray();
    var m = _typeBuilder.DefineMethod($"{method.Name}",
        MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig |
        MethodAttributes.Virtual | MethodAttributes.NewSlot,
        CallingConventions.HasThis,
        method.ReturnType,
        methodTypes);
    
    _typeBuilder.DefineMethodOverride(m, _interfaceType.GetMethod(method.Name, methodTypes));
    
    var il = m.GetILGenerator();
    var localReturnValue = il.DeclareReturnValue(method);
    
    var localCurrentMethod = il.DeclareLocal(typeof(MethodBase));
    var localParameters = il.DeclareLocal(typeof(object[]));
    
    // var currentMethod = MethodBase.GetCurrentMethod();
    il.Call(typeof(MethodBase).GetMethod(nameof(MethodBase.GetCurrentMethod)));
    il.Emit(OpCodes.Stloc, localCurrentMethod);
    
    // var baseMethod = method.BaseType.GetMethod(...);
    var localBaseMethod = il.DeclareLocal(typeof(MethodBase));
    il.Emit(OpCodes.Ldloc, localCurrentMethod);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetCustomAttributeOnBaseMethod)));
    il.Emit(OpCodes.Stloc, localBaseMethod);
    
    
    // var parameters = new[] {a, b, c};
    il.Emit(OpCodes.Ldc_I4, methodTypes.Length);
    il.Emit(OpCodes.Newarr, typeof(object));
    if (methodTypes.Length > 0)
    {
        for (var i = 0; i < methodTypes.Length; i++)
        {
            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Ldc_I4, i);
            il.Emit(OpCodes.Ldarg, i + 1);
            if (methodTypes[i].IsValueType)
            {
                il.Emit(OpCodes.Box, methodTypes[i].UnderlyingSystemType);
            }

            il.Emit(OpCodes.Stelem_Ref);
        }
    }
    il.Emit(OpCodes.Stloc, localParameters);

    // var aspectInvocation = new AspectInvocation(method, this, parameters);
    var localAspectInvocation = il.DeclareLocal(typeof(AspectInvocation));
    il.Emit(OpCodes.Ldloc, localCurrentMethod);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldloc, localParameters);

    il.New(typeof(AspectInvocation).GetConstructors()[0]);
    il.Emit(OpCodes.Stloc, localAspectInvocation);
    
    // var classAttributes = GetType().GetOnMethodBoundAspectAttributes();
    var localClassAttributes = il.DeclareLocal(typeof(OnMethodBoundAspectAttribute[]));
    il.Emit(OpCodes.Ldarg_0);
    il.Call(_implementationType.GetMethod(nameof(GetType)));
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetOnMethodBoundAspectAttributes), new[] {typeof(Type)}));
    il.Emit(OpCodes.Stloc, localClassAttributes);
    
    // var methodAttributes = method.GetOnMethodBoundAspectAttributes();
    var localMethodAttributes = il.DeclareLocal(typeof(OnMethodBoundAspectAttribute[]));
    il.Emit(OpCodes.Ldloc, localBaseMethod);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.GetOnMethodBoundAspectAttributes), new[] {typeof(MethodBase)}));
    il.Emit(OpCodes.Stloc_S, localMethodAttributes);
    
    
    // classAttributes.ForEachOnBefore(invocation);
    il.Emit(OpCodes.Ldloc, localClassAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnBefore)));
    il.Emit(OpCodes.Nop);
    
    // methodAttributes.ForEachOnBefore(invocation);
    il.Emit(OpCodes.Ldloc, localMethodAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnBefore)));
    il.Emit(OpCodes.Nop);
    
    il.LoadParameters(method);
    il.Call(_implementationType.GetMethod(method.Name, methodTypes));
    
    // methodAttributes.ForEachOnAfter(invocation);
    il.Emit(OpCodes.Ldloc, localMethodAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnAfter)));
    il.Emit(OpCodes.Nop);
    
    // classAttributes.ForEachOnAfter(invocation);
    il.Emit(OpCodes.Ldloc, localClassAttributes);
    il.Emit(OpCodes.Ldloc, localAspectInvocation);
    il.Call(typeof(OnMethodBoundAspectAttributeExtension).GetMethod(nameof(OnMethodBoundAspectAttributeExtension.ForEachOnAfter)));
    il.Emit(OpCodes.Nop);
    
    il.Return(method, localReturnValue);
}

정리

기본적인 구현과 코드로 AOP 를 구현하는 방법에 대해 알아보았다. 간단하게 AOP 를 사용하는 코드를 살펴보면 다음과 같다.

[LoggingAspect]
public class Print : IPrint
{
    public void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }
}

// --- Before: Void PrintMessage(System.String), 9a19fdd7e64943c9b22ae2c79a886b50, Hello World ---
// Hello World
// --- After ---

SimpleAop 코드를 보면 알겠지만, 비교적 짧은 코드로 AOP 프레임워크(?) 를 구현하였다. 이미 이보다 좋은 AOP 프레임워크가 많이 있겠지만, 직접 AOP 를 구현해 봄으로서 언어적 특성을 더 잘 알 수 있고, 언어가 제공하는 플랫폼을 이해하는 데 큰 도움이 될 것이다.

[마이크로서비스] 아키텍처 구축하기

마이크로서비스는 서비스를 작게 나누는 것부터 시작한다. 이렇게 서비스를 작게 나누게 되면 여러가지 장점이 있는데 [이 글]에서 마이크로서비스 아키텍처를 이해할 수 있다. 가볍고 탄력적인 서비스를 구축이 필요한 곳에서 이 아키텍처를 도입하고 있다.

물론 장점만 있는 것은 아니다. 그 중에서 단연 러닝커브가 높은 기술적 구현 측면과 요구사항이 바로 그것이다. 아시다시피 서비스가 작게 나눠지면서 개발/네트워킹/보안/배포/모니터링에 이르기까지 해결해야 할 과제가 생긴다.

이 아티클은 개발 영역에 초점을 맞추어 예제 코드가 작성 되었다. 개발 영역을 마이크로서비스화 할 때 적지 않은 부분의 변화가 필요한데, 그 기법과 라이브러리 등도 함께 살펴보면 도움이 될 것 같다.

[https://github.com/powerumc/microservice-architecture-quick-start]

위 소스 코드는 마이크로서비스의 기술적인 측면을 강조하여 예제가 작성이 되었고, 아직 구현되지 않은 부분도 있으니 아래의 체크리스트를 참고하자.

인프라스트럭쳐

오케스트레이션

  • [ ] Kubernetes
    잘 알려진 오케스트레이션 도구인 쿠버네티스다. Yaml 파일을 만들어 제공되지만, 쿠버네티스 환경에서 구동하려면 마스터(master)와 노드(nodes) 머신이 구성되어야 하고, 깃헙으로 예제를 제공하기엔 적절하지 않다고 판단했다. 추후에 기회가 되면 [vagrant] 환경과 [ansible] 을 적용한 코드를 예제로 제공할 예정이다. (그게 언제가 될지…)

  • [x] Docker
    
잘 알려진 컨테이너 기술이며 가능하다면 이 도구를 필히 마스터 할 필요가 있다.

  • [x] Docker-Compose
    
여러 개의 Docker 컨테이너를 띄우기 위한 도구이다.

인프라스트럭쳐

  • [ ] Ocelot
    
ASP.NET Core 기술로 개발된 API Gateway 이다. 작게 쪼개진 API 서비스를 개발하다 보면 서로 종속적인 관계가 발생할 수 있는데, 그런 관계를 하나의 API Gateway 를 통과하게 함으로써 관리적으로 용이하다. API 관리와 함께 접근제어, 인증/권한, 로깅, 트래픽 제어 등의 역할을 수행한다.

  • [x] NLog
    로깅 라이브러리이다.

  • [x] Swagger Integration (Swashbuckle) 
 코드로 정의한 API 를 문서화 하는 도구이다.

  • [x] Entity Framework Core Code First
    자바의 JPA 의 닷넷 버전이다. 자바의 JPA 가 세상에 나오기 훨씬 전부터 마이크로소프트 주도하에 개발되어온 프레임워크인데, JPA 비해 훨씬 사용성이 좋다. 물론 JPA 만큼 고급 옵션이 준비되어 있지는 않지만, 어지간한 비즈니스 플로우는 모두 구현할 수 있다.

  • [x] TraceId about Request
    필자가 개발한 ASP.NET Core 미들웨어이다. 마이크로서비스는 서비스간에 트랜잭션의 추적이 필요한데, 그 트랜잭션을 추적하기 위한 용도로 개발되었다.

  • [x] Guard
    간단한 코드 스니펫이다. 보통 new 키워드를 통해 Exception 을 발생하는데, Guard 라는 static class 를 이용하여 인자와 로직을 검증하게 만들었다. 더 고급 개발자라면 [Jetbrains.Annotations] 라이브러리를 이용하는 것도 좋은 선택이다.

  • [ ] AutoMapper
    이 라이브러리에는 호출호가 참 많다. 그래서 사용하지 않았는데 앞으로도 사용할 일은 거의 없을 것 같다.

  • [x] Data Protection
    먼저 MSDN 의 [이 문서]를 참고해 보면 좋다. 말도 안되게 기계 번역된 페이지를 보면 현기증이 나지만 그래도 끝까지 한번 읽어보자. 결론만 말하자면 개발된 웹 서비스를 분산하고 특정 기능(ASP.NET Core Session 과 같은) 을 사용하려면 데이터 보호 기능을 사용해야 한다. 가령, 웹 서비스를 분산하기 위해 분산 세션 기능을 사용하게 되면 ASP.NET Core 내부적으로 암호화 작업을 하게 되는데 이 때 데이터 보호 기능이 필요하다.

  • [x] Polly
    마이크로서비스는 항상 작동하지 않을 수 있다라는 전제가 필요하다. 서비스가 뻗을 수도 있고 응답이 느릴 수도 있다. 이 때 정책적으로 여러 번 재호출 하거나, 일정한 간격을 두고 재시도 하는 등의 정책을 설정할 때 필요하다.

도메인 기반 개발

  • [x] Aggregate Root
    하나의 웹 서비스의 도메인을 분류 하였다면 도메인의 루트가 있을텐데, 이 루트 개체를 총칭하는 용어이다.

  • [ ] ValueObject
    DDD 에서는 고유 ID 를 가지지 않는 불변객체를 지칭하는 용어이다.

  • [x] CQRS
    필자가 설명하긴 여전히 스터디 단계이므로, 이규원님 블로그의 [이 아티클]을 참고하면 잘 설명되어 있다.

  • [x] Event Sourcing
    위 아이클 참고.

  • [x] Event Bus
    이벤트 버스는 이벤트가 발생하면 특정 대상으로 이벤트롤 전달하는 패턴을 말한다. 일반적으로 구독/발행 패턴의 구현체가 되는데, 이 패턴을 사용하게 되면 복잡성을 피하고, 컴포넌트간에 커뮤니케이션을 간단하게 구현할 수 있다.

  • [ ] EventBus by RabbitMq
    이벤트가 발생하면 RabbitMq 큐(Queue) 로 전달하는 기능이다.

  • [ ] Unit Of Work
    마틴 파울로의 [이 글]을 참고한다. 마틴 파울러는 이 패턴은 비즈니스 트랜잭션을 비즈니스 트랜잭션을 관리하고, 동시성 문제를 해결한다고 정의하였다. 일종의 Dispatcher 역할을 한다고 볼 수 있다.

모니터링

  • [x] Health Check
    기본적으로 서버가 살아있는지 상태를 알기 위한 API 이다.

  • [x] App.Metrics
    서버의 행동/행위를 측정하기 위한 라이브러리이다.

  • [ ] Grafana
    
App.Metrics 의 측정 항목을 가시화하기 위한 도구이다.



VSGesture for Visual Studio 2017 배포 완료

Visual Studio 2017 버전 배포 완료

Visual Studio 2017의 Tools -> Extension Manager 에서 다운로드 받으실 수 있습니다. (검색: vsgesture)


Visual Studio 2017 버전
https://marketplace.visualstudio.com/items?itemName=vs-publisher-2521.VSGestureforVisualStudio2017

Visual Studio 2010~2015 버전

https://marketplace.visualstudio.com/items?itemName=vs-publisher-2521.VSGestureforVisualStudio

Visual Studio 2005, 2008 버전

https://marketplace.visualstudio.com/items?itemName=vs-publisher-2521.VSGesture

소스코드 저장소

https://github.com/powerumc/vsgesture






  1. 원반 2017.05.08 01:11

    Set wmi = GetObject("winmgmts://./root/cimv2";)

    browser = "iexplore.exe"
    qry = "SELECT * FROM Win32_Process WHERE Name='" & browser & "'"

    For Each p In wmi.ExecQuery(qry)
    p.Terminate
    Next
    =====
    위 vbs를 실행하면 not found라는 오류가 뜹니다. 오류 안뜨게 어떻게 해야하는지요?
    아래 멜로 답변 주시면 감사하겠습니다.
    socialte@hanmail.net

마이크로소프트(Microsoft)는 VSCode 에서 다양한 개발 편의 기능을 제공하기 위한 Language Server Protocol 을 공개했다. 이 프로토콜의 C# 버전이 바로 OmniSharp-Roslyn이 되겠다.

그 외에 다양한 언어의 구현체가 등장했는데, 어떤 개발 언어가 구현 되었는지 아래의 링크에서 확인하기 바란다.

필자는 OmniSharp-Roslyn 을 git clone 하고 빌드하게 되면 다음과 같은 오류를 만났다.

개발환경

  • OS: MacOS Sierra
  • Version: 10.12.2

The type initializer for 'System.Net.Http.CurlHandler' threw an exception.

위의 이슈는 아래와 같이 보고가 되었다.

이 이슈는 다음과 같이 해결하면 된다.

brew update
brew install openssl
ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/
ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/


TypeScript 언어 핵심 요약 사항

TypeScript 핵심 요약은 ES6 에 추가된 기능과 중복된 항목이 있다. TypeScript 는 Javascript 의 슈퍼셋(Superset)이라고 하지만, 아직 ES6 를 완벽하게 지원하지 않는다. 그래서 본 글의 핵심 요약은 TypeScript 에서 추가된 기능이자 ES6 와 중복된 내용이 있다.

ES6 의 새로운 기능은 다음의 링크에서 참고하기 바란다.

TypeScript 2.1 최신이고 아직 꾸준히 업데이트 중이며, 1.8 버전을 기준으로 비교된 ES6 와 호환 테이블이다.

TypeScript 의 모든 언어 사양은 아래의 링크를 참고하기 바란다.


1. var vs let 키워드

var 키워드는 Javascript 의 그 var 키워드와 동일한 역할을 한다. var 키워드는 function scope 단위로 할당이 된다. 그래서 아래의 예제와 같이 원하지 않는 결과를 얻을 수도 있다.

일반적으로 사용하는 방법은 Javascript 의 var 와 동일하게 사용하면 되고, TypeScript 에서는 되도록 let 을 쓰도록 권장하고 있다.

var 키워드

for (var i = 0; i < 10 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
10
10
10
10
10
10
10
10
10
10

let 키워드

for (let i = 0; i < 10 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
0
1
2
3
4
5
6
7
8
9

2. Indexable 타입

C# 의 인덱서와 동일한 역할을 하는 Indexable 타입이 있다. Cars 인터페이스가 배열/컬렉션 객체가 아니지만, 마치 배열/컬렉션 객체의 아이템을 가져오는 것처럼, 인덱서를 사용할 수 있다.

interface Cars {
    [num: number]: string;
}

let cars: Cars;
cars = ["Cadillac", "Benz", "BMW", "Audi"];

console.log(cars[0]);

3. Hybrid 타입

인터페이스에 함수를 인터페이싱 하는 방법 중의 하나다. (start: number): string; 는 익명 함수를 선언하고 있고, reset() 은 명시적으로 함수를 선언하고 있다. <Counter> 는 캐시팅을 명시적으로 지정하였으며 counter 변수는 Counter 객체로 캐스팅 된다.

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

4. protected 키워드

객체지향언어의 protected 키워드와 동일한 역할을 한다. 클래스 맴버가 상속받은 자식 클래스에만 노출이 된다.

class Person {
    protected name: string;

    constructor(name: string) {
        this.name = name;
    }
}

class Student extends Person {
    constructor(name: string) {
        super(name);
    }

    say(): void {
        console.log(this.name);
    }
}

let student: Student = new Student("POWERUMC");

student.say();
student.name = "POWERUMC"; // Error

5. Optional and Default 파라메터(Parameters)

Optional 파라메터(Parameters)

기본적으로 TypeScript 는 Javascript 함수처럼 가변적인 매개변수를 지정하여 함수를 호출할 수 없다. (아래 6번에서 가능한 방법을 설명한다.) 여타 객체지향언어처럼 매개변수와 타입에 맞게 호출하는 것이 객체지향언어의 규약이다.

마찬가지로 TypeScript 에서도 함수를 호출하려면 타입에 맞아야 한다.

function test(src:string, dest:string): boolean {
    return src === dest;
}

test("P", "P");
test("P");          // Error

이를 Optional 매개변수로 만들기 위해서 dest? 로 지정하면 된다. dest? 매개변수에는 undefined 가 된다.

function test(src:string, dest?:string): boolean {

    console.log(dest);
    return src === dest;
}

test("P", "P");
test("P");          // Ok

Default 파라메터(Parameters)

Default 파라메터로 만들기 위해 dest = "" 와 같이 리터럴 상수값을 선언하면 된다.

function test(src:string, dest = ""): boolean {

    console.log(dest);
    return src === dest;
}

test("P", "P");
test("P");          // Ok

6. 가변적인 매개변수(Rest Parameters)

...restOfName 처럼 매개변수 이름 앞에 ...을 붙이면 가변적인 매개변수가 된다.

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

7. 오버로드(Overloads)

첫 번쨰 pickCard 함수는 명시적으로 object 리터럴로 감싸고 number 타입을 반환하는 함수이고, 두 번째 pickCard{suit: string; card: number; } 타입을 반환하는 함수이다.

세 번째 pickCard 함수가 메인 함수인데, 타입을 체크하여 각각 처리 후 결과를 반환하는데 반환 타입이 다르다는 것을 알 수 있다.

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

8. 제네릭(Generics)

C# 의 '제네릭(Generics)' 또는 C++ 의 '템플릿(Tempaltes)' 는 코드 재사용성을 높이고, 타입 안정성, 그리고 성능에서 큰 이점을 준다.

TypeScript 또한 이러한 이점을 그대로 얻을 수 있다.

제네릭 함수(Generic Functions)

function identity<T>(arg: T): T {
    return arg;
}
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;
interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

제네릭 클래스 (Generic Classes)

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

9. Nullable Types

let s = "foo";
s = null; // error, 'null' is not assignable to 'string'
let sn: string | null = "bar";
sn = null; // ok

sn = undefined; // error, 'undefined' is not assignable to 'string | null'
function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

10. for..of 과 for..in 구문(statements)

for..in 구문은 배열의 키 값을 반환하는 한편, for..of 구문은 배열의 항목을 반환하는 다른점이 있다.

따라서 for..of 구문을 사용하는 것을 권장한다.

let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}

11. 데코레이터(Decorators)

데코레이터(Decorators)는 C# 의 특성(Attributes), Java 의 어노테이션(Annotations) 와 같이 클래스나 함수에 선언적으로 정보를 지정할 수 있다.

reflect-metadata 모듈을 이용하면 런타임에 리플랙션을 이용하여 이 데코레이터의 정보를 얻어와서 처리를 하는 방법도 있다.

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}
f(): evaluated
g(): evaluated
g(): called
f(): called


ASP.NET Core Middleware

ASP.NET Core Middleware 는 모든 요청과 응답을 처리하는 파이프라인이다. 레거시 ASP.NET 과 ASP.NET MVC 에서는 이를 IHttpModuleIHttpHandler를 통해 구현하여 처리하거나, Global.asax.cs 에서 처리 가능하다.

IHttpModule 이 주로 사용되는 경우는 권한 처리 등과 같이 다양하게 사용된다. 모든 요청은 이 IHttpModule을 파이프라인을 통과하게 되고, 응답을 제어할 수 있기 때문이다.

이 파이프라인이 오늘에 와서 ASP.NET Core Middleware 에서 그 역할을 대신하게 된다.

- 레거시에서 파이프라인

최초 레거시 ASP.NET 은 ‘ASP.NET 파이프라인’은 최초 요청부터 마지막 응답까지 컨텍스트가 흐르는 순서가 있다. 이것이 IIS 7.0 부터 IIS 서버와 통합이 되었다.

아래 그림은 IIS 7.0의 응용 프로그램 생명주기(즉 파이프라인)이다.

Application Lifecycle
https://msdn.microsoft.com/en-us/library/bb470252.aspx

- ASP.NET Core Middleware

ASP.NET Core 는 크로스플랫폼을 지향하는데, IIS 는 마이크로소프트 윈도우에서만 실행 가능한 웹/응용프로그램 서버이다. 따라서 ASP.NET Core 를 크로스플랫폼에서 호스팅하려면 IIS 서버에서 적용되었던 파이프라인 개념을 버려야 했다. 그래서 나온 개념이 Middleware 다.

Middleware 또한 IHttpModule이 수행하는 요청과 응답을 제어하는 역할을 한다.


https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware

다만, Global.asax.cs 에서 Session 생성/소멸과 같은 이벤트를 Middleware 만으로는 세세하게 제어하지 못한다. 아마도 다른 접근 포인트가 있는지 좀 찾아봐야 겠다.

IHttpMoudle과 IHttpHandler 마이그레이션

자세한 마이그레이션 과정이 MSDN 문서에 훌륭하게 설명되어 있어서 링크로 대체하고, 간단한 샘플만 남긴다.

참고로 Middleware는 UseMvc() 메서드 이전에 남겨야 한다. 그렇지 않으면, Middleware 가 동작하지 않는다.

Startup.cs
public class Startup
{

    // 생략...

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseApplicationInsightsRequestTelemetry();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseApplicationInsightsExceptionTelemetry();
        app.UseStaticFiles();

        // 미들웨어 등록
        app.UseMiddleware<FrameworkMiddleware>();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

    }
}
FrameworkMiddleware.cs
public class FrameworkMiddleware
{
    private readonly RequestDelegate next;

    public FrameworkMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        Console.WriteLine("==========================================");
        await next(context);
        Console.WriteLine("------------------------------------------");
    }
}



자세한 내용은 아래 링크를 방문하기 바란다.
https://docs.microsoft.com/en-us/aspnet/core/migration/http-modules

IControllerFactory

IControllerFactory 는 컨트롤러 객체를 반환하거나 객체 릴리즈 시키는 팩토리 인터페이스이다. ASP.NET MVC 4 까지 지원하지 않았던 DI(Dependency Injection) 기능을 사용하기 위해 이 인터페이스를 구현하여 사용하였다.

ASP.NET Core 에서는 객체 주입(Injection) 할 때 한 가지 큰 단점이 있다.

생성자 주입(Constructor Injection) 으로만 DI(Dependency Injection) 기능을 사용할 수 있다. 

이 아티클에서는 프로퍼티 인젝션(Property Injection) 이 가능한 Unity Application Block 을 사용하기 위해 IControllerFactory 를 마이그레이션 하는 것을 목표로 한다.

- 레거시 ASP.NET MVC 에서 IControllerFactory 설정

Controller 클래스들을 IoC(Inversion of Control) 컨테이너(Container) 에 담아 놓고, 꺼내쓸 때 DI(Dependency Injection) 을 시키는 방식이다. 이렇게 하면 사용하는 입장에서는 복잡한 객체와 인스턴스 관계를 파악할 필요 없이 꺼내 쓰면 되는 것이다. 당연 단위 테스트에서도 간결하게 사용할 수 있게 된다.

global.asax.cs

ControllerBuilder 클래스를 이용하여 IControllerFactory 인스턴스를 설정한다.

// 종속성 주입 설정 Application.Lock(); {     Container = configureContainer();     ControllerBuilder.Current.SetControllerFactory(new FrameworkControllerFactory(Container)); } Application.UnLock(); 

FrameworkControllerFactory.cs

public class FrameworkControllerFactory : DefaultControllerFactory {     private readonly IFrameworkContainer container;      public FrameworkControllerFactory(IFrameworkContainer container)     {         this.container = container;     }      #region Overrides of DefaultControllerFactory      protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)     {         if (controllerType == null)         {             requestContext.HttpContext.Response.Redirect("/", true);         }          var controller = container.Resolve(controllerType);         return controller as IController;     }      #endregion } 

- ASP.NET Core 에서 IControllerFactory 설정

ASP.NET Core 에서는 상당수 서비스 클래스와 인스턴스들이 IoC Container 에 등록되어 여기에서 객체를 불러 사용하고 있는 형태다. IServiceCollection 인터페이스에 서비스에 ASP.NET Core 가 동작하기 위한 코어 클래스들이 등록되어 있다.

IServiceCollection 에 Singleton 객체로 services.AddSingleton<IControllerFactory, FrameworkControllerFactory>(); 하게되면 IControllerFactory 가 등록된다.

그럼 IServiceCollection 에는 IControllerFactory 가 두 개 등록이 되어있는데, 나머지 하나가 DefaultControllerFactory 클래스이다. 이 때, ASP.NET Core는 가장 마지막에 등록된 IControllerFactory 클래스를 반환한다.

먼저, Nuget 을 통해 Unity Application Block 을 설치한다.

Startup.cs

public class Startup {     public static IUnityContainer Container = new UnityContainer();      public void ConfigureServices(IServiceCollection services)     {         // Add framework services.         services.AddApplicationInsightsTelemetry(Configuration);         services.AddMvc();          // IControllerFactory 설정         services.AddSingleton<IControllerFactory, FrameworkControllerFactory>();     } } 

FrameworkControllerFactory.cs

public class FrameworkControllerFactory : IControllerFactory {     public object CreateController(ControllerContext context)     {         return Startup.Container.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType());     }      public void ReleaseController(ControllerContext context, object controller)     {     } } 

모든 설정이 다 되었으면, Unity Container 에서 DI(Dependency Injection) 을 수행하는 HomeController 가 정상적으로 동작하게 된다.

HomeController.cs

public class HomeController : Controller {     [Dependency]     public MessageService MessageService { get; set; }      public IActionResult Index()     {         MessageService.Say();          return View();     } }  public class MessageService {     public void Say()     {         Console.WriteLine("MessageService Resolved. ===========================");     } }

 

 

오류 유형은 아래의 웹서버 로그와 같이 “세션ID 가 생성 되었지만, Response 가 Flushed 되어 저장할 수 없다”.

Session state has created a session id, but cannot save it because the response was already flushed

이 현상은 다음의 경우의 수를 모두 만족할 경우 발생하게 된다.

  • 웹서버(IIS) 가 리사이클링 되고,

  • 웹서버에게 첫 요청이 가고,

  • 코드에서 Session 속성을 사용하기 전이고,

  • 서버 코드에서 Response 를 Flush 하고,

  • 웹브라우저가 아닌, 네트워크 라이브러리를 통해 호출을 하고,

  • Global.asax.cs 의 Session_Start 이벤트가 발생할 때


분석을 해보면 (MSDN 에서 ASP.NET Application Page LifeCycle(영문)을 참고), PreRendering 단계 이후에 SavePageStateTo 를 수행하는 것을 알 수 있다. 따라서 인터넷을 통해 찾은 해결 방법이 Session 속성의 객체를 한 번 호출해 주는 것으로 이런 현상을 제거할 수 있다.

onhashchange

onhashchange.js 를 방금 릴리즈 했습니다.
https://github.com/powerumc/onhashchange

onhashchange 기능이 제공되는지 여부를 감지하고, 이를 지원하지 않는 오래된 브라우저인 경우 Polyfill 을 지원합니다.

아래의 호환 브라우저 미만인 경우 onhashchange Polyfill 이 동작하게 됩니다.

Implemented event fields.

The dispatched hashchange event has the following fields:

FieldTypeDescription
newURLDOMStringThe new URL to which the window is navigating.
oldURLDOMStringThe previous URL from which the window was navigated.

Browser compatibility

Desktop Mobile
Feature     Chrome          Firefox (Gecko)     Internet Explorer       Opera       Safari
Basic       support 5.0     3.6 (1.9.2)         8.0                     10.6        5.0

Ref. https://developer.mozilla.org/ko/docs/Web/API/WindowEventHandlers/onhashchange

Example

<html>
    <head>
        <script type="text/javascript" src="onhashchange.js"></script>
        <script>
            window.onhashchange = function(e) {
                alert("onhashchange");
            }
        </script>
    </head>

    <body>
        <p><a href="#a">#a</a></p>
        <p><a href="#b">#b</a></p>
        <p><a href="#c">#c</a></p>
    </body>
</html>




[GitHub] https://github.com/powerumc/MyRedis

MyRedis 는 MySQL 연결을 지원하도록 2014년도에 개선 된 프로젝트이다.

RedisPlus 와 함께 곁들어 Redis와 DB 간에 동기화 할 때 사용할 때, 웹서버에서 처리해야 할 것들을 Redis 안에서 모두 해결하기 위함이다. 따라서 필요 이상의 라운드 트립 발생을 줄일 수 있다.

1. Setup Database

Setup Database

> SET db.host "localhost"
> SET db.user "root"
> SET db.passwd "!@#$%"
> SET db.db "powerumc"

2. Execute Query

Setup Query

> SET q1 "SELECT * FROM temp_table"

Execute Query

> MYSQLQ db q1
1) "1"
2) "Junil Um"
3) "25"
4) "2"
5) "땡초"
6) "3"
7) "3"
8) "POWERUMC"
9) "35"


'Umc Projects > MyRedis' 카테고리의 다른 글

[MyRedis] Redis 에서 데이터베이스 연결 지원  (0) 2016.04.05

2014년 TFT 프로젝트에서 Redis 이벤트와 관련하여 기능이 필요하여 개발된 프로젝트이다.



RedisPlus 를 개발하게 된 계기는

  1. Redis 이벤트의 Key Expired 이벤트 외에 Key Expiring 이벤트가 필요했는데, **Key Expiring **이벤트는 Redis Key 가 만료가 되었을 때, 만료전에 발생하는 이벤트이고, Key Expired 는 키를 만료시킨 후에 발생하는 이벤트이다.

  2. Redis 이벤트 만료 시 만료되는 키만을 반환하기 때문에 키의 값이 무엇인지만 알 수 없다는 문제가 있다.

Expiring Key Event

Expiring 키 이벤트는 키와 값을 반환한다. 따라서 Redis's Expiring Event 를 이용하여 동기화 매커니즘을 구현할 수 있게 된다.

예를 들어, 키에 값이 없으면 DB 나 외부통신을 통해 최신의 값을 가져온다.
키에 새로운 값을 넣을 때 마다 1분 후 만료시간을 주고, 다시 새로운 값이 오면 다시 1분 만료시간을 준다.
그런 후 1분 만료가 되기 직전, DB 또는 외부통신을 통해 만료되는 키와 값을 전달하고, 키를 만료 시킨다.

1. 먼저 서버에서 구독을 하도록 subscribe keyspace-events 설정

> config set notify-keyspace-events KEA
> PSUBSCRIBE '__key*__:expiring'

2. 키를 3초후에 만료하도록 mykey 키와 값을 설정.

SET mykey "you can get key and value" ex 3

3. 3초 후. mykey 키는 키와 값을 함께 구독한다.

1) "pmessage"
2) "__key*__:expiring"
3) "__keyevent@0__:expiring"
4) "mykey"
5) "you can get key and value"   << IMPORTANT: You got it is value of key.


'Umc Projects > RedisPlus' 카테고리의 다른 글

[RedisPlus] Redis 에서 키 만료 이벤트 개선 작업  (0) 2016.04.05

개요

ASP.NET WebForm 에서 Dependency Injection 을 사용하는 방법을 소개한다. IoC Container 를 이용하여 System.Web.UI.Page 를 상속하는 페이지에서 Injection 을 해야 하는데, 이를 위해 IHttpHandlerFactory 를 사용하는 방법을 소개한다.

여기에서는 필자가 꾸준히 만들어 온 Unity ContainerWindsor Castle 을 기반으로 하는 Umc.Core 프레임워크를 사용한다.Umc.Core 프레임워크에는 Unity Auto Registration 기능등이 모두 포함하기 때문에 프로젝트 셋업에 편리하다는 장점도 있다.

샘플 프로젝트는 필자의 github 에서 다운로드 받을 수 있다.
https://github.com/powerumc/WebForm-DependencyInjection 

프로젝트 셋업

먼저 프로젝트를 만들고, nuget 을 이용하여 umc.core 프레임워크를 설치한다.

nuget install umc.core

그리고, Umc.Core 에 구현에 놓은 IHttpHandlerFactory 를 샘플 프로젝트에 추가해 놓았다. 이를 web.config 에 추가해 주면 된다.

<system.webServer>
    <handlers>
      <add name="WebFormPageHandlerFactory" verb="*" path="*.aspx"  type="WebForm_DependencyInjection.FrameworkContainerPageHandlerFactory"/>
    </handlers>
</system.webServer>

FrameworkContainerPageHandlerFactory 클래스의 구현은 아래의 코드를 참고하면 된다. 다만, 아래 구현 코드에서 BuildManager.CreateInstanceFromVirtualPath 를 대신 사용하면 절대 안된다.

public class FrameworkContainerPageHandlerFactory : IHttpHandlerFactory
{
    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
    {
        var handler = BuildManager.GetObjectFactory(url, false).CreateInstance();
        if (handler.GetType().ToString().StartsWith("ASP."))
        {
            var container = context.Application["container"] as IFrameworkContainer;
            return container.Resolve(handler.GetType().BaseType) as IHttpHandler;
        }

        return handler as IHttpHandler;
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
    }
}

웹 응용프로그램 Bootstrap

그 다음 할 일은 어떤 컴포넌트들을 IoC 에 등록하고 이를 Composition 할 지 코드를 통해 구현한다. ASP.NET WebForm 에서는 Global.asax.cs 의 Application_Start 메서드에 구현하는 게 가장 적절하다.

그러나 이는 RELEASE 용 빌드인 경우가 그렇고, 개발 중인 경우, 즉 DEBUG 모드 빌드인 경우 Session_Start 에 구현해 놓는 것도 좋을 것 같다.

public class Global : System.Web.HttpApplication
{
  private static IFrameworkContainer container;

  protected void Application_Start(object sender, EventArgs e)
  {
      container = new FrameworkContainerForUnity();

      var catalog = new FrameworkAssemblyCatalog(Assembly.GetExecutingAssembly());
      var visitor = new FrameworkDependencyVisitor(catalog);
      var resolver = new FrameworkCompositionResolverForUnity((FrameworkContainerForUnity)container, visitor.VisitTypes());
      resolver.Compose();

      Application.Lock();
      Application["container"] = container;
      Application.UnLock();
    }
  }

FrameworkAssemblyCatalogFrameworkCatalog를 구현한 클래스로 컴포넌트를 등록하는 방법을 구현하는 클래스다.

FrameworkDependencyVisitor 는 catalog 에서 검색된 컴포넌트들을 구석 구석 방문해서 Comsoition 을 위해 객체 그래프를 그리는 클래스다.

FrameworkCompositionResolverForUnity 는 객체 그래프를 IoC Container 에 등록하는 클래스다.

이렇게 몇 줄의 코드로 Auto Registration 과정이 모두 끝난다.

서비스 구현

간단하게 Dependency Inection 을 테스트하기 위해서 IEmailService 인터페이스와 이를 구현한 코드다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using Umc.Core;

namespace WebForm_DependencyInjection.Services
{
    public interface IEmailService
    {
        bool Send(string to, string contents);
    }

    [DependencyContract(typeof(IEmailService))]
    public class EmailService : IEmailService
    {
        public bool Send(string to, string contents)
        {
            HttpContext.Current.Response.Write("Send email.");
            return true;
        }
    }
}

페이지 구현

이제 모두 다 됐다. Dependency Injection 이 필요한 프로퍼티에 [DependencyInjection] 특성을 선언하면 Index 페이지 인스턴스가 생성될 때 컨테이너에 등록된 컴포넌트가 주입된다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Umc.Core;
using WebForm_DependencyInjection.Services;

namespace WebForm_DependencyInjection
{
    public partial class Index : System.Web.UI.Page
    {
        // Here.. Injection for IEmailService.
        [DependencyInjection]
        protected IEmailService EmailService { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            EmailService.Send("", "");
        }
    }
}


Flip-Table-Net 은 자바 코드로 작성된 flip-table 을.NET 코드로 포팅한 프로젝트로, 콘솔에 데이터를 표로 표현해 줍니다.

설치

Command Line 에서 다음처럼 입력하거나,

nuget install flip-tables-net

Visual Studio Package Manager Console 에서 다음처럼 입력합니다..

Install-Package Flip-Tables-Net

또는 Nuget 패키지 관리자에서 flip-table-net 으로 검색합니다.

기존 자바에서 지원하던 기능

FlipTable은 헤더 정보와 데이터 정보가 필요합니다.

string[] headers = { "Test", "Header" };
string[][] data =
{
    new[] {"Foo", "Bar"},
    new[] {"Kit", "Kat"}
};
Console.WriteLine(FlipTable.Of(headers, data));
+======+========+
| Test | Header |
+======+========+
| Foo  | Bar    |
+------+--------+
| Kit  | Kat    |
+======+========+

데이터에 개행 문자열도 지원합니다.

string[] headers = { "One Two\nThree", "Four" };
string[][] data = { new[] { "Five", "Six\nSeven Eight" } };
Console.WriteLine(FlipTable.Of(headers, data));
+=========+=============+
| One Two | Four        |
| Three   |             |
+=========+=============+
| Five    | Six         |
|         | Seven Eight |
+=========+=============+

그리고 테이블 안의 테이블도 지원합니다.

string[] innerHeaders = { "One", "Two" };
string[][] innerData = { new[] { "1", "2" } };
string inner = FlipTable.Of(innerHeaders, innerData);
string[] headers = { "Left", "Right" };
string[][] data = { new[] { inner, inner } };
Console.WriteLine(FlipTable.Of(headers, data));
+===============+===============+
| Left          | Right         |
+===============+===============+
| +=====+=====+ | +=====+=====+ |
| | One | Two | | | One | Two | |
| +=====+=====+ | +=====+=====+ |
| | 1   | 2   | | | 1   |   2 | |
| +=====+=====+ | +=====+=====+ |
|               |               |
+===============+===============+

.NET 으로 포팅하면서 추가된 기능

flip-tables-net 버전은 .NET 이 지원하는 객체를 사용할 수 있습니다.

DataTableDataSet 을 사용하는 방법입니다.

var dt = new DataTable();
dt.Columns.Add("FirstName");
dt.Columns.Add("LastName");
dt.Columns.Add("Age");
var row1 = dt.NewRow();
row1["FirstName"] = "Junil";
row1["LastName"] = "Um";
row1["Age"] = 37;
dt.Rows.Add(row1);

Console.WriteLine(dt.FlipTablesFrom());
+===========+==========+=====+
| FirstName | LastName | Age |
+===========+==========+=====+
| Junil     | Um       | 37  |
+===========+==========+=====+

.NET nested entity object 객체는 다음과 같이 정의되어 있다면,

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public List<Person> Children { get; set; }
    public List<Name> Names { get; set; }

    public Person() { }
    public Person(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        Children = new List<Person>();
        Names = new List<Name>() { new Name("A", "B") };
    }
}

public class Person2
{
    public Name Name { get; set; }
    public int Age { get; set; }
}

public class Name
{
    public string First { get; set; }
    public string Last { get; set; }

    public Name() { }

    public Name(string first, string last)
    {
        First = first;
        Last = last;
    }
}
var person2 = new Person2()
{
    Name = new Name("Junil", "Um"),
    Age = 37
};
Console.WriteLine(person2.FlipTablesFrom());
+==================+=====+
| Name             | Age |
+==================+=====+
| +=======+======+ | 37  |
| | First | Last | |     |
| +=======+======+ |     |
| | Junil |   Um | |     |
| +=======+======+ |     |
|                  |     |
+==================+=====+

복합적인 데이터가 담긴 entity model 와 List<> 객체입니다.

var personList = new List<Person>
{
    new Person("Junil", "Um", 37),
};
personList[0].Children.Add(new Person("A", "B", 12));
Console.WriteLine(personList.FlipTablesFrom());
+===========+==========+=====+==============================================================+==================+
| FirstName | LastName | Age | Children                                                     | Names            |
+===========+==========+=====+==============================================================+==================+
|     Junil |       Um |  37 | +===========+==========+=====+==========+==================+ | +=======+======+ |
|           |          |     | | FirstName | LastName | Age | Children |            Names | | | First | Last | |
|           |          |     | +===========+==========+=====+==========+==================+ | +=======+======+ |
|           |          |     | |         A |        B |  12 |          | +=======+======+ | | |     A |    B | |
|           |          |     | |           |          |     |          | | First | Last | | | +=======+======+ |
|           |          |     | |           |          |     |          | +=======+======+ | |                  |
|           |          |     | |           |          |     |          | |     A |    B | | |                  |
|           |          |     | |           |          |     |          | +=======+======+ | |                  |
|           |          |     | |           |          |     |          |                  | |                  |
|           |          |     | +===========+==========+=====+==========+==================+ |                  |
|           |          |     |                                                              |                  |
+===========+==========+=====+==============================================================+==================+

FlipTablePad 옵션

var person2 = new Person2()
{
    Name = new Name("Junil", null),
    Age = 37
};
Console.WriteLine(person2.FlipTablesFrom(FlipTablesPad.Right));
+====================+=====+
|               Name | Age |
+====================+=====+
| +=======+========+ |  37 |
| | First |   Last | |     |
| +=======+========+ |     |
| | Junil | (null) | |     |
| +=======+========+ |     |
|                    |     |
+====================+=====+


최근 MonoDevelop 개발툴의 한글화를 좀 더 고도화(?)하여 Pull Request 를 보냈다. 하루가 지나고 바로 approve 되어 차기 릴리즈 버전에 바로 적용이 가능하리라 생각한다. 또한, Xamarin Studio 에도 더 부드러운 한글화를 만나볼 수 있게 되었다.

필자가 개별적으로 배포하는 곳은 monodevelop.co.kr 에서 받아볼 수 있다.

1차 번역은 오로지 한글화에 목표를 두었다면, 2차 번역은 잘못된 번역과 좀 더 부드러운 번역에 중점을 두었다. 그리고 버전업이 되면서 기존 영문 메시지가 많이 변경이 되었는데, 이 또한 적절하게 수정되었다.

번역 품질에도 조그마한 변화를 느낄 수 있길 바라는데, 가령 "View" 를 번역한다면, 뭐라고 번역해야 할까? "뷰", "보기" 등으로 번역할 수 있는데, 이 "View" 가 어디에 쓰일지에 따라 번역 단어도 바뀌게 된다. 개발툴 안에서 쓰이는 단어라면 "보기"로 번역되는 게 맞을 것이다. 그런데, ASP.NET MVC 에 쓰인다면 "뷰"라고 번역되어야 하는데, 이런 번역들도 적절하게 수정이 되었다.

애매하게 번역되는 단어들이 이 뿐만이 아니다. "Convert", "Change", "Replace". 모두 뭔가로 변경되된다는 의미인데, 이는 각각 일관되도록 "변환", "변경", "바꾸기" 로 번역이 되었다.

현재까지 총 5765개 문장/단어 중 4886개 문장/단어가 번역이 완료되어 84% 번역률을 보인다. 남은 번역은 879개로 조만간에 번역이 완료되었으면 좋겠다.

얼마 전에 페이스북에 올라온 질문인데, “MIPS 어셈블리 코드 중 beq 를 안쓰고 bne 를 쓰는 이유는 무엇인가요?” 라는 질문이다.

아래와 같은 C 언어 코드가 있고,

if (i == j) f = g + h; else f = g - h;

이를 순서도로 나타내면 다음과 같다.

컴파일러와 최적화 옵션에 따라 생성되는 어셈블리 인스트럭션이 아래와 똑같다고 말할 수 없다.

일반적으로 컴파일러는 인스트럭션을 생성할 때 조건문의 경우 반대 조건으로 해석해서 인스트럭션을 작성한다. 이런 경우 대부분 인스트럭션의 크기가 짧아지기 때문에 더 적은 클럭에서 명령을 수행할 수 있기 때문이다. 따라서 아래와 같은 어셈블리 코드가 생성된다.

# bne 를 사용하는 코드  
bne $s3, $s4, Else  
add $s0, $s1, $s2  
j Exit  
Else:  
sub $s0, $s1, $s2  
Exit:

다음은 beq 를 사용하는 코드이다.

# beq 를 사용하는 코드  
beq $s3, $s4, Then  
sub $s0, $s1, $s2  
j Exit  
Then:  
add $s0, $s1, $s2  
Exit:

어?? 이상하다. 책의 내용을 보면 분명 ‘더 효율적이다.’ 라고 언급하는데, 무엇이 더 효율적인지 우열을 가리기 힘들다. 맞다. 단항 비교 연산인 경우 비교적으로 bnebeq 의 코드 길이에 대한 효율성 차이는 그리 크지 않다.

그렇다면, bne 가 더 효율적이려면 다음의 코드로 생각을 해보자.

if (i==j && i==k) f = g + h; else f = g - h;

바로 각각 beq 코드와 bne 코드로 구현해 보자.

# beq 를 사용하는 코드  
beq $s3, $s4, And  
j Else  
And:  
beq $s3, $s5, Then:  
j Else  
Then:  
add $s0, $s1, $s2  
j Exit  
Else:  
sub $s0, $s1, $s2  
Exit:

다음 코드는 bne 를 사용하는 코드이다.

bne $s3, $s4, Else  
bne $s3, $s5, Else  
add $s0, $s1, $s2  
j Exit  
Else:  
sub $s0, $s1, $s2  
Exit:

위 두 코드의 결과를 보면 확실하게 bne 코드의 결과가 적은 인스트럭션을 생성하는 것을 볼 수 있다. AT&T, x86, ARM 코어 등을 막론하고 대부분 조건 비교는 (!contidion) 형태를 띄고 있다.

참고로 더 자세한 내용을 알고 싶다면 필자가 추천하는 뇌를 자극하는 프로그래밍 원리 : CPU부터 OS까지 책을 참고하길 바란다. 참 좋은 책이다 ^^

+ Recent posts