'UMC BLOG'에 해당되는 글 475건

  1. 2020.05.05 [mono] Mono 웹 서버와 OWIN 웹 서버의 크래시 이슈 패치
  2. 2019.10.16 [svn] Mac Catalina 업그레이드 후 SVN 경로 문제
  3. 2019.10.16 크로스 플랫폼에서 데이터 무결성
  4. 2019.10.14 [mono] mono-service 버그 패치
  5. 2018.11.08 [ASP.NET Core] 최신 버전에서 React+Redux+Javascript+TypeScript 를 사용하는 방법
  6. 2018.08.28 AOP 프레임워크 이해와 개발
  7. 2018.08.27 [마이크로서비스] 아키텍처 구축하기
  8. 2017.04.05 [VSGesture] VSGesture for Visual Studio 2017 배포 완료 (1)
  9. 2017.01.23 Language Server Protocol, OmniSharp-Roslyn 빌드 오류 해결
  10. 2017.01.17 TypeScript 언어 핵심 요약 사항
  11. 2016.11.16 [ASP.NET Core] Middleware, IHttpModule과 IHttpHandler 마이그레이션
  12. 2016.11.15 [ASP.NET Core] IControllerFactory 설정 및 마이그레이션
  13. 2016.08.09 [ASP.NET] Session state has created a session id, but cannot save it because the response was already flushed
  14. 2016.05.13 [onhashchange] hash 를 감지하는 onhashchange.js Polyfill 라이브러리
  15. 2016.04.05 [MyRedis] Redis 에서 데이터베이스 연결 지원
  16. 2016.04.05 [RedisPlus] Redis 에서 키 만료 이벤트 개선 작업
  17. 2016.02.28 ASP.NET WebForm 에서 Dependency Injection 사용하는 방법
  18. 2016.02.20 데이터를 표로 표현해주는 Flip-Tables-Net
  19. 2016.01.28 [MonoDevelop] 두 번째 한글화 버전 승인완료
  20. 2015.05.12 [MIPS] beq 를 안쓰고 bne 를 쓰는 이유는 무엇인가요?
  21. 2015.04.23 [Swift] Xcode 6.3, Swift 1.2 업그레이드 시 언어 사양이 변경된 부분 정리
  22. 2015.04.16 [MonoDevelop Korean] 최신 빌드 오류 정보 (1)
  23. 2015.03.24 [마감][베타 다운로드] 중간 번역 (Inter-Translate) 베타 버전 다운로드 공개
  24. 2015.02.25 [MonoDevelop] v5.7.2.2 한글 버전 배포 공지
  25. 2015.01.04 [Mac App][일시무료] Inter-Translate (중간 번역 앱)
  26. 2014.11.16 [Github/Javascript-OOP-AOP-IoC] 자바스크립트 객체지향 프로그래밍 (github)
  27. 2014.09.28 [보안] Bash 원격코드실행 취약성 패치하기 (5)
  28. 2014.09.17 [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
  29. 2014.08.17 [Handlebars] Handlebars.java 버전의 with helper 버그 패치 커밋
  30. 2014.08.06 [Java] Java 8 Interface, default 메서드의 고찰 (7)

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 릴리즈 버전에서는 이 문제가 해결되니 기다리면 될 것 같다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

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
Posted by 땡초 POWERUMC
TAG Catalina, Mac, 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 를 다운로드 받는다고 생각하면 정말 끔찍한 일이다.

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

Posted by 땡초 POWERUMC

댓글을 달아 주세요

[mono] mono-service 버그 패치

Mono 2019. 10. 14. 08:57 |

개요

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 를 유지하는 것이 좋을 것 같다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

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


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

Posted by 땡초 POWERUMC

댓글을 달아 주세요


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 를 구현해 봄으로서 언어적 특성을 더 잘 알 수 있고, 언어가 제공하는 플랫폼을 이해하는 데 큰 도움이 될 것이다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

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

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

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

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

[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 의 측정 항목을 가시화하기 위한 도구이다.



Posted by 땡초 POWERUMC

댓글을 달아 주세요

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






Posted by 땡초 POWERUMC

댓글을 달아 주세요

  1. 원반 2017.05.08 01:11 Address Modify/Delete Reply

    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/


Posted by 땡초 POWERUMC
TAG c#, OmniSharo

댓글을 달아 주세요

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


Posted by 땡초 POWERUMC

댓글을 달아 주세요

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

Posted by 땡초 POWERUMC

댓글을 달아 주세요

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. ===========================");
    }
}


Posted by 땡초 POWERUMC

댓글을 달아 주세요

오류 유형은 아래의 웹서버 로그와 같이 “세션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 속성의 객체를 한 번 호출해 주는 것으로 이런 현상을 제거할 수 있다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

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>


Posted by 땡초 POWERUMC

댓글을 달아 주세요



[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
Posted by 땡초 POWERUMC

댓글을 달아 주세요

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
Posted by 땡초 POWERUMC

댓글을 달아 주세요

개요

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("", "");
        }
    }
}


Posted by 땡초 POWERUMC

댓글을 달아 주세요

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) | |     |
| +=======+========+ |     |
|                    |     |
+====================+=====+


Posted by 땡초 POWERUMC

댓글을 달아 주세요

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

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

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

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

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

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

Posted by 땡초 POWERUMC

댓글을 달아 주세요

얼마 전에 페이스북에 올라온 질문인데, “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까지 책을 참고하길 바란다. 참 좋은 책이다 ^^

Posted by 땡초 POWERUMC

댓글을 달아 주세요

얼마 전 Swift 1.2 버전으로 업그레이드를 실시했다. 뭐, 자의에 의해서 한 것은 아니고, Xcode 6.3.1 버전으로 업데이트 하니 자연스럽게 Swift 언어 버전도 업데이트 된 것!

다음은 Swift 1.2 로 업데이트하면서 나에게 발생한 오류를 정리해 본다.


1. NSStringString 타입 캐스팅(형변환)은 명시적으로 하도록 변경

이렇게 동작하던 코드는 더 이상 암시적으로 캐스팅을 하면 안된다.

func print(string: String) {  
       // print  
}

let str : NSString = “POWERUMC”  
print(str)

Swift 1.2 부터는 위의 코드는 다음과 같이 명시적으로 캐스팅을 해야 한다.

func print(string: String) {  
       // print  
}

let str : NSString = “POWERUMC”  
print(str as String)

2. 다운케스팅은 as! 키워드를 사용하도록 변경

모든 상속 관계의 타입 캐스팅(형변환)은 as 키워드를 사용했지만 이제 다운케이팅의 경우는 as! 를 사용하도록 변경되었다.

request.mutableCopy() 메서드의 반환 타입이 AnyObject인데 이를 NSMutableURLRequestas 키워드로 캐스팅을 하면 되었지만,

var newRequest = request.mutableCopy() as NSMutableURLRequest

Swift 1.2 부터는 아래와 같이 as! 를 사용해서 다운캐스팅을 해야 한다.

var newRequest = request.mutableCopy() as! NSMutableURLRequest

3. init() 를 재정의 하지 말고, super.init() 호출도 하지 않도록 디자인 변경

객체지향 프로그래밍을 하다보면 으레 형식적으로 기본 생성자를 만들기 마련이다. (대체적으로 컴파일러가 알아서 해 주는 경우가 많다)

다음과 같이 더 이상 super.init() 으로 초기화 하면 오류가 발생한다.

override init() {  
    super.init()  
}
Swift 1.2 부터는 super 클래스에서 무조건 초기화가 되도록 변경되어 초기화 코드를 넣지 말아야 한다.
init() {  
    // super.init() <-- 절대 기본 생성자에서 이 코드를 쓰지 말자.  
}
또, Swift 1.2 부터 기본 생성자는 재정의 하지 않도록 한다.
override init() {   // <--- init() 을 재정의 하지 말자.  
}

이전에 init() 생성자에서 초기화 코드를 넣거나 로직을 넣은 경우 골치가 아파진다. 따라서, 이 안에 있던 코드는 다른 곳으로 빼거나 override func init() 선언을 init() 으로 변경하여 재정의나 오버라이드 되지 않게 해야 한다.

4. Optional Type 은 반드시 명시적으로 사용하도록 변경

이전에는 Optional Type 을 사용할 경우 일부분 컴파일러가 알아서 처리(?)를 해 주었기 때문에 ! 또는 ? 을 사용하지 않아도 되었지만, 이런 경우 런타임에 오류가 발생하는 경우가 생길 수 있다.

Swift 1.2 부터는 Optional Type 은 명시적으로 ! 또는 ? 을 사용해야 한다.

class POWERUMC {  
    init?() {  
    }
}

class SomeClass {  
    func makeString() -> POWERUMC {  
        return POWERUMC()!   // <-- ! 또는 ? 를 붙여주자. 다른 방법으로는 반환 타입을 정확히 명시하면 됨.  
    }
}

5. 제거된 일부 프로퍼티

아래의 코드는 일부 사라진 속성을 다른 방법으로 코드를 수정하도록 권장하는 오류가 발생한다. 이 이외에도 친절하게 오류로 설명해 주니 크게 어렵지는 않을 것이다.

// 아래의 코드의 utf16count 속성은 없어졌다. (Deprecated 아님, 제거됨!)  
request.URLString.utf16Count

// Swift 1.2 부터는 아래와 같이 사용하자.  
count(request.URLString.utf16)


Posted by 땡초 POWERUMC

댓글을 달아 주세요

Xamarin사에서 MonoDevelop 오픈 소스 재단을 인수하면서 너무 돈을 밝히는 게 아닌가 싶을 정도로 상업적인 기업으로 변했다. M$(Microsoft)와 긴밀하게 관계가 유지되면서 돈 버는 방법도 너무 M$와 비슷해져 비호감 기업 반열에 충분히 올라갈 것으로 기대한다.

그건 그렇고 필자가 진행하고 있는 MonoDevelop Korean 빌드에 오류가 발생했다. 물론 Xamarin사에 인수되기 전부터 한글화 작업을 진행했던 터라 이번 빌드 문제가 조금 언짢아 진다.


문제 원인: 누락된 Xamarin 라이브러리

./configure —profile=mac 명령행으로 빌드 구성을 한 후에

make 빌드를 수행하면 다음과 같이 이전에 보지 못했던 라이브러리 누락 오류가 발생한다.

$ make  
if test -d ".git"; then \  
        git submodule update --init --recursive || exit 1; \  
    fi  
Making all in external  
make[2]: *** No rule to make target `/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/lib/i386/full/Xamarin.Mac.dll', needed by `Xamarin.Mac.dll'.  Stop.  
make[1]: *** [all-recursive] Error 1  
make: *** [all-recursive] Error 1

해결 방법

먼저 Xamarin Studio 다운로드 페이지에서 최신 버전을 다운로드 받으면, 그 안에서 누락된 라이브러리 파일들을 찾을 수 있다. 누락된 라이브러리 파일은 아래와 같이 3개.

  • Xamarin.Mac.dll
  • Xamarin.Mac.dll.mdb
  • libxammac.dylib

아래와 같이 순서대로 진행하면 된다.

1. Xamarin.Mac.Framework 폴더 생성

$ mkdir -p /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/lib/i386/full

2. 프레임워크 폴더에 복사

Xamarin.Mac.dll과 Xamarin.Mac.dll.mdb 파일을 프레임워크 폴더에 복사한다.

$ find "/Applications/Xamarin Studio.app/Contents/Resources/lib/monodevelop/AddIns/Xamarin.Sketches/Xamarin.Interactive.Agents.Mac/" -name 'Xamarin.Mac.dll*' -exec cp -f {} "/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/lib/i386/full/" \;

libxammac.dylib 파일을 프레임워크 폴더에 복사한다.

$ find "/Applications/Xamarin Studio.app/Contents/Resources/lib/monodevelop/AddIns/Xamarin.Sketches/Xamarin.Interactive.Agents.Mac/" -name 'libxammac.dylib' -exec cp -f {} "/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/lib/" \;


Posted by 땡초 POWERUMC

댓글을 달아 주세요

  1. 컴포지트 2015.04.16 09:20 Address Modify/Delete Reply

    저도 자마린이 너무 상업적으로 나가니 상당히 비호감이라고 생각하긴 했습니다.
    그래서 그런지 로슬린이 무척이나 기대할 수 밖에요.

[마감] 중간 번역 (Inter-Translate) 베타 버전 다운로드 공개

중간 번역(Inter-Translate) 앱이 탄생하게 된 계기는 다음과 같습니다.

구글에서 제공하는 번역기의 공개 API는 번역 문장의 길이에 제한이 있어 한글 문장 약 1,500자가 넘으면 번역이 되지 않고, 번역 횟수에도 제한이 있습니다. 이 두 가지의 문제를 해결하기 위해 우회적인 방법으로 제한 없이 매우 긴 문장도 번역할 수 있는 앱입니다.

사용자의 피드백을 받아 기능을 더 개선하기 위해 제 블로그에서 베타 빌드 버전을 공개합니다. iOS 버전도 곧 테스트를 위해 공유할 예정입니다.

익숙한 Objective-C 를 뒤로 하고, 공부도 해 볼겸 Swift with Objective-C 두 가지 언어로 개발이 되었습니다.

개선을 위한 피드백은 제 개인 이메일로 보내주시면 됩니다. powerumc at gmail.com

앱스토어 다운로드


(유튜브 Full 영상)




영문을 한글로 번역하는 것이 힘드시죠?
번역 품질을 더 매끄럽게 만들기 위해 중간 번역(Inter Translation) 앱을 이용해 보세요.

구글은 일본어에 대한 번역 데이터베이스가 더 풍부한 점을 이용하여, 번역할 문장을 일본어로 번역한 후 이를 다시 원하는 언어로 번역합니다.

일반적으로 한글을 직접 번역하는 것 보다 훨씬 품질이 좋습니다.

즉시 번역 예)
Hello -> 안녕하세요.

중간 번역 예)
Hello -> こんにちは -> 안녕하세요.

특징
1. 즉시 번역과 중간 번역을 선택적으로 사용
2. 입력 글자수에 제한이 없음
3. 즉시 번역과 중간 번역 차이점 비교 기능


스크린샷




Posted by 땡초 POWERUMC

댓글을 달아 주세요

- MonoDevelop v5.7.2.2 한글 버전 빌드 업데이트

2013년부터 꾸준히 진행해 오던 MonoDevelop v5.7.2.2 한글 버전의 새로운 빌드를 업데이트 했습니다.

MonoDevelop v5.7.2.2 한글 버전은 다음의 링크에서 다운로드 받을 수 있습니다.


- 왜 MonoDevelop을 써야하나?

외국 Xamarin 기업에서 Mono 오픈소스 재단을 인수하면서 Mono 가 폭풍성장을 하고 있습니다.

이제 따른 부작용이 Xamarin이 Mono를 통해 수익을 얻으려 하는 것이죠. 물론 돈은 벌어야 하니까요. 따라서 Xamarin Studio 를 사용하고 Xamarin.*.dll 라이브러리에도 GPL 라이선스 제한을 받게 됩니다.

이에 반해 Mono와 MonoDevelop은 MIT/X11 & LGPL2 라이선스이므로 적절한 전략을 통해 크로스 플랫폼 데스크탑/모바일 개발이 가능할 수 있습니다. Unity 게임 개발에도 MonoDevelop 이죠. ^^

더 궁금하신 점이 있으시면 언제든지 이메일 주시기 바랍니다. (댓글이나 방명록 잘 확인을 안해요;;)

Posted by 땡초 POWERUMC

댓글을 달아 주세요

Inter Translate (중간 번역 앱) - 현재 심사중

영문을 한글로 번역하는 것이 힘드시죠?
번역 품질을 더 매끄럽게 만들기 위해 Inter Translate (중간 번역) 앱을 이용해 보세요.

구글은 일본어에 대한 번역 데이터베이스가 더 풍부한 점을 이용하여, 번역할 문장을 일본어로 번역한 후 이를 다시 원하는 언어로 번역합니다.

일반적으로 한글을 직접 번역하는 것 보다 훨씬 품질이 좋습니다.

  • 즉시 번역 예) 영어 -> 한국어로 번역
  • 중간 번역 예) 영어 -> 일본어 -> 한국어로 번역

특징

  1. 즉시 번역과 중간 번역을 선택적으로 사용
  2. 입력 글자수에 제한이 없음
  3. 즉시 번역과 중간 번역 품질을 비교
  4. 귀여운 고양이 배경 화면



Posted by 땡초 POWERUMC

댓글을 달아 주세요

Javascript-OOP-AOP-IoC / 자바스크립트 객체지향 프로그래밍

자바스크립트 객체지향 프로그램을 쉽게 하기 위한 소스 코드를 github 에 공개(https://github.com/powerumc/Javascript-OOP-AOP-IoC)했다.

자바스크립트로 객체지향 프로그래밍을 잘 하려면 배워야 하는 것들이 참 많다. 함수형 프로그래밍과 자바스크립트의 prototype 기반의 chain, 함수를 인스턴스로 사용하고, 객체지향적인 몇 가지 자바스크립트 패턴을 익혀야 하는 데, 쉽지만은 않을 것이다. C

가장 간단한 객체지향 코드를 보자. 이 코드는 Program 클래스를 상속 받은 Outlook 클래스가 있고, run() 메서드로 실행하는 코드다.

function INHERITANCE(PARENT, CLASS) {
	for(var p in PARENT) if(PARENT.hasOwnProperty(p)) CLASS[p] = PARENT[p];

	var PROXY 					= function() { };
	PROXY.prototype 			= PARENT.prototype;
	CLASS.prototype				= new PROXY();
	CLASS.prototype.constructor = CLASS;
}

var Program = (function() {
	Program.prototype.version = "1.0.0";

	return Program;
});

var Outlook = (function() {
	INHERITANCE(Program, Outlook);
	function Outlook() {
		Program.apply(this, arguments);
	}

	Outlook.prototype.run = function() { console.log("[" + this.version + "] Running... "); }
	
	return Outlook;

})(Program);

var outlook = new Outlook();
outlook.run();

너무 성급하게 이해하지 않아도 된다.

조금이라도 쉽게 Javascript 객체지향을 하기 위해 만든 라이브러리를 공개했으니...

설치

  • npm
npm install javascript-oop-aop
  • bower
bower install javascript-oop-aop-ioc

1. 기초

이 라이브러리를 이용하면 매우 쉽게 클래스를 선언할 수 있습니다. oop.class(...) 를 사용합니다.

oop.class( [parents,] classInfo )

  • 클래스 선언
var Program = oop.class({
	say: function() { return "Hello"; }
});

var p = new Program();
p.say();

// return "Hello"
프로퍼티 선언
  • 기본적인 프로퍼티 선언
// Define class.
var Program = oop.class({
	say: function() { return "Hello"; },
	name: "엄준일"
});

var p = new Program();
console.log("My name is ", p.name);

// output
My name is 엄준일
  • 사용자 정의 get/set 프로퍼티 선언
var Program = oop.class({
	say: function() { return "Hello"; },
	name: "엄준일",
	age: { get: function()      { return this._age; },
		   set: function(value) { this._age = value; }
});

var p = new Program();
p.age = 35;
console.log("My age is ", p.age);

// output
My age is 35

2. 상속

oop.class( parents, classInfo )

  • 부모 클래스 상속하기
// Define parent class
var Program = oop.class({
	version: "1.0.2",
	show: function() { 
		console.log("openning window."); 
		/* some code.. */
	}
});

// Define class.
var Outlook = oop.class( Program, {
	run: function() { console.log("running outlook program."); }
});

// Run code.
var outlook = new Outlook();
console.log("version " + outlook.version);
outlook.run();
outlook.show();

// Output
version 1.0.2
running outlook program.
openning window.
  • 자기 자신 참고 (this or self)
var Program = oop.class({
	version: "1.0.2",
	show: function() { 
		console.log("openning window.");
		/* some code.. */ }
});

var Outlook = oop.class( Program, {
	run: function(self) { // inject 'self' argument name.
		console.log("running outlook program.");

		// *** HERE ***
		// a method call inhertianced Program.show method.
		self.show();
	}
});

var outlook = new Outlook();
console.log("version " + outlook.version);
outlook.run();
//outlook.show();      remove this line.

// Output
version 1.0.2
running outlook program.
openning window.
부모 인스턴스 참조

var Program = oop.class({
	run: function() { console.log("run Program.") }
});

var Outlook = oop.class( Program, { // HERE inheritance Program class.
	run: function(base) \ 
		console.log("run Outlook.");  

		// *** HERE ***
		// You can call parent method from base keyword.
		base.run();
	}
});

// Output
// run Outlook.
// run Program.

3. 인젝션 (주입)

oop.inject( [argument], ... )

  • 매개변수 주입
var Program = oop.class({
	version: "v1.0"
});

var Outlook = oop.class( Program, {
	version: "v2.0",
	run: function(base, self) { 
		console.log("base version: "   , base.version)
		console.log("current version: ", self.version);
	}
});

var outlook = new Outlook();
outlook.run();

// Output
base version: v1.0
current version: v2.0
  • 컨테이너로부터 주입

4. 가로채기 - AOP

oop.interception( function, behavior )

oop.interceptionBehavior( before, after, exception, finally_ )

  • 클래스 또는 메서드 가로채기

    • 메서드 가로채기
var Program = oop.class({
	run: function(msg) { console.log("run Program. ", msg); }
});

// *** HERE ***
// Setup the interception a method
var p = new Program();
oop.interception( p.run, oop.behaviors.LoggingBehavior );

// Call a 'run' method.
p.run("Now running...");

// Output
------ enter interception ------
[Thu Nov 13 2014 09:29:41 GMT+0900 (KST)]  {}
run Program.  Now running...
------ end interception ------
  • 클래스 인스턴스 가로채기
var Program = oop.class({
	run: function()       { console.log("run Program.", msg); },
	terminate: function() { console.log("Terminated the Program.") }
});

// *** HERE ***
// Pass class instance arguments
var p = new Program();
oop.interception( p, oop.behaviors.LoggingBehavior );

// Call a 'run' method.
p.run("Now running...");
p.terminate();

// Output
------ enter interception ------
[Thu Nov 13 2014 09:29:41 GMT+0900 (KST)]  {}
run Program.  Now running...
Terminated the Program.
------ end interception ------
  • 사용자 정의 가로채기 (Behaviors)

    • 사용자 정의 가로채기 정의

가로채기 행위를 사용자 정의로 만드시려면 oop.interceptionBehavior 메서드를 호출합니다.

var customBehavior = oop.interceptionBehavior(
	function() { console.log("before"); },
	function() { console.log("after"); },
	function() { console.log("throw exception"); },
	function() { console.log("finally"); }
);

var Program = oop.class({
	run: function() { console.log("run Program."); }
});

var p = new Program();
oop.interception(p,  customBehavior);
p.run();

// Output
before
run Program.
after
finally

코드가 실행 중 예외가 발생할 경우 다음 처럼 exception 함수가 실행됩니다.

var Program = oop.class({
	run: function() { 
		console.log("run Program."); 
		throw "crashing... "; 
}});

var p = new Program();
oop.interception(p,  customBehavior);
p.run();

// Output
before
run Program.
throw exception crashing...   // HERE exception behavior.
finally


Posted by 땡초 POWERUMC

댓글을 달아 주세요


오늘 팀 동료를 통해 bash 취약점이 있다는 내용을 들었고, cnet.com 링크를 통해 확인할 수 있다.

bash 보안 취약성 패치하기

제가 배치한 버전은 bash 4.3.25 (이 버전의 패치 코드 25개) 이며, 이 버그는 원격 코드 실행 버그로 매우 위협적인 버그이다.

맥에서는 당시(2014–09–26) MacPort, Homebrew 에서 패치 버전을 제공하지 않는 관계로, 직접 소스 코드를 컴파일 하는 방법으로 해결하였다.

최신 bash 버전으로 패치하고 컴파일 하는 스크립트 코드를 필자의 github 에 커밋하였다.
- https://github.com/powerumc/Patch-Bash-Vulnerability

맥/리눅스 에서 아래의 명령을 실행하면 바로 패치 하도록 했다.

curl https://raw.githubusercontent.com/powerumc/Patch-Bash-Vulnerability/master/patch-bash-4.3.25.sh | sh

참고 (버그 내용)

bash 원격 코드 실행 취약성

쉘에서 아래의 명령을 실행하여 echo 메시지가 확인되면 원격 실행 코드 버그가 있는 버전입니다.

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

코드 실행 결과 (원격 코드 실행 가능한 취약성)

vulnerable
echo this is a test

패치 후 원격 코드 실행 불가능

패치가 완료되면 원격가 실행되지 않는다.

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

코드 실행 결과

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for \`x'
this is a test


Posted by 땡초 POWERUMC

댓글을 달아 주세요

  1. 이반린 2014.09.29 10:59 Address Modify/Delete Reply

    안녕하세요. 서버관리자 입니다. 제가 관리하는 서버가 BASH 취약점이 있는데요.
    외부적으로 포트를 열수가 없어서 수동으로 bash 쉘을 패치해야 하는데
    어떻게 해야 하는지 알려주시면 감사하겠습니다.
    부탁드립니다.

  2. 123124124 2014.09.29 14:59 Address Modify/Delete Reply

    RedHat, CentOS 일 경우
    rpm -Uvh bash............. 하시면 되요

  3. 최혁선 2014.09.29 15:42 Address Modify/Delete Reply

    안녕하세요. 영문페이지로가서 그대로 설치하였더니.....
    $ env x='() { :;}; echo vulnerable' bash -c "echo this is a test" 명령어를 입력하면 결과에
    sh: error importing function definition for `BASH_FUNC_module()'
    위에 오류가 납니다.. 모듈이 Import되어 있지 않다고 나오는것 같은데. 혹시 이런 증상 해결 방법이 있을까요? 쉘스크립트가 정상적으로 실행이 되는지 테스트 해봐야겠네요..

개요

  • 윈도우  <-> 맥 <-> 리눅스 등 멀티 컴퓨터에서 마우스와 키보드를 공유할 수 있는 소프트웨어로 편하게 개발하기
  • Pair Programming 시에 키보드의 마우스를 주거니 받거니 할 필요 없이 각각 키보드와 마우스를 이용하여 개발하기

 

설치하기

1. 공식 홈페이지

http://synergy-project.org/

 

2. 다운로드 링크

중요 : 현재 다운로드가 공식 홈페이시를 통해 불가능해 졌습니다. 따라서 아래의 링크를 통해 다운로드를 하시기 바랍니다. (저희 팀의 브리츠님께서 알려주신 정보입니다)

Synergy Nightly Buildhttps://synergy-project.org/nightly/

 

Synergy 가 판매 및 기부 방식으로 변경되면서, 아래의 링크에서 다운로드 받을 수 없습니다.

 

Stable version (1.5.1 - Aug 21, 2014) | Beta version (1.6.0 - Aug 26, 2014)

ChangeLog

 윈도우

1.5.1: 64-bit32-bit

  • 위 버젼이 유료로 바뀐 듯 합니다. 아래는 1.5.0 윈도우 버젼 입니다.

http://www.techspot.com/downloads/downloadnow/4978/?evp=8b86258a901d3a0aa2536050c205a7e6&file=2

 Mac OS X

1.5.1: 10.910.810.710.610.5

  Debian & Ubuntu Linux

1.5.1: 64-bit32-bit

  Red Hat & Fedora Linux

1.5.1: 64-bit32-bit

 소스 코드

1.5.1: tar.gz (guide)

다른 다운로드

 

 

구성하기

 

1. 서버 컴퓨터 구성 (OSX 기준)

1.1 시너지를 실행하고, 'Configure Server...' 버튼을 클릭하여, 서버 구성을 시작한다.


 

1.2. 가상 모니터 설정


 

1.3. 필요한 경우 암호화 통신을 구성한다.


 

1.4. 하단의 'Start' 버튼을 클릭하여 서버 구동 시작

(스크린샷 없음)

 

 

2. 클라이언트 구성 (리눅스 기준) ( 캡쳐는 OSX 에서 함)

2.1. 시너지를 실행하고 중간의 'Client (use another computer's keyboard and mouse)' 컨트롤을 체크하고, '1. 서버 컴퓨터 구성' 의 Server IP 를 입력한다.


 

2.2. 시너지의 구성(Preferences) 또는 설정(Settings) 메뉴를 선택하여 클라이언트 설정을 마무리 한다.


 

연결 확인

아래의 이미지와 같이 가상 모니터(POWERUMC) 는 내 컴퓨터의 왼쪽에 위치해 있으므로, 마우스를 화면 왼쪽으로 이동해 본다.

화면 왼쪽을 넘어서면 시너지의 클라이언트로 연결된 컴퓨터에서 마우스와 키보드가 입력된다.

(단, OSX 경우 Secure Input 모드로 진입하게 될 경우 시너지가 올바르게 동작하지 않음)


 



'O/S' 카테고리의 다른 글

[Synergy] 여러 컴퓨터에서 키보드, 마우스 공유  (0) 2014.09.17
Posted by 땡초 POWERUMC

댓글을 달아 주세요

회사에서 Handlebars.java 와 관련된 이슈가 공유가 되었다.

Handlebars 가 Javascript 버전과 Java 버전의 #with helper 결과가 동일하지 않습니다.

우선 이 이슈 버그를 해결한 코드는 필자의 github 저장소 https://github.com/powerumc/handlebars.java.bug-fix 에 커밋 되어 있고, 원본 저장소의 이슈 번호 #314, Pull Request #315 에 등록 되었다.

Handlebars vs Handlebars.java

이 테스트에서 사용되는 handlebars 데이터는 다음과 같습니다.

{ "company": { "ko": "쿠팡",
               "en": "Coupang" }}

그리고 handlebars 템플릿은 다음과 같다.

{{#with company}}
  우리 회사는 {{ko}}
  My company is {{company.en}}
{{/with}}
- Javascript handlebars 결과
  우리 회사는 쿠팡
  My company is 
- Handlebars.java 결과
  우리 회사는 쿠팡
  My company is Coupang

문제 원인

Handlebars.java 를 반나절 정도 분석하고 나니 대충 (정말 대충) 어떻게 흘러가는 지 조금 이해가 되었다.

문제는 Handlebars.java 에서는 #with helper 에서 value 값을 resolving 하지 못하면 parent object (=context) 에서 찾는다. #with helper 에게 전달된 데이터를 model 이라고 하면 전달된 데이터 전체를 context 로 불린다. 그래서 model 의 parent 는 context 가 된다.

위의 상황을 보면 My company is {{company.en}} 와 같은 코드의 model 에서 {#with company}}company.en을 찾지 못해서 context 에서 company.en을 찾게 된다.

문제 해결 방법

문제의 원인을 파악했으니 코드를 디버깅해 알겠지만 튜닝 포인트가 매우 다양하다. #with helper 전체를 뜯어 고칠 수 도 있고, 내부적으로 CompositeValueResolver 를 튜닝하거나, 그 외에 다양한 방법으로 고칠 수 있다.

가장 간단하게 이 버그를 픽스하기 위해 또 반나절 정도를 적용해 보고, 가장 적은 코드로 튜닝할 수 있는 코드를 만들었다.

Options.java 코드에서 wrap(final Object) 메서드를 다음과 같이 픽스하였다. 딱 세 줄만 추가해 주면 된다.

public Context wrap(final Object model) {
  if (model == context) {
    return context;
  }
  if (model == context.model()) {
    return context;
  }
  if (model instanceof Context) {
    return (Context) model;
  }
  if (model.getClass() == LinkedHashMap.class) {  // 이 코드부터...
    return Context.newContext(model);
  }                                               // 여기까지 추가...
    return Context.newContext(context, model);
  }

그럼 아래의 이슈가 되는 테스트 코드가 무사히 통과하고,Handlebars.java 의 모든 테스트도 통과한다.

public class Issue314 extends AbstractTest {

  @Test
  public void withHelperSpec() throws IOException {
    String context = "{ obj: { context: { one: 1, two: 2 } } }";

    shouldCompileTo("{{#with obj}}{{context.one}}{{/with}}",     context, "1");
    shouldCompileTo("{{#with obj}}{{obj.context.one}}{{/with}}", context, "");
 }
}


Posted by 땡초 POWERUMC

댓글을 달아 주세요

얼마 전 자바8 람다 나머지 이야기를 보면서 평소 필자가 알던 Java와는 완전히 달라 보였다. 필자가 알고 있던 Java는 보수적이지만 정통적이라고 생각 해왔는데 과감히 이 생각을 깨졌다.



Java 8 Lambda 에 대해 궁금한 부분은 필자가 예전에 작성한 아티클을 참고하기 바란다.

Java 8 Interface 변경 사항 default 키워드

오라클의 Defining an Interface 문서에 의하면 Java Interface의 정의는 변경되었다. Java Interface는 abstract methods, default methods, static methods 를 정의할 수 있다고 한다.

The interface body can contain abstract methods, default methods, and static methods.

명세에 따르면 Java Interface에 default 키워드를 통해 메서드를 구현할 수 있다. 또, 이를 구현하는 클래스는 Interface의 메서드를 @Override 할 수 있다.

Oracle 문서에서 아래와 같은 예제를 볼 수 있다.

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }  
}

Java 8 Interface, 그 인터페이스는 그 인터페이스가 아니다.

원래 객체지향 언어에서 Interface는 그 시그너처와 선언이 변하지 않는다는 것을 전제로 하여 다형성(polymophism)을 정의하는 객체간의 규약이다. 그러나 Java8 Interface는 Interface를 업그레이드하는 개념을 도입하였다.

Now users of your code can choose to continue to use the old interface or to upgrade to the new interface.
Alternatively, you can define your new methods as default methods

객체지향 언어의 객체 규약을 정의함에 있어 항상 논의 되는 것이 Interface와 Abstract 클래스 둘 중 어떤 것을 쓸 것이가에 대한 것이다. 어떤 것을 사용해도 무방하겠지만 객체 간의 규약(서로간의 약속)은 Interface 로 정의해야 한다. 구현체가 없는 온전한 인터페이스 역할을 해야 하기 때문이다.

특히 분산 객체(distributed object)는 분산된 두 객체의 규약, 즉 Interface 의 프록시(proxy)를 통해 분산 객체를 사용한다. Java8 Interface는 구현체가 포함될 수 있으므로 더 이상 분산 객체를 정의하는 규약으로 부적합하다.

이는 반드시 분산 객체 뿐만 아니라, 일반적인 객체로서도 문제의 소지가 충분하다. 아래는 이런 현상을 약간 억지스럽게 구현한 코드이다.

이를 통해 알 수 있는 것은 Java 8 Interface 로 규약된 분산 객체에 구현 코드가 포함이 되므로 더 이상 ‘규약’이라는 표현 자체가 규약이 될 수 없을 것 같다.

interface Duck {
    void say();
    void walk();
}

interface MyDuck extends Duck { }

interface YourDuck extends Duck {
    default void walk() { say(); }
}

class MyDuckImpl implements MyDuck {
    @Override
    public void say() { System.out.println("MyDuck: 꽥~"); }

    @Override
    public void walk() { System.out.println("MyDuck: 뒤뚱~"); }
}

class YourDuckImpl implements YourDuck {
    @Override
    public void say() { System.out.println("YourDuck: 꽥꽥꽥~"); }
}

public class Main {
    public static void main(String[] args) {
        new MyDuckImpl().say();
        new YourDuckImpl().say();

        new MyDuckImpl().walk();
        new YourDuckImpl().walk();
    }
}

// 결과
MyDuck: 꽥~
YourDuck: 꽥꽥꽥~
MyDuck: 뒤뚱~
YourDuck: 꽥꽥꽥~

상대적인 취약해진 Java 8 Interface

일반적으로 실행 파일(executable file)이나 라이브러리(library) 파일은 코드와 데이터 등을 구조적으로 저장하고 링크(link) 과정을 거친 바이너리 파일이다. 반면 Java는 각 클래스 파일을 컴파일하면 .class 확장자를 가진 바이너리 파일을 출력한다.

일반적으로 코드와 데이터가 모두 포함된 바이너리 파일(일반적으로 portable executable)은 가리키는 offset 에 따라 RVA(relative virtual address)/VA(virtual address)와 매핑된다. 따라서 바이너리 파일을 비정상적으로 수정(삽입/삭제)하면 offset 정보도 함께 변경해 주어야 한다. 그렇지 않으면 메모리에 로드될 때 코드가 정상적으로 동작하지 않을 수 있다.

반면 Java 의 컴파일 된 바이너리는 클래스 별로 컴파일되어 .class 파일로 출력되기 때문에 이런 offset 정보가 필요가 없다. 컴파일된 코드가 논리적인 구조에 따라 물리적인 파일을 생성하지 않는다. 할 수 있는 건 Stack Size 정도 늘리거나 줄일 수 있다.

따라서 Java 8 Interface 의 .class 파일에 악의적으로 default 메서드 구현을 임의로 수정하면 간편하게 응용 프로그램 전체에 영향을 끼치도록 소정의 목적을 달성할 수 있다. (상대적으로 Java의 바이너리가 더 취약하다는 의미이다.)

다시 불거지는 다중 상속 문제

Java 8 Interface는 다시 다중 상속 문제에서 자유롭지 못하게 되었다.

Java는 단일 상속만 가능하지만 Interface는 다중으로 구현할 수 있다. 이는 C++의 다중 상속으로 인해 발생하는 문제점과 복잡성으로 Java는 단일 상속 구조를 택하게 되었다.

다중 상속으로 발생하는 모호성에 대해서도 Java 8 Interface 가 내놓은 해결책은 C++ 의 해결책과 다를 바가 없다.

아래는 Java 8 Interface 다중 상속(구현)으로 인한 모호성 문제 해결 방법이다.

public interface OperateCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

public class FlyingCar implements OperateCar, FlyCar {
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

아래는 C++ 의 다중 상속으로 인해 발생하는 모호성을 해결 하는 방법이다.

#include <iostream>  
using namespace std;  

class OperateCar {  
public:  
    int startEngine(EncryptedKey *key) {  
        // Implementation
    }  
};  

class FlyCar {  
public:  
    int startEngine(EncryptedKey *key) {  
        // Implementation
    }  
};  

class CCC : public OperateCar, public FlyCar {  
public:  
    int startEngine(EncryptedKey *key){  
        OperateCar::startEngine(key);  
        FlyCar::startEngine(key);
    }  
};  

위의 Java와 C++ 소스 코드를 비교해 보면 결국 Java 8 Interface는 다중 상속의 개념이 다시 도입된 것을 알 수 있다.

참 아이러니 하다.

결론

어느 것이 답이 될 수는 없다. 이전까지 지향하고 고수하던 객체지향이 반드시 정답은 아닐 것이다. 다만, Java가 추구하던 Interface의 개념이 그 동안 알고 있던 개념과 이치에 맞지 않고, 어쩔 도리 없이 구현한 스팩일 수 있다.

필자처럼 C# 5.0의 가장 최신 스팩까지 쭉 경험한 개발자라면 도저히 용납할 수 없을 지도 모르겠다.

까마득한 2007년도에 릴리즈한 C# 3.0 스팩의 Lambda, LINQ, Extension Methods 의 언어 스팩과 비교해보면 Java 8 는 너무나도 기대에 미치지 못한 방법으로 구현해 놓았다는 게 실망스럽다.

아직 Java 8 모든 것을 훓어 본 것이 아니므로 필자가 오해하고 있는 부분은 정정해 주길 바란다. (이념, 철학, 사상적인 부분은 사양한다)

참고로 이해를 돕기 위해 2007년도 C# 3.0 스팩과 2014년 Java 8 스팩을 비교하면 다음과 같다.

  • C# Lambda = Java Lambda
  • C# Extension Methods = Java Stream API
  • C# LINQ = Java 엔 없다!


Posted by 땡초 POWERUMC

댓글을 달아 주세요

  1. 경준씨 2015.06.30 10:16 Address Modify/Delete Reply

    메소드가 void startEngine면 아래과 같이 적어도 됩니다.
    public class FlyingCar implements OperateCar, FlyCar {
    public void startEngine(EncryptedKey key) {
    FlyCar.super.startEngine(key);
    OperateCar.super.startEngine(key);
    }
    }
    그러나 메소드가 int startEngine라면
    public class FlyingCar implements OperateCar, FlyCar {
    public int startEngine(int key) {
    FlyCar.super.startEngine(key);
    return OperateCar.super.startEngine(key);
    }
    }
    이런식으로 자바의 인터페이스는 무조건 Override를 해야합니다.
    이 Override를 상속할 때 return이 void값은 둘다 받을 수 있지만 부모의 return 값은 둘 중의 하나만 명시적으로 선택해야 return값으로 쓸 수 있습니다.

  2. brian 2016.09.09 17:18 Address Modify/Delete Reply

    감사합니다. 잘보고 갑니다.

  3. 이창현 2016.11.10 16:12 Address Modify/Delete Reply

    C#의 LINQ 를 Java 8 의 Stream 에 대응시킬 수 있지 않나요?

    • 땡초 POWERUMC 2016.11.11 09:32 신고 Address Modify/Delete

      LINQ 는 C# 언어와 통합된 쿼리식을 말합니다.
      가령 이런거죠.
      var query = from i in list
      select i;

      Java StreamAPI 와 대응되는 것은 C# 의 Where, Select 와 같은 확장 메서드라고 보시면 될 것 같습니다.

  4. 2017.04.05 19:56 Address Modify/Delete Reply

    비밀댓글입니다