태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

'2009/04'에 해당되는 글 169건

  1. 2009/04/25 블로그 이전 완료 by dalbong2 (1)
  2. 2009/04/25 Hundreds Island ! 들어들 봤나 by dalbong2
  3. 2009/04/25 여기는 바기오 ! by dalbong2
  4. 2009/04/25 어학연수 준비중 by dalbong2
  5. 2009/04/24 개발 프레임워크 만들기 대장정 33 - Spring.NET의 Result Mapping by dalbong2
  6. 2009/04/24 개발 프레임워크 만들기 대장정 32 - Spring.NET의 MVC 패턴 지원 by dalbong2
  7. 2009/04/24 개발 프레임워크 만들기 대장정 31 - Spring.NET의 데이터 액세스 III by dalbong2
  8. 2009/04/24 개발 프레임워크 만들기 대장정 30 - Spring.NET의 데이터 액세스 II by dalbong2
  9. 2009/04/24 개발 프레임워크 만들기 대장정 29 - Spring.NET의 데이터 액세스 I by dalbong2
  10. 2009/04/24 개발 프레임워크 만들기 대장정 28 - Spring.NET의 Web Services 지원 by dalbong2
  11. 2009/04/24 개발 프레임워크 만들기 대장정 27 - Spring.NET의 advice 종류와 적용 by dalbong2
  12. 2009/04/24 개발 프레임워크 만들기 대장정 26 - AOP 적용 예제 II by dalbong2
  13. 2009/04/24 개발 프레임워크 만들기 대장정 25 - AOP 적용 예제 I by dalbong2
  14. 2009/04/24 개발 프레임워크 만들기 대장정 24 - Aspect Oriented Programming 개념 II by dalbong2
  15. 2009/04/24 개발 프레임워크 만들기 대장정 23 - DI( Dependencies Injection ) 설정 by dalbong2
  16. 2009/04/24 개발 프레임워크 만들기 대장정 22 - 샘플 프로젝트 및 Spring 컨테이너 by dalbong2
  17. 2009/04/24 개발 프레임워크 만들기 대장정 21 - Aspect Oriented Programming 개념 I by dalbong2
  18. 2009/04/24 개발 프레임워크 만들기 대장정 20 - Spring.NET::IoC by dalbong2
  19. 2009/04/24 개발 프레임워크 만들기 대장정 19 - Spring.NET by dalbong2
  20. 2009/04/24 개발 프레임워크 만들기 대장정 18 - Unity 구조 정리 by dalbong2
  21. 2009/04/24 개발 프레임워크 만들기 대장정 17 - Unity 컨테이너 확장예제 II부 by dalbong2 (1)
  22. 2009/04/24 개발 프레임워크 만들기 대장정 16 - Unity 컨테이너 확장예제 I부 by dalbong2
  23. 2009/04/24 개발 프레임워크 만들기 대장정 15- ObjectBuilder 계속 by dalbong2
  24. 2009/04/24 개발 프레임워크 만들기 대장정 14- Unity 컨테이너 확장? ObjectBuilder ? by dalbong2
  25. 2009/04/24 개발 프레임워크 만들기 대장정 13- Method Call Injection by dalbong2
  26. 2009/04/24 개발 프레임워크 만들기 대장정 12- Property(setter) Injection by dalbong2
  27. 2009/04/24 개발 프레임워크 만들기 대장정 11- Contructor Injection by dalbong2
  28. 2009/04/24 개발 프레임워크 만들기 대장정 10- Dependency Injection 패턴 by dalbong2
  29. 2009/04/24 개발 프레임워크 만들기 대장정 09- Config를 사용한 등록작업 by dalbong2
  30. 2009/04/24 개발 프레임워크 만들기 대장정 08- 사용자 정보, 애플리케이션 컨텍스트 객체 추가하기 by dalbong2

블로그 이전 완료

Others 2009/04/25 19:30
블로그를 티스토리로 이전했다. 그러나 아직 잘못된 링크를 수정하는 작업등 몇 가지가 남아있다.
포스트는 겨우 수작업으로 이전을 했지만, 방명록, 댓글까지는 복구하지 못했다 -_-;;
흔적을 남겨주신 분들께 죄송한 마음이다.

5개월 정도 프로젝트에서 손을 놨더니 적응할 시간이 좀 필요한듯 하다.
몇개월 지나지도 않았는데, 왜 이렇게 새로운 기술이나 개념들이 많이 나왔는지...
IT 개발자로 살아남는 것이 어렵다는 것을 새삼 느끼게 된다.

이제 내 생에 가장 여유로왔던 시간으로 기억될 것 같은 순간이 지났다.
이제부터 새로운 마음으로 글쓰기를 다시 시작해볼까 한다.
마음 추스리고 시동을 거는 것이 좀 늦는 타입이라 언제 어떤 주제로 글을 다시 쓰게 될지는 모르겠다.



'Others' 카테고리의 다른 글

MIXSH인증용 새 페이지  (0) 2009/11/19
[메모] White Box testing tool for .NET  (0) 2009/10/27
블로그 이전 완료  (1) 2009/04/25
Hundreds Island ! 들어들 봤나  (0) 2009/04/25
여기는 바기오 !  (0) 2009/04/25
어학연수 준비중  (0) 2009/04/25
Posted by dalbong2

매일 작은 골방에 쳐박혀 영어만 쳐다보고 있으니 답답함이 한 가득이 되었다. 그렇다고 영어가 되는 것도 아니다. 아무 생각없이 머리를 비울때가 된 것 같아 검색하다 발견한 곳이 Hundreds Island 라는 섬이다. 섬이라기 보다는 100개의 섬으로 이뤄진 섬군(群)이다.

 

나는 정말 사람이 사는 섬들이 백개 모여있는 줄 알았다. 그러나 섬이라기 보다는 차라리 큰 바위였다.

 

이런 배를 타고 몇몇 섬들을 일주 시켜준다.

위에서 본 섬 모습이다. 무슨 거북떼들이 쉬고 있는 것 같기도 하고...

물은 정말 깨끗하다. 사진상으는 흐릿하게 나왔지만...

얼마나 깨끗한지 물만 한번 찍어봤다.

내가 타고 갔던 Helen이라는 배이다.

섬에는 특별히 먹을 것이 없다길래, 먹을 것을 사들고 들어갔는데 도저히 먹을 수 없는 것들이라 버렸다. 배가 고파서 땀을 삐질 흘리고 있는 모습이다.

 

여기가 어디냐면...Jolibee라는 필리핀 fast food점이다. 왜 찍었냐고? 잊을 수 없을 것 같아서...

그날 특별히 먹은 것도 없고 해서 뭔가로 허기를 채우려고 들어갔다. 위에 게시되어 있는 주문판을 보고 french fries하고 mango pie를 주문했다.  내 영어가 해독불가능하면 손가락으로 가리켜주면 될 것이라는 예상을 하고 말이다. "one large french fries, one mango pie" 하고 외쳤다. 근데 문제가 one mango pie에서 생겼다.

주문을 받는 알바남이 뭐라 다시 되뭇는다. 내가 손가락으로 가리키면서 저것을 달라고 해도 계속 뭔가를 되묻는다. "excuse me?"를 했다. 뭐라한다. 그러고 나서도 몇번을 excuse me?를 외쳤다. 나중에는 "pardon me?"로 바꿔서 외쳤다. 옆 사람들이 모인다. 결국에 내가 하는 말...

"Please, Don't ask me any more. give me anything! "

뒤에 짐작해 보니, mango pie도 여러 종류의 과일이 섞이게 있는데, 어떤것을 먹을 지 과일들을 열거해 줬던 것 같다.

어쨌든 맛은 좋았다.


내가 이 섬에 대해서 오해한 것이 2가지다. 수상 가옥이 있다는 정보가 있었는데, 잘못된 정보였다는 것이고, 다른 하나는 이 섬에 가면 아무 생각없이 머리를 비우기에 적당하는 것이었다.  맞긴 하다. 내가 있는 곳에서 이곳에 도착하려면 3번을 갈아타서 4~5 시간을 가야했다. 올때도 그랬다. 하루 여행의 9~10시간을 차만 탔다. 자다가 깨면 경치 보고 자다가 깨면 경치보고 잠결에 다른 생각을 할 수 가 없었다. Hundreds Island ! 아무 생각없이 즐기기엔 딱 좋은 섬이다.

Posted by dalbong2

여기는 바기오 !

Others 2009/04/25 19:28

필리핀에 도착한지 한 달정도다. 그동안 절간같은 건물에서 살았다. 시내로 나오려면 큰맘 먹지 않고는 쉽지 않은 곳이었다. 인터넷도 되지 않았다.

새로운 건물로 옮겼다. 이제 무선 인터넷도 된다.

아래 사진은 기존 절간같은 곳에서 내려다본 풍경이다.

매일 점심때즘이면 안개가 적군처럼 쳐들어온다.

 

산 중턱에 있는 집에서 정상까지 쉽게 올라갈 수 있는 길이 나 있다.


몇달해서 말하고 듣기가 될 지는 모르겠다. 지금은 가능성을 타진해 보고 있다.


여전히 관리자 모드로는 들어가지 못하고 있다. 그래도 Windows Live Writer를 이용하면 포스팅은 가능하다.

Posted by dalbong2

어학연수 준비중

Others 2009/04/25 19:25

몇 달간 해외 어학 연수를 하기로 했다.

선수금을 지불하고 등록 절차중이다.

필리핀의 바기오라는 지방인데 좀 외진 곳인듯하다. 이곳의 "더유닛"이라는 학원에 등록했다. 

여기서 혼자서도 할 수 있는 자생력 정도까지만 키워올 생각이다.


또 연재를 "막무가내"로 종료해야 할 것 같다.

아직 Spring.NET에 대해서 할 것이 남았는데..쓰으.

흑흑 또 뒷심 부족이다.


▶ IoC 컨테이너에 대해 좀 더

지금까지의 Configuration 설정에 의해서 컨테이너에 생성된 객체들은 Singleton 객체들이었다. 즉 애플리케이션 실행동안 하나의 각 타입별로 하나의 객체만 생성된다. 그러나 때로는 singleton 타입이 아닌 객체(prototype 객체)로 설정할 수도 있다.

ProxyFactoryObject 속성

- IsSingleton

- TargetName. p.139

▶ Attribute를 이용한 AOP 프로그래밍

▶ Expression evaluaion 프레임워크

- Spring.Expressions

▶ Validation 프레임워크 - Spring.Validation

▶ NHibernate.net

▶ iBatis.NET

▶ Ajax 지원

▶ Spring.NET MVC 패턴 애플리케이션

▶ 예외 처리

▶ Localization & Message Source

▶ 실제 적용 전략

- 스마트클라이언트, 웹애플리케이션

- 실제로 기업형 애플리케이션에 적용하기 위한 전략 및 예제를 만들어본다. Good!!


돌아와서 할 수 있게 되길 바란다.

Posted by dalbong2

▶ Result Mapping 이란.


Result Mapping ! 이것이 뭐냐면 "애플리케이션의 흐름"을 제어하는 방법중의 하나다. 애플리케이션 흐름? Response.Redirect, Server.Transfer 등이 바로 애플리케이션의 흐름을 제어하는 메소드들이다.  여기서 말하는 애플리케이션의 흐름은 페이지의 수행 결과에 따라서 이 페이지 저 페이지로 리다이렉트되는 것을 말하고 있다. Spring.NET에서는 이렇게 결과에 따라서 적절한 페이지로 리다이렉트될 수 있도록 사전에 매핑을 설정할 수 있는 방법이 있다.

페이지 수행 결과 리다이렉트될 대상 페이지
"SUCCESS" OK.aspx
"FAIL" Sorry.aspx

Spring.NET에서는 이런 설정을 configuration에 포함시킬 수 있다는 것이다. 그리고 실제의 aspx.cs에서는 Response.Redirect, Server.Transfer 메소드를 없앤다. Spring.NET이 지원하는 Result Mapping 방법을 이용하면 Controller가 직접 다른 View를 참조할 필요가 없다. 예를 들어 만약 수행 결과가 "SUCCESS" 인 경우 "success.aspx" 페이지로 리다이렉트되어야 한다면 OK.apsx로 설정되어 있는 내용을 success.aspx로 변경만 하면 된다. success.aspx로 넘겨야 하는 파라미터가 있다면 그 값도 configuration 할 수 있다.


▶ 기존 방법의 MVC 패턴 위배


aspx.cs에서 이런 메소드를 사용해서 직접 개발자가 페이지를 대상 페이지로 리다이렉트시키는 방법은 앞에서 말한 MVC 패턴에도 맞지 않는 부분이 있다. 앞에서 말한 MVC 패턴에서는 개발 작업에서 UI를 비즈니스 로직에서 분리시키는 것이 중요한 목표였다. 그러나  Controller 즉 Page 객체에서 직접 Redirect, Transfer 메소드를 사용하면 다음과 같은 상태가 되고 만다.

이런 상태가 되면 애플리케이션의 흐름이 변경된 경우 재 컴파일이 필요하고 필요하면 테스트, 배포 작업도 다시 해야 할 수 있다. 


▶ 수행 결과 추상화 -  Result  클래스


Spring.NET에서는 페이지의 수행 결과를 Spring.Web.Support 네임스페이스의 Result 타입으로 추상화하고 있다. 예를 들어 페이지 수행 결과 "SUCCESS", "FAIL"을 표현할 수 있는 객체이다.

다음과 같은 시나리오를 생각해보자. 사용자 등록 페이지 UserRegistration.aspx가 있다고 하자. 이 페이지에서 저장 버튼을 클릭하면 사용자 정보를 저장하고 나서 홈 페이지 Default.aspx로 리다이렉트되고 그리고 사용자 등록 페이지에서 취소 버튼을 클릭하면 로그인 페이지 login.aspx로 이동한다고 하자.  여기서 "홈 페이지로 이동"하고 "로그인 페이지로 이동"하는 것을 Spring.Web.Result로 표현하면 아래에서 첫번째, 두번째 <object> 설정과 같다. 

<object id="homePageResult" type="Spring.Web.Result, Spring.Web">

  <property name="TargetPage" value="~/Default.aspx"/>

  <property name="Mode" value="Transfer" />

  <property name="Parameters">

    <dictionary>

      <entry key="literal" value="My Text"/>

      <entry key="name" value="%{UserInfo.FullName}" />

      <entry key="host" value="%{Request.UserHostName}"/>

    </dictionary>

  </property>

</object>


<object id="loginPageResult" type="Spring.Web.Result, Spring.Web">

  <property name="TargetPage" value="Login.aspx"/>

  <property name="Mode" value="Transfer" />

</object>


<object type="UserRegistration.aspx" parent="basePage">

  <property name="UserManager" ref="userManager"/>

  <property name="Results">

    <dictionary>

      <entry key="SUCCESS" value-ref="homePageResult"/>

      <entry key="CANCEL" value-ref="loginPageResult"/>

    </dictionary>

  </property>

</object>

각 Result 타입에는 해당 결과로 인해 리다이렉트될 대상 페이지를 나타내는 TargetPage 속성이 있다.  위 설정에서 "homePageResult"라는 결과가 사용될때는 "~/Default.aspx"로 리다이렉트되고 "loginPageResult" 결과가 사용될때는 "Login.aspx"로 이동한다. 그리고 Result 타입에는 Mode 속성이 있다. 이 속성은 리다이렉트 방법을 나타낸다. 이 속성의 값으로는 "Transfer", "TransferNoPreserve", "Redirect"가 있는데 설정하지 않으면 "Transfer"가 사용된다. "TransferNoPreserve"는 Tranfer 메소드에서 "preserveForm=false"와 같다.

public void Transfer(
   string path,
   bool preserveForm
);

preserveForm 인자가 false이면 QueryStringForm 컬렉션 데이터는 지워진다.

세번째 <object/> 요소에서는 "UserRegistration.aspx" 페이지에서 이 Result 객체를 참조해서 사용하고 있다는 것을 설정하고 있다. 즉 UserRegistration.aspx의 실행 결과가 "SUCCESS"로 설정되는 경우는 "homePageResult"에서 정의한대로 리다이렉트로되고 결과가 "CANCEL"로 설정되는 경우는 "loginPageResult"에서 설정한대로 리다이렉트된다.

만약 리다이렉트될 대상 페이지에 파라미터를 넘기고 싶다면, Result 타입의 딕션너리 속성 Parameters을 사용한다. 이 속성에 <entry/> 요소를 파라미터가 필요한대로 포함시킨다. homePageResult 결과 설정예를 보면, "literal", "name", "host" 파라미터가 추가되어 있다. "literal" 속성처럼 리터럴 문자열이 파라미터 값으로 사용될 수도 있지만, 호출하는 페이지의 속성 값을 동적으로 설정할 수도 있다. "name", "host" 파라미터에는 현재 호출하는 페이지 즉 UserRegisteration 페이지의 UserInfo 속성의 FullName 속성값이 설정된다. 현재 호출하는 페이지를 나타내는 Page 객체에 FullName 속성을 갖는 UserInfo 속성이 public 속성으로 노출되어야 한다는 것을 말한다. 참고로 이때  "%{....}" 내부의 문자열을 해석하는 expression evaluation 프레임워크가 사용된다.

이렇게 추가된 파라미터들은 Result의 속성 Mode에 따라서 대상 페이지로 다르게 전달된다. Mode를 redirect로 설정하면 모든 파라미터들은 문자열로 변환되어 쿼리 문자열에 추가된다. 반면 transfer로 설정되면 HttpContext.items에 추가되어 대상 페이지로 전달된다.

UserRegistration.aspx 페이지에서 결과를 설정하는 코드 예를 보면 다음과 유사하게 된다.

protected override void OnInit(EventArgs e)

{

    //...

    this.saveButton.Click += new EventHandler(this.SaveUser);

    this.cancelButton.Click += new EventHandler( this.Cancel );

    //...

}


private void SaveUser(Object sender, EventArgs e)

{

    UserManager.SaveUser(UserInfo);

    SetResult("SUCCESS");

}


private void Cancel(Object sender, EventArgs e)

{

    SetResult("CANCEL");

}

Spring.Web.UI.Page에서 제공하는 SetResult 메소드를 사용하면 현재 페이지의 결과를 지정할 수 있다. 이제 이렇게 지정된 결과와 설정에 따라서 적절한 페이지로 리다이렉트된다.


정리를 하자면, Response.Redirect("~/Default.aspx") 대신에 SetResult("SUCCESS")를 사용한다는 것이다. 그래서 음....UI와 비즈니스 로직 분리,  MVC 패턴 준수 뭐 그렇다는 것이다.

이상!

Posted by dalbong2

Spring.NET의 트랜잭션을 알아볼까 NHibernate를 설명해볼까 했었다. 일단 둘 다 개념은 어느 정도 알겠는데 막상 글로 옮기려다 보니 쉽게 정리가 안된다. 해서 또 뒤로 미루겠다. 대신 이번에는 UI 프레임워크에 대해서 알아보도록 하겠다.

여기서 말하는 UI 프레임워크란 Spring.NET이 지원하고 있는 MVC 패턴을 말한다. 패턴을 공부하다보면 주로 제일 먼저 나오는 패턴중의 하나이다. 달봉이도 자바쪽 프로그래밍에 대해서는 잘 모르지만 이야기를 들어보면 자바쪽 웹 프로그램쪽에서는 MVC 패턴에 기반한 프로그래밍이 예전부터 이뤄지고 있다고 한다. 그래서 많은 개발자가 처음 프로그래밍을 배우면서부터 자연스럽게 이 패턴에 익숙해진다는 것이다.

우선 많은 사람들이 MVC 패턴에 대해서 들어봤겠지만 한번 더 간단히 정리해보고 가자. 상세히는 하지 않겠다. 왜? 말빨을 지원해줄만한 지식이 딸린다.  일단 많이 본 그림을 다시 보자.

패턴 공부를 시작한 사람들이라면 많이 봤을 그림이다. 그렇지만 좀 시간만 지나면 다시 까먹는다. 그림이 뭘 말하는지 까먹고 또 까먹고. 다시 볼때마다 네모와 화살표만 보인다-_-;; 달봉이도 그랬다. 현실적으로 이 패턴을 활용할 기회가 없었기 때문이다. 

이 패턴에 대한 좀 더 아카데믹한 설명은 다른 전문 패턴 설명 문서를 참조하기를 바란다. 달봉이의 추측성 설명보다는 그쪽이 더 바람직할 것이다. 다만 여기서는 이 패턴이 ASP.NET으로 어떻게 구현될 수 있는지 HOW-TO 위주로 알아본다. 다음 그림은 이 패턴의 각 요소에 대응되는 ASP.NET 웹 애플리케이션의 요소이다.

아직 다는 이해가 되지 않지만 익숙한 aspx, aspx.cs를 보니 쪼옴 숨이 트일 것이다. "애플리케이션 도메인 객체"라는 새로운 요소가 나타나 있다. MVC 패턴의 Model에 해당하는 것이 "애플리케이션 도메인 객체"로 되어 있다. 기존의 .NET 애플리케이션에서는 데이터 액세스 레이어, 비즈니스 레이어 그리고 UI 레이어간에 데이터를 전달할때 흔히 Dataset 객체를 사용하는 경우가 흔했다. 이런 구조에 익숙한 사람이라면 "애플리케이션 도메인 객체"라는 용어를 들어볼 기회가 별로 없었을 듯 싶다. 

사용자 정보를 관리하는 페이지를 예로 해서 MVC 패턴을 좀 더 이해해보도록 하자. MVC 패턴에 맞게 구성하면 다음과 유사하게 될 것이다. "사용자 정보"라는 사용자 정의 객체가 필요한데, 이것이 Model 객체가 된다.  이 Model 객체는 사용자에 대한 정보를 가지고 있게 된다. 이 "사용자 정보" 객체는 사용자에 대한 정보( 사원번호, 이름, 현재 부서 등 )를 간직한다.  이 객체는 그림에서처럼 Controller인 Page 객체로부터 상태 변경 요청을 받기도 하고 View로 부터 상태 조회 요청을 받기도 한다.

aspx는 특정 시점의 사용자 정보 즉 실제 Model 객체의 상태를 보여주는 View 를 제공한다.

Controller인 Page 객체는 사용자가 View를 통해 입력한 Model 객체에 대한 정보를 HTTP로 받아서 해석한다. 그런 다음 Model 객체로 전달해서 상태 변경을 요청한다. 또는 Model 객체에서 변경된 상태를 View에 보내서 반영을 요청하기도 한다.

Controller는 도메인 객체의 현재 상태를 조회해서 UI의 컨트롤에 출력을 요청하기도 한다. 그러나 상태를 UI에 출력하기 위해서는 그림에서처럼 때로는 View에서 직접 도메인 객체에 접근해서 그 상태롤 요청하는 경우도 있을 수 있다. 뒤의 샘플 코드에서 보겠지만 미리 살짝 언급하면 사용자가 입력한 정보를 Model 객체에 반영하고 Model 객체의 상태를 UI에 출력하는 방법으로 데이터 바인딩 기술이 사용될 수 있다.  

참조를 나타내는 화살표 방향을 이해해 두는 것도 중요할 듯 싶다.  달봉이가 이해한 대로 그렸지만 용어나 그림이 정확한지는 달봉이도 잘 모르겠다.

아직 이 구조가 몸에 착 달라붙지는 않을 것이다. 샘플 코드를 보자. 다음에 보여주는 코드를 통해서는 MVC  패턴이 실제로 어떻게 구현되는지 그 구조 이해에 중점을 두도록 한다. 화살표가 제대로 구현되고 있는지 확인해 보도록 한다. 이 샘플 코드는 Spring.NET의 소스 코드와 함께 제공되는 샘플 프로젝트중에서 SpringAir.Web.2005를 참조하고 있다.

다음 샘플 코드에서 보여줄 페이지에서는 사용자가 비행기 예약을 하고 취소할 수 있는 페이지이다. 이 페이지를 위한 Model 객체가 어떻게 정의되어 있는지 먼저 보자. 


▶ Model - Trip 객체


namespace SpringAir.Domain

{

    [Serializable]

    public class Trip

    {

        #region Fields


        private TripMode mode = TripMode.RoundTrip;

        private TripPoint startingFrom = new TripPoint();

        private TripPoint returningFrom = new TripPoint();


        #endregion


        #region Constructor (s) / Destructor


        public Trip()

        {

        }


        public Trip(TripMode mode, TripPoint startingFrom, TripPoint returningFrom)

        {

            this.mode = mode;

            this.startingFrom = startingFrom;

            this.returningFrom = returningFrom;

        }


        #endregion


        #region Properties


        public TripMode Mode

        {

            get { return this.mode; }

            set { this.mode = value; }

        }


        public TripPoint StartingFrom

        {

            get { return this.startingFrom; }

            set { this.startingFrom = value; }

        }


        public TripPoint ReturningFrom

        {

            get { return this.returningFrom; }

            set { this.returningFrom = value; }

        }


        #endregion


        /// <summary>

        /// Returns a <see cref="System.String"/> representation of this

        /// <see cref="SpringAir.Domain.Trip"/>.

        /// </summary>

        /// <returns>

        /// A <see cref="System.String"/> representation of this

        /// <see cref="SpringAir.Domain.Trip"/>.

        /// </returns>

        public override string ToString()

        {

            StringBuilder buffer = new StringBuilder();

            buffer

                .Append(Mode).Append(", from ")

                .Append(StartingFrom).Append(" to ")

                .Append(ReturningFrom);

            return buffer.ToString();

        }

    }

}

코드를 보면 알겠지만, Trip 클래스는 출발지와 반환지를 표현하기 위해서 TripPoint 타입을 사용해서 StartingFrom, ReturningFrom 속성으로 노출하고 있다. 그리고 그 여행이 편도인지 왕복인지를 나타내기 위해서 TripMode 타입의 Mode 속성을 노출하고 있다.

aspx.cs에서 DataSet 객체를 구성해서 바로 비즈니스 레이어 객체를 호출해서 넘기는 방식의 코딩에 익숙한 대부분의 ASP.NET 웹 애플리케이션 개발자들에게는 이런 Model 객체가 익숙하지 않을 것이다. 그러나 MVC 패턴에서는 조회나 수정에서 이런 Model 객체를 사용하게 된다. 따라서 비즈니스 설계 또한 필요하다면 이 Model 객체를 도출할 수 있도록 수정될 필요도 있을 것이다.

이제 특정 시점에서의 이 Model 객체의 정보(상태)를 출력하는 UI를 보도록 하자.


▶ View - TripForm.aspx


<asp:Content ID="body" ContentPlaceHolderID="body" runat="server">

  <div style="text-align: center">

    <h4>

      <asp:Label ID="caption" runat="server"></asp:Label>

    </h4>

    <spring:ValidationSummary ID="validationSummary" runat="server" />

    <table>

      <tr class="formLabel">

        <td>&nbsp;</td>

        <td colspan="3">

          <spring:RadioButtonGroup ID="tripMode" runat="server">

            <asp:RadioButton ID="OneWay" onclick="showReturnCalendar(false);" runat="server" />

            <asp:RadioButton ID="RoundTrip" onclick="showReturnCalendar(true);" runat="server" />

          </spring:RadioButtonGroup>

        </td>

      </tr>

      <tr>

        <td class="formLabel" align="right">

          <asp:Label ID="leavingFrom" runat="server" />

        </td>

        <td nowrap="nowrap">

          <asp:DropDownList ID="leavingFromAirportCode" runat="server" />

          <spring:ValidationError id="departureAirportErrors" runat="server" />

        </td>

        <td class="formLabel" align="right">

          <asp:Label ID="goingTo" runat="server" />

        </td>

        <td nowrap="nowrap">

          <asp:DropDownList ID="goingToAirportCode" runat="server" />

          <spring:ValidationError id="destinationAirportErrors" runat="server" />

        </td>

      </tr>

      <tr>

        <td class="formLabel" align="right">

          <asp:Label ID="leavingOn" runat="server" />

        </td>

        <td nowrap="nowrap">

          <spring:Calendar ID="leavingFromDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />

          <spring:ValidationError id="departureDateErrors" runat="server" />

        </td>

        <td class="formLabel" align="right">

          <asp:Label ID="returningOn" runat="server" />

        </td>

        <td nowrap="nowrap">

          <div id="returningOnCalendar">

            <spring:Calendar ID="returningOnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />

            <spring:ValidationError id="returnDateErrors" runat="server" />

          </div>

        </td>

      </tr>

      <tr>

        <td class="buttonBar" colspan="4">

          <br/>

          <asp:Button ID="findFlights" OnClick="SearchForFlights" runat="server"/>

        </td>

      </tr>

    </table>

텍스트가 없는 몇개의 레이블 컨트롤이 있다. 텍스트값은 런타임시에 할당된다. 이것은 애플리케이션의 지역화와 관계된 것으로서 지금의 MVC 패턴 설명에서는 별로 중요하지 않은 부분이다. 중요한 것은 UI에 입력 컨트롤들이 있다는 것이다 .  라디오 버튼 그룹 컨트롤인 tripMode, 드롭 다운 리스트 컨트롤인 leavingFromAirportCode, goingToAirportCode 그리고 달력 컨트롤인 departureDate, returnDate 등이 배치되어 있다. 어떤 것은 ASP.NET에서 제공하는 표준 컨트롤이고 어떤 것은 Spring에서 제공하는 커스터마이징 컨트롤이다. 이 컨트롤들은 Model 객체의 상태값을 출력하게 될 것이다. 그 출력은 MVC의 Controller가 담당한다고 했다. ASP.NET에서는 코드 비하인드 페이지의 Page 객체가 담당하게 된다.

이제 이 Model 객체의 상태값을 UI 컨트롤에 출력하는 Page 객체를 보도록 하자.


▶ Controller 객체 - TripForm 페이지 객체


public partial class TripForm : Page

{

    #region Fields


    private const string DisplaySuggestedFlights = "displaySuggestedFlights";


    private IBookingAgent bookingAgent;

    private IAirportDao airportDao;

    private Trip trip;

    #endregion


    #region Properties


    /// Biz 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.

    public IBookingAgent BookingAgent

    {

        set { bookingAgent = value; }

    }


    /// Dao 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.

    public IAirportDao AirportDao

    {

        set { airportDao = value; }

    }

    /// 이 도메인 객체의 상태는 정의한 바인딩 규칙에 따라 UI의 컨트롤이 제공하는 값으로 채워진다. 

    public Trip Trip

    {

        get { return trip; }

        set { trip = value; }

    }



    #endregion


    #region Model Management and Data Binding Methods


    //--> 베이스 페이지의 Init 이벤트에서 포스트백이 아닌 경우 호출된다.

    protected override void InitializeModel()  

    {

        trip = new Trip();

        trip.Mode = TripMode.RoundTrip;

        trip.StartingFrom.Date = DateTime.Today;

        trip.ReturningFrom.Date = DateTime.Today.AddDays(1);

    }


    //--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.

    protected override void LoadModel(object savedModel)

    {

        trip = (Trip)savedModel;

    }


    // --> 베이스 페이지의 PreRender 이벤트에서 호출된다.

    protected override object SaveModel()

    {

        return trip;

    }



    //--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.

    //--> InitializeModel()보다 먼저 호출된다.

    protected override void InitializeDataBindings()

    {

        BindingManager.AddBinding("tripMode.Value", "Trip.Mode");

        BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");

        BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");

        BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");

        BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");

    }


    #endregion


    #region Page Lifecycle Methods


    protected override void OnInitializeControls(EventArgs e)

    {

        if (!IsPostBack)

        {

            BindAirportDropdowns();

        }

    }



    /// 페이지가 로딩되면서, 출발지, 도착지를 나타내는 드롭다운 컨트롤이 채워진다.

    private void BindAirportDropdowns()

    {

        ArrayList airportList = new ArrayList();

        airportList.Add(new Airport(0, string.Empty, string.Empty, "-- " + GetMessage("selectAirport") + " --"));

        airportList.AddRange(airportDao.GetAllAirports());


        leavingFromAirportCode.DataSource = airportList;

        leavingFromAirportCode.DataTextField = "Description";

        leavingFromAirportCode.DataValueField = "Code";

        leavingFromAirportCode.DataBind();


        goingToAirportCode.DataSource = airportList;

        goingToAirportCode.DataTextField = "Description";

        goingToAirportCode.DataValueField = "Code";

        goingToAirportCode.DataBind();

    }

    #endregion


    #region Controller Methods


    protected void SearchForFlights(object sender, EventArgs e)

    {

        if (Validate(trip, tripValidator))

        {

            FlightSuggestions suggestions = this.bookingAgent.SuggestFlights(Trip);

            if (suggestions.HasOutboundFlights)

            {

                Session[Constants.SuggestedFlightsKey] = suggestions;

                SetResult(DisplaySuggestedFlights);

            }

        }

    }


    #endregion

}

이 코드에서 어떤 일이 일어나는지 차례대로 정리해보자.

1. 페이지가 처음 로딩될때( IsPostback == false )부터 보자. InitializeModel 메소드는  페이지가 처음 로딩될때만 호출된다. 그 메소드에서는 Trip객체가 생성되고 기본 속성값으로 상태가 세팅된다.  페이지가 렌더링되기직전 즉 베이스 페이지의 PreRender 이벤트에서 SaveModel 메소드가 호출되는데 이때 Trip 객체가 베이스 클래스로 반환되어 HTTP 세션에 캐싱된다.

2. 그런 다음 페이지가 포스트백될때 LoadModel 메소드가 호출되는데 이때 베이스 클래스에서는 앞에서 HTTP 세션에 저장한 Trip 객체을 복원해서 LoadModel의 인자로 넘겨준다.

샘플 페이지에서는 Model이 Trip 객체하나로 구성되어 있지만 실제로는 여러개의 Model 객체로 구성된 딕션너리가 SaveModel 메소드에서 저장되고 LoadModel 메소드에서 복원될 것이다.

3. InitailizeDatabindings 메소드에서는 View의 컨트롤과 Model 객체의 속성들간의 바인딩 규칙을 지정하고 있다. 이 메소드는 페이지가 처음 호출될때 호출되어 바인딩 규칙을 구성하여 캐싱하고 이후부터는 캐싱된 결과를 이용한다. 바인딩 규칙을 추가하기 위해서 BindingManager 속성의 AddBinding 메소드를 사용하고 있는데, 넘겨지는 인자들이 모두 문자열로 되어 있다. 이 문자열들은 컨트롤의 속성, 적절한 Model 객체의 속성으로 파싱된다. 이때 Spring.NET의 Expression Language를 사용하게 된다. 그 파싱 규칙도 이해해 둘 필요가 있을 것이다. 이 규칙을 이해하면 폼위의 컨트롤과 Model 객체외에도 바인딩의 대상을 넓혀 좀 더 유용하게 응용할 수 있을 것이다.

4. 폼 위의 버튼에 대한 핸들러로 SearchForFlights 메소드가 정의되어 있다. 이 메소드를 보면 View의 컨트롤에 대한 참조가 전혀 없다. 이 메소드에서는 injected된 서비스 레이어의 BookingAgent 객체와 Model 객체 trip만을 사용하고 있다. 이곳에서 만약 서비스 레이어의 객체를 호출한 결과를 이용해서 Model 객체 trip의 상태를 변경하면 자동으로 View의 컨트롤의 상태 출력도 변경되어 있을 것이다.


샘플 페이지에서 MVC 패턴을 구현해서 controller 객체 즉 TripForm에서 View쪽의 컨트롤에 대한 참조를 없애는 것이었다. 즉 View와 Controller를 디커플링시키는 것이다. 비즈니스 로직이 포함될 수 있는 Controller쪽에서는 View측의 어떠한 컨트롤에 대한 참조도 없기때문에 좀 더 자유롭게 Controller쪽에 있을 지도 모르는 비즈니스 관련 코드를 좀 더 자유롭게 수정할 수 있게 된다. 이런 MVC 패턴 구현이 가능하게 된 것은 Spring.NET의 웹 프레임워크에서 제공하는 바인딩 기술때문이라는 것을 마지막으로 지적하고 싶다.

Controller 역할을 다시 생각해보자. 사용자로부터 입력된 정보를 Model 객체에 반영하고 Model 객체의 변경된 상태를 View에 출력하도록 요청한다고 했다.  TriplForm 객체는 이런 일을 앞에서 말한 바인딩 기술로 구현하고 있다.  즉 View와 Model의 상태 동기화를 바인딩 기술을 이용하고 있다.

앞의 TripForm 객체가 상속받고 있는 Page는 ASP.NET에서 제공하는 표준 객체가 아니다. Spring.Web.UI에 포함된 객체로서 Spring에서 표준 페이지 객체를 상속해서 확장한 객체이다. 이 객체에서는 BindingManager라는 속성을 노출시키고 있는데 이 속성이 반환하는 객체가 "바인딩 객체"로서 View와 Model 객체간의 바인딩을 관리한다. 이 바인딩 관리자는 컨트롤과 Model 객체의 속성간의 바인딩 규칙을 개발자로부터 입력받아야 한다. 앞의 코드중에서 바인딩 규칙을 제공하는 부분을 다시 보면 다음과 같다. 페이지가 로딩되면서 다음과 같은 코드가 실행되어(처음 로딩될때. 포스트백시에는 실행되지 않는다) 바인딩 규칙을 바인드 관리자에게 알려준다.

   protected override void InitializeDataBindings()

    {

        BindingManager.AddBinding("tripMode.Value", "Trip.Mode");

        BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");

        BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");

        BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");

        BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");

    }

바인딩 관리자 및 바인딩에 대한 자세한 내용은 다음에 기회대는 대로 알아보도록 하겠다.

MVC 패턴에서 Controller가 하는 역할 즉 사용자가 입력한 정보를 Model객체에 반영하고 Model객체의 변경된 상태를 View에 반영하는 역할을 Spring.NET이 제공하는 페이지의 바인딩 관리자가 담당하고 있는 것이다. 바인딩 관리자는 View와 Model에 있는 객체를 직접 참조하는 대신에 Expression Evaluation 프레임워크( 레퍼런스 문서 11장)를 통해서 양쪽의 속성을 연결하는 것이다. Spring.NET의 Expression Evaluation 프레임워크가 MVC 패턴 구현에 핵심 역할을 하고 있다는 것을 마지막으로 지적하고 싶다. 앞에서도 말했듯이 이 Expression Evaluation 프레임워크를 좀 더 이해하는 것이 필요할 것으로 보인다. 또한 바인딩의 유효성 검사를 위해서 Validation 프레임워크( 레퍼런스 문서 12장)를 사용하고 있는데 이 또한 공부거리로 보인다.


▶  Spring.NET의 MVC 패턴 지원


View와 Controller는 모두 Model에 의존하고 있지만, Model은 View와 Controller 어떤 것도 참조하지 않고 있다는 것을 알아차리는 것이 중요하다고 보여진다. View, Controller와 Model의 분리는 UI 상관없이 Model의 테스트가 자유롭게 이뤄질 수 있다는 것이다. 또한 View와 Controller의 분리 또한 중요한 이점중의 하나이다.

이 두 이점은 모두, MVC 패턴을 이용하면 비즈니스 로직에서 UI를 분리할 수 있다는 장점을 제공할 수 있다는 것이다.

[ 추가 내용 ]

Spring.NET이 MVC 패턴을 지원하는 것은 결국 이렇게 UI와 비즈니스 로직을 분리할 수 있는 기반을 제공하고 있다는 것을 말한다.


ASP.NET 팀에서는 현재 ASP.NET MVC 프레임워크를 제작했다. 그리고 그 프레임워크에 대한 소스 코드도 제공되고 있다.  코드플렉스 사이트에서 ASP.NET MVC Preview 2 소스를 받아볼 수 있다. 코드는 Visual Studio 2008용 솔루션 파일로 묶여져 있다. 앞으로는 ASP.NET에서도 MVC 개발 패턴에 대한 적극적 지원이 있지 않겠냐는 생각이다. 해서, 이즈음해서는 애플리케이션 개발자라면 MVC 패턴의 개념 정도는 이해하고 있을 필요가 있겠다 하겠다.

Posted by dalbong2

지난 포스트에서 말한대로 이번에는 Spring.NET에서 지원하는 OR매핑( Object Relational Mapping) 기능에대해서 알아본다. AdoTemplate의 Execute류의  메소드를 이용하면 CRUD 모두가 가능하다. 그러나 조회의 경우 Spring.NET의 데이터 접근 모듈에서는 좀 더 특별한 API를 제공한다. 지금까지의 개발 방식에서는 보통 조회를 하면 DataSet으로 넘어오고 이것을 그대로 Biz 레이어, UI 레이어로 넘겨서 레코드별로 루프를 돌면서 필요한 데이터를 꺼내서 작업을 했었다. 그러나 Spring.NET에서는 조회된 각 레코드를 사용자 정의 객체와 매핑시킬 수 있는 기회를 제공하고 있다. 예를 들어 여러 건의 사용자 정보 레코드가 조회되었을 경우 하나의 레코드는 하나의 UserInfo 객체로 변환된다. 그래서 Biz 레이어나 UI 레이어로 반환될때는 전체 레코드는 UserInfo 컬렉션으로 변환되어 반환된다.

개발자는 도메인 객체 UserInfo를 정의해야 한다. 도메인 객체는 애플리케이션의 서버측과 클라이언트측 모두에서 참조되어 사용될 수 있다. 따라서 별도의 어셈블리로 분리하여 개발하는 것이 보통이다. 개발자는 또한 레코드의 컬럼과 UserInfo의 속성을 연결시켜주는 매핑 정보를 제공해야 한다.  이런 작업을 OR 매핑( Object Relational Mapping )작업이라고 한다. OR매핑 작업을 좀 더 편하게 할 수 있는 전문적인 OR매핑툴도 있다. 대표적인 것으로 NHibernate라는 것이 있는데 이것은 뒤에 별도로 살펴볼 것이다.

Spring.NET에서는 QueryWith로 시작하는 데이터 접근용 메소드가 많은데 이런 메소드를 이용하면 개발자가 OR 매핑 작업을 할 수 있는 기회를 제공받을 수 있게 된다. OR매핑 작업이 수행되는 구조는 앞에서의 AdoTemplate의 Execute류 메소드를 이용하는 콜백 구조와 유사한 구조를 갖는다.


▶  OR매핑 작업 구조



AdoTemplate객체의 Execute를 사용할때는 DB에 접근하는 작업을 콜백객체의 콜백함수에서 개발자가 직업 수행했었다. 게릿? QueryWith류의 메소드를 사용하면 DB 접근해서 조회하는 작업은 AdoTemplate에서 수행하게 된다. 그리고 Execute 메소드를 사용할때도 직접 콜백함수에서 조회된 테이블의 각 레코드를 사용자 정의 객체로 변환해서 반환하면 된다. 그러나 QueryWith류의 메소드를 이용하면 그런 작업을 좀 더 편하게 할 수 있다. 콜백 함수에서 파라미터로 받는 것은 IDataReader 객체이다. 어떤 타입의 콜백 객체를 사용하느냐에 따라서 IDataReader 객체는 전체 조회결과를 받는냐 아니면 한 레코드씩을 받느냐가 결정된다. 콜백 객체의 타입은 다음 3 종류가 있다.

콜백객체 타입 설명
IResultSetExtrator/ResultSetExtractorDelegate - AdoTemplate으로 부터 조회 결과를 전부 넘겨 받는다. 즉 커서가 처음 위치에 있는 IDataReader 객체를 넘겨받는다.
- 클라이언트 코드로 넘겨줄 사용자 정의 객체를 모두 구성해서 반환해준다. 
- 클라이언트 코드에서는  QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.
IRowCallback / RowCallbackDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는다. 즉  커서가 현재 위치로 이동한 상태의 IDataReader 객체를 건네받는다.
- 레코드별 사용자 정의 객체를 콜백 객체에 쌓아둔다.
- 클라이언트 코드에서 나중에 콜백 객체에서 구성해둔 사용자 정의 컬렉션을 가져간다.
IRowMapper / RowMapperDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는 것은 이전 로우 콜백 타입과 같다.
- 레코드별 사용자 정의 객체 컬렉션을 AdoTemplate쪽에서 담당한다.
- 클라이언트 코드에서는 QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.

어떤 타입의 콜백 객체를 사용하느냐에 따라서 QueryWith 메소드, 그리고 QueryQith 메소드내에서 호출되는 콜백 함수가 달라진다. 그러나 콜백함수의 인자로 넘어가는 것은 언제나 IDataReader 객체이다. 이 객체도 다시 어떤 타입의 콜백객체를 사용하느냐에 따라서 콜백 객체에서 받는 IDataReader객체의 현재  상태가 달라질 수 있다.

IDataReader 객체는 forward-only 속성이 있다. 즉 테이블의 레코드를 가리키는 현재 커서는 항상 앞으로만 움직일 수 있다. 콜백 객체의 타입은 선정은 이 커서의 위치에 영향을 줄 수 있다. 만약 IResultSetExtractor / ResultSetExtractorDelegate를 선택했다면 현재 커서가 움직이지 않은 상태의 원래의 IDataReader 객체를 넘겨받는다. 콜백 함수에서는 하나씩 앞으로 커서를 움직이면서 OR매핑 작업을 구현해야 한다.  그러나 Row로 시작하는 나머지 두 타입은 콜백 객체에서는 커서가 진행된 IDataReader 객체를 받는다. 레코드 루핑은 AdoTemplate쪽에서 일어난다. 이 경우 콜백 함수에서는 사용자 정의 객체를 하나씩만 생성하면 된다.

그러나 이 경우에도 두 타입에는 차이가 있다. 사용자 정의 객체의 집합을 어디에서 관리하느냐 하는 문제를 다르게 두 타입별로 해결하고 있다. IRowCallback / RowCallbackDelegate 타입은 사용자 정의 객체의 집합을 콜백 객체에서 간직하고 있다. 그래서 클라이언트 코드는 그 집합을 나중에 직접 콜백 객체에서 가져가야 한다. 그러나 IRowMapper / RowMapperDelegate를 사용하면 사용자 정의 객체의 결과 집합을 AdoTemplate에서 간진한다. 콜백 함수에서는 레코드를 받아서 하나씩 사용자 정보 객체를 생성해서 AdoTemplate으로 반환해준다. AdoTemplate에서는 콜백 함수에서 받을 객체를 차곡차곡 쌓아두었다가 QueryWith 메소드가 종료될때 클라이언트 코드로 객체 집합을 반환해준다.

이제 몇가지 콜백 객체 유형별로 개발 샘플 코드를 보도록 하자.


▶ ResultSetExtractor 타입의 콜백 객체 사용


다음은 ResultSetExtrator 타입의 콜백 객체를 사용하는 구조에서의 DAO 객체의 정의 일부이다. ResultSetExtractorDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    /// <summary>

    /// A simple DAO that uses Generic.AdoTemplate ResultSetExtractor functionality

    /// </summary>

    public class ResultSetExtractorDao : AdoDaoSupport

    {

        ...

        private string customerByCountryAndCityCommandText =

                @"select ContactName from Customers where City = @City and Country = @Country";

        public virtual IList<string> GetCustomerNameByCountryAndCity(string country, string city)

        {

            // note no need to use parameter prefix.


            // This allows the SQL to be changed via external configuration but the parameter setting code

            // can remain the same if no provider specific DbType enumerations are used.


            IDbParameters parameters = CreateDbParameters();

            parameters.AddWithValue("Country", country).DbType = DbType.String;

            parameters.Add("City", DbType.String).Value = city;


            return AdoTemplate.QueryWithResultSetExtractor(CommandType.Text,

                                                           customerByCountryAndCityCommandText,

                                                           new CustomerNameResultSetExtractor<List<string>>(),

                                                           parameters);

        }

        ...

GetCustomerNameByCountryAndCity 메소드는 country와 city 파라미터값을 받아서 해당하는 고객 집합을 IList<string> 타입으로 반환하는 DAO 객체의 메소드이다. 이 메소드 내부에서는 넘겨받은 country, city값을 DB 파라미터로 변환해서 AdoTemplate에 넘겨줄 준비를 하고 있다. DbParameter를 구성하는 코드는 어렵지 않으니 눈치껏 이해하기 바란다. 이제 적절한 QueryWith 메소드를 선택해야 한다.  코드에서는 ResultSetExtractor 타입의 객체를 콜백객체로 받을 수 있는 메소드로서 QueryWithResultSetExtrator 메소드를 사용하고 있다. 넘겨주는 구체적인 콜백 객체는 CustomerNameResultSetExtrator<List<string>> 타입의 객체이다.

콜백 객체는 개발자가 정의해야 하는 타입으로서 샘플에서는 그 구현을 다음처럼 하고 있다. 이 콜백 객체도 같은 페이지에 internal로 정의되어 있다.

internal class CustomerNameResultSetExtractor<T> : IResultSetExtractor<T> where T : IList<string>, new()

{

    /// <summary>

    /// Implementations must implement this method to process all

    /// result set and rows in the IDataReader.

    /// </summary>

    /// <param name="reader">The IDataReader to extract data from.

    /// Implementations should not close this: it will be closed

    /// by the AdoTemplate.</param>

    /// <returns>An arbitrary result object or null if none.  The

    /// extractor will typically be stateful in the latter case.</returns>

    public T ExtractData(IDataReader reader)

    {

        T customerList = new T();

        while (reader.Read())

        {

            string contactName = reader.GetString(0);

            customerList.Add(contactName);

        }

        return customerList;

    }

}

실제 사용될 구체적인 콜백객체는 IResultSetExtractor<T>를 구현하고 있다. 그리고 그 인페이스에서 정의하고 있는 콜백 함수 ExtractData()를 구현하고 있다. 이 메소드는 AdoTemplate에서 파라미터로 IDataReader 타입의 객체 reader를 받고 있는데 이 객체는 커서가 움직이지 않은 초기상태로 넘어온다. 클라이언트 코드 즉 DAO 객체로 넘겨줄 최종 값은 reader를 이용해서 개발자가 이곳에서 모두 구성해야 한다. 코드에서도 while 문을 돌면서 필요한 반환값을 구성한 다음 반환하고 있다.

이 코드에서는 반활될 값으로 특별히 사용자 정의의 객체를 사용하고 있지는 않다. 그래서 OR 매핑 작업은 구현하고 있지 않다. 그러나 만약 반환값이 IList<string>이 아니라 IList<사용자정의타입>으로 되었다고 하면 while 문을 돌면서 사용자 정의 객체를 생성해서 리스트에 추가하면 된다.

while문에서 통해서 구성된 최종 반환값이 반환되면 이 값이 결국 DAO객체에서 호출을 시작한 메소드의 반환값으로 된다는 것을 알 수 있다. 그래서 Biz 레이어 객체에게로 넘어갈 것이다.


▶ RowCallback 타입의 콜백 객체 사용


이제 RowCallback 타입의 콜백 객체를 사용해서 OR 매핑을 구현하는 구조를 알아보자. 이 타입의 콜백 객체를 사용하면 DAO 객체로 반환될 최종 객체 집합이 사용자 정의의 콜백 객체에 있는 구조가 된다고 했다.  코드를 보자. 우선 DAO객체의 호출 메소드이다. RowCallbackDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowCallbackDao : AdoDaoSupport

    {

        private string cmdText = "select ContactName, PostalCode from Customers";


        public virtual IDictionary<string, IList<string>> GetPostalCodeCustomerMapping()

        {

            PostalCodeRowCallback statefullCallback = new PostalCodeRowCallback();

            AdoTemplate.QueryWithRowCallback(CommandType.Text, cmdText,

                                            statefullCallback);


            // Do something with results in stateful callback...

            return statefullCallback.PostalCodeMultimap;

        }

    }

    ...

RowCallbackDao라는 타입의 DAO객체를 정의하고 있다. 이 객체에 GetPostalCodeCustomerMapping() 메소드에서 DB 데이터에 액세스하고 그 결과를 조작하려는 작업을 하고 있다. 우선 RowCallback을 사용하는 OR 매핑 구조에서는 AdoTemplate의 QueryWithRowCallback 메소드를 호출하고 있다. 앞의 코드에서는 DB 접근에 필요한 값이 없어서 DB 파라미터를 구성하는 코드는 없다. QueryWithRowCallback에는 콜백 객체로서 PostalCodeRowCallback 타입의 객체 statefulCallback이 넘겨지고 있다.

이 객체의 구현은 다음과 같다. 같은 페이지에 정의되어 있다.

internal class PostalCodeRowCallback : IRowCallback

{

    private IDictionary<string, IList<string>> postalCodeMultimap =

        new Dictionary<string, IList<string>>();


    public IDictionary<string, IList<string>> PostalCodeMultimap

    {

        get { return postalCodeMultimap; }

    }


    /// <summary>

    /// Implementations must implement this method to process each row of data

    /// in the data reader.

    /// </summary>

    /// <remarks>

    /// This method should not advance the cursor by calling Read()

    /// on IDataReader but only extract the current values.  The

    /// caller does not need to care about closing the reader, command, connection, or

    /// about handling transactions:  this will all be handled by

    /// Spring's AdoTemplate

    /// </remarks>

    /// <param name="reader">An active IDataReader instance</param>

    /// <returns>The result object</returns>

    public void ProcessRow(IDataReader reader)

    {

        string contactName = reader.GetString(0);

        string postalCode = reader.GetString(1);

        IList<string> contactNameList;

        if (postalCodeMultimap.ContainsKey(postalCode))

        {

            contactNameList = postalCodeMultimap[postalCode];

        }

        else

        {

            postalCodeMultimap.Add(postalCode, contactNameList = new List<string>());

        }

        contactNameList.Add(contactName);

    }

}

IRowCallback 인터페이스는 ProcessRow라는 메소드 하나만을 정의하고 있다. 이 메소드가 AdoTemplate의 QueryWithCallback에서 호출하는 콜백함수이다.

이 QueryWithCallback 메소드에서는 DB 조회 결과를 가지고 있으면서 IDataReader 객체의 루프를 돈다. 그래서 커서가 하나씩 앞으로 움직인 상태의 reader 객체를 콜백 함수로 넘겨주는 것이다. 콜백 함수가 AdoTemplate 객체로부터 콜백될때 넘겨받는 IDataReader 타입의 객체 reader의 커서는 이미 필요한 만큼 움직인 상태이다. 따라서 콜백 함수에서는 현재 readerd 객체에서 필요한 필드의 값을 뽑아 사용하면 된다. 이 콜백 메소드는 반환값이 없다. 구성된 값을 반환하는 대신에 로컬 변수인 postalCodeMultimap에 Add 시키고 있다.

즉 결과값을 콜백 객체에서 자체적으로 관리하고 있다. 그런 다음 최종 구성값을 외부에서 접근할 수 있도록 public 속성 PostalCodeMultimap을 통해서 노출시키고 있다. DAO 객체의 GetPostalCodeCustomerMapping() 메소드의 return문을 보면 이곳에서 콜백 객체의 공개 속성에 접근하고 있는 것을 볼 수 있다.

이 구조에서 AdoTemplate는 조회 결과의 루프만 돌면서 콜백 함수에 각 레코드를 건네주기만 하면 된다. 각 레코드의 정보로 반환될 값을 구성하고 관리하는 작업은 모두 콜백 객체에서 담당해야 한다. 이런 구조는 DB에서 조회된 결과에 레코드별로 추가할 가공 작업이 많은 경우 편리할 것이다.


▶ RowMapper 타입의 콜백 객체 사용


다음은 AdoTemplate에서 DB조회 결과에 대한 루핑 작업뿐만 아니라 콜백 객체에서 생성된 결과 집합도 관리하는 구조이다.  콜백 객체에서는 레코드의 값을 이용해서 필요한 작업을 한 후 그 결과값을 AdoTemplate로 반환만 해 주면 된다. RowMapperDao.cs 페이지에 샘플 코드가 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowMapperDao : AdoDaoSupport

    {

        private string cmdText = "select Address, City, CompanyName, ContactName, " +

                            "ContactTitle, Country, Fax, CustomerID, Phone, PostalCode, " +

                            "Region from Customers";



        public virtual IList<Customer> GetCustomersWithDelegate()

        {

            return AdoTemplate.QueryWithRowMapperDelegate<Customer>(CommandType.Text, cmdText,

                        delegate(IDataReader dataReader, int rowNum)

                            {

                                Customer customer = new Customer();

                                customer.Address = dataReader.GetString(0);

                                customer.City = dataReader.GetString(1);

                                customer.CompanyName = dataReader.GetString(2);

                                customer.ContactName = dataReader.GetString(3);

                                customer.ContactTitle = dataReader.GetString(4);   

                                customer.Country = dataReader.GetString(5);

                                customer.Fax = dataReader.GetString(6);

                                customer.Id = dataReader.GetString(7);

                                customer.Phone = dataReader.GetString(8);

                                customer.PostalCode = dataReader.GetString(9);

                                customer.Region = dataReader.GetString(10);

                                return customer;

                            });

        }

    }

}

RowMapperDao라는 DAO 객체를 정의하고 있고 GetCustomersWithDelegate()라는 업무 메소드를 정의하고 있다. 이 메소드에서 DB 작업을 위해서 AdoTemplate을 사용하고 있는데, 조회된 결과를 조작하기 위해서 QueryWithRowMapperDelegate() 메소드를 호출하고 있다. 이 메소드의 인자로 앞 포스트에서 본 것과 유사한 익명 델리게이트 객체를 넘겨주고 있다. 콜백 객체와 콜백 함수 등 구조를 좀 더 명확히 하고 싶다면 익명 델리게이트 대신에 표준 구조로 변환해보길 권한다. 이 작업은 앞 포스트를 참조한다.

RowMapper를 사용하는 콜백 구조에서의 콜백 함수에서는 한 레코드에 대한 정보를 인자로 넘겨진 IDataReader 객체로부터 얻어서 반환에 필요한 값을 구성하고 구성된 값을 자신에게 남겨둘 필요없이 바로 반환해주면 된다. AdoTemplate에서는 반환된 값을 모두 차곡차곡 모아두었다가 클라이언트 코드( DAO 객체)넘겨준다. 


이제 알겠지만, Spring.NET에서 제공하는 방법은 전문적인 OR 매핑 방법은 아니다. 단지 Spring.NET에서 제공하는 방법을 사용하면 개발자가 수동으로 OR 매핑을 할 수 있는 기회를 제공받을 수 있다는 것이다. 개발자가 수동으로 해야 한다는 것은 불편한 일이다. 이렇든 저렇든 Spring.NET이 제공하는 이 3가지 OR 매핑 방법을 사용했을 경우, 혹시라도 DB 테이블의 컬럼이 변경되거나 사용자 정의 객체의 구조가 변경되면 소스 코드를 다시 빌드해야 하는 것은 피할 수 없다. 그러나 이런 불편은 또 프레임워크에서 질색을 하는 단점중의 하나이다.  좀 더 전문적인 OR매핑툴 NHibernate을 사용하면 좀 더 발전된 매핑 작업을 할 수 있지 않을까 기대해본다.  그럼 다음 포스트에서. 아니 모르겠다. 트랜잭션을 먼저 해야 할지. 이것을 먼저 해야 할지. 먼저 준비되는 것부터 하기로 한다.

Posted by dalbong2

앞에서 AdoTemplate을 이용하는 코딩 구조를 알아봤다. 이 포스트에서는 AdoTemplate를 이용해서 DB 데이터에 액세스하는 코드를  살펴본다. Dao 객체, AdoTemplate의 Execute를 호출하기, 이 호출시 콜백 객체( ICommandCallback 객체 또는 CommandDelegate 객체)를 넘겨주기, 콜백객체에서 AdoTemplate에서 넘겨준 command 객체를 이용해서 DB에 접근하기 등의 과정을 염두에 두면서 코드를 따라가 보자. 레이어관점에서 봤을때 DAO객체나 콜백객체 그리고 Spring.NET의 AdoTemplate는 모두 데이터 액세스 레이어에 속하는 객체들이다.


▶  AdoTemplate를 이용하는 샘플 코드


AdoTemplate를 이용할때 개발자가 개발해야 하는 부분은 무엇인가.  비즈니스 설계에 맞게 DAO 객체를 만들어야 하고 그리고 콜백 객체를 만들어야 한다.  Spring.DataQuickStart.2005 샘플 프로젝트에는 샘플 DAO 객체와 콜백 객체가 구현되어 있다.

많은 페이지가 있지만, 그 중에서 CommandCallbackDao.cs 페이지를 보자.  이 페이지에는 CommandCallbackDao 타입의 DAO를 객체를 정의하고 있고 그리고 ICommandCallback, CommandDelegate 타입의 콜백 객체의 사용을 모두 보여주고 있다. 


▶ 콜백객체로 ICommandCallback 객체 사용하기


먼저 콜백객체로 인터페이스 ICommandCallback를 사용하는 경우를 보자.

private class PostalCodeCommandCallback<T> : ICommandCallback<T> where T : ResultObject, new()

{

    private string postalCode;

    public PostalCodeCommandCallback(string postalCode)

    {

        this.postalCode = postalCode;

    }


    public T DoInCommand(DbCommand command)

    {

        T resultObject = new T();


        // 예제에서는 명령문을 지정하는 부분이 빠져 있다. 에러다.

        command.CommandText = cmdText;
        command.CommandType = CommandType.Text; //CommandType.StoredProcedure;

        DbParameter p = command.CreateParameter();

        p.ParameterName = "@PostalCode";

        p.Value = postalCode;

        command.Parameters.Add(p);


        resultObject.count = (int)command.ExecuteScalar();

        return resultObject;

    }

}

제너릭 타입의 ICommandCallback 인터페이스를 상속해서 PostalCodeCommandCallback 타입이 구현되고 있다.  이 구현체의 DoInCommand() 메소드를 AdoTemplate객체의 내부에서 콜백하게 된다.  DoInCommand 메소드 내부네서는 command 객체를 이용해서 실제로 DB 작업을 하게 된다. DbCommand 객체 command에는 이미 DB작업에 필요한 커넥션 정보와 명령( sql, 저장 프로시져 등)이 있다. 마지막으로 DAO 객체에서 넘겨준 값 postalCode를 이용해서 파라미터를 구성해서 command 객체에 넘겨주면 된다. 그 다음 command 객체의 메소드 ExecuteScalar()를 호출해서 실제 DB 작업을 하고 반환값을 받아온다.  참고로 현재 샘플 소스에서는 붉은 색 부분의 코드가 빠져 있다. command 객체에 전달할 명령문cmdText와 명령문의 타입을 지정해줘야 한다. 이 부분을 콜백 객체 외부로부터 전달받던 내부에 하드 코딩하던 이 정보가 있어야 한다.

제너릭 타입 T는 DoInCommand 메소드의 반환값 타입으로 사용되는데,  where 절 부분을 보면 제너릭 타입 T의 구체적인 타입으로 ResultObject 타입을 사용하고 있다.  그리고 그 정의는 다음과 같이 되어 있다. int는 제너릭 타입으로 사용될 수 없기 때문에 이런 래핑 타입이 필요하다.

public class ResultObject

{

    public int count;

}

단순히 int 값을 가지고 있는 타입이긴 하지만 현실의 실제 프로젝트에서의 반환값은 이 보다 더 복잡한 정의가 될 것이다.

이제 정의된 콜백 객체를 이용해서 DAO 객체에서 AdoTemplate 객체를 호출하는 코드를 보자.

public class CommandCallbackDao : AdoDaoSupport

{


    private string cmdText = "select count(*) from Customers where PostalCode = @PostalCode";


    ...


    public virtual int FindCountWithPostalCode(string postalCode)

    {

        // Type inference allows you not to explicitly write .Execute<ResultObject>


        return AdoTemplate.Execute(new PostalCodeCommandCallback<ResultObject>(postalCode)).count;

    }

    ...

DAO 객체 CommandCallbackDao의 FindCountWithPostalCode() 메소드를 보자. AdoTemplate 객체의 Execute() 메소드를 호출하고 있는데 이 AdoTemplate은 CommandCallbakDao 타입이 상속받고 있는 부모 타입 AdoDaoSupport 타입의 속성으로 정의되어 있다.

AdoDaoSupport 타입의 AdoTemplate 속성을 호출하면 AdoTemplate 타입의 객체가 반환된다. AdoDaoSupport 타입의 AdoTemplate 속성에 대한 정의를 보면 다음과 같다.

public class AdoDaoSupport : DaoSupport

{

    private AdoTemplate adoTemplate;

    ...

    public AdoTemplate AdoTemplate

    {

        set

        {

            adoTemplate = value;

        }

        get

        {

            return adoTemplate;

        }


    }

    ...

샘플 프로젝트에서는 AdoTemplate 속성에 인스턴스를 할당하는 작업에 Spring.NET 컨테이너의 Inversion of Control 기능을 사용하고 있다. DAO 객체가 정의되어 있는 Spring.DataQuickStart.2005 프로젝트를 사용하는 클라이언트 프로젝트 Spring.DataQuickStart.Test.2005 프로젝트를 보면 환경 설정 파일이 있다. DataQuickStart/GenericTemplate 폴더에 있는 ExampleTests.xml 파일을 보자. 다음은 그 일부이다.

<object id="commandCallbackDao" type="Spring.DataQuickStart.Dao.GenericTemplate.CommandCallbackDao, Spring.DataQuickStart">

  <property name="AdoTemplate" ref="adoTemplate"/>

</object>

String.DataQuickStart.Dao.GenericTemplate 네임스페이스하의 CommandCallbackDao 객체를 사용할때는 그 속성 AdoTemplate에 "adoTemplate"라는 id로 참조하고 있는 객체를 자동으로 할당하라는 표시이다. 참조하고 있는 객체를 따라가 보면 다음과 같은 정의가 있다.

<object id="adoTemplate" type="Spring.Data.Generic.AdoTemplate, Spring.Data">

  <property name="DbProvider" ref="dbProvider"/>

  <property name="DataReaderWrapperType" value="Spring.Data.Support.NullMappingDataReader, Spring.Data"/>

</object>

id가 "adoTemplate"로 설정되어 있는 객체에 대한 설정을 보면 Spring.Data.Generic 네임스페이스하의 AdoTemplate에 대한 정의를 나타내고 있다. 이 정의의 DbProvider라는 속성은 다시 "dbProvider"로 정의되어 있는 객체를 참조하고 있다. 역시 AdoTemplate객체가 인스턴스화될때에는 이 id로 정의되어 있는 객체가 자동으로 인스턴스화되어 속성에 할당될 것이다. 참조하고 있는 dbProvider라는 id의 객체 정의를 보면 다음과 같다.

<db:provider id="dbProvider"

              provider="SqlServer-2.0"

              connectionString="Data Source=.\SQL2005;Initial Catalog=Northwind;Persist Security Info=True;User ID=springqa;Password=springqa"/>

provider값으로 "SqlServer-2.0"을 사용하고 있는데 이 값은 .NET V2.0에 있는 MS SQL 서버 , provider v2.0.0.0을 프로바이더로 사용하고 있다는 의미이다. 그리고 이 요소에는 DB 연결정보도 있다. 

Spring.NET 컨테이너가 DAO 객체 CommandCallbackDao 객체의 인스턴스를 생성하면 결국 DB에 연결하기 위해서 필요한 정보를 갖는 AdoTemplate객체의 인스턴스도 자동 생성되어 CommandCallbackDao 객체의 AdoTemplate 속성에 할당되게 된다.

이제 다시 CommandCallbackDao 객체의 FindCountWithPostalCode() 메소드를 호출하는 부분으로 가 보자. 이 메소드에서는 AdoTemplate 객체의 Execute() 메소드를 호출하면서 인자로 이전에 정의한 콜백객체 PostalCodeCommandCallback<ResultObject>객체를 넘겨주고 있다. 이때 DB 작업에 필요한 변수값 postalCode도 함께 넘겨준다. 그럼 AdoTemplate에서는 DbCommand 객체를 정의해서 ICommandCallback에서 정의한 DoInCommand()를 호출하는데 사용한다. 그 이후는 앞의 코드에서 보는대로이다.


▶ 콜백객체로 CommandDelegate 타입의 객체 사용하기


public class CommandCallbackDao : AdoDaoSupport

{


    private string cmdText = "select count(*) from Customers where PostalCode = @PostalCode";


    /// <summary>

    /// Finds the number of customers with the given postal code.

    /// </summary>

    /// <param name="postalCode">The postal code.</param>

    /// <returns>Number of customers with the given postal code.</returns>

    public virtual int FindCountWithPostalCodeWithDelegate(string postalCode)

    {

        // Using anonymous delegates allows you to easily reference the

        // surrounding parameters for use with the DbCommand processing.


        return AdoTemplate.Execute<int>(delegate(DbCommand command)

               {

                   // Do whatever you like with the DbCommand... downcast to get

                   // provider specific funtionality if necesary.

 

                   command.CommandText = cmdText; 

                   DbParameter p = command.CreateParameter();

                   p.ParameterName = "@PostalCode";

                   p.Value = postalCode;

                   command.Parameters.Add(p);

 

                   return (int)command.ExecuteScalar();

 

               });

    }

    ...

AdoTemplate객체에서 사용하고 있는 Execute() 메소드는 다음과 같은 정의의 버전을 사용하고 있다.

public T Execute<T>(CommandDelegate<T> del)

CommandDelegate<T> 타입의 델리게이트 객체를 제공하기 위해서 익명 델리게이트 객체를 사용하고 있다. 익명 델리게이트 객체? 지금 Execute 메소드의 파라미터를 넘겨받는 괄호( )안에서 인라인 형식으로 델리게이트가 가리킬 코드를 정의하고 있다. 이 코드는 DbCommand 객체를 넘겨받는 메소드이다. 이 코드 자체를 Execute 메소드의 인자로 넘기는 것이 아니라 이 코드가 정의하고 있는 메소드를 가리키고 있는 델리게이트 객체를 넘기고 있는 것이다.

앞의 익명 델리게이트를 사용하는 코드를 완전한 모습으로 재정의해서 구성하면 다음과 같은 유사한 모양이 될 것이다. 

public virtual int FindCountWithPostalCodeWithDelegate(string postalCode)

{


    CallbackObject<ResultObject> callbackObject = new CallbackObject<ResultObject>(cmdText, postalCode);

    return AdoTemplate.Execute<ResultObject>(

        new CommandDelegate<ResultObject>(callbackObject.CallbakcMethod ) ).count;


}

private class CallbackObject<T> where T : ResultObject, new()

{

    string cmdText = string.Empty;

    string postalCode = string.Empty;

    public CallbackObject(string cmdText, string postalCode)

    {

        this.cmdText = cmdText;

        this.postalCode = postalCode;

    }


    public T CallbakcMethod(DbCommand command)

    {

        T resultObject = new T();

        command.CommandText = cmdText;

        DbParameter p = command.CreateParameter();

        p.ParameterName = "@PostalCode";

        p.Value = postalCode;

        command.Parameters.Add(p);

        resultObject.count = (int)command.ExecuteScalar();

        return resultObject;


    }

}

콜백 메소드에서 정의한 코드가 간단하다면 익명 델리게이트 표현을 사용한 방법을 사용해서 콜백 객체의 타입과 메소드를 만들지 않아도 될 것이다.  그러나 실제 현실 프로젝트에서는 코드의 통일성이 중요하기 때문에 다소 복잡하더라도 이런 정식 표현이 더 바람직할 것으로 보인다. 이렇게 원래의 모습으로 변형시켜 놓으면 이제 ICommandCallback 타입의 콜백객체를 사용할때와 구조는 유사하게 된다.

지금까지는 AdoTemplate 객체와 콜백 객체를 사용해서 DB 데이터에 접근하고 조작하는 구조에 대한 이야기였다.

AdoTemplate의 Execute형의 메소드를 사용하면  DB의 데이터를 조작하고 쿼리하는 모든 작업을 할 수 있다. 그러나 AdoTemplate에는 데이터 쿼리를 위한 좀 더 특별한 메소드 타입이 더 있다.  QueryWith를 접두어로 하고 있는 메소드류가 그것인데, 이 타입의 메소드를 사용하면 OR 매핑( Object Relational Mapping )이라는 작업을 할 수 있다. 이 OR 매핑 작업을 해주면 조회되는 레코드를 애플리케이션단에서 정의하고 있는 객체로 변환시킬 수 있게 된다. 이 방법을 사용하면 데이터 액세스 레이어의 DAO 객체로 DataSet 대신에 사용자 정의 객체 집합을 반환해 줄 수 있게 된다.  이 방법에 대해서는 다음 포스트에. 휴~ !

Posted by dalbong2

일반적인 기업형 애플리케이션은 대부분 N티어 구조를 갖는다.   다음 그림은 간단한 N티어 애플리케이션을 표현하고 있다. 참고로  티어는 물리적인 의미이고 레이어는 논리적인 의미라고 한다.

물리적으로 UI 레이어는 웹 애플리케이션의 경우는 웹 서버 그리고 윈폼 애플리케이션은 클라이언트 PC가 될 것이다. 그리고 서비스 레이어와 데이터 액세스 레이어는 보통 하나의 미들티어 서버에 존재한다. 물론 더 복잡한 물리적 구조로도 존재할 수 있다. 

Spring.NET의 IoC 컨테이너는 UI 애플리케이션 서버(PC)에서도 적용가능하고 미들티어 서버에서도 적용가능하다. 앞 포스트까지는 UI를 제공하는 웹 서버/사용자 PC 또는 미들티어 서버에서 실행될 수 있는 IoC 컨테이너에 대한 얘기를 했다. 그리고 UI 서버와 비즈니스 서버간의 통신 방법중의 하나인 웹 서비스에 대한 Spring.NET의 지원 얘기도 했다.

이제 남아 있는 큰 주제는 Spring.NET의 트랜잭션과 데이터 액세스 지원 얘기이다. 트랜잭션 이야기를 먼저 할지 데이터 액세스 이야기를 먼저 할지 고민하다 데이터 액세스부터 하기로 결정했다. 왜냐면 참고 문서를 읽다 보니까 그쪽이 먼저 이해가 되었다. 


■ Spring.NET의 Data액세스 지원


Spring.NET에서는 DB에 접근할 때 ADO.NET 기술을 이용할 수도 있고, NHibernate기술을 이용할 수도 있다.  우선 ADO.NET을 이용하는 기술을 알아본다. ADO.NET이 제공하는 .NET 프레임워크의 표준 API를 이용해서 DB에 접근할 수도 있다. 그렇지만 Spring.NET에서는 ADO.NET 기술을 한번 래핑한 API를 제공하는데 이 API를 사용하면 편리하다.

Spring.NET에서 제공하는 래핑 API도 두 종류로 구분할 수 있다. 하나는 "template"기반의 방식이고 하나는 객체 지향 기반의 방식이다(  이런 이름이 붙여진 것이 이해는 개인적으로 이해는 된다. 그렇지만 불행히도 지금 이것을 말로 표현할 정도는 아니다). 여튼 하나는 템플릿 방식이고 하나는 객체 지향 방식인가보다 -_-;; 이 방식에 따라서 사용자의 DB 접근 프로그래밍 스타일이 달라진다.

"template"기반의 방식을 이용하면 DB에 접근해서 작업을 하는데 AdoTemplate라는 클래스를 사용하게 된다. 즉 데이터 액세스 레이어의 객체( Data Access Object, 이하 DAO로 표현한다 )는 AdoTemplate객체를 사용해서 모든 DB 작업을 하게 된다.

객체 지향 기반의 DB 접근 방식에서는 DB에 대한 작업을 구분해서 각 작업을 클래스화했다. DB에 대한 작업은 그 성격에 따라 C( Create), R( Read), U( Update), D( Delete ) 나뉠 수 있다. 이 중에서 CUD작업은 DB에 영향을 미치기는 하나 조회하는 값이 없다. 이런 작업을 위해서 AdoNonQuery 타입을 제공하고 있고 그리고 읽기 전용의 작업을 위해서 AdoQuery 타입을 제공한다. 그리고 저장 프로시져를 이용하는 작업을 위해서 StoredProcedure라는 타입을 제공하고 있다. 그러나 이 방식의 DB 접근은 아직 널리 사용되지는 않은 모양이다.

이 포스트에서는 AdoTemplate객체를 이용하는 "template"기반의 방식를 설명할 것이다. 만약 객체 지향 기반의 프로그래밍 스타일을 알고 싶다면 Spring.NET에서 제공하는 레퍼런스 문서의 20.15절을 참고하기 바란다( 링크하나 걸어주고 싶은데 맘뿐이다. 쓰으...).


▶ AdoTemplate이용 구조


AdoTemplate를 이용해서 DB 데이터에 액세스하는 작업을 초 간단히 개념적으로 그렸다.

클라이언트 코드라 함은 여기서는 DAO( 데이터 액세스 객체)가 된다. 클라이언트 코드는 Spring.NET 프레임워크의 AdoTemplate에서 콜백될 객체를 넘겨준다. "콜백된다"는 것은 "다시 호출된다"는 의미로 쉽게 생각하자. 그러니까 AdoTemplate객체는 클라이언트 코드에서 넘겨받은 "콜백 객체를 다시 호출"하게 된다. 물론 AdoTemplate도 콜백객체를 어떻게 호출할지를 사전에 알 수 있다. 어떻게 아는지는 뒤에 보자. 야튼 이때 콜백 객체를 호출할때 AdoTemplate은 command 객체라는 것을 생성해서 호출 인자로 넘겨준다. command 객체에는  DB 액세스에 필요한 커넥션 정보가 포함되어 있다. 그리고 호출하는 클라이언트 코드의 트랜잭션 컨텍스트를 바탕으로 한 트랜잭션 정보도 설정되어 있다. AdoTemplate 객체를 작업하면 개발자가 작성한 소스에 DB 커넥션 정보나 트랜잭션 정보를 설정하는 코드는 없어도 된다는 이야기다.  콜백 객체에서는 넘겨받은 command 객체를 이용해서 이제 DB를 대상으로 실행 명령( sql문 또는 저장 프로시져 등 )을 수행하면 된다. 

방금 AdoTemplate 객체가 클라이언트 코드에서 넘겨받은 콜백 객체를 어떻게 호출할 수 있는지 알 수 있다고 했다. 이것은 AdoTemplate에서도 알고 있는 인터페이스와 델리게이트를 사용하기때문이다. 다시 말하면 클라이언트 코드에서 넘어오는 사용자 정의 콜백 객체는 AdoTemplate도 인식할 있는 사전에 정의한 인터페이스 또는 델리케이트를 구현한 객체여야 한다는 조건이 있다. 그 인터페이스와 델리게이트의 타입으로 ICommandCallback, CommandDelegate 정의가 되어 있다. 아래 그림에서는 이런 인터페이스 또는 델리게이트 기반의 콜백 구조를 보여주고 있다.


▶ ICommandCallback, CommandDelegate 기반의 콜백 구조


클라이언트 코드에서는 AdoTemplate객체의 Execute() 메소드를 호출할때 콜백 객체를 넘겨준다. 다음 그림을 보자. 

 

AdoTemplate 클래스에는 Execute() 메소드가 파라미터 타입에 따라서 여러 버전이 존재한다. 즉 ICommandCallback 또는 CommandDelegate이외의 다른 인터페이스, 델리게이트 객체를 받을 수도 있다는 것이다. 그림에서는 그 중에서 대표적인 두 개의 Execute() 버전을 보여주고 있다. 하나는 인터페이스 ICommandCallbakc 타입의 객체 c 를 받고 다른 하나는 델리게이트 CommandDelegate 객체 d를 받고 있다.

ICommandCallback 파라미터를 받는 버전을 먼저 보자. 우선 호출하는 코드의 컨텍스트에서 정보( DB 커넥션 정보, 트랜잭션 정보)를 구해서 command 객체를 생성한다. 그런 다음 파라미터로 받은 ICommandCallback 객체 c의 DoInCommand() 메소드를 호출하면서 콜백 객체로 방금 생성한 command객체를 넘겨준다. 이제 사용자 정의 콜백객체의 DoInCommand()에서는 넘겨받은 command객체를 이용해서 DB에대해서 명령을 수행한다.  이때 command객체에는 DB 액세스에 필요한 커넥션 정보, 실행 명령(sql, 저장 프로시져등)가 있다.

CommandDelegate 타입의 객체 d를 받는 Execute() 버전도 유사하게 작동한다. 역시 호출하는 클라이언트 코드의 컨텍스트에서 필요한 정보를 얻어서 command 객체를 생성한다. 그런 다음 넘겨받은 델리게이트 객체 d를 호출해서 command 객체를 콜백 객체로 넘겨준다. command 객체를 넘겨받은 콜백 객체의 사용자 정의 콜백함수에서는 command 객체를 이용해서 DB 액세스를 하게 된다. 이제 이 콜백 구조를 이용하는 샘플 코드를 보자.


▶  AdoTemplate를 이용하는 샘플 코드


이건 다음에 하자.

Posted by dalbong2

Spring.NET IoC 컨테이너나 Spring.NET이 지원하고 있는 AOP 프로그래밍에 대해서 아직 해야할 얘기는 남아있다. 객체 타입 즉 singleton, prototype으로 설정하는 방법 및 객체의 생명주기에 대한 얘기, Attribute를 이용해서 AOP를 구현하는 방식등등. 필요한 얘기이기는 하지만 나중에 하기로 하자. 우선 전체적인 애플리케이션을 구성하는 구성 기술들을 Spring.NET이 어떻게 지원하는지를 알아본다.


■ Spring.NET 웹 서비스 구조


먼저 Spring.NET이 .NET의 Web Services를 어떻게 보완, 지원해주는지 알아보자. Visual Studio.NET 또는 WSDL 커맨드 툴을 이용해서 클라이언트측 프락시를 만들어서 웹 서비스를 사용했던 기존의 구조는 다음처럼 표현할 수 있겠다.

Spring.NET 팀에서는 이 구조가 문제가 있었다고 봤다는 거다. 문제라기 보다는 좀 더 효율적인 구조로 가고 싶었다는 것이다.

기존의 구조에서는 클라이언트측에서 생성되는 프락시가 곧바로 클래스로 구현되었다는 것이다. WSDL기반의 클래스이다. Spring.NET 팀에서는 프락시가 곧바로 클래스로 구현되는 대신에 서비스 인터페이스를 구현하는 구조로 가기를 원했었다. 그렇게 가면 추후에 웹 서비스 구현체의 수정을 좀 더 유연하게 할 수 있다는 것이다.

그림에서 "웹 서비스용 클래스"란 WebService, WebMethod같은 어트리뷰트를 가지고 있는 클래스를 말한다. 즉 일반 클래스에 웹 서비스 목적의 어트리뷰트가 섞인 상태이다.

Spring.NET 팀에서는 다음과 같은 구조의 웹 서비스 구조를 구현했다.

우선 서버측을 보면 "PONO"라는 것이 있다. 이것은 "plain old .NET object" 약자인데, 일반 클래스를 말한다. 자바 버전의 Spring에서는 POJO라는 용어를 사용하고 있다. 앞의 그림의 "웹 서비스용 클래스"라는 용어와 대비된다. 웹 서비스용 클래스에는 웹 서비스 노출에 필요한 어트리뷰트가 섞인 웹 서비스 전용 클래스이다. 그러나 PONO는 사용자 정의의 일반 클래스 객체이다. 즉 Spring.NET 웹 서비스에서는 웹 서비스로 노출시키기 위한 특별한 클래스가 존재하는 것이 아니라 일반 타입의 객체를 사용하고 있다는 것이다. 이 PONO는 웹 서비스로 노출될 수 있지만, 코드를 수정하지 않고 그대로 .NET Remoting, Enterprise Service(COM+)에 사용될 수 있다는 것을 Spring.NET 팀에서는 목표로 했다는 것이다.

그러나 웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의하고 그것을 구현하고 있어야 한다. 만약 웹 서비스로 노출시키고 싶은 메소드와 .NET Remoting으로 노출시키고 싶은 메소드가 다르다면 다른 인터페이스를 사용하면 된다. 여러 개의 인터페이스를 구현하는 PONO에서 어떤 메소드를 노출시킬지를 인터페이스를 선택함으로써 변경할 수 있다.

그러나 Spring.NET이 웹 서비스 인프라를 완전히 새롭게 구현한 것은 아니다. 이 말은 웹 서비스 타겟용 객체로 PONO를 사용해도 결국은 WebService 또는 WebMethod 같은 어트리뷰트로 꾸며진 웹 서비스용 클래스를 만들어내야 한다는 것이다. 그림에 표현된 서버측 프락시는 이렇게 만들어진 최종 웹 서비스용 클래스이다. 클라이언트측에서는 이 서버측 프락시를 바라보게 된다.

다음은 클라이언트측 프락시 구조에 대해서 알아보자. 클라이언트측에서는 여전히 WSDL 기반의 프락시를 사용한다. 이것은 여전히 클래스로 구현된다. 그러나 서비스 인터페이스를 상속해서 또다른 프락시를 만들어 내고 있다. 해서 Spring.NET이 만들어내는 프락시는 프락시의 프락시( proxy for proxy )인 셈이다. 클라이언측 코드에서는 이 두번째 프락시 객체를 참조하게 된다. 서버측 및 클라이언트측의 프락시는 실행시 동적으로 만들어진다.

최종적으로 이런 구조를 만들어내기위해서 Spring.NET에서는 서버측과 클라이언트측에서 프락시 객체를 만들어내는 객체를 제공한다. 이 객체들은 팩토리 패턴을 이용해서 프락시 객체를 만들어낸다.

서버측 프락시 생성 객체 : WebServiceExporter, 클라이언트측 프락시 생성 객체 : WebServiceProxyFactory


■ Spring.NET의 프락시용 팩토리 객체


▷ WebServiceExporter

이 타입은 Spring.Web.Services 네임스페이하에 정의되어 있다. 서버측 프락시 생성 객체의 타입 이름을 보면 "웹 서비스를 노출"시키는 기능을 하는 객체라는 것을  표현하고자 한 것 같다. 이것은 PONO를 래핑하고 있는 서버측 프락시 객체를 웹 서비스로 노출시키는 객체로 해석될 수 있겠다. PONO 객체에 대한 정보를 받아들여서 클라이언트에 노출되는 웹 서비스용 클래스를 만들어내는데, 이 결과물이 서버측 프락시 객체이다.

▷ WebServiceProxyFactory

이 객체는 WSDL 기반의 프락시 클래스를 만들고 다시 서비스 인터페이스 정보를 이용해 클라이언트에서 사용할 웹 서비스 프락시 객체를 만들어낸다. (WebServiceProxyFactory를 사용하면 웹 서비스 메소드에서 반환하는 사용자정의 타입도 클라이언트측에 자동 생성해줄 수 있는 건가? 이것은 나중에 해봐야 겠다. )

런타임시 Spring.NET은 이 두 객체를 이용해서 앞의 그림 같은 최종 구조를 만들어내는 것이다. 런타임시의 웹 서비스 최종 구조가 생성되는 과정과 실행 구조를 보면 다음과 같다.

클라이언트 코드에서는 WSDL 정보와 서비스 인터페이스에 대한 정보를 WebSeriviceProxyFactory에 전네주고 프락시 객체를 요청한다(1번 절차). 팩토리 객체는 전달받은 정보를 이용해서 프락시 객체를 동적으로 생성해서 건네준다(2번절차). 이때 WSDL 정보는 노출된 웹 서비스에 접근할 수 있는 URL 형태로 주면 된다. 이 과정이 프로그램적으로 될 수도 있겠지만 설정을 이용할 수도 있다. 그래서 WebServiceProxyFactory를 <object/>로 설정하고 이 객체를 IoC 컨테이너에 요청하게 되면 이 팩토리 객체 자체에 대한 참조가 반환되는 것이 아니라 이 팩토리 객체가 생성한 객체 즉 클라이언트측 프락시에 대한 참조가 반환된다.

클라이언트 코드에서는 IoC 컨테이너에서 절달받은 프락시 객체를 통해서 서버측으로 HTTP 요청을 보낼 수 있다(3번 절차). 이 요청은 일반 ASP.NET 웹 서비스 처리 핸들러가 받는 것이 아니라 Spring.NET에서 제공하는 핸들러가 받도록 설정을 변경해야 한다. Spring.NET의 웹 서비스 핸들러는 IoC 컨테이너에게 PONO 정보를 건네주면서 WebSerivceExporter 객체를 요청한다(4번절차). IoC 컨테이너는 이 요청에 대해서 WebServiceExporter객체를 직접 보내주는 것이 아니라 PONO를 이용해서 웹 서비스용 클래스를 동적으로 만들어내서 핸들러에게 반환해준다(5번 절차). 핸들러는 클라이언트에서 요청한 서비스를 프락시에 요청하고 나서(6번 절차) 그 반환값을 이용해서 HTTP 반응을 만들어서 클라이언트 프락시로 보낸다(7번 절차). 클라이언트측 프락시는 결과를 클라이언트 코드로 넘겨주게 되고 이로써 한 사이클의 웹 서비스 요청/반응이 끝나게 된다. 휴 ~~.

따라서 객체들의 참조 구조를 간단하게 정리하면 다음과 유사한 구조로 되겠다.


■ 서버측에서 PONO를 웹 서비스로 노출하기


앞에서 팩토리 객체들을 프로그램적으로 호출해서 프락시 객체에 대한 참조를 얻을 수도 있지만, 설정을 통해서도 가능하다고 했다. 이 포스트에서는 설정을 통해서 얻는 방법을 알아본다. 왜냐고? 첫번째는 내맘이고, 둘째는 설정을 통한 방법에 익숙해지는 것이 IoC 컨테이너에 대한 개념이 좀 더 빨리 그려질 수 있을 것 같아서이다. 세번째는 설정을 통한 방법을 이해하면 API를 이용하는 방법은 혼자서도 할 수 있다(아닌가).

<objects xmlns="http://www.springframework.net">

  <object id="helloWorld" type="HelloWorldApp.HelloWorldService, HelloWorldApp">

    <property name="Message" value="안녕하세요!"/>

  </object>

  <object id="HelloWorldService" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="helloWorld"/>

    <property name="Namespace" value="http://MySpringSample_WS/HelloWorldService"/>

  </object>

</objects>


<system.web>

  <httpHandlers>

    <add verb="*" path="*.asmx"

        type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>

  </httpHandlers>

  <httpModules>

    <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>

  </httpModules>

</system.web>

첫번째 <object/>는 웹 서비스로 노출될 PONO 객체에 대한 정보이다. HelloWorldApp 네임스페이 아래에 HelloWorldService라는 이름의 클래스를 웹 서비스의 타겟 객체로 지정하고 있다. 이 객체에 대한 정의는 다음과 같다. 

namespace HelloWorldApp

{

    public interface IHelloWorld

    {

        string HelloWorld();

    }


    public class HelloWorldService : IHelloWorld

    {

        private string message;

        public string Message

        {

            set { message = value; }

        }


        public string HelloWorld()

        {

            return this.message;

        }


        public string SayNo()

        {

            return "No";

        }

    }

}

웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의해야 한다고 했다. 코드에서는 IHelloWrold라는 인터페이스를 정의해서 구현하고 있다. 이 인터페이스는 PONO 객체의 메소드중에서 웹 서비스로 노출시킬 메소드를 지정하는 역할을 하기도 한다. 이 HelloWorldService에는 두 개의 메소드가 있지만, 이 코드에서는 메소드 HelloWorld()만을 노출시키고 있는 것이다. 설정에서는 PONO 객체 HelloWorldService를 id를 "helloWorld"로 지정하고 있다.

다음 <object/>에는 이 PONO에 대한 웹 서비스용 프락시 클래스를 만들어 낼 WebServiceExporter에 대한 정의가 있다. 어떤 PONO에 대한 프락시를 생성할지를 첫번째 <property/>의 TargetName으로 표현하고 있다. 이 설정에서는 앞에서 설정한 HelloWorldService에 대한 id를 지정해주고 있다. 설정에서는 노출될 웹 서비스의 네임스페이스도 지정해주는 설정이 있다.


■ 웹 서비스 요청에 대한 Spring.NET Http Handler 지정하기


Spring.NET은 웹 서비스에 대한 요청을 핸들링하는 사용자 정의 핸들러를 제공하고 있다: Spring.Web.Services아래에 있는 WebServiceHandlerFactory 이다. 앞의 설정에서는 클라이언트로부터의 asmx 파일에 대한 요청이 오면, ASP.NET가 제공하는 웹 서비스 핸들러 대신에 Spring.NET이 제공하는 WebServiceHandlerFactory가 작동하도록 하고 있다.

클라이언트에서 올라오는 asmx에 대한 HTTP요청에는 어떤 WebServiceExporter에 대한 요청인지를 알 수 있는 정보가 있다.

클라이언트에서 오는 HTTP 요청은 다음과 같은 형식의 URL을 갖는다. 

http://서버/~/서비스명.asmx

서비스명 : 서버측에 지정된 WebServiceExporter 객체의 id

앞의 요청 URL의 형식에서 "서비스명"에 해당하는 값이 web.config에서 지정된 WebServiceExporter의 id에 해당한다. asmx에 대한 요청을 받은 핸들러 WebServiceHandlerFactory는 요청 URL로부터 WebServiceExporter에 대한 정보를 얻고 IoC 컨테이너에 이 객체를 요청한다. IoC는 WebServiceExporter에 설정된 PONO 객체에 대한 정보를 통해서 프락시 객체를 생성해서 핸들러에게 건네줄 것이다. 이제 SOAP 프락시로부터 호출할 메소드, 메소드에 전달할 인자에 대한 정보를 구해서 프락시는 요청한 서비스 메소드를 호출하고 결과를 클라이언트에 보내주면 된다.


■ 클라이언트측의 WebServiceProxyFactory 객체 설정하기

이제 클라이언트에서의 WebServiceProxyFactory에 대한 설정을 보자. 서비스 인터페이스에 대한 정보와 노출된 웹 서비스에 대한 URL을 팩토리 객체에 설정하면 된다.

<object id="helloWorldService"

    type="Spring.Web.Services.WebServiceProxyFactory, Spring.Services">

  <property name="ServiceUri" value="http://localhost/HelloWorldWeb/helloworldservice.asmx"/>

  <property name="ServiceInterface" value="HelloWorldApp.IHelloWorld, HelloWorldApp"/>

</object>

이 설정에서는 helloworldservice.asmx를 ServiceUri 속성에 지정하고 있다. 이 값은 앞에서도 말한것처럼 서버측에 가서는 helloworldserivce라고 지정된 WebServiceExporter 객체를 찾는데 사용된다. 그리고 사용할 서비스 인터페이스로는 IHelloWorld로 지정하고 있다.

이제 이 팩토리 객체를 이용하는 코드이다.

IApplicationContext ctx = ContextRegistry.GetContext();


IHelloWorld helloWorld = (IHelloWorld )ctx.GetObject("helloWorldSerivce");

helloWorld.HelloWorld();


■ Spring.Calculator.Web.2005  예제 보기


앞 포스트에서도 보았지만 Spring.Calculator.Web.2005 프로젝트를 실행하면 default.aspx 페이지가 실행된다. 

default.aspx 페이지를 보면 다음과 같다.

<td align="center">

  <h2>

    <a href="calculatorService.asmx">CalculatorService</a>

  </h2>

  <br />

  <h2>

    <a href="calculatorServiceWeaved.asmx">CalculatorServiceWeaved</a>

  </h2>

</td>

물론 Spring.Calculator.Web.2005 프로젝트를 보면 asmx 웹 서비스 페이지는 없다.

이 예제에서는 웹 서비스의 클라이언트측 프락시는 사용하지 않고 있다. ASP.NET에서 제공하는 테스트 페이지가 클라이언트 코드 역할을 한다.

이 예제는 서버측의 WebServiceExporter만을 설정해서 사용하고 있는데, web.config를 보면 다음과 같은 설정이 있다. 

<context>

  <resource uri="config://spring/objects"/>

  <resource uri="~/Config/webServices.xml"/>

  <resource uri="~/Config/webServices-aop.xml"/>

</context>

설정이 webServices.xml과 webServices-aop.xml에 분리되어 있다. 설정을 이런식으로 분리할 수 있다는 것도 유용한 정보일 것이다. 이 두 파일을 보면 그곳에 서버측 프락시 팩토리에 대한 설정이 있다. 이 중에서 webServices-aop.xml의 내용이다.

<objects xmlns="http://www.springframework.net">


  <description>webServices-aop</description>


  <object id="calculatorServiceWeaved" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="calculatorWeaved" />

    <property name="Namespace" value="http://SpringCalculator/WebServices" />

    <property name="Description" value="Spring Calculator Web Services" />

  </object>


</objects>

id가 calculatorServiceWeaved  로 되어 있고, TargetName 속성은  calculatorWeaved 으로 되어 있다.  이 속성이 참조하고 있는 calculatorWeaved 는 web.config에 정의되어 있는 객체이다.  이제 클라이언트 코드에서 calculatorWeaved가 참조하고 있는 객체에 대한 메소드를 호출하면 서버측에서 처리할 수 있다.

Posted by dalbong2

■ advice 종류

▶ around advice

앞에서 알아본 CommonLoggingAroundAdvice 타입은 around advce중의 하나였다. 즉 인터페이스 IMethodInterceptor를 상속해서 Invoke() 메소드를 구현하고 있다. 아래는 IMethodInterceptor의 정의이다.

namespace AopAlliance.Intercept

{

    public interface IMethodInterceptor : ...

    {

           object Invoke(IMethodInvocation invocation);

    }

}

이 메소드의 인자로 넘어오는 invocation은 인터셉트된 타겟 객체에 대한 호출을 나타낸다. 이 인자의 Proceed() 메소드를 호출하면 인터셉트되어서 중단된 타겟 메소드 호출이 계속 진행된다.. Invoke()의 간단한 구현 코드이다.

...

public object Invoke(IMethodInvocation invocation)

{

    Console.WriteLine("Before invocation");

    object returnValue = invocation.Proceed();

    Console.WriteLine("After invocation and before return");

    return returnValue;

}

...

Proceed()가 호출되기 전에 원하는 작업을 할 수 있고, 호출된 후 그리고 반환값이 클라이언트 코드로 넘어가기 전에 또한 원하는 작업을 할 수 있다. 이곳에서 리턴되는 반환값을 조작해서 추가 정보를 넣거나 또는 제거할 수도 있다. 이처럼 타겟 메소드 호출 전, 후에 원하는 작업을 할 수 있는 advice를 around advice라고 한다.

참고로 이 advice가 설정되는 xml을 다시 보면 아래와 같다.

<!-- Aspect -->


<object id="commonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<!-- Applies AOP on the contact service. -->

<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>commonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

이렇게 설정해 놓으면 타겟 객체( calculator)의 메소드가 호출이 될때 commonLoggingAroundAdvice의 Invoke()에 의해서 인터셉트된다는 것이다. 가릿?

▶ before advice

before, after advice는 이름에서 예상되는 것처럼 호출전에 또는 호출된 후( 반환값을 반환하기전에)에만 끼어들기가 가능한 좀 더 간단한 advice이다.  before advice에서는 타겟 객체를 호출하기 위해서 Proceed()를 호출할 필요가 없다.  before advice를 구현하려면 인터페이스 IMethodBeforeAdvice를 상속해서 메소드 Before()를 구현해야 한다. 

public interface IMethodBeforeAdvice : ...

{

    void Before(MethodInfo method, object[] args, object target);

}

Before() 메소드는 타겟 메소드가 호출되기 전에 Spring.NET 프레워크에 의해서 호출된다. 이곳에서 필요한 사용자 정의 작업을 할 수 있다. method는 현재 인터셉트된 메소드에 대한 정보이고 args는 그 메소드를 호출할때 넘겨준 인자들에 대한 정보를 가지고 있다. 그리고 target은 타겟 객체에 대한 참조를 가지고 있다.

CommonLoggingAroundAdvice와 유사하게 CommonLoggingBeforeAdvice같은 advice를 구현해 놓고 앞에서처럼 설정을 하면 타겟 객체의 메소드들이 호출되기 전에 Before()가 호출된다는 것이다. 가릿? 가릿!

Before()를 수행하다 예외가 발생하면 이 예외는 이 메소드를 호출한 호출자로 전달된다.

▶ after advice

after advice 객체를 구현해서 설정해놓으면 타겟 메소드가 호출된 후에 클라이언트 코드로 반환되기 전에 호출될 수 있는데, 호출되는 메소드는 IAfterReturningAdvice 인터페이스를 구현한 객체의 AfterReturning()이다.

public interface IAfterReturningAdvice : ...

{

    void AfterReturning(object returnValue, MethodInfo method, object[] args, object target);

}

AfterReturning() 메소드에 전달되는 인자를 통해서 반환값, 타겟 메소드등에 대한 정보에 접근할 수 있다.

before advice에서는 예외가 발생하면 실행 경로를 역으로 진행해서 호출한 호출자에게 예외를 전달했다. 그러나 after advice는 실행 경로를 계속 유지한다. 그러나 리턴값을 반환하지 않고 예외를 반환한다.  실행 경로(excution path)란 advice 체인이 실행되는 순서를 말하는데 아래의 advice 체인에서 보여주는 그림을 참조하라.

▶ throws advice

throws advice 객체는 예상대로 타겟 객체에서 예외가 발생할때 호출되는 객체이다. 정확히는 throws advice가 적용된 후의 실행 경로상에서 예외가 발생할때 호출된다. 자세한 내용은 뒤의 "advice 체인"을 설명하는 곳을 참조한다.  이 녀석도 다른 녀석들처럼 throws advice가 되기 위해서는 상속해야 하는 인터페이스가 있다.

public interface IThrowsAdvice : IAdvice

{

}

근데 이 녀석은 다른 녀석들과는 다르게 구현해야 하는 메소드는 없다. 단지 구현되는 타입이 throws advice임을 나타내기만 하는 마커 인터페이스(marker interface 또는 태그 인터페이스 tag interface라고도 한다)이다. 그럼 예외가 발생했을때 Spring 프레임워크는 구현체의 어떤 메소드를 호출하게 될까. 구현체의 AfterThrowing() 메소드를 호출한다. Spring 프레임워크에 하드 코딩되어 있다고 볼 수 있겠다. AfterThrowing() 메소드에 전달되는 인자도 다음 두 유형중의 하나여야 한다.

void AfterThrowing(Exception ex)

void AfterThrowing( MethodInfo method, Object[] args, Object target, Exception ex)

다음은 실제 구현체에 대한 간단한 예제이다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex) // 실제로 이렇게 두 메소드이 예외 타입이 동일하게 구현하면 에러난다. 이유는 조금 아래에 있다.

    {

        // 예외정보로 필요한 예외 처리를 한다.

    }


    public void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

    {

        // 메소드, 호출 인자, 타겟 객체에 대한 정보, 예외 정보로 필요한 예외 처리를 한다.

    }

}

그럼 왜 다른 advice처럼 아래와 유사한 형식으로 인터페이스를 정의하지 않았을까.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(Exception ex); // 왜 이와 유사한 메소드를 정의하지 않았을까?

    void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

}

이렇게 하지 못하는(아니 하지 않는) 이유는 Spring 프레임워크에서 AfterThrowing() 메소드로 전달되는 예외 타입별로 핸들링을 할 수 있는 구조를 제공하기 위한 것이다. 다음과 같은 예외 처리 구조에 대해서는 익히 알고 있을 것이다.

//사용자 정의 예외 객체

public class MyException : Exception

{

}

...

public void method()

{

    try

    {

     // 작업...

    }

    catch (SqlException ex1)

    {

        // SqlException  예외를 처리한다.

    }

    catch (MyException ex2)

    {

        // MyException 예외를 처리한다.

    }

    catch (Exception ex3)

    {

        //Exception 예외를 처리한다.

    }

}

예외별로  다른 처리를 하고 싶다면 이런 구조적인 예외 메커니즘을 이용한다.

Spring.NET에서도 이런 유사한 구조를 제공하고자 한다. 해서 예외가 발생하면 Spring 프레임워크는 그 예외의 타입을 인식해서 적용된 throws advice에 구현되어 있는 AfterThrowing() 메소드들의 마지막 인자 즉 예외 객체의 타입과 비교를 한다. 그래서 만약 일치하는 예외 타입의 인자를 갖거나 또는 일치하는 예외 타입이 없다면 호환될 수 있는 예외 타입을 가지고 있는 AfterThrowing()을 호출한다. 만약 정의된 메소드중에서 발생한 예외의 타입과 호환되는 예외 타입의 인자를 갖는 예외 핸들링 메소드가 없다면 발생한 예외는 상위의 호출자로 버블링된다.

만약 AfterThrows() 메소드들중에서 발생한 예외 객체의 타입과 동일한 타입의 예외 인자를 갖는 메소드가 두개이상이라면? 런타임시 예외가 발생한다.

첫줄을 보면 하나의 메소드( AfterThrowing())안에 동일한 예외 타입의 인자를 갖는 메소드가 동시에 정의될 수 없다는 내용이다. 앞에서 예로 든 ConsoleLoggingThrowsAdvice 코드는 따라서 잘못된 것이다. 앞의 코드는 다음처럼 수정해서, AfterThrowing()의 마지막 인자인 예외 타입은 서로 달라야 한다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex)

    {

        Console.Out.WriteLine("Exception handler applied");

    }

    public void AfterThrowing(MyException ex)

    {

        Console.Out.WriteLine("MyException handler applied");

    }

    public void AfterThrowing(MethodInfo method, Object[] args, Object target, SqlException ex)

    {

        Console.Out.WriteLine("SqlException handler applied");

    }

}

얘기가 길어졌다. 이제 앞에서 던진 질문, throws advice에서 IThrowsAdvice에 AfterThrowing() 메소드를 포함하고 있지 않은 이유를 생각해보자. 간단하다. AfterThrowing() 메소드의 마지막 인자 즉 예외 객체의 타입을 미리 알 수 없다는 것이다. 사용자 정의 예외 타입을 사용한다면 어떻게 미리 알 수 있어서 인터페이스 메소드로 포함시키겠는가.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(MyException ex);//??

}

IThrowsAdvice를 상속하는 모든 구현체는 이 예외 타입의 메소드를 구현해야 한다는 얘기다. 해서 Spring에서는 이 방법을 버리고 런타임시, 실제 발생한 예외의 타입과 구현되어 있는 예외 타입을 비교해서 어떤 AfterThrowing()을 호출할지를 결정하는 방법을 선택했을 것이라는 순전히 개인적인 추측이다. 다른 이유가 있는지는 모르겠다.


■  advice 체인과 advice 실행 순서


앞에서 계속 미뤘던 advice 체인 개념을 알아보자. 타겟 객체의 하나의 pointcut에 대해서 하나만 advice를 적용할 수 있는 것은 아니다. 하나의 pointcut에 대해 앞에서 설명한 여러 종류의 advice 객체들을 여러개 적용할 수 있다. 타겟 객체 앞에 여러개의 advice가 체인처럼 연결되어 놓여져 있다. 타겟 메소드를 호출하면 체인처럼 설정되어 있는 모든 advice들을 호출에 적용하고 나서 최종적으로 타겟 메소드가 호출되는 것이다.  다음은 여러개의 advice들을 적용한 설정 예제이다.

<objects xmlns="http://www.springframework.net">


  <object id="beforeAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice1, Spring.AopQuickStart.Common" />


  <object id="beforeAdvice2"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice2, Spring.AopQuickStart.Common" />


  <object id="afterAdvice1"

    type="Spring.AopQuickStart.Aspects.ConsoleLoggingAfterAdvice, Spring.AopQuickStart.Common" />


  <object id="aroundAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingAroundAdvice, Spring.AopQuickStart.Common" />


  <object id="throwsAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingThrowsAdvice, Spring.AopQuickStart.Common" />


  <object id="myServiceCommand" type="Spring.Aop.Framework.ProxyFactoryObject">

    <property name="Target">

      <object type="Spring.AopQuickStart.Commands.ServiceCommand, Spring.AopQuickStart.Common" />

    </property>

    <property name="InterceptorNames">

      <list>

        <value>throwsAdvice1</value>       

        <value>beforeAdvice1</value>

        <value>aroundAdvice1</value>

        <value>afterAdvice1</value>

        <value>beforeAdvice2</value>

      </list>

    </property>

  </object>


</objects>

before, around, after, throws advice들이 모두 적용되었고 그리고 before advice는 ConsoleLoggingBeforeAdvice1, ConsoleLoggingBeforeAdvice2 두 개가 적용되었다. 다음과 같은 advice 체인을 상상할 수 있다.

여기서 생각해 볼 문제가 하나 있다. advice가 실행되는 순서이다. 예제처럼 beforeAdvice2가 afterAdvice1보다 뒤에 설정되어 있다고 해서 그 적용 순서도 뒤일까. 이렇게 되면 말이 되지 않는다. after advice는 타겟 객체를 호출하고 나서 적용되는  advice이고 before advice는 타겟 객체가 호출되기 전의 advice이다. 설정은 뒤에 오더라도 적용되는 순서는 beforeAdvice2가 먼저여야 한다.

정리하면 advice 체인의 순서가 실제 적용되는 순서는 아니라는 것이다. advice 타입에 따라서 그 적용 순서는 바뀔 수 있다. 다음 그림은 예제에서 설정된 advice들이 실행되는 순서를 그림으로 보여주고 있다.

다음은 앞에서의 예제대로 설정해서 샘플 프로그램을 작성해서 실행한 결과이다.

여기서 한가지 주의할 것은 throws advice는 제일 먼저 설정되어야 한다는 것이다. 그래야 이 후의 advice 적용시 예외가 발생하더라도 그 예외도 설정한 throws advice에서 핸들링할 수 있다. throws advice가 적용되기 전에 중간의 advice에서 예외가 발생하면 그 예외에 대해서는 throws advice가 적용되지 않는다.


지금까지 AOP에 대한 이야기였다. Spring.NET의 중요한 구성 요소중의 하나가 AOP 프레임워크이지만, Spring.NET의 IoC 컨테이너가 AOP에 종속되는 것은 아니다. 이것은 원하지 않는다면 AOP를 사용하지 않고도 Spring.NET 프레임워크를 사용할 수 있다는 것이다.

실전 프로젝트에서도 Spring.NET 원형 그대로를 사용할 수도 있겠지만, Spring.NET 프레임워크를 기반으로 해서 그 프로젝트 상황에 맞도록 한단계 더 추상화된 개발 프레임워크를 제작할 수도 있을 것이다. 이때 다시 프레임워크를 만드는 입장이 되어서 AOP를 사용하면 프레임워크다운 프레임워크가 될 수 있을 것이다. 개발자에게 다양한 Cross cutting concerns에 걸쳐서 동일한 코드를 Copy&Paste하도록 하는 것보다는 하나의 advice 모듈로 구현한 것을 공통팀에서 관리하는 것이 훨씬 더 효율적일 것이라는 것이다. 남은 것은 이제 advice로 구현할 수 있는 cross cutting concerns를 추상화하고 설계하는 것이다.

다음 포스트에서는 별다른 주제가 없으면 Spring.NET이 지원하는 ASP.NET Web Services에 대한 이야기가 될 것이다.

Posted by dalbong2

다음은 AOP를 적용하기 위한 설정으로서 앞 포스트에서 보여준 Spring.Calculator.Web.2005 의 web.config의 일부분이다.

<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<!-- Applies AOP on the contact service. -->

<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

이 설정은 타겟 객체 AdvancedCalculator의 모든 메소드에 대해서 설정된 advice 로직이 적용된다고 했다. 오늘은 타겟 객체의 특정 메소드만 호출할때에만 CommonLoggingAroundAdvice가 적용되도록 하는 설정법을 알아본다. 즉 특정 메소드를 호출하는 경우에만 로그가 남게 될 것이다. 그러기 위해서는 advice가 적용될 부분 즉 pointcut을 지정해줘야 한다. 그러나 불행히도 현재 사용하고 있는 샘플 프로젝트중에는 이 설정에 대한 예가 없다.  따라서 기존의 설정을 조금 수정해야 한다.  


■ pointcut 필터링하기


<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!-- Advisor (advice + pointcut). -->


<object id="regularExpressionMethodCutAdvisor" type="Spring.Aop.Support.RegularExpressionMethodPointcutAdvisor, Spring.Aop">

  <property name="pattern" value="Add*"/><!-- pointcut -->

  <property name="advice" ref="CommonLoggingAroundAdvice"/><!-- advice -->

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>


<!-- Applies AOP on the contact service. -->


<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>regularExpressionMethodCutAdvisor</value>

    </list>

  </property>

</object>

조금이 아닌가?

변한 부분은 두 군데 있다. Advisor라고 주석이 달린 부분의 코드와 ProxyFactoryObject를 설정하는 부분에서 InterceptorNames 속성에 CommonLoggingAroundAdvce에 대한 id값을 추가하는 것이 아니라 추가된 advisor 객체의 id값을 추가하고 있다.

지난 포스트에서 advice와 pointcut을 합친 것을 advisor라고 했다. 앞의 설정에서는 pointcut 단독의 설정 대신에 advisor를 사용해서 advice와 advice가 적용될 pointcut을 함께 표현하고 있다. advice와 pointcut를 설정하기 위해서 RegularExpressionMethodPointcutAdvisor라는 타입을 사용하고 있는데 이 타입의 이름에서 의미있는 두가지 표현이 포함되어 있다:  RegularExpression, MethodPointcut.

AOP 기본 개념중에서 joinpint, pointcut이 있다. joinpoint란 프로그램이 실행되는 동안의 특정 순간들을 말한다. 예를 들어 메소드가 호출되는 순간, 특정 예외가 발생하는 순간등. pointcut이란 실제로 advice가 적용되는 joinpoint들의 집합을 말한다( Spring.NET 레퍼런스 문서. 13.1.1. AOP concepts 절 참조 ). joinpoint가 개념적인 용어라면 pointcut은 실제로 Spring같은 프레임워크에서 구현되어서 advice가 삽입될 곳을 지정하는 지정하는 객체라 할 수 있겠다.

다시 앞의 advisor 타입의 이름에 대한 이야기로 가서, MethodPointcut은 이런 pointcut중에서도 메소드 pointcut이라는 것을 나타내고 있다. 즉 특정 메소드의 호출을 pointcut으로 지정하는 advisor라 하겠다. 그 특정 메소드를 지정하기 위해서 정규식을 사용하겠다는 것이다. 앞의 설정은 타겟 객체의 메소드중에서 메소드명이 "Add"로 시작하는 것에만 advice CommonLoggingAroundAdvice를 적용하겠다는 것을 표현하고 있다. 이런 설정을 포함하고 있는 RegularExpressionMethodPointcutAdvisor 객체의 id를regularExpressionMethodCutAdvisor으로 설정하고 있다.

ProxyFactoryObject는 InterceptorNames 속성에 그 id를 추가시키고 있다. 타겟 객체의 어떤 메소드를 호출할때 advice를 적용할지에 대한 정보가 모두 regularExrpessionMethodCutAdvisor에 포함되어 있다. 따라서 런타임시에 ProxyFactoryObject가 타겟 객체에 대한 프락시 객체를 동적으로 생성해 낼 때에 그 정보를 이용해서 적절한 메소드 호출의 전, 후에 로깅 advice를 적용하는 AOP 프락시를 만들어 낸다.

web.config의 설정을 이렇게 변경하고 Spring.Calculator.Web.2005  웹 애플리케이션을 다시 실행시켜 본다.

AOP가 적용되는 예를 보기 위해서는 두번재 링크를 클릭해야 한다. 이 링크를 클릭하면 웹 서비스를 테스트 할 수 있는 화면이 출력된다.

참고로 웹 애플리케이션 프로젝트를 봐도 calculatorServiceWeaved.asmx 파일은 없다. Spring.NET의 프레임워크 중에서 특별할 HttpHandler를 제공하고 있는데 이것을 사용하면 .asmx 파일에 대한 Http 요청이 오면 이것을 알아서 처리해 준다. 사용자 입장에서는 마치 asmx 파일이 서버에 존재하는 것처럼 보여진다. 이것에 대해서는 뒤에서 Spring.NET이 Web Services를 지원하는 부분을 알아보면서 살펴볼 것이다.

여튼 이 테스트 페이지에서 마음대로 메소드를 호출해서 테스트를  해 보고 나서 남은 로그를 살펴보자.

2008-08-20 20:50:31,984 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'
2008-08-20 20:50:32,015 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '3'

...

Add 메소드에 대한 로그만 남아 있는 것을 알 수 있다.  regularExpressionMethodCutAdvisor advisor의 설정으로 advice가 적용될 메소드가 필터링된것이다.

RegularExpressionMethodPointcutAdvisor를 사용하면 앞에서처럼 정규식을 이용해서 advice가 적용될 메소드(!)를 지정해줄 수 있었다. 이 외에도 NameMatchMethodPointcutAdvisor를 이용하면 지정한 메소드명과 일치하는 타겟 객체의 메소드에만 advice가 적용될 수 있도록 하는 advisor 타입도 있다.

pointcut 필터링에 대한 이야기는 이것으로 마무리하고 한가지만 더 알아보겠다.


■ AOP용 인터페이스 지정하기


만약 ProxyFactoryObject 객체가 넘겨받은 타겟 객체가 하나의 인터페이스만을 구현하것이 아니라 여러개의 인터페이스를 구현한 경우를 생각해보자.  앞의 예제에서의 AdvancedCalculator는 인터페이스 IAdvancedCalculator만을 구현하고 있다. 따라서 ProxyFactoryObject가 AOP 인터페이스를 생성할때 선택이 필요없었다. 그러나 타겟 객체가 두개 이상의 인터페이스를 구현하고 있다면 어떤 인터페이스에 대한 AOP가 적용되어야 할지를 지정해줘야 한다. ProxyFactoryObject의 속성 ProxyInterfaces가 이런 목적으로 사용된다.

<!-- Applies AOP on the contact service. -->


<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>expressionMethodCutAdvisor</value>

    </list>

  </property>

  <property name="ProxyInterfaces">

    <list>

      <value>Spring.Calculator.Interfaces.IAdvancedCalculator, Spring.Calculator.Contract</value>

    </list>

  </property>

</object>

이 속성은 타입 형식을 표현하는 "네임스페이스.타입, 어셈블리명" 형식의 문자열을 받는다. 타겟 객체가 하나의 인터페이스만을 구현하고 있다면 이 속성은 생략해도 된다.


오늘은 여기까지.  다음 포스트에서는 advice 종류 즉 around, before, after, throws advice에 대한 얘기를 하고 이어서 Spring.NET의 Web Services 지원 얘기를 해 보겠다.

Posted by dalbong2

■ 예제 설명

앞에서 본 샘플 프로젝트 솔루션의 구조이다.

Spring.Calculator.Web 프로젝트를 실행시켜보면 다음과 같은 결과 페이지가 보인다.

첫번째 링크는 단순한 웹 서비스 메소드를 호출하고 있다. AOP가 적용된 메소드를 호출하기 위해서는 두번째 링크를 클릭해야 한다. 이번 포스트에서는 두번째 링크에 대한 웹 서비스를 AOP 예제로 삼겠다.  두번째 링크를 클릭하면 다음과 같은 웹 서비스 테스트 화면이 나온다.

노출된 메소드중에서 Add 메소드를 클릭해서 적절히 값을 넣고 호출한다.

이 메소드를 호출하고 나서 남는 로그는 다음과 같다. 

2008-08-18 23:09:34,406 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'

2008-08-18 23:09:34,421 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '2'

그러나 웹 서비스 메소드에는 로그를 남기는 코드는 없다. 별도의 advice를 사용해서 로그를 남기고 있다. 이 샘플에서는 AOP를 구현하기 위해서 개발자가 해야 할 일을 앞에서 설명한 대로 차례로 진행해보자. 


■ 타겟 객체 정의


먼저 타겟 객체에 대한 소스를 보자. 먼저 두번째 링크가 호출하는 클래스는 Spring.Calculator.Services.2005 프로젝트의 AdvancedCalculator 를 보면 다음과 같다.

public class AdvancedCalculator : Calculator, IAdvancedCalculator

{

    #region Fields

    private int memoryStore = 0;

    #endregion


    #region Constructor(s) / Destructor

    public AdvancedCalculator()

    {}


    public AdvancedCalculator(int initialMemory)

    {

        memoryStore = initialMemory;

    }

    #endregion


    #region IAdvancedCalculator Members

    public int GetMemory()

    {

        return memoryStore;

    }

    public void SetMemory(int memoryValue)

    {

        memoryStore = memoryValue;   

    }

    public void MemoryClear()

    {

        memoryStore = 0;

    }

    public void MemoryAdd(int num)

    {

        memoryStore += num;

    }


    #endregion

}

이 클래스에는 Add 메소드가 없다. 상속을 하고 있는 부모 클래스 Calculator에서 구현하고 있다. 그리고 앞 포스트에서 말한대로 타겟 객체가 되기 위해서는 현재 버전의 Spring.NET( v 1.1.2)에서는 반드시 인터페이스를 구현해야 한다고 했다. 코드를 보면 IAdvcancedCalculator를 상속해서 구현하고 있다.

public interface IAdvancedCalculator : ICalculator

{

    int GetMemory();

    void SetMemory(int memoryValue);

    void MemoryClear();

    void MemoryAdd(int num);

}

IAdvancedCalculator 인터페이스는 ICalculator를 상속해서 인터페이스 정의를 물려받고 있다. ICalculator 인터페이스와 그것을 구현하고 있는 Calculator 클래스 코드는 다음과 같다.

public interface ICalculator

{

    int Add(int n1, int n2);

    int Substract(int n1, int n2);

    DivisionResult Divide(int n1, int n2);

    int Multiply(int n1, int n2);

}

public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    public int Substract(int n1, int n2)

    {

        return n1 - n2;

    }


    public DivisionResult Divide(int n1, int n2)

    {

        DivisionResult result = new DivisionResult();

        result.Quotient = n1 / n2;

        result.Rest = n1 % n2;

        return result;

    }


    public int Multiply(int n1, int n2)

    {

        return n1 * n2;

    }


    #endregion

}

인터페이스들은 구현 클래스들과는 다른 프로젝트 Spring.Calculator.Contract.2005에 구현되어 있다. 만약 클라이언트 애플리케이션과 서버 애플리케이션이 분리되어 있다면 클라이언트에서는 인터페이스 어셈블리만 참조하면 된다. 물론 서버측에서는 인터페이스 어셈블리와 구현 어셈블리가 같이 참조되어야 한다.


■ advice 코딩하기


이제 타겟 객체를 호출할때 weaving될 advice 코드를 살펴본다. 샘플에서는 프로젝트 Spring.Aspects.2005에 구현되어 있다.

현재는 두개의 로깅 advice가 구현되어 있다. 이 중에서 웹 애플리케이션에서는 CommonLoggingAroundAdvice를 사용해서 로그를 남기고 있다. 이 advice 코드를 보면 다음과 같다.

public class CommonLoggingAroundAdvice : IMethodInterceptor

{

    #region Logging

    private static readonly ILog LOG = LogManager.GetLogger(typeof(CommonLoggingAroundAdvice));

    #endregion


    #region Fields

    private LogLevel _level = LogLevel.All;

    #endregion


    #region Properties

    public LogLevel Level

    {

        get { return _level; }   

        set { _level = value; }

    }

    #endregion


    #region IMethodInterceptor Members


    public object Invoke(IMethodInvocation invocation)

    {

        Log("Intercepted call : about to invoke method '{0}'", invocation.Method.Name);

        object returnValue = invocation.Proceed();

        Log("Intercepted call : returned '{0}'", returnValue);

        return returnValue;

    }


    #endregion


    #region Private Methods

    private void Log(string text, params object[] args)

    {

        switch(Level)

        {

            case LogLevel.All :

            case LogLevel.Debug :

                if (LOG.IsDebugEnabled) LOG.Debug(String.Format(text, args));

                break;

            case LogLevel.Error :

                if (LOG.IsErrorEnabled) LOG.Error(String.Format(text, args));

                break;

            case LogLevel.Fatal :

                if (LOG.IsFatalEnabled) LOG.Fatal(String.Format(text, args));

                break;

            case LogLevel.Info :

                if (LOG.IsInfoEnabled) LOG.Info(String.Format(text, args));

                break;

            case LogLevel.Warn :

                if (LOG.IsWarnEnabled) LOG.Warn(String.Format(text, args));

                break;

            case LogLevel.Off:

            default :

                break;

        }

    }

    #endregion

}

이 advice는 타겟 객체의 메소드를 호출할때 적용된다. 타겟 객체의 어디서, 어떻게 적용될지를 선택할 수 있는 방법이 바로 IMethodInterceptor 인터페이스이다. 이 인터페이스에서는 단지 object Invoke()만을 정의하고 있다.

IMethodInterceptor를 구현하고 있는 advice가 타겟 객체에 적용될때는 타겟 객체의 메소드를 호출하면 항상 IMethodInterceptor인터페이스의 Invoke()가 호출된다. 코드에서 Invocation.Proceed(); 부분이 advice가 캡쳐한 원래의 호출을 다시 타겟 객체로 전달하는 부분이다. 타겟 객체로 호출을 전달하기 전에 예제의 advice에서는 로그를 남기는 작업을 하고 있다. 로그를 남기는 Log()에 대해서는 지금 이곳에서는 중요한 부분이 아니므로 넘어가도록 한다. Proceed()를 호출하고 나서 반환값을 받고서도 클라이언트로 바로 넘기지 않는다. 반환되기 전의 순간도 캡쳐할 수 있다. 예제 advice에서는 타겟 객체에서 반환하는 값에 접근해서 그 값을 로그로 남기고 있다. 그런 다음 최종적으로 클라이언트 코드로 반환값을 넘겨주고 있다. 만약 타겟 객체가 개발자가 개발g한 비즈니스 객체이고 개발 프레임워크에서 이와 같은 advice를 개발해서 적용한다면 얼마나 유용할지 짐작이 갈 것이다.

이렇게 타겟 메소드의 호출 전 후를 캡쳐할 수 있는 기회를 제공하는 advice를 "around advice"라고 한다. IMethodInterceptor는 around advcie의 Spring 프레임워크의 구현이다. 그외에도 before advice, after advice, throws advice등이 있고 이것을 각각 구현한 Spring.NET의 인터페이스들이 있다. 이것에 대해서는 뒤에서 다루기로 하고 지금은 Spring.NET 애플리케이션에서 AOP를 적용하는 전체적인 절차를 계속 알아보도록 하자.

지금까지의 내용을 보면, 인터페이스가 있었고 그리고 인터페이스를 구현한 타겟 객체가 있었다. 그리고 타겟 객체의 메소드를 호출할때 적용될 advice가 있었다. 이제 실제로 advice를 타겟 객체에 적용하기 위한 설정이 필요하다.


■  advice 적용 설정하기( ProxyFactoryObject 설정하기 )


advice를 타겟 객체에 적용하기 위한 설정은 다시 말하면 ProxyFactoryObject 객체 설정과 같은 말이다. 앞의 포스트에서 Spring.NET에서는 프락시 패턴을 이용해서 AOP를 구현하고 있다고 했다. 그리고 advice가 적용된(weaving된) 프락시를 AOP 프락시로 표현했는데, 이 AOP 프락시를 런타임시에 동적으로 생성해내는 객체가 바로 ProxyFactoryObject라고 했다. 클라이언트 코드에서는 타겟 객체에 대한 참조와 advice를 건네주고 ProxyFactoryObject객체로부터 타겟 객체에 대한 AOP 프락시를 받는다고 했다. 이 시나리오를 설정을 통해서 구현하면 다음과 같다. 이 시나리오를 코드상에서 프로그램적으로 구현할 수 있는 API도 제공하고 있다. 이 설정은 웹 프로젝트 Spring.Calculator.Web.2005 의 web.config의 부분이다.

<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!-- Service -->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

첫번째 <object/>요소에 타겟 객체에 적용될 advice가 정의되어 있다. id는 CommonLoggingAroundAdvice로 하고 있는데, 다른 <object/>에서 이 객체를 참조할때 id 값을 이용할 수 있다. advice는 Spring.Aspects.Logging 네임스페이스에 포함된 CommonLoggingAroundAdvice 클래스에 정의되어 있다는 것을 type 어트리뷰트값을 통해서 나타내고 있다.

두번째 <object/> 요소에서는 타겟 객체에 대한 정의를 표현하고 있다. id는 calculator로 하고 있고 타겟 객체의 타입은 어셈블리 Spring.Calculator.Services의 Spring.Calculator.Services 네임스페이스 아래에 있는 AdvancedCalculator 클래스에서 정의하고 있다.

세번째 <object/>요소는 바로 앞의 advice객체와 타겟 객체를 인자로 받아들이는 AOP 인터페이스 제너레이터 ProxyFactoryObject에 대한 정의이다. ProxyFactoryObject 타입의 속성중에는 Target, InterceptorNames( 대소문자 무관)가 있는데 Target 속성을 통해서 타겟 객체에 대한 참조를 받고, InterceptorNames 속성을 통해서 around advice를 받고 있다. 타겟 객체에 대한 참조를 지정할때 <property/>요소의 ref 어트리뷰트를 사용하는데 그 값으로는 앞에서 AdvancedCalculator 객체를 정의하고 있는 <object/>의 id 어트리뷰트를 지정하고 있다. 그리고 InterceptorNames 속성은 여러개의 advice를 지정할 수 있다. 그래서 <list/>요소 내부에 <value/> 요소를 사용해서 advice를 추가하고 있는데, 다른 advice 객체가 있다면 <value/>를 더 추가할 수 있다. 여기서 <value/>의 값으로 지정된 CommonLoggingAroundAdvice는 advice를 정의하고 있는 첫번째 <object/>요소의 id값이다.

이로써 특정 advice를 특정 타겟 객체에 적용하는 작업은 끝났다.  클라이언트 코드에서는 이제 다음과 같은 방법으로 AOP 프락시에 대한 참조를 얻을 수 있다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorWeaved");

컨텍스트 객체의 GetObject() 메소드에 ProxyFactoryObject 객체에 대한 id값을 넘겨주면 원한는 타겟 객체에 대한 AOP 프락시를 받을 수 있다. 반환되는 객체가 ProxyFactoryObject 객체 자체가 아니라 그 타겟 객체에 대한 프락시임을 다시 한번 더 상기하자. 이제 AOP 프락시를 통해서 클라이언트측 코딩을 해 나가면 된다.

현재 웹 샘플 Spring.Calculator.Web.2005 에서는 클라이언트 코드에서 타겟 객체에 대한 참조를 이용하는 코드가 없다. 현재의 웹 샘플 코드에서는 서버측에서만 AOP를 적용하는 코드가 있다. 타겟 객체의 메소드를 호출하는 클라이언트 코드는 앞의 그림과 같은 ASP.NET에서 제공하는 테스트 페이지를 사용하고 있다. 앞에서와 유사한 코드는 프로젝트 Spring.Calculator.ClientApp.2005의 Program.cs 파일에 있다.

Spring.Calculator.Web.2005 프로젝트에 있는 web.config의 설정은 웹 서비스로 노출된 타겟 객체에 대한 설정이다. 따라서 클라이언트측 로그는 없지만 서버측 객체 호출에 대한 로그는 남는다.


■  웹 애플리케이션 실행하기


Logs폴더 하위의 log.txt 파일을 열어보면 웹 서비스 메소드가 호출될때 남겨진 로그를 볼 수 있다. 참고로 실행중에는 VS.NET의 솔루션에서 오픈하지 말고, 윈도우 탐색기에서 오픈하라. 다음은 메소드 Add()와 Divide()를 호출한 후에 남은 로그 내용이다.

2008-08-20 00:08:52,468 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'
2008-08-20 00:08:52,484 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '3'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Divide'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned 'Quotient: '2'; Rest: '1''


지금까지의 설정처럼 하면 타겟 객체의 모든 메소드를 호출할때마다 CommonLoggingAroundAdvice의 내용이 적용될 것이다. 그러나 때로는 타겟 객체의 특정 메소드를 호출하는 경우에만 설정한 advice들이 적용되기를 바랄 수도 있을 것이다. 


■ pointcut 코딩하기


타겟 객체의 특정 메소드만 호출할때 로그를 남기고 싶다면 앞에서와 같은 기본적인 설정만으로는 부족하다. 해서 좀 더 특별한 설정이 필요하다. 특별한 설정이란 바로 타겟 객체의 pointcut을 지정하는 것이다. AOP의 일반적인 이론에서는 여러 joinpoint( advice가 weaving될 수 있는 포인트. 예를 들어 속성 값이 변하기 전, 후)가 있을 수 있겠지만, 현재 AOP 프락시를 이용하여 AOP를 구현하는 방법에서는 메소드만이 pointcut의 대상이 된다. 말이 점점 어려워진다 -_-;;

여튼 현재의 Spring.NET 버전에서는 메소드만이 advice가 적용될 수 있고 메소드중에서 특별한 메소드만 advice가 적용될 수 있도록 하는 방법이 있다. Spring.Aop.Support네임스페이스 아래에 있는 RegularExpressionMethodPointcutAdvisor 타입을 이용해서 그런 설정을 할 수 있는데, 이 타입은 정규식을 이용하고 있다. 즉 특정 정규식에 일치하는 메소드명을 갖는 타겟 메소드에만 advice를 적용시키는 설정을 할 수 있다.

불행히도 현재 사용하고 있는 샘플 프로젝트중에는 이 설정이 없다.  설정을 조금 수정해야 한다.  시간 관게상 이 작업은 다음 포스트에서 하도록 한다. 

Posted by dalbong2

바로 예제 설명으로 들어가려 했으나 아무래도 AOP 개념에 대해 좀 더 설명이 필요할 것 같다. Aspect Oriented Programming하면 떠올라야 하는 개념은 "타겟 객체에 대한 호출을 중간에서 인터셉트할 수 있는 방법"이라는 것이다. 개발 프레임워크 입장에서 생각해본다면 얼마나 근사한 구조인가. 타겟 객체( 개발자가 개발)에 대한 모든 호출( 개발자가 만든 코드에서의 호출)을 개발 프레임워크에서 캐취할 수 있다는 것은 많은 장점을 가지고 있다.

그리고 실제로 AOP를 구현하기 위해서 개발자가 AOP의 컨셉을 모두 개발할 필요는 없다. advice는 개발자가 C#문법을 이용해서 일반 객체를 정의하듯이 구현하면 된다. 그러나 advice나 pointcut 자체는 Spring.NET의 IoC 컨테이너가 관리한다. Spring.NET의 AOP를 구현하기위해서 개발자가 해야 할 일을 정리하면 다음과 같다.

▶ 타겟 객체 개발하기

타겟 객체란 AOP가 적용될 타입의 객체를 말한다. 예를 들어 다음에 설명하는 advice( 예로, 메소드 호출 전 후에 로그를 남기는 코드)를 적용하고 싶은 객체를 말한다. 애플리케이션용 클래스중에서 선택한 클래스가 AOP 프로그래밍의 타겟객체로 설정될 수 있다.  뒤에 설명하겠지만 타겟 객체가 되기 위해서는 인터페이스를 구현해야 하는 제한이 있다.

▶ advice 코딩하기

필요한 aspect를 코딩한다. 그러나 대부분의 구현은 이미 되어 있다:로깅, 트랜잭션, 보안 등.  예를 들어 로깅 모듈로 Log4net이 있다.  그리고 하나의 타겟 객체에 대한 여러개의 advice가 차례로 적용될 수 있다.

▶ pointcut 지정하기

pointcut을 지정할 수 있는 기본적인 방법 또한 Spring.NET에서 제공하고 있다. 예를 들어 정규식을 사용하는 방법으로 타겟 객체에서 "Do"로 시작하는 메소드에 대해서 Advice를 적용하라는 식의 설정을 할 수 있다.

▶ Advice를 타겟 객체에 적용하는 작업하기.

결정된 advice와 pointcut을 configuration하는 작업이다.


■ ProxyFactoryObject를 이용하여 AOP 프락시 생성하기


Spring.NET에서는 프락시 패턴을 이용해서 AOP를 적용할 수 있는 구조를 만든다.

그림을 보면 클라이언트 코드는 직접 타겟 객체를 참조하지 않고 프락시를 통해서 호출하고 있다. 프락시는 클라이언트의 호출을 받으면 바로 타겟 객체의 메소드를 호출하지 않는다. 3번 호출처럼 필요하다면  Aspect 모듈을 호출한다. 이때 Cross Concerns( advice로 구현된다)에서는 타겟 객체의 메소드를 호출하기 전에 advice의 내용(로깅, 트랜잭션 작업 시작 등)을 수행할 수 있다. 그리고 나서 타겟 객체의 메소드를 호출한다. 또한 타겟 객체로부터의 반환을 바로 클라이언트로 넘기지 않는다. 타겟 객체의 반환이 클라이언트로 넘겨주기 전에 또한 필요하다면 Aspect 모듈을 호출해서 마무리 작업( 호출후의 로깅작업, 트랜잭션 작업 등)을 할 수 있다. 그리고 나서 최종적으로 클라이언트에 반환값을 넘겨준다. 이처럼 AOP를 구현하기 위한 프락시를 "AOP 프락시"라고 한다. 이 AOP 프락시에는 타겟 객체의 메소드를 호출하는 코드만 있는 것이 아니라 구현된 advice를 호출하는 메소드 또한 포함되어 있다.

Spring.NET에서는 이런 AOP 프락시를 만드는 녀석이 바로 ProxyFactoryObject이다( 수정일자: 2009.05.24. AOP 프락시를 생성하는 방법은 여러가지가 있다. ObjectNameautoProxyCreator, DefaultAdvisorAutoProxyCreator 등. Spring.NET 레퍼런스 문서 13.5, 13.9절등을 참고한다.) 최종적으로 앞의 그림과 같은 실행 구조가 되기 위해서, 클라이언트는 프락시가 필요한 타겟 객체를 ProxyFactoryObject에게 넘겨주면 ProxyFactoryObject는 타겟 객체에 대해서 advice 호출을 포함하고 있는 AOP 프락시를 생성해서 클라이언트에 넘겨준다. AOP 프락시를 생성하는 작업은 런타임에 일어나는데, 프락시 클래스용 IL코드를 만들어 내기위해서 System.Reflection.Emit 네임스페이스의 클래스들을 사용한다. 

ProxyFactoryObject가 AOP프락시를 런타임시에 동적으로 생성한다는 것은 런타임시에 weaving(advice를 적절한 pointcut에 끼워넣는 작업)이 수행된다는 것이다.  weaving은 AOP를 구현하는 방식에 따라 컴파일시에 또는 클래스 로딩시에 수행될 수 있다. 그러나 Spring.NET에서는 현재 런타임시만을 지원하고 있다. 또한 현재 버전(version 1.1.2)에서는 AOP 프락시를 생성하고자 하는 타겟 객체는 반드시 하나 이상의 인터페이스를 구현해야 한다.  ( 반드시 하나 이상의 인터페이스를 구현해야 하는 제약은 버전 1.1 X부터는 없어졌다고 한다. 아래 최만석님의 댓글 및 다음 링크에서 "13.5.4. Proxying Classes"절을 참조한다.  이 내용에 따르면 일반 클래스도 AOP 프락시의 타겟 객체로 사용될 수 있지만, 이 방법 또한 가상 메소드만 AOP 프락시를 통해서 노출될 수 있다는 제약이 있다. )

public interface ICalculator

{

    int Add(int n1, int n2);


   //....

}


public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    // ....

    #endregion

}

Calculator를 AOP의 타겟 객체로 만들고 싶다면 반드시 ICalculator같은 인터페이스를 구현 하는 구조로 설계해야 한다는 것이다. 그러나 추후 버전에서는 다른 방식의 AOP 프락시 제너레이터도 제공할 계획이란다.
앞에서 수정한 부분을 참고한다. 반드시 인터페이스를 구현할 필요는 없다.

참고로 .NET 자체에서도 AOP용 프락시를 생성하는 방법이 있다. 그러나 그런 AOP용 프락시를 생성하기 위해서는 타겟 객체가 ContextBountObject 타입을 상속받아야 한다는 단점이 있다. 컨텍스트 기반(Context-bound )의 프락시라고 한다는데, 컨텍스트 스위치와 .NET 리모팅 인프라의 오버헤드때문에 성능적으로도 그렇게 효과적이지는 않다고 한다.


■ ProxyFactoryObject를 configuration하기


Spring.NET에서는 Spring.Aop.Framework.ProxyFactoryObject 클래스를 이용해서 타겟 객체(advised 객체)에 대한 프락시를 얻는다고 했다.  이 ProxyFactoryObject를 이용하면 좋은 점은 advice나 pointcut을 configuration에 포함시킬 수 있다는 것이다. 다음 configuration을 보자.

<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>

<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

3개의 객체가 정의되어 있다. 첫번째는 advice 객체에 대한 정의이다. 두번째 객체는  AOP가 적용될 타겟 객체에 대한 정의이다. 세번째 객체에 대한 정의는 첫번째와 두번째 객체에 대한 참조를 받아들여서 타겟 객체에 대한 AOP 프락시를 만들어낼 ProxyFactorObject에 대한 정의이다. 이 설정에 대한 구체적인 설명은 뒤에서 하기로 한다.

이렇게 설정한 내용을 Spring.NET의 컨테이너가 해석할 수 있다는 것이다. ProxyFactoryObject 객체에 대한 이와 같은 설정을 해석할 수 있다는 것이 무슨 의미인지는 이렇다. ProxyFactoryObject에 대한 참조를 얻는 과정은 여느 객체를 얻는 방식과 동일하다.

IApplicationContext ctx = ContextRegistry.GetContext();


IAdvancedCalculator calculator= (IAdvancedCalculator) ctx.GetObject("calculatorWeaved");

이때 ctx.GetObject("calculatorWeaved")가 반환하는 것은 ProxyFactoryObject 객체에 대한 참조가 아니라, Spring.NET 컨테이너는 ProxytFactoryObject에 대한 요청을 인식할 수 있고 이것에 대한 요청은 다르게 처리한다. 즉 이 팩토리가 참조하고 있는 타겟 객체( Target 속성이 가리키는 타입)에 대한 프락시의 참조를 반환한다는 것이다.  이때 참조된 프락시는 설정된 advice, 여기서는 CommonLoggingAroundAdvice의 내용이 weaving된 상태이다.


여기까지 하고 다음 포스트에서 앞에서 알아본 예제를 살펴보도록 하자.

Posted by dalbong2

IoC( Inverse of Control  제어권의 역전, 역제어)와 DI( Dependencies Injection)은 같은 의미로 사용된다. 시간적으로 보면 IoC라는 용어가 먼저 나왔고 뒤에 Martin Fowler라는 사람이 개념상 더 적절하지 않냐면서 내놓은 것이 DI다. 필자의 눈에는 차이점을 잘 모르겠고, 개발자에게는 당장 별로 중요한 차이는 아닐듯하다. 컨테이너가 객체를 생성할때 그 객체가 필요로 하는 의존 객체들을 자동 생성해서 할당해준다는 것이다.

컨테이너가 (대상)객체들을 생성할때 의존객체들을 할당해주기 위해서는 대상객체는 의존 객체들을 외부에서 받아들일 수 있는 public 입구(?)가 있어야 한다. 그래야 컨테이너가 대상 객체를 생성해서 공개된 입구로 객체를 할당해 줄 수 있다. 대상객체 내부에서 private으로 생성하는 객체들에 대해서는 컨테이너가 할당해줄 수 없다. 공개된 입구는 어떤 것이 있는가. 바로 public 생성자,public setter 속성,  public 메소드가 있다. 즉 contructor injection, setter injection, method injection DI가 수행될 수 있기 위해서는 각각에 해당하는 public API가 있어야 한다.

constructor injection : 파라미터가 없는 기본 constructor밖에 없다면 DI고 뭐고 수행될 것이 없겠지만, 만약 파라미터가 있는 공개 constructor라면 constructor injection의 대상이 될 수 있다. 즉 대상 객체를 컨테이너가 생성할때, 파라미터에 해당하는 필요한 인자들이 있다면 컨테이너가 알아서 스스로 인자들을 생성해서 대상 객체를 생성하는데 사용하는 것을 constructor injection이라고 한다.

namespace X.Y

{

    public class Foo

    {

        public Foo(Bar bar, Baz baz)

        {

            //...

        }

    }

    public class Bar

    {

        //...

    }


    public class Baz

    {

        //...

    }

}

코드에서는 Foo의 객체를 생성할때 Bar타입의 bar객체와 Baz 타입의 baz객체를 이용하고 있다. 코드에서 Foo객체를 요구하면 bar, baz를 자동으로 생성해서 이것들을 이용해서 Foo객체를 생성해서 반환해준다.

constructor injection의 대상이 되려면 대상 객체와 constructor의 파라미터들이 모두 컨테이너가 인식할 수 있도록 설정되어 있어야 한다. 설정 방법은 조금 후에 앞에서 봤던 예제의 web.config의 내용으로 알아볼 것이다.

setter injection : 대상객체가 필요한 의존 객체를 대상 객체에 할당해 주기 위해서는 당연히 getter 속성 메소드는 이용할 수 없다. 따라서 property injection이라고 해도 setter injection을 의미하게 된다. 그러나 property injection같은 용어는 아직 보지 못했다. setter 메소드를 이용해서 의존 객체를 할당하는데, 물론 쓰기 가능한 모든 속성을 자동 할당하지는 않는다. 대상 객체를 생성하고 나서 그 객체의 속성중에서 설정을 통해서 요청한 속성에 대해서만 injection이 발생한다.

대상객체를 의존 객체들로 초기화하기 위해서 constructor DI를 사용할지 setter DI를 사용할지에 대한 규칙은 없다. 직접 개발을 하고 있는 상태라면 개발자의 기호에 맞게 사용하면 될 것 같다. 그러나 이미 개발되어 있는 3th파티 클래스의 코드를 구할 수가 없다면 이미 구현되어 있는 상태에 따라 달라질 수 밖에 없다. 예를 들어 공개된 속성이 없고 모든 의존 객체들을 대상 객체 constructor에서 모두 받는다면 어쩔 수 없이 contructor DI를 사용해야 할 것이다.

constructor injection, setter injection에 비해서 method injection은 그렇게 많이 사용되지 않는다. 이것에 대해서는 추후에 알아보겠다.

이제 constructor, setter injection에 대한 설정을 알아보도록 하자.

<objects xmlns="http://www.springframework.net">


   <!-- Aspect -->

   <object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

    <property name="Level" value="Debug"/>

  </object>

   <!-- Service -->

   <object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

  <object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

    <property name="target" ref="calculator"/>

    <property name="interceptorNames">

      <list>

        <value>CommonLoggingAroundAdvice</value>

      </list>

    </property>

  </object>

</objects>

<objects/>요소안에 3개의 객체가 등록되어 있다. 첫번째 등록된 객체를 보면, 어셈블리 Spring.Aspects에 포함되어 있는 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 CommonLoggingAroundAdvice라는 id로 등록되어 있다. 그리고 이 객체는 public 속성으로 Level 이 있는데, 이것의 값을 "Debug"로 설정하고 있다. 지금은 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 뭐하는 녀석인지 몰라도 된다.

애플리케이션의 코드상에서 이 객체( id="CommonLoggingAroundAdvice")를 요청하게 되면 컨테이너는 일단 주어진 타입 정보를 통해서 인스턴스를 하나 생성하고 나서 Level 속성에 Debug 값을 설정한다.
애플리케이션이 시작되면 CommonLoggingAroundAdvice 인스턴스가 하나 생성되고( 기본적으로 Spring.NET 컨테이너에 생성되는 객체는 singleton이다), Level 속성에 Debug값을 설정한다.

근데 CommonLoggingAroundAdvice 타입의 Level 속성을 보면 반환값의 타입이 enum LogLevel 라는 열거형을 사용하고 있다. 그러나 여기에 할당될 값은 value어트리뷰트에 문자열 "Debug"이 설정되어 있다. 문자열을 LogLevel이라는 열거형에 할당할 수 있나? 없다. 그럼 어떻게 ? 똑똑한 Spring.NET 컨테이너는 대상 객체의 Level 속성이 LogLevel 열거형이라는 것을 알고 "Debug"를 열거형의 문자열 값중의 하나로 인식해서 LogLevel.Debug를 속성에 할당해준다. 즉 문자열 "Debug"를 열거형 값 LogLevel.Debug로의 변환을 자동으로 해준다. 이 과정에 TypeConvert라는 것이 사용되고 있다는 것을 알아두고 나중에 더 깊이 있는 공부를 할 필요가 있겠다.

두번째 객체는 "calculator"라는 id로 Spring.Calculator.Services.AdvancedCalculator 타입의 객체가 등록되고 있다. 이 객체에서는 DI를 위한 설정은 없다. 단순히 객체 등록만 하고 있다.

세번째로 등록되는 객체에서도 두 개의 setter injection을 위한 설정이 있다.  ProxyFactoryObject 타입을 보면 공개된 두개의 속성 Target, InterceptorNames이 있다. 대소문자는 구분하지 않고 있다.  대상객체 ProxyFactoryObject의 Target 속성은 object 타입을 받는 속성이다. <property/>요소의 ref 어트리뷰트를 사용해서 Target 속성에 할당될 객체를 지정하고 있다. ref 어트리뷰트의 값으로는 XML에 설정된 다른 객체의 id(또는 name)값을 지정해주면 된다. 두번째 속성 InterceptorNames는 문자열 배열 string[]이다. 문자열 배열을 지정할때는 <list/> 하위 요소를 사용해서 필요한대로 <value/>요소를 사용해서 값을 추가하면 된다.

ProxyFactoryObject를 코드에서 요청할때 컨테이너는 인스턴스를 하나 생성하고나서 두개의 공개 속성을 통해서 초기화를 마친 후 준비된 객체를 코드에 반환한다.

예제에서는 나와 있지 않지만, 앞에서 보인 Foo 객체에 대한 constructor injection을 위한 설정을 보이면 다음과 같다.

<object id="fooObject" type="X.Y.Foo, X.Y">

  <constructor-arg name="bar" ref="barObject"/>

  <constructor-arg name="baz" ref="bazObject"/>

  <!--추가된 인자-->

  <constructor-arg name="arg" value="strArg"/>

</object>

<obejct id="barObject" type="X.Y.Bar,X.Y"/>

<object id="bazObject" type="X.Y.Baz,X.Y"/>

컨테이너가 생성되면서 설정된 모든 객체들에 대한 정보(Object definitions)들이 컨테이너에 로딩된다.  그리고 나서 DI가 수행되는 것은 실제로 대상 객체에 대한 인스턴스가 요청될때이다. DI가 수행될때 의존 객체들은 XML 설정 파일이 아니라 컨테이너의 로딩된 object definitions에서 검색된다. 따라서 XML상 의존 객체들이 대상 객체보다 뒤에 설정되어 있어도 상관없다.

앞에서 소개한 문서의 5.3. Dependencies 절을 보면 알겠지만, DI를 위한 설정 표현이 여러 버전이 있다. 예를 들어 다음과 같은 표현도 있다.

<property name="target">

   <ref object="calculator"/>

</property>

각기 표현마다 다른 장단점이 있을 수 있다. 또한 이곳에는 기타 여러가지 설정을 위한 요소 및 어트리뷰트에 대해서 설명한 내용이 있다. 한번 정독해보는 것도 좋을 듯 싶다.

오늘은 여기까지.  자자!

추가 설명

컨테이너에 등록을 했으면 이제 코드에서 객체를 얻는 API도 필요할 것이다. Spring 컨테이너에 등록( 또는 생성)되어 있는 객체에 대한 참조를 얻는 API는 다음과 같다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorService");

IApplicationContext는 System.Core.dll의 Spring.Context 네임스페이스에 포함되어 있다.

Posted by dalbong2

이 예제는 Spring.NET 레퍼런스 문서의 QuickStart에서 설명되고 있는 예제중의 하나이다. 3.1.7절에 나와 있다.

이 예제를 통해서 설명할 주용 내용은 다음과 같다.

▶컨테이너에 객체들을 등록하는 설정

- 이게 무슨 말인지 기억나는가? Unity Application Block의 컨테이너 프레임워크에서와 유사 아니 동일한 개념의 작업이다. 물론 객체를 등록하는 설정 표현(syntax)은 다르다.

▶DI(dependencies Injection) 설정

- Unity Application Block의 setter injection, constructor injection을 기억하는가?

▶AOP 설정

- aspect(Cross concerns )를 메인 로직에 weaving(minxin)하는 작업에 대한 예제를 보게 될 것이다. aspect중에서 예제에서는 로깅 aspect가 예로 보여진다.

▶ 음...그리고 필요한대로.

- 다음 예제에서는 설명할 것이 아주 아주 많다. 간단한 샘플 코드를 보지도 않고 바로 예제로 건너뛰었기때문에 앞에서 말한 항목들외에도 Spring.NET의 기본 설정들에 대해서 먼저 설명이 필요할 것이다. 그리고 Spring.NET에서 제공하는 Web Services관련 기능도 이해해야 한다. ASP.NET Web Services를 만들때 지금까지 .asmx 파일을 추가했어야 했지만 Spring.NET에서는 그럴 필요가 없다. 이런 저런 얘기를 하다보면 다음 예제를 완전히 알아보는데 몇 회의 포스트가 필요할 것이다.

우선 예제 프로젝트를 보면 다음과 같다.

■ 프로젝트 구조

■ 프로젝트 설명

프로젝트 설명
Spring.Calculator.Contract 계산기의 기본적인 오퍼레이션을 정의하고 있는 인터페이스 ICalculator를 포함하고 있다. 그리고 IAdvancedCalculator도 포함하고 있는데 계산 결과가 저장될 메모리 계산에 사용될 인터페이스 메소드들이 정의되어 있단다. 그리고 이 프로젝트에는 도메인 객체 DivisionResult도 포함하고 있다. 도메인 객체라면 여러 레이어(주로 3계층의 애플리케이션이 주로 개발된다)에 걸쳐서 참조되는 객체들이다. 비즈니스 인터페이스와 도메인 객체는 서버측( 리모팅 서버, 웹 서비스 서버)과 클라이언트측( 윈폼 애플리케이션, 웹 애플리케이션)에서 사용될 것이기에 별도의 프로젝트에 분리시켜 놓고 있다.
Spring.Calculator.Services ICalculator와 IAdvancedCalculator에 대한 구현이 포함되어 있다. 이 구현이 서비스로서 노출될 것이다. 각각의 구현 클래스의 이름은 Calculator, AdvancedCalculator로 되어 있다. AdvancedCalculator는 IAdvancedCalculator를 구현했을뿐만 아니라 Calculator를 상속하고 있다. 필자는 솔직히 AdvancedCalculator가 확실히 정의되지 않는다. -_-;; 몰라도 상관없을 것 같기에 그냥 넘어간다.
Spring.Calculator.RemoteApp 리모트 객체 AdvancedCalculator 인스턴스를 호스팅할 서버측 애플리케이션을 포함하고 있다.
Spring.Aspects 리모트 객체에 적용할 로깅 advice들을 가지고 있다. 이 advice들을 통해서 AOP 프로그래밍을 적용하는 예를 보여줄 것이다.
Spring.Calculator.ClientApp 클라이언트측 애플리케이션을 포함하고 있다.  그러나 이번 예제에서는 사용하지 않을 것이다.
Spring.Calculator.Web Spring.Calculator.ClientApp대신에 클라이언트 애플리케이션으로 사용한다.

■ 프로젝트의 아키텍쳐

그림이 좀 이상하긴 하지만... 그림에서 나타내려고 하는 것은, 앞의 예제 프로젝트에서는  하나의 비즈니스 객체( Spring.Calculator.Services 프로젝트의 AdvancedCalculator 객체)를 .NET Remoting 서비스로도 노출시키고 웹 서비스로도 노출시키고 있다는 것을 보여주려고 했다. 그리고 그 객체를 COM+에 등록해서 사용하고 있다.

이 구조는 상황에 따라서 조금씩 변경된다. 웹 서비스를 이용해서 타겟 객체에 접근한다고 했을때 클라이언트는 웹 브라우저가 아니라 웹 애플리케이션을 나타내고 있다.  또 상황에 따라서는 .NET Remoting 애플리케이션, 웹 서비스 애플리케이션, COM+애플리케이션이 참조하는 타겟객체는 실제 객체에 대한 참조가 아니라 타겟 객체의 프락시에 대한 참조일 수도 있다.

클라이언트 애플리케이션은 타겟 객체에 대한 인터페이스를 호출하고 있고 서버측 애플리케이션도 타겟 객체에 대한 인터페이스를 참조하고 있다. 이때 인터페이스는 타겟 객체과 구현하고 있는 순수한 인터페이스는 아니다. 물론 순수한 인터페이스를 참조할 수도 있지만 현재 샘플에서는 AOP를 적용하고 있다. 타겟 객체를 호출할때 로그를 남기는 작업을 수행하고 있다. 이 구조에서 좀 더 정확히 표현하자면 "AOP 인터페이스"가 될 것이다. advice 코드가 적절한 pointcut에 weaving된 형태의 인터페이스이다. AOP 인터페이스의 메소드를 호출하게 되면 호출 전 or/and 후에 로그가 남게 된다. 이게 무슨 말인지는 뒤의 포스트에서 설명되고 있다: 시리즈 25 참조.

여튼 앞에서 구성하고 있는 Visual Studio.NET 솔루션에 포함된 프로젝트들의 전체적인 구조는 이렇다는 것이고 필요하다면 상황별로 자세한 설명을 하도록 하겠다. 그러나 이 중에서 웹 서비스를 호출하는 구조를 중심으로 알아볼 것이다.

■ configuration

Spring.Calculator.Web.2005 프로젝트의 web.config 파일을 보면 다음과 같다. 원래 소스에서는 logging섹션이 spring섹션보다 먼저 나오는데, spring섹션을 먼저 설명해야 할 것 같아서 순서를 조금 변경했다.

<?xml version="1.0"?>

<configuration>


  <configSections>

     <sectionGroup name="spring">

      <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>

    </sectionGroup>

    <sectionGroup name="common">

      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />

    </sectionGroup>

    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>

  </configSections>


  <spring><!-- 이번 포스트에서 설명한다-->

     <context>

      <resource uri="config://spring/objects"/>

      <resource uri="~/Config/webServices.xml"/>

      <resource uri="~/Config/webServices-aop.xml"/>

    </context>


    <objects xmlns="http://www.springframework.net">

      <description>Definitions of objects to be exported.</description>


      <!-- Aspect -->


      <object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

        <property name="Level" value="Debug"/>

      </object>


      <!-- Service -->


      <object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>


      <object id="calculatorWeaved" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

        <property name="target" ref="calculator"/>

        <property name="interceptorNames">

          <list>

            <value>CommonLoggingAroundAdvice</value>

          </list>

        </property>

      </object>


    </objects>

   </spring>


  <system.web> <!-- 나중에 설명한다.-->

    <httpHandlers>

      <add verb="*" path="*.asmx" type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>

    </httpHandlers>

    <httpModules>

      <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>

    </httpModules>

    <compilation debug="true"/>

    <customErrors mode="RemoteOnly"/>

    <authentication mode="Windows"/>

    <authorization>

      <allow users="*"/>

    </authorization>

    <trace enabled="false" requestLimit="10" pageOutput="true" traceMode="SortByTime" localOnly="true"/>

    <sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20"/>

    <globalization requestEncoding="utf-8" responseEncoding="utf-8"/>

  </system.web>


  <common> <!-- 나중에 설명한다.-->

    <logging>

      <factoryAdapter type="Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Common.Logging.Log4Net">

        <!-- choices are INLINE, FILE, FILE-WATCH, EXTERNAL -->

        <!-- otherwise BasicConfigurer.Configure is used -->

        <!-- log4net configuration file is specified with key configFile -->

        <arg key="configType" value="INLINE" />

      </factoryAdapter>

    </logging>

  </common>


  <log4net> <!-- 나중에 설명한다-->

    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">

      <file value="Logs/log.txt"/>

      <appendToFile value="true"/>

      <rollingStyle value="Size"/>

      <maxSizeRollBackups value="10"/>

      <maximumFileSize value="100KB"/>

      <staticLogFileName value="true"/>

      <lockingModel type="log4net.Appender.RollingFileAppender+MinimalLock"/>

      <layout type="log4net.Layout.PatternLayout">

        <conversionPattern value="%date [%-5level] %logger - %message%newline"/>

      </layout>

    </appender>

    <!-- Set default logging level -->

    <root>

      <level value="DEBUG"/>

      <appender-ref ref="RollingFileAppender"/>

    </root>

    <!-- Set logging for Spring.Aspects -->

    <logger name="Spring.Aspects">

      <level value="DEBUG"/>

    </logger>

    <!-- Set logging for Spring.Calculator -->

    <logger name="Spring.Calculator">

      <level value="DEBUG"/>

    </logger>

    <!-- Set logging for Spring -->

    <logger name="Spring">

      <level value="INFO"/>

    </logger>

  </log4net>


</configuration>

■ configuration과 Spring 컨테이너

configuration 구조는 .NET의 표준 구조를 그대로 이용하고 있다. web.config를 이용하고 있으니 당연한 이야기다. 대부분 Spring.NET용 설정만 포함되어 있지만, 기존에 알고 있는 web.config 설정 요소들도 포함될 수 있다.  system.web 섹션을 보면 httpHandler와 httpModule을 등록하는 ASP.NET 설정이 포함되어 있다. spring.net 관련 설정은, spring섹션을 보면 알겠지만 Spring만의 configuration 내용을 별도의 파일에 xml형태로 저장할 수도 있다.

이렇게 설정된 정보들은 Spring 컨테이너가 생성되면서 모두 컨테이너에 읽혀진다. 다음은 레퍼런스 문서에 나와있는 Spring 컨테이너를 설명하는 그림이다.

먼저 spring 섹션 중심으로 설명을 할 것이다. 이 섹션이 Spring 컨테이너를 이해하는 기본 설정을 포함하고 있기 때문이다. system.web, common/logging, log4net 섹션은 나중에 설명한다. 미리 간단하게나만 설명하면 system.web 섹션에 등록되어 있는 httpHandlers때문에 개발자는 asmx 파일을 만들지 않고도 웹 서비스를 노출시킬 수 있게 된다. 그리고 common/logging, log4net은 실제로 로깅을 구현하는 모듈과 로깅 정책을 등록하는 곳이다.

애플리케이션이 시작되면 Spring 컨테이너는 configuration 설정( 문서에서는 "configuration 메타데이터" 라고 하고 있다 )을 읽여들이는데, configuration 설정은 Spring 컨테이너에게 객체를 어떻게 생성하고(예를 들어, singleton으로 생성할지 prototype(일반 객체)으로 생성할지), 객체 생성에 필요한 정보 등을 알려주는 역할을 한다. Spring 컨테이너는 이런 설정 정보를 이용해서 애플리케이션에서 제공하고 있는 클래스들을 이용해서 필요하면 즉시 객체를 생성하는등 시스템이 가동될 준비를 마친다.

■ 컨테이너 생성에 필요한 설정들 - 리소스

실제로 Spring 컨테이너 객체를 생성할때는 첫번째로 필요한 것이 컨테이너를 설정할 리소스들에 대한 정보이다. 이런 리소스들이 대부분 객체들에 대한 정의가 될 것이다. 리소스들이 포함되어 있는 경로에 대한 정보는 <context/resource/>요소로 표현되는데, 이것을 파싱히는 녀석은 <configSections/>에 등록되어 있는 context 섹션 핸들러 WebContextHandler이다.

<configSections>

  <sectionGroup name="spring">

    <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

  </sectionGroup>

</configSections>


<spring>

  <context>

    <resource uri="config://spring/objects"/>

    <resource uri="~/Config/webServices.xml"/>

    <resource uri="~/Config/webServices-aop.xml"/>

  </context>

</spring>

WebContextHandler가 Spring 컨테이너를 생성할때 필요한 객체들에 대한 정보는 <context/resource/>요소들을 통해서 얻는다. 이 예제에서는 Spring 컨테이너에 로딩될 객체들에 대한 정보를 <context/resource/>요소로 읽어들이고 있고 이것이 이 요소의 가장 일반적인 용도이다.  그렇지만 다른 리소스도 이 요소를 통해서 로딩할 수 있다.

<context/resource/>요소는 현재 컨테이너 컨텍스트( 애플리케이션 컨텍스트가 아니다. 애플리케이션과 컨테이너는 의미가 다르다. 뒤에 설명할 기회가 있을것이다. 있을가? )을 구성하는 리소스들에 대한 경로를 가지고 있는데, 리소스에 따라서 그것에 접근할 수 있는 다양한 경로 포맷을 갖는다. 리소스는 파일일 수도 있고, 어셈블리내에 포함된 리소스일 수도 있다. 현재 지원되고 있는 리소스의 종류 및 접근 포맷은 다음과 같다.

리소스 접근 포맷 접근 포맷 파싱 모듈
어셈블리에 포함된 데이터 assembly://<AssemblyName>/<NameSpace>/<ResourceName> AssemblyResource
.NET configuration 파일( web.config등)에 포함되어 있는 있는
커스텀 configuration 섹션에 저장된 데이터
config://<path to section> ConfigSectionResource
파일 시스템의 파일 file://<filename> FileSystemResource
Http, Https 프로토콜로 접근할 수 있는 데이터 표준적인 http, https 표현 UriResource

표에서 리소스컬럼은 어떤 리소스인지를 설명하고 있고 접근 포맷은 그 리소스에 접근하기 위해서 사용하고 있는 Uri 포맷을 나타낸다. 그리고 참조고 마지막 컬럼에 접근 포맷을 해석해서 해당 리소스에 접근해서 읽어오는 모듈명을 표시했다. 앞의 예제 web.config에서는 configuration 섹션의 리소스와 http 리소스를 이용하겠다고 설정하고 있다( 근데 "~"로 시작하는 리소스 접근 포맷이 파일 접근 포맷인지, http 접근 포맷인지 확신은 없다. 다만 ASP.NET에서 사용하는 "~"가 나타내는 것이 웹 경로인 것을 보면 http 리소스 일것으로 추측된다 ).

예제에서는 <context/resource/>에서 표시하는 리소스 경로의 내용을 읽어들여보면 모두 <objects/> 요소를 가지고 있다. 리소스 핸들러는 xml 파일이나 <objects> 섹션에 포함된 리리소스들이 모두 객체들에 대한  정의라는 것을 알게 된다. 핸들러는 <spring/objects/> 섹션에 설정되어 있는 <object/>들뿐만 아니라 두개의 xml 파일에 정의되어 있는 <object/>요소들을 읽어들인다. 필자도 이곳에 정의되어 있는 객체들에 대해서 아직은 잘 모른다. 이때 <objects/>를 파싱하기 위해서 호출되는 핸들러가 바로 <configSections/>에 등록된 DefaultSectionHandler이다.

  <configSections>

    <sectionGroup name="spring">

      <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>

    </sectionGroup>

  </configSections>


  <spring>

     <context>

      <resource uri="config://spring/objects"/>

    </context>

     <objects xmlns="http://www.springframework.net">

     ...

    </objects>


  </spring>

두 섹션 <spring/context/>와 <spring/objects/>이 바로 Spring 컨테이너를 정의하는 섹션이다. 두 섹션이 바로 new를 통한 컨테이너 객체 생성 코드에 해당한다는 것이다( 실제로 프로그램적으로도 컨테이너 객체를 생성할 수있는 API가 있다). 예제에서처럼 컨테이너를 설정하는 내용이 별도의 xml 파일로도 존재할 수 있는데, 그런 경우에는 컨테이너를 생성할때 해당 파일의 모든 내용들을 읽어들인다.

■ 객체 정의 <object/>요소

Spring configuration은 최소한 하나의 객체 정의를 가지고 있어야 한다. 그러다 대부분 하나 이상의 객체를 정의하는 요소 <object/>가 <objects/>에 포함된다. 이렇게 <object/>요소에 객체에 대한 정보( 타입과 어셈블리등)를 제공하는 것을 "Spring 컨테이너에 객체를 등록한다"고 표현하고 있다. 이 객체 정의 요소는 애플리케이션이 시작되면 실제 객체에 해당된다. 즉 new를 사용해서 생성한 실제 객체에 해당한다. 그러나 이렇게 configuration에 등록하면 Spring 컨테이너가 대신 자동으로 생성해준다.

<objects xmlns ="http://www.springframework.net">

  <object id="..." type="...">

    <!-- 이 객체가 의존하고 있는 다른 객체 그리고 필요한 설정이 있다면 이곳에 표현한다-->

  </object>

  <object id="..." type="...">

    <!-- 이 객체가 의존하고 있는 다른 객체 그리고 필요한 설정이 있다면 이곳에 표현한다-->

  </object>

  <!-- 더 많은 객체 정의가 가능하다.-->


</objects>

      
애플리케이션별로 예를 들어 웹 애플리케이션에서는 이곳에 어떤 객체들을 등록시키수 있을까 하는 문제는 나중에 좀 고민해서 실전 애플리케이션 개발 전략을 다루는 포스트를 별도로 제작해 볼 계획이다. 미리 예상해보면 비즈니스 레어어 또는 데이터 액세스 레이어의 객체들이 이곳에 등록될 수 있지 않을까 하는 추측을 해 볼 수 있겠다. 지금은 Spring.NET 프레임워크의 개념 파악에 집중하도록 한다.

객체를 생성하는데 필요한 정보들이 있다. 어떤 정보들이 필요할까.

* 타입 이름 : 컨테이너에 생성될 객체가 정의되어 있는 실제 클래스( concrete class)

* 객체가 어떻게 거동할 것인가에 대한 설정 즉 singleton이냐 아니면 prototype(singleton이 아닌 객체를 말한다)이냐. 생명주기 콜백함수( 객체가 초기화되거나 해제될때 호출될 함수)가 있다면 어떤 함수인가.

* 객체가 자신의 작업을 하기 위해서 의존하고 있는 다른 객체들이 있다면 그 의존 객체들( dependencies 또는 collaborators로 표현한다)에 대한 정보 설정

* 또 있는데 무슨 말인지 잘 모르겠다-_-;;

이런 정보들을 표현하기 위해서 <object/>의 어트리뷰트 또는 인라인 <object/> ( <object/>하위에 포함된 다른 <object/>요소)및 여러 요소들을 이용하고 있다.

특징 객체 정보
type 객체를 정의하고 있는 클래스 및 클래스를 포함하고 있는 어셈블리명
id/name 등록되는 객체들은 고유한 아이디를 가지고 있다. 고유한 객체를 나타내기 위해서 <object/>요소의 "id" 또는 "name" 어트리뷰트를 사용한다.
singleton 객체를 singleton으로 생성할지에 대한 여부를 알리는 어트리뷰트. 기본값은 true로서 아무 표시가 없으면 기본적으로 singleton으로 생성된다. 즉 컨테이너별로 객체가 하나 생성된다. singleton으로 표시된 객체는 컨테이너가 생성되면서 객체들도 생성된다. singleton="false"로 되어 prototype으로 설정된 객체들은 실제로 코드상에서 객체가 요청될때 생성된다.
<object/>하위요소  
<constructor-arg/>하위 요소  
<property />하위 요소  

뒤의 3개의 하위 요소들은 DI( dependencies injection)과 관련된 설정들이다. 뒤에서 별도로 공간을 마련해서 설명하도록 하겠다( ^^). 퇴근하자. 아자. 졸려!

다 써 놓고 보니 포스트 구조가 좀 이상하다. 샘플을 제일 먼저 보여주고서는 Spring 컨테이너 얘기만 하고 끝나다니. 야튼 이 샘플은 계속 가지고 가겠다.

Posted by dalbong2

Aspect지향 프로그래밍! 프레임워크 입장에서는 아주 쓸모있고 중요한 개념이다. 개발자들의 코딩을 화~악 줄여줄 수 있고 또한 프로젝트가 진행하고 있는 도중에도 개발자들의 코드 수정없이 프레임워크단에서 갑의 요청 사항을 최대한 흡수해 줄 수 있는 완충 역할을 할 수 있는 방법이다.

그러나 얼른 와 닫지 않는 용어이다. Object Oriented Programming이라는 용어를 처음 들어을때도 이런 떨떠름한 기분이었을까 하는 생각이 든다. Object가 뭔지 정의를 정확히 내리라면 머뭇거리게 되지만, 그래도 우리는 이것에 대해 이해는 하고 있다. 문장의 주어 또는 목적어로 사용될 수 있는 "놈"들이다.  "이 녀석의 어떤 메소드를 호출하면 ..." 또는 "저 녀석의 어떤 메소드를 호출해줘야 ~ 할 수 있다"처럼 마치 이야기의 대상처럼 사용할 수 있는 것이 object이다.

그럼 aspect란? longman 사전을 찾아보면 다음처럼 정의되어 있다 : "one part of a situation, idea, plan etc that has many parts". 그리고는 다음과 같은 예문이 나와 있다 : "Dealing with people is the most important aspect of my work. 사람을 다루는 일이 내 일중의 가장 중요한 일이다". "전체중의 부분 또는 전체중의 단면"을 의미한다고 하겠다.

소프트웨어 개발에서의 aspect도 의미적으로는 이와 비슷한 개념으로 정리될 수 있을 것 같다. 비즈니스 로직을 실제로 구현하다보면 필요한 비즈니스 로직 구현외에도 기능성 코딩을 해야 하는 경우가 많다. 예를 들어 로깅이나 예외처리, 트랜잭션처리등은 비즈니스 요구와는 직접적인 상관은 없지만 계속 반복되는 기능들이다. 이런 기능들을 애플리케이션을 만들때마다 또는 하나의 애플리케이션에서 다른 비즈니스 로직을 구현할때마다 계속 반복해서 코딩하기 보다는 처리 모듈들을 "단면!"별로 분리해서 구현하자는 것인데, 이런 각각의 단면 모듈들(로깅, 예외처리, 트랜잭션처리, 보안처리등)을 aspect라 하고 있다.

비즈니스 계층의 메소드를 개발할때 다음과 같은 형식의 코딩에 대한 경험이 있을 것이다.

public void 메소드()

{

    // 메소드 시작 로깅

    // 메소드 호출

    // 메소드 종료 로깅

}

또는 다음과 같은 형식으로 트랜잭션을 처리해본 경험도 있을 것이다.

public void 메소드()

{

    // 트랜잭션 설정 및 시작

    try

    {

        // 비즈니스 로직 구현


    }

    catch

    {

        // 트랜잭션 롤백


    }

}

순수한 비즈니스 로직 구현 코드와 로깅, 트랜잭션 처리 코드가 섞여 있고 이런 부가적인 코드는 메소드마다 복사되어서 사용되었다. 이런 로깅 그리고 트랜잭션 처리 코드는 특정 비즈니스 로직에서만 사용되는 것이 아니다. 아래 그림에서처럼 계좌이체 모듈, 입출금모듈, 이자계산 모듈 등 여러 관심 모듈에 걸쳐서(cross) 공통적으로 필요한 모듈들이다. 따라서 aspect를 cross concerns이라는 용어로도 표현한다. 다음 그림에서는 로깅, 보안, 트랜잭션과 같은 cross concerns 구현하고자 하는 비즈니스 관심 모듈의 관계를 개념적으로 표현하고 있다.

(객체 지향을 넘어서 관점 지향으로 AOP. http://www.zdnet.co.kr/builder/dev/java/0,39031622,39147106,00.htm )

객체 지향에서는 객체를 분리해내고 그것을 설계하는 것이 중요하듯이 aspect 지향에서는 앞에서와 같은 cross concerns을 정의하고 분리해서 설계하는 것이 중요하다. 객체 지향으로 설계된 객체들을 구현하는 툴로서 C++, C#이 있듯이 aspect 지향으로 설계된 aspect들을 메인 로직과 혼합하는 작업을 가능하도록 하는 툴들이 있다. .NET계열에서는 Spring.NET이 그 대표적인 예라 하겠다. 그러나 aspect 자체를 구현한 코드는 객체 지향 언어를 사용해서 구현한다.

비즈니스 로직과 그것을 구현하기 위한 핵심 클래스 및 메소드는 객체 지향 설계(OOA)로 도출될 수 있다. 그러나 이런 aspect들은 이런 객체 지향 방법론으로 도출할 수 없었다. 그러니 계속해서 같은 목적( 로깅, 예외처리, 트랜잭션 처리)을 갖는 코드가 조금씩 변경되어서 copy&paste 방식으로 이곳 저곳에서 반복되어서 삽입될 수 밖에 없었지만, aspect지향의 컨셉과 그것을 구현할 수 있는 툴들의 제공으로 이제는 코드가 좀 더 깔끔하게, 좀 더 비즈니스 중심으로 될 수 있게 된것이다.

이쯤되면, AOP란 OOP를 대신하는 프로그래밍 기법이 아님을 인식할 수 있었으리라 본다. 오히려 OOP를 기본으로 하되 그것이 처리할 수 없는 부분을 보충해주는 프로그래밍 방법이라 하겠다.

"AOP를 구현한다"는 것은 "분리된 cross concerns을 실제로 코드로 구현하고 그 코드를 필요한 비즈니스 관심 모듈의 적절한 위치에 삽입하는 작업"이라고 할 수 있겠다. 이런 AOP 구현을 이해하기 위해서는 이해해야하는 하위 개념들이 있다. 이런 개념들은 조금은 낯선 용어들로 표현되고 있다.

advice(또는 interceptor)
advice가 바로 앞에서 말한 "단면"을 구현한 코드이다. 즉 로깅, 트랜잭션등을 구현한 코드를 말한다. 이것을 interceptor라고도 한다. 두 표현 모두 옆에서 치고 들어오는 것들 표현하고 있다. advice 즉 충고 또는 훈수라는 것도 옆에서 갑작스레 치고 들어오는 것은 마찬가지다. 메인 비즈니스 로직에 추가되어 부가적인 훈수를 두는 코드를 말한다.

joinpoint

advice가 치고 들어올 수 있는 포인트들이다. cross concerns 모듈의 메인 비즈니스 로직에 삽입이 가능한 후보 위치를 말한다. 비즈니스 로직을 구현한 메소드가 호출되기 전 또는 후, 반환값이 반환되기 전 , 예외가 던져지는 지점, 클래스가 초기화되는 곳, 필드를 액세스하는 부분등이 모두 advice가 삽입될 수 있는 후보 포인트들이다. 그러나 모든 jointpoint가 실제로 advice가 삽입되는 곳은 아니다.

pointcut

joinpoint중에서 실제로 advice가 적용될 위치를 나타낸다. joinpoint가 개념적인 것이라면 툴마다 실제로 구현하고 있는 pointcut은 다를 수 있다. 뒤에서 보게 되겠지만 특정 pointcut를 나타내는 타입들이 Spring.NET에도 이미 구현되어 있다.

advisor

pointcut + advice를 말한다. 즉 어디서(where, pointcut) 무슨 일(what, advice)이 일이 일어날지를 정의한다. advisor가 바로 aspect의 실제 구현된 모습이라고 할 수 있다.

advised object /advised method

문서를 보다 보면 advised된 객체 또는 메소드라는 말을 보게 된다. advice 코드가 삽입된, 적용된 객체 또는 메소드라는 의미이다. advisor에 의해 훈수를 받은 객체 또는 메소드라는 것이다.

AOP는 일반적인 프로그래밍 방법이다. 즉 Spring.NET만의 개념은 아니다.  Spring.NET에서는 이런 AOP 개념들을 모두 구현하기 위한 방법을 제공하고 있지만, Spring.NET의 IoC컨테이너는 이 AOP 기술에 의존하고 있지는 않다. 즉 Spring.NET 사용자는 원한다면 AOP를 사용하지 않아도 된다는 것이다.

그러나 반복되는 코드를 단지 어트리뷰트를 사용해서 선언적(declarative)인 방식으로 해결할 수 있다면 코드가 깔끔해질 수 있을 것이고 유지, 보수에도 효과적인 방법이 될 수 있을 것이다. Spring.NET에서는 AOP를 구현할 수 있는 모든 준비를 갖춰놓고 있다. 사용자는 이제 HOW-TO만 배우면 되는 것이다.

나중에 알게 되겠지만, Spring.NET에서 AOP 개념은 프락시를 이용하고 있다. 그리고 프락시에 대한 소유권은 프레임워크에서 가지게 된다. 개발자가 타겟 객체를 요구할때 프레임워크에서는 그 객체에 대한 프락시를 반환하는 패턴을 이용하게 된다. 이 프락시를 잘 이용하면 개발자의 일명 삽질이 상당히 줄어들 수 있다.

프락시 코드를 프레임워크단이 가진다는 의미는 타겟 객체의 메소드 호출을 프레임워크단에서 모두 catch할 수 있다는 것인다. 즉 실제로 타겟 객체의 메소드 호출을 수행하기 전에 그리고 메소드 호출을 수행하고 나서의 순간들을 모두 프레임워크에서 포착할 수 있게 되어 필요한 작업을 할 수 있다. 필요한 작업이란 예를 들어 타겟 메소드를 호출하기 전에 타이머를 실행시켜 놓은 다음 타겟 메소드의 호출이 종료된 후 타이머의 시간을 재서 메소드의 실행 시간을 체크할 수도 있다는 것이다. 또 다른 예로 프락시를 통해서 타겟 메소드에 대한 정보를 얻어서 적절한 로그를 남기는 작업을 프레임워크단에서 처리할 수도 있다. 이런 작업들이 개발자들의 코드 수정없이 프레임워크단에서 일괄적으로 처리될 수 있다는 것이다. 즉 프락시 패턴을 이용하게 되면 프로젝트 진행 도중에 비즈니스 로직과 상관없는 추가 요구 사항은 최대한 프레임워크단에서 커버할 수 있는 구조가 된다는 것이다.

다음 포스트에서는 AOP 관련 샘플을 통해서 Spring.NET이 지원하는 방법을 알아보겠다. 앞에서 보이지 못한 IoC 예제 코드 즉 객체를 등록하고 설정하는 방법에 대한 것도 이 예제에서 함께 설명하도록 하겠다.

Posted by dalbong2

앞으로 포스트를 진행해가는 방법으로는 개념 설명과 그 개념에 필요한 샘플을 적절히 혼합해가는 방법을 사용하겠다. IoC, AOP처럼 새로 등장하는 개념들은 먼저 설명을 하겠다. 그리고 Spring.NET에서 제공하는 다른 유틸성 기능은 어떻게 사용하는지에 대한 샘플 코드로 바로 들어갈 것이다.  필요한 코드들은 sourceforge.net에서 제공하는 샘플들을 사용하도록 하겠다.

이번 포스트에서는 IoC에 대해서 먼저 알아볼 것이다. IoC? 어디서 많이 들어본 것 같은가? 국제 올림픽위원회 ? No ! Inversion of Control의 약자로서 "역제어" 정도로 해석될 수 있겠는데 영 어색하다. 의미는 이렇다. 기존의 프로그래밍에서는 객체 생성의 제어권을 개발자가 가졌다면 그 제어권이 이제 "반대측"으로 넘어갔다는 것이다. 여기서 "반대측"이란 바로 컨테이너를 말한다.  Spring.NET도 컨테이너 프레임워크중의 하나로서 Spring.NET 컨테이너가 객체를 생성하고 그 생명주기를 제어한다는 것이다.

앞에서 알아본 Unity Application Block 또한 컨테이너 프레임워크중의 하나로서 IoC를 구현하고 있는 프레임워크중의 하나이다. 컨테이너에서  객체들을 생성하고 lifecycle, scope를 관리하고 또는 적절한 곳에서 필요하다면 dependency를 inject해주는 역할을 한다. dependencies inject하면 setter injection, contructor injection 그리고 method injection이 있다는 것을 이전 포스트에서 알아봤다.

개념적으로는 어려울게 없다. IoC 하면 "컨테이너 프레워크", "dependency injection"을 떠올리면 된다. 객체를 컨테이너에 등록하는 절차 및 injection이 일어날 곳 그리고 어떤 dependencies가 어느 위치에서 inject될지에 대한 정보를 configuration을 통해서 할 수 있다. 물론 프로그램적으로도 가능하지만 configuration을 이용하는 방법이 더 실질적인 방법이다. 여기까지만 떠올릴 수 있다면 개념은 잡힌 것이다.

IoC 또는 DI(dependencies injection)이라는 용어를 누가 만들었고 어떤 차이점이 있는지는 지금 단계에서는 그닥 중요하지  않다. 나중에 내공이 생기고 관심이 있다면 좀 더 개인적으로 찾아보면 될 것이다.

다음 포스트에서는 아주 유용한 개념을 소개할 것이다. AOP( Aspect Oriented Programming)!

Posted by dalbong2

이제 다시 개발 프레임워크 얘기로 가 보도록 하겠다. 이제부터는 Spring.NET 프레임워크를 알아볼 것이다. 앞에까지는 Unity Application Block을 알아봤는데 사실 필자는 이것보다 Spring.NET 프레임워크에 대해서 먼저 들었다. 그러나 마음먹고 공부해본적은 없다. 알고 있는 것은 단지 오픈 소스 프로젝트라는 것 그리고 Spring이라는 이름으로 자바쪽에서 먼저 나왔고 Spring.NET 프레임워크는 자바 버전이 .NET쪽으로 포팅된 것이라는 것 정도이다.

며칠동안 틈나는 대로 Spring.NET 레퍼런스를 읽어보고 있다.  www.springframework.net에 가보면 문서 및 관련 소스를 받아 볼 수 있다. 원서라서 속도가 나질 않아서 아직 다 읽어보지는 못했다. Unity 블럭에서처럼  포스팅을 하면서 공부를 해야 할 것같다. Spring과 관련된 자바진영의 문서는 꽤 있는 듯하다. 그래서 개념은 자바진영의 문서를 통해서 잡고 구현만 .NET진영의 문서를 참조해야 겠다고 생각했다. 그래서 한글로 된 자바진영의 책을 먼저 읽었고 다음으로 앞의 사이트에서 다운받은 레퍼런스 문서로 공부하고 있는 중이다. 이 문서의 메인 주제들을 정리해보면 다음과 같다.

▶ IoC 개념

▶ Configuring

  - configuring object with xml

  - creating objects automatically

  - using parent and child object definition

▶ 객체 Scope

▶ 객체 생명 주기 관리 - 생명주기 관련 interfaces

- IInitializingObject/ init-method

- IDisposable / destory-method

- IObjectPostProcessor

▶ Spring.NET 커스터마이징

- 객체 생성 -> IObjectPostProcessor.PostProcessBeforeInitailization -> IInitializeingObject/init-method의 콜백메소드 AfterPropertiesSet -> IObjectPostProcessor.PostProcessAfterInitailization

▶ 메세지 리소스 관리/사용하기

▶ Validation framework

▶ Aspect Oriented Programming with Spring.NET

▶ 트랜잭션 관리

▶ 예외처리

▶ Object Relational Mapping( ORMapping )

▶ Spring.NET Web Framework

▶ ASP.NET Ajax

▶ Enterprise Application에 Spring.NET 적용 전략

▶ Testing

이 주제들 하나 하나가 모두 굵직 굵직하다. 이것들을 하나씩 붙잡고 개념과 구현을 설명해나가야 할까하는 문제는 아직 결정을 내리지 못하고 있다. 이것들을 모두 설명하기에는 너무 많은 시간이 소모될 것 같고 그다지 투자 대비 효과도 좋지 않을 것 같다는 생각이다.

그렇다면 개념별 Quick start 샘플 중심으로 갈 것이냐 아니면 하나의 샘플로 시작해서 개념들을 완성시켜나가는 방식으로 갈 것이냐. 아니면 Spring.NET 사이트에서 제공하고 있는 샘플 중심으로 공부를 해 나가야 할 것이냐. 아직 결정하지 못했다.

다음 포스트가 언제 올려질지는 모르겠지만, 그 포스트가 올려질때 Spring.NET 스터디 진행방법도 결정될 것으로 보인다.

Posted by dalbong2

뭔가 아쉽다. 쉽게 쓰려고 했는데, 배가 산으로 가버린듯한 기분이다. 아직 Unity 컨테이너 구조가 어떻게 되었는지 머리에 들어오지 않는 사람이 있을 듯하다. 그래서 이번 포스트에서는 Unity 구조를 필자가 파악하고 있는 한도내에서 좀 간단히 정리하려고 한다.

Unity  컨테이너에서는 프로그램적으로 모든 필요한 환경 설정을 할 수 있는 방법을 지원하지만 주로 설정을 위해서는 cofiguration을 이용한다.  다음 그림은 Unity 구성을 간단하게 표현해보았다.

■Config 타입 등록

그림의 좌측에는 config 설정이 있다. 프로그램이 실행되기 시작하면 이곳에 설정된 정보는 컨테이너로 등록된다. 등록이 완료되고 나면 컨테이너에 익스텐션도 몇개 그림처럼 확장되어 있을 것이다. 그림에 configuration의 Configure() 메소드가 호출되고 있다는 것을 보여주고 있다. 이게 뭐냐면....

코드상에는 config의 각 요소에 해당하는 타입이 정의되어 있다.

UnityConfigurationSection

UnityContainerElement

UnityTypeElement

UnityContainerExtensionElement

UnityContainerExtensionConfigurationElement  등등

이 타입들은 인터페이스를 하나 구현하고 있는데, IContainerConfigurationCommand이다. 이 인터페이스 정의는 다음과 같다.

public interface IContainerConfigurationCommand

{

    void Configure(IUnityContainer container);

}

각 요소의 타입에서는 Configure() 메소드를 구현해야 한다. 이 메소드의 인자로는 요소가 등록되어야 하는 컨테이너 참조가 건네진다. 각 요소의 타입의 Configure()에서는 컨테이너에 자신을 등록하는 작업을 해야 한다. 또한 요소 자신이 포함하고 있는 자식 요소들이 있다면 모든 자식들의 Configure()를 호출해서 스스로 컨테이너에 등록하는 작업을 하도록 해줘야 한다. 그리고 그 자식은 또한 자신의 자식들의 Configure()를 호출해주는 작업을 해야 하고....도미노 게임같은 상황이 떠오르지 않나?

도미노 게임의 첫번째 요소는 <unity/>요소에 해당하는 타입 UnityConfigurationSection 부터이다. 이 객체의 Configure()를 호출하면 그 이하의 자식들은 알아서 컨테이너로 등록된다. Program.cs 파일등에는 다음과 같은 유사한 코드가 있는데 바로 도미노의 시작부분이다.

// config 정보 읽어들이기

UnityConfigurationSection section;

section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");


//Unity 컨테이너에 config 정보를 설정한다.

section.Containers.Default.Configure(stdContainer);

타입을 등록하면 기본적으로 인스턴스가 생성되지는 않는다. 단지 타입만 등록된다. 이것이 기본적으로 구현되어 있는 거동(behavior, 거동이란 번역이 어렵나?)이다. 컨테이너에서 직접 요소를 등록하지는 않는다. 대신에 Registering이라는 이벤트를 발생시킨다. 그럼 등록되어 있는 이벤트 핸들러가 실행된다. Unity 컨테이너에는 기본적으로 UnityDefaultBehaviorExtension의 OnRegister()가 등록되어 있다. 이 핸들러에서 컨테이너에 타입을 등록하는 작업을 해준다. 이런 구조로 가는 것은 바로 사용자 정의가 편해지기 때문이다. 기본적인 거동을 변경하고 싶다면 이 이벤트 핸들러를 재정의해서 등록해주면 되는 것이다.  컨테이너 자체의 코드를 수정하지 않아도 된다. 야튼 각설하고.

■타입의 인스턴스에 접근

다음은 이렇게 등록된 타입의 인스턴스에 대한 참조에 접근하는 방법이다.

stdContainer.Resolve<실제타입>()

익스텐션으로 등록한 타입의 인스턴스를 얻는 방법은 이것과 다르다.

container.Configure<익스텐션타입>()

익스텐션의 경우는 인스턴스를 얻는 메소드가 약간 이상하게 보이기는 하다. 왜 이렇게 이름을 사용했는지 모르겠다.

정리한다고 해 놓고서는 뭘 정리한건지 모르겠네. 쓰으...

근데, 왜 이렇게 타입을 등록하고 등록 정보를 갖는 컨테이너라는 것을 사용하는 것일까? SI 프로젝트에 이 프레임워크를 어떻게 사용할 수 있는가?

SI 프로젝트에는 화면이 매우 많다. 대형 프로젝트인 경우는 몇 천번 페이지 이상이 된다. 이 수천페이지에서는 공통으로 사용하는 모듈이 있다면 이 녀석을 공통으로 등록해 놓고 싶지 않겠는가? 공통 코드를 불러오는 부분이라든가. 사용자 정보를 조회할 수 있는 모듈에 접근하는 부분이라든가. 또는 현재 페이지의 사용자에 대한 권한을 조회할 수 있는 부분 등. 이렇게 공통적인 부분은 <type>에 등록해 놓고 사용하면 좋을 것이다. 그러나 모든 페이지에 대한 타입을 이곳에 등록하는 것은 무리일 것 같다. 페이지에 대한 타입은 이전처럼 메뉴 관리 프로그램에 등록하고 메뉴 조회 모듈을 사용하는 사용하는 것이 바람직할 듯 보인다.

또한 <extension> 부분은 앞에서 말한 사이트별 프레임워크를 만드는 곳으로 적합할 것이라는 생각이다. 앞에서 예로 든 사용자 정보 클래스처럼 사이트별로 별도의 구현을 해야 하는 부분이라든지 하는 부분을 사이트에서 직접 구현해 이곳에 등록해주면 적합할 것 같다.

역시, 뭔가 좀 덧붙여도 여전히 뭘 정리한 건지 모르겠다. 쓰으..

휴일이 또 이렇게 간다.

Posted by dalbong2

지난 포스트에 계속 이어진다. 혹시 지난 포스트를 읽지 않았다면 먼저 체크해보고 이번 포스트를 읽어가길 바란다. 이번 포스트 다 써놓고 보니 무지 길다. 실시간으로 이해를 하면서 쓰는 글이다 보니 이 모양이다. 다시 읽어보고 싶지 않다. 쓰으...틀린곳 또는 이해가 되지 않은 부분 있다면 코멘트 부탁한다.

■ 예제 시나리오

이 예제에서는 두 개의 Unity 컨테이너를 생성한다. 하나는 표준 컨테이너 stdContainer이고 하나는 확장된 기능을 갖는 customContainer이다. stdContainer는 .NET 애플리케이션이 실행되면 자동으로 생성되는 기본 AppDomain으로 어셈블리를 로딩한다. 그리고 customContainer는 기본 AppDomain에서 별도의 AppDomain을 하나 더 생성해서 그쪽으로 어셈블리를 로딩한다. 이때 추가로 생성되는 AppDomain은 섀도우 복사를 지원하는 것으로서 로딩할 어셈블리를 ShadowFiles 속성에 설정된 위치에서 검색한다. 그런 다음 임시 복사 위치로 복사한다. 가릿? OK! 아래 그림을 보라.

양쪽 AppDomain으로 각각의 어셈블리를 로딩하고 그곳에서 특정 타입( StdClass, SatClass)의 객체를 생성해서 각각의 Speak() 메소드를 호출한다. 근데, 이전 포스트에서 각각의 AppDomain은 독립적으로 작동한다고 해 놓고서는 그림을 보면 기본 AppDomain에 있는 customContainer객체가 추가 생성된 AppDomain의 객체B의 메소드를 호출하고 있다. 쓰으. 필자도 예상 못했던 부분이다. 예제를 대충 훓어봤을때는 AppDomain의 기본 개념내에서 예제가 작성되었을 줄 알았다. 이렇게 AppDomain간에 통신이 가능하다는 것을 예제로 바로 보여줄 줄은 몰랐다. 뭐 그렇다고 크게 당황하지 말자. 분명 AppDomain은 static 멤버조차도 서로 공유하지 않는 독립적인 공간이다. 그러나 두 공간간의 통신도 가능한데 물론 조건이 있어야 한다. 뒤에 예제 소스 코드를 보면 알겠지만,  객체B의 타입 정의에서처럼 MarshalByRefObject 타입을 상속받아야 한다. MarshalByRefObject에 대해서는 여기까지!

추가로 생성된 AppDomain이 타입 SatClass가 AssemblyB.dll에 포함되어 있다는 것을 알고( 어떻게 알까? config에 설정해놨다) 그 어셈블리를 찾는다. 뒤에서 추가로 AppDomain이 생성되는 부분( MyExtension의 CreateDomain() 부분)을 살펴보면 알겠지만 추가 AppDomain의 기본 디렉토리는 ShadowFiles 로 설정한 곳이다. 그러나 추가 AppDomain은 이곳에 있는 어셈블리를 직접 로딩하지 않는다. 추가 AppDomain은 섀도우 복사를 하는 도메인으로 설정되어 있기때문에 일단 임시 복사본 폴더로 어셈블리를 복사를 한다. 그런 다음 그 복사본을 AppDomain으로 로딩한다. 외부의 개발자는 이제 언제든지 AssemblyB.dll 파일의 내용을 변경해서 다시 ShadowFiles 폴더로 원본을 덮어쓸 수 있다. 그럼 다음에 생성되는 추가 AppDomain은 새로운 버전의 것을 다시 섀도우 복사해서 사용한다. 이 예제에서는 ShadowFiles 폴더로 새로운 버전의 어셈블리를 복사한 다음, 추가 AppDomain을 내렸다가 다시 올리는 작업은 수동으로 하고 있다.

FileSystemWatcher같은 녀석들을 사용해서 ShadowFiles 폴더의 파일이 변경되면 자동으로 추가 AppDomain을 내리고 다시 생성하는 작업을 하도록 해주면 좀 더 멋있는 예제가 될 것이다. 이것이 ASP.NET의 시스템의 중단없는 애플리케이션 업데이트 내부 구조이다(맞나?).

그러나 다시 한번 말하지만, 이 예제는 예제의 멋을 부릴려고 작성한 것은 아니다. 예제에서 보이고 있는 새로운 strategy는 외부에서 지정한 특정 AppDomain으로 객체를 생성하는 일을 한다.  이 예제에서는 이런 strategy를 만드는 방법 그리고 그것을 Unity 컨테이너에 추가하는 방법을 보이고자 한 것이다.  가릿?

■ 컨테이너 확장 절차

우선 예제에서 strategy를 만들고 Unity 컨테이너를 확장하는 절차에 대해서 전체적으로 알아보도록 하자. 바로 코드로 헤딩하는 것보다는 이렇게 절차에 대한 그림을 염두에 두고 분석해나가는 것이 코드 이해에 도움이 될 것이다.

필요한 타입을 작성하고 config 파일에 등록하는 작업을 반복하고 있다. 물론 어떤 순으로 하든 상관없다. Unity 컨테이너 익스텐션을 하려면 이런 작업들이 있다는 것만 머리에 그리면 되겠다. 각 단계별 작업을 좀 더 알아본다. 그리고 새롭게 작성하는 타입들의 베이스 타입을 보여주고 있다. 이 베이스 타입은 이 예제에서 사용하고 있는 타입이다. 타입 hierarchy상 정확히 이 타입이 아니고 좀더 부모 타입이거나 좀 더 자식 타입이어도 될 것이다. 

단계 하는일 베이스 타입
Strategy 작성 ( MyBuildup ) 객체 빌드/해제하는 동안 하고 싶은 일을 구현하는 곳
예제에서는 외부에서 설정해준 특정 AppDomain으로 객체를 생성하는 일을 한다.
IBuildStrategy
Extension 작성( MyExtension) i) MyBuildup 객체를 생성해서 strategy 체인에 추가한다.
ii) 섀도우 복사용 AppDomain 객체를 생성해서 MyBuildup 객체에 건네준다.
UnityContainerExtension
Config 등록 앞에서 작성한 MyExtension을 config 파일에 등록한다.  
Extension Config 핸들러 작성 i) Extension용 config 설정 정보를 파싱해서 가지고 있다.
ii) Extesion 객체를 config 정보를 이용해서 초기화하거나 필요한 작업을 해준다.
UnityContainerExtensionConfigurationElement
Config 등록 앞에서 작성한 Extension Config 핸들러를 config 파일에 등록한다.  
테스트용 객체 타입 생성 현 예제에서는 두 개의 도메인에 각각 로딩될 StaClass와 SatClass 타입을 정의하고 있다. 그리고 두 개의 타입은 IMyInterface를 구현하고 있다. 이 인터페이스는 간단한 Speak() 메소드만을 정의하고 있다. 그리고 앞의 SatClass는 MarshalByRefObject를 상속하고 있다. 왜?  타입의 객체는 추가생성되는 AppDomain에서 인스턴싱된다. 그러면서 기본 AppDomain에서 호출되고 있다. 그렇게 되려면 원격 객체는 이 타입을 상속받아야 한다고 했다.  
테스트용 객체 타입 등록 앞에서 작성한 테스트용 객체 타입을 config에 등록한다.  

■ 소스 설명

이제 구현되어 있는 예제 소스를 보도록 하자. 블로그에서는 샘플 코드 조각들만을 제시하고 있어서 한번 실행시켜 보기 위해서는 수동으로 개발 구조를 재구성해야 했다. 프로젝트명이 정확히 일치하지는 않는다. 특히 UnityExtension 프로젝트는 원래 AppDomainUnity이었다. 그러나 익스텐션 부분이라는 것을 좀 더 강조하고 싶어서 앞의 이름을 사용하기로 했다. 기분나빠하려나? 쓰으... 

AppDomainUnity.AssemblyA, AppDomainUnity.AssemblyB, AppDomainUnity.Common 세개의 프로젝트는 테스트용 비즈니스 객체들이다.  Common에 인터페이스 IMyInterface가 정의되어 있고 AssemblyA와 AssemblyB에는 이것을 구현하고 있는 StdClass와 SatClass가 있다.

public interface IMyInterface

{

    string Speak();

}

namespace NickField.AppDomainUnity.AssemblyA

{

    public class StdClass : IMyInterface

    {

        public string Speak()

        {

            return "I'm StdClass object";

        }

    }

}

namespace NickField.AppDomainUnity.AssemblyB

{

    public class SatClass : MarshalByRefObject, IMyInterface

    {

        public string Speak()

        {

            return "I'm SatClass object";

        }

    }

}

StdClass의 객체는 표준 컨테이너 stdContainer에 로딩되어 호출될 것이고, StaClass의 객체는 확장 컨테이너 customContainer에 로딩되어 호출된다. 원격에 생성될 타입 SatClass는 MarshalByRefObject를 상속하고 있다. 원격 AppDomain에 어셈블리가 로딩된다면 클라이언트 AppDomain에서는 어떻게 그것의 타입을 알 수 있을까?  타입을 등록하고 있는 config 파일을 함 보자. 다음은 예제에서 사용하고 있는 전체 config 내용이다.

<unity>

  <typeAliases>

    <typeAlias alias="IMyInterface"   

                      type="NickField.AppDomainUnity.Common.IMyInterface, AppDomainUnity.Common" />

    <typeAlias alias="AssemblyAObject" 

                      type="NickField.AppDomainUnity.AssemblyA.StdClass,  AppDomainUnity.AssemblyA" />

    <typeAlias alias="AssemblyBObject" 

                      type="NickField.AppDomainUnity.AssemblyB.SatClass,  AppDomainUnity.AssemblyB" />

  </typeAliases>                             

  <containers>

    <!-- Default Container -->

    <container>

      <types>

        <type type="IMyInterface" mapTo="AssemblyAObject" name="local" />

      </types>

    </container>

    <!-- Dynamic Container -->

    <container name="customContainer">

      <types>

        <type type="IMyInterface" mapTo="AssemblyBObject" name="remote" />

      </types>

      <extensions>

        <add type="NickField.UnityExtension.MyExtension, UnityExtension" />

      </extensions>

      <extensionConfig>

        <add name="MyExtensionConfigHandler" 

                   type="NickField.UnityExtension.MyExtensionConfigHandler, UnityExtension"

            domain="CustomAppDomain" shadowFiles="Shadow" />

      </extensionConfig>

    </container>

  </containers>

</unity>

기본 컨테이너에는 "AssemblyAObject" 즉 "NickField.AppDomainUnity.AssemblyA.StdClass,  AppDomainUnity.AssemblyA"만이 등록되어 있다. 그리고 확장 컨테이너에는 "AssemblyBObject" 즉 "NickField.AppDomainUnity.AssemblyB.SatClass,  AppDomainUnity.AssemblyB"만이 등록되어 있다. 이것은 기본 컨테이너에서는 SatClass 타입에 대해서는 알 수 없다는 것이다. 그럼 어떻게 확장 컨테이너의 객체 B에 대한 참조를 받아올 수 있겠나?

다행히도 config를 보면 SatClass 타입이 구현하고 있는 IMyInterface를 기본 컨테이너에서도 알고 있다는 것이다. 바로 기본 컨테이너에서 별칭으로 해서 사용하고 있는 type="IMyInterface"가 <typeAlias>요소에 선언되어 있는 type="NickField.AppDomainUnity.Common.IMyInterface, AppDomainUnity.Common" 을 가리키고 있는데 그것이 바로 SatClass에서 구현하고 있는 베이스 타입이기도 하다. 이 인터페이스 타입을 통해서 기본 컨테이너에서도 SatClass 객체에 대한 참조를 할당해줄 수 있는 것이다. ( 앞의 그림 참조) 말이 너무 어렵나? 쓰으...

지금 필자가 하고 싶은 말은 그러나 이것이 아니다.  config 스키마에 대한 설명을 하려는 것이다. 즉 <container/>요소별로는 각각 사용하고자 하는 실제(concrete) 타입을 등록하고 그리고 각각의 컨테이너에서 공통으로 사용하는 타입은 이렇게 <container/>상위에 있는 <typeAlias/>를 사용하면 되지 않을까 하는 생각이다. 실제로 <typeAliases> 및 <typeAlias>가 이런 목적으로 정의되었는지는 모르겠지만 말이다. 별걸 아닌것을 힘들게 말했나? 야튼 이 인사, 어렵게 만드는데는 뭔가 있다니까. 이게 중요한 게 아닌데.

이제 컨테이너를 확장하는 프로젝트 UnityExtension 부분을 보도록하자. 

▶ 사용자정의 strategy 작성

namespace NickField.UnityExtension

{

    public class MyBuildup : IBuilderStrategy

    {

        AppDomain _AppDomain;

        public AppDomain AppDomain

        {

            set

            {

                _AppDomain = value;

            }

            get

            {

                return _AppDomain;

            }

        }

        public void PreBuildUp(IBuilderContext context)

        {

            Type targetType = BuildKey.GetType(context.BuildKey);

            if (AppDomain != null)

            {

                context.Existing = AppDomain.CreateInstanceAndUnwrap(targetType.Assembly.FullName, targetType.ToString());

                context.BuildComplete = true;

            }

        }

        public void PostBuildUp(IBuilderContext context) { }

        public void PreTearDown(IBuilderContext context) { }

        public void PostTearDown(IBuilderContext context) { }

    }

}

MyBuildup은 AppDomain 인스턴스에 대한 참조를 내부적으로 가지고 있다. 그리고 이 인스턴스는 외부에서 설정될 것이다(어떻게 아냐고? 내부에서 생성하는 곳이 없으니까-_-;;).  그리고 사용자 정의 strategy 타입 MyBuildup은 IBuilderStrategy를 상속하고 있다. 이 인터페이스는 앞에서 설명한 것처럼 네개의 메소드를 정의하고 있다. 이 중에서는 MyBuildup은 하나의 메소드를 구현하고 있다 : PreBuildUP().

이 메소드에서는 외부에서 생성한 AppDomain 인스턴스의 CreateInstanceAndUnwrap() 이용해서 추가된 AppDomain으로 지정한 타입의 객체를 로딩하는 작업을 한다. 그리고 그것에 대한 참조를 반환한다. 이거 좀 이상하지 않는가? 필자는 처음에 CreateInstanceAndUnwrap() 메소드가 단순히 주어진 타입에 대한 인스턴스를 생성해서 반환하는 것으로만 생각했다. 그래서 좀 더 구글링을 해보니 객체를 동적으로 생성해서 참조를 반환해줄뿐만 아니라 이 메소드를 호출한 AppDomain 인스턴스로 생성한다는 것을 알게 되었다. 나만 이제 알게 되었나? 야튼 정말 targetType이 포함된 어셈블리를 추가 AppDomain으로 로딩해서 그곳에서 객체를 생성했을까? 영문이긴 하지만 궁금하다면 다음 블로그를 참조할 수 있다.

Executing Code in Another AppDomain(http://blogs.msdn.com/suzcook/archive/2003/06/12/57169.aspx)

CreateInstanceAndUnwrap()에서 넘겨 받은 객체에 대한 참조는 현재 빌드 컨텍스트 객체의 Existing 속성에 할당하고 있다. 컨텍스트라는 용어가 나오는데 앞에서도 "애플리케이션 컨텍스트"라고 해서 한번 들어봤다. 필자도 어떻게 이 녀석을 설명하면 좋을지 고민이다. "애플리케이션 컨텍스트"라 하면 애플리케이션 차원에서 필요한 정보가 있다면 이 객체를 통해서 얻을 수 있다. 만약 빌드 컨텍스트라고 하면 Unity 컨테이너 정확히 ObjectBuild가 객체를 생성해서 반환해 주기까지의 과정 즉 객체를 빌드해가는 과정에 대한 정보를 가지고 있는 객체라고 할 수 있겠다. 과정중에서 추가한 정보는 뒤에 오는 단계에서 컨텍스트를 통해서 접근할 수 있다. 도중에 정보를 제거할 수도 있다. 가릿?

생성한 객체를 빌드 컨텍스트 객체의 Existing 속성에 할당하고 나서 BuildComplete 라는 속성을 true로 하고 있다. BuildComplete라는 속성이 뭔지 도움말을 찾아봤더니 현재 빌드하고 있는 객체는 이제 완전하니 더 이상 이 객체에 어떤 작업도 하지 말하는 표시라고 한다. 즉 이 후에 올 strategy 체인에서는 어떤 작업도 이 객체에 하지 않게 된다. 가릿? OK

MyBuildup 타입은 AppDomainUnity 어셈블리의 NickField.UnityExtension 네임스페이스에 포함되어 있다.

▶  Extension 작성

사용자 정의 strategy를 strategy 체인에 추가하는 녀석이 누구라고 했는지 기억하나? 앞의 표를 참고하라. Unity에서는 익스텐션이라고 부르는 넘이다. 이제 MyBuildup strategy를 체인에 등록할 익스텐션 MyExtension을 작성한다.

namespace NickField.UnityExtension

{

    public class MyExtension : Microsoft.Practices.Unity.UnityContainerExtension, IDisposable

    {

        private MyBuildup strategy = new MyBuildup();

        private bool disposed;

        protected override void Initialize()

        {

            Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);

        }

        /// <summary>    /// Build the new appdomain allowing   

        /// </summary>    /// <param name="domain"></param>   

        /// <param name="shadowFiles"></param>   

        public void CreateAppDomain(string domain, string shadowFiles)

        {

            AppDomainSetup appDomainSetup = new AppDomainSetup();

            appDomainSetup.ApplicationBase = Environment.CurrentDirectory + @"\" + shadowFiles;

            appDomainSetup.ShadowCopyFiles = "true";

            strategy.AppDomain = AppDomain.CreateDomain(domain, null, appDomainSetup);

        }

        ...

    }

}

MyExtension에서는 처음 부분을 보면 아예 사용자 정의 strategy MyBuilup 객체를 하나 생성해서 가지고 있다.

        private MyBuildup strategy = new MyBuildup();

이 코드를 보면 컨테이너를 통해서 MyBuildup 객체를 얻는 것이 아니라 직접 new를 사용해서 그 객체를 얻고 있다. MyBuildup이 config에 등록되지 않은 것을 보니, MyExtension의 전용 strategy라는 것을 추측할 수 있다. 왜 그런 추측을 하냐고? 원래 Unity 컨테이너의 목적이 타입의 직접 참조가 아닌 config 등록에 의한 동적 참조를 그 목적으로 하고 있기 때문에. 뭔 소린지 원. 야튼 중요한 것은 strategy 객체가 있다면 이것을 체인에 등록해주는 전용 익스텐션을 두는 것을 확장의 표준 구조로 생각해도 될 것이라는 것이다.

Initialize() 메소드에서 바로 strategy 체인에 등록하는 코드가 나오고 있다.

        protected override void Initialize()

        {

            Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);

        }

이 녀석의 override 되고 있는 것을 보니 상위 타입 또는 그보다 더 상위 타입 어딘가에서 자동 호출되고 있을 것이라는 추측을 해 볼 수 있다. Unity에서 정의한 stage 중에서 preCreation이라는 단계의 strategy 목록에 추가하고 있다. 즉 익스텐션을 초기화하는 과정에서 체인에 등록하는 작업을 해주면 되겠다는 것을 알 수 있다.

그리고 그 아래에 public으로 된 CreateAppDomain() 메소드가 정의되어 있다.

        public void CreateAppDomain(string domain, string shadowFiles)

        {

            AppDomainSetup appDomainSetup = new AppDomainSetup();

            appDomainSetup.ApplicationBase = Environment.CurrentDirectory + @"\" + shadowFiles;

            appDomainSetup.ShadowCopyFiles = "true";

            strategy.AppDomain = AppDomain.CreateDomain(domain, null, appDomainSetup);

        }

이곳에서 바로 MyBuilup이 객체 빌드시 사용하게 될 AppDomain을 생성해서 그 참조를 MyBuildup으로 넘겨주고 있다. 

CreateAppDomain() 메소드를 보면 인자로 domain과 shadowFiles 두개의 문자열을 받고 있다. domain은 AppDomain을 생성할 때 그리고 shadowFiles값은 특히 섀도우용 AppDomain을 생성할때 필요한 값이다.

이 코드 설명을 잠시 하자면, ShadowCopyFiles 속성에 "true"값을 설정해서 현재 생성되는 AppDomain 인스턴스가 섀도우 복사를 지원하도록 하고 있다. 그리고 ApplicationBase 속성을 config에 설정된 shadowFiles 값으로 하고 있다. 즉 기본 AppDomain의 현재 디렉토리에 하위 폴더로 shadowFiles에 설정한 디렉토리가 있어야 하고 이 AppDomain에서 사용할 타입을 포함하고 있는 어셈블리들이 있어야 한다. 이 값들은 변경 가능성이 다분히 있는 것으로서 config 파일에 설정하는 것이 바람직하다.

익스텐션을 작성하면 그것이 필요로하는 정보가 있을 수 있다. 이런 정보는 대부분 그 익스텐션 전용일 것이라는 것이다. 다시 말하면 익스텐션을 제작하게 되면 그 익스텐션 전용 정보를 제공하기 위해서 config 설정을 이용할 수 있으면 편할 것이다. 물론 Unity에서는 그런 시나리오를 지원하고 있다.

▶   MyExtension용 config설정

앞의 전체 config 중에서 익스텐션을 등록하고 그 익스텐션 전용 config 설정을 등록하는 부분이다.

      <extensions>

        <add type="NickField.UnityExtension.MyExtension, UnityExtension" />

      </extensions>

      <extensionConfig>

        <add name="MyExtensionConfigHandler" 

                   type="NickField.UnityExtension.MyExtensionConfigHandler, UnityExtension"

            domain="CustomAppDomain" shadowFiles="Shadow" />

      </extensionConfig> 

사용자 정의 익스텐션 자체는 <extensions>요소에 add 시킬 수 있다. 문법적인 표현으로 하자면 있는 타입은 UnityContainerExtension( 또는 그 베이스 타입인 IUnityContainerExtensionConfigurator)를 상속한 타입이면 이곳에 등록해서 사용할 수 있다.

그리고 사용자 정의 익스텐션에서 사용하는 데이터가 있다면 이것도 config에 설정할 수 있다. <extensionConfig/>요소의 하위 요소 <add/>를 통해서 익스텐션에서 사용하는 데이터를 설정할 수 있다. 이곳의 어트리뷰트 예를 들어 위의 domain, shadowFiles 같은 사용자 정의 어트리뷰트를 필요한대로 추가할 수 있다. 이런 익스텐션을 위한 설정은 Unity 사용자 마음대로 원하는 대로 정의할 수 있는 대신, 이것을 해석할 수 있는 모듈도 Unity 사용자가 제공해줘야 한다. 다시 말하면 익스텐션용 config 설정을 파싱할 수 있는 config 핸들러가 있어야 한다는 것이다. 그 핸들러의 풀네임과 어셈블리가 <add/>의 type 어트리뷰트에서 제공하고 있는 타입의 의미이다. 그 타입은 Unity 컨테이너에 name 어트리뷰트의 이름으로 등록된다. 가릿?  그럼 예제에서의 익스텐션 config 핸들러에 대한 정의를 보도록 한다.

▶   MyExtension용 config 핸들러 작성

     

namespace NickField.UnityExtension

{

    public class MyExtensionConfigHandler : UnityContainerExtensionConfigurationElement

    {

        /// <summary>   

        /// Name of the non-default application domain   

        /// </summary>   

        [ConfigurationProperty("domain")]

        public string Domain

        {

            get { return (string)this["domain"]; }

            set { this["domain"] = value; }

        }

        /// <summary>   

        /// Relative location of replaceable assemblies   

        /// </summary>   

        [ConfigurationProperty("shadowFiles")]

        public string ShadowFiles

        {

            get { return (string)this["shadowFiles"]; }

            set { this["shadowFiles"] = value; }

        }

        /// <summary>   

        /// Assign the new domain name to the custom container   

        /// </summary>   

        /// <param name="container"></param>   

        public override void Configure(Microsoft.Practices.Unity.IUnityContainer container)

        {

            container.Configure<MyExtension>().CreateAppDomain(Domain, ShadowFiles);

        }

    }

}

앞의 표에서 말했지만, 익스텐션 config가 하는 일은 2가지가 있다. 사용자 정의 익스텐션에서 필요로 하는 데이터를 config에서 읽어 가지고 있는다. 그리고 그 데이터를 해당 익스텐션으로 건네주거나 또는 익스텐션을 자동 생성해서 필요한 작업을 해준다.

MyExtensionConfigHandler에 정의된 속성들 Domain, ShadowFiles를 보면 <extensionConfig/> 하위 요소 <add/>의 어트리뷰트 domain, shadowFiles를 나타내고 있다. 그리고 상위 타입의 메소드 Configure()를 오버라이딩하고 있다. 이 메소드에서 MyExtension 타입의 익스텐션 객체를 받아와서 새로운 AppDomain 객체를 생성하는 메소드를 호출하고 있다. 

개발자의 코딩없이,  config에 익스텐션을 등록하고 그리고 익스텐션 config 핸들러를 등록하는 것만으로도 익스텐션 인스턴스를 생성해서 원하는 작업을 해 줄수도 있다는 것이다. 이런 작업을 하는 시기는 언제 ? config 내용을 읽어서 컨테이너 인스턴스를 세팅하는 시기 ! 그런 언제 컨테이너를 세팅하는 것이 가장 좋을까? 컨테이너 인스턴스를 생성하고 나서 바로 !

그럼 컨테이너 인스턴스를 호출하고 바로 config 내용으로 컨테이너 인스턴스를 설정하는 코드를 보겠다. 바로 시작 프로그램에서 하는 작업이다.

▶   시작 프로그램 작성

이제 앞에서 작성한 익스텐션과 익스텐션 config 핸들러를 테스트해 보는 시작 프로그램을 작성한다. MainConsole 이라는 이름으로 콘솔 프로젝트를 하나 추가했다.

다음은 Program 클래스에 대한 정의이다.

class Program

{

    private readonly IUnityContainer stdContainer;

    private IUnityContainer customContainer;

    public Program()

    {

        stdContainer = new UnityContainer();

        customContainer = new UnityContainer();


        // config 정보 읽어들이기

        UnityConfigurationSection section;

        section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");


        //Unity 컨테이너에 config 정보를 설정한다.

        section.Containers.Default.Configure(stdContainer);

        section.Containers["customContainer"].Configure(customContainer);

    }

    public void Unload()

    {

        customContainer.Dispose();

    }

    public void Reload()

    {

        customContainer = new UnityContainer();

        UnityConfigurationSection section;

        section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");

        section.Containers["customContainer"].Configure(customContainer);

    }

    public void WaitForUpdate()

    {

        Console.WriteLine("Container has been unloaded ... waiting for update");

        Console.Read();

    }

    public void Spill()

    {

        Console.WriteLine(stdContainer.Resolve<IMyInterface>("local").Speak());

        Console.WriteLine(customContainer.Resolve<IMyInterface>("remote").Speak() );

    }

    static void Main(string[] args)

    {

        Program p = new Program();

        p.Spill();

        p.Unload();

        p.WaitForUpdate();

        p.Reload();

        p.Spill();

        p.WaitForUpdate();

    }

}

먼저 두개의 unity 컨테이너를 생성하고 있다. customContainer도 stdContainer와 같이 생성되고 있다. 이것은 두 컨테이너 모두 기본 AppDomain에서 생성되었다는 것을 말한다. 그런 다음 config 파일의 "unity" 섹션을 읽어들이고 있다. unity 섹션 하위에 기본 컨테이너와 확장 컨테이너에 대한 설정이 모두 포함되어 있다. 그런 다음 다음 코드가 실행되고 있다.

        //Unity 컨테이너에 config 정보를 설정한다.

        section.Containers.Default.Configure(stdContainer);

        section.Containers["customContainer"].Configure(customContainer);

착각하지 말 것은, section 객체의 Containers 속성은 컨테이너 인스턴스 컬렉션을 말하는 것이 아니라 <Container/>요소 컬렉션을 나타낸다. 코드는 Default <cotainer/> 설정을 stdContainer 인스턴스에 전달하는 작업을 하는 것이고, 두번째 코드는 name 어트리뷰트의 값이 "customContainer"값인 <container/>요소의 설정 내용을 이용해서 customContainer 인스턴스에 전달하는 작업을 한다. 이때 익스텐션 config 핸들러의 가상 함수(override 함수) Configure()가 내부에서 호출된다.

        public override void Configure(Microsoft.Practices.Unity.IUnityContainer container)

        {

            container.Configure<MyExtension>().CreateAppDomain(Domain, ShadowFiles);

        }

시작 프로젝트 MainConsole의 bin/Debug 폴더를 보면 Shadow폴더가 있다. 이 폴더 명은 MyExtension용 확장 config <extensionConfig/>하위에 설정된 ShadowFiles 어트리뷰트의 값이다. 이곳에 두번째 AppDomain에서 사용할 어셈블리들이 있어야 한다.

▶   사용자 경험

이제 테스트를 위해서 간단히 준비를 하자. 우선 프로젝트들을 두 개의 솔루션으로 분리해서 구성하자. 첫번째 솔루션은 계속 실행되고 있는 있을 것이고 두번째 솔루션은 수정해서 다시 빌드하는 작업을 해야 할 것이기때문이다.

시작 프로젝트와 UntiyExtension 프로젝트를 하나의 솔루션으로 묶자.

그리고 테스트용 비즈니스 객체용 프로젝트들을 하나의 솔루션으로 묶자.

그리고 테스트용 비즈니스 프로젝트용 빌드 결과들을 모아놓을 폴더를 하나 만들자.

- 최초 버전의 테스트용 비즈니스 프로젝트의 빌드 결과물들을 우선 "TestBizAssembly" 폴더에 모아두자.

- 비즈니스 프로젝트의 빌드  Output경로를 이 폴더로 두지는 말라. 이제 시작 프로젝트의 참조에 이 비즈니스 프로젝트 결과들을 추가하자.

- 그런 다음 AppDomainUnity.Assembly.dll과 AppDomainUnity.Common.dll을 시작 프로젝트의 bin/Debug/Shadow 폴더로 복사하자.

이제 실행을 시켜보자.

이제 SatClass의 Speak() 메소드 내용을 수정하자. 이때 실행 프로그램은 종료하지 않는다.

public string Speak()

{

    return "I'm SatClass object. version 2.0";

}

- 그런 다음 이 클래스가 포함된 AppDomainUnity.AssemblyB 프로젝트를 빌드해서 결과물을 bin/Debug/Shadow 폴더로 복사하자.

이제 다시 실행을 해 보자.

확장 끝이다 !! 허허..

▶   Unity 컨테이너 익스텐션의 결론

이제 정신을 좀 가다듬고 정리를 해 보자.

Unity 컨테이너 익스텐션을 위해서는 "strategy 정의".... "익스텐션 정의"... "익스텐션 config 핸들러"....! 를 작성해야 한다.

이제 끝내야 겠다. 뭔가 아쉽지만.

하지만 이 예제는 분석하고 보니 간단한 편이다. 이 예제에서는 policy 객체를 제작하지는 않았다. 다음 포스트에서는 완전한 익스텐션 제작에 대한 예제를 분석해볼까. Unity Application Block을 다운받아 설치하면 두개의 예제가 설치된다 : EventBroker, Stoplight. 이 중에서 EventBroker 예제를 보면 Policy를 사용하는 완전한 확장 예제가 있다.
다음에 이것을 할지. 아니면 Enterprise Library 블락 설명을 시작해볼지. 모르겠다. 최종적으로는 마이크로소프트에서 제공하는 모든 블락을 사용해서 개발 프레임워크를 하나 만들어볼까 하는 욕심이 앞선다.  혹시 이 글을 읽는 독자의 요청이 있다면 먼저 고려해보겠다. 그럼, 휘릭~

Posted by dalbong2

이제 Unity 컨테이너를 확장하는 방법을 예제를 통해서 알아보자. 이 포스트에서는 어떻게 strategy를 작성하고 어떻게 객체 생성 과정에 끼워넣을 수 있는지를 알아본다. 그러면서 컨테이너 익스텐션에 대해서도 알아본다.

그러나 필자도 생업이 있는지라 예제를 구상하고 구현할 시간이 없다. 대신에 필자도 공부를 할 겸 훌륭하신 분들이 미리 만들어놓은 예제를 분석하는 수준에서 대신하려 한다. 물론 포스트를 그대로 번역만 하지는 않을 것이다. 필자가 필요한 대로 재구성해서 설명하겠다.

이것이 저작권 또는 지적 재산권 뭐 그런거 침해로 봐야 하는지는 모르겠다. 해외 아티클을 번역해주는 블로그도 많은데....만약 그 아티클을 내 생각대로 재해석해서 설명하면 그것은 법적으로 문제가 있나? 문제가 있을 수 있겠다. 원작자가 실컷 해결책을 만들어 놨는데, 뒤에 오는 넘이 그것을 잘 설명하겠다고 다른 사람에게 다르게 설명하면, 콜럼버스의 달걀이라고 원작자가 기분나쁠 수도 있지 않을까나. 에이 몰것다. 이 넘은 외국 넘이니까 나한테 소송걸일은 없겠지.

포스트의 제목은 Unity with AppDomains로 되어 있다. 이 예제는 strategy도 만들어서 strategy 체인에 끼워넣는 간단한 예와 그리고 Unity 컨테이너 익스텐션을 만드는 방법도 보여주고 있다. 각 stage별 strategy 객체들은 하나의 리스트로 묶여 있는 구조로 되어 있다. 이것을 strategy 체인이라 한다. 앞의 그림을 보라. 체인처럼 보이지 않나? 아님 말고. 그리고 여기서 말하는 "Unity 컨테이너 익스텐션"이라 함은 이전까지 말했던 "Unity 컨테이너 확장하기"라는 말과는 구분된다. 이전까지 필자가 "Unity 컨테이너 확장"이라고 한 것은 기존의 프레임워크를 커스터마이징할 수 있는 모든 방법을 다 포함하는 일반적인 표현이었다. 상속도 포함되고 플러그인 방식의 확장등 모든 확장을 의미한다. 

이 예제에서 말할 "Unity 컨테이너 익스텐션"이라는 표현에서 "익스텐션"을 우리말로 번역하면 "Unity 컨테이너 확장"이 되어 앞에서 말했던 일반적인 용어와 구분하기 쉽지 않다. 그래서 구분을 위해서 발음 그대로를 유지했다.

사실 이 예제를 분석하면서 "Unity 컨테이너 익스텐션"이 뭔지를 짐작할 수 있었다. 처음에 "익스텐션"이란 말만 듣고서는 상속에 의한 확장을 상상했다. Java 에서는 상속을 extend로 표현하고 있다. 그리고 필자도 평소 상속이라는 말 대신에 확장이라는 말을 자주 사용하곤 했다. 아마 그런 습관때문이었나 보다. 그런데 예제를 보고 나서 필자가 오해를 했다는 것을 알 수 있었다.
물론 기존의 기본 컨테이너 기능을 유지하면서 상속을 통해서 새로운 컨테이너를 만드는 확장도 가능할 것이다. 그러나 "Unity 컨테이너 익스텐션"에서 말하는 것은 차라리 플러그인 방식의 확장이라고 보는 것이 옳을 것이다.  새로운 객체를 컨테이너에 로딩시키고 그것의 기능을 컨테이너에서 사용하는 것이다. 예를 들어 익스텐션 객체를 컨테이너에 만들어 두고 그 익스텐션을 통해서 ObjectBuilder에 strategy를 하나 더 끼워넣는 작업을 할 수 있다. 이곳에서 설명하는 예제가 그렇다. 이렇게 하려면 익스텐션 객체도 필요하지만 새로운 strategy 객체도 필요하다. "Untiy 컨테이너 익스텐션"이라고 하면 주로 이런 의미로 사용되는 듯 하다. 그러나 새로운 익스텐션을 stratey 설치외에도 다른 목적으로 사용해도 될 것이다. 안된다는 법이 있겠는가. 아니면 말고.

우선 예제를 위한 사전 지식을 알아보자.

■ 사전 지식

어플리케이션이 실행되는 동안에 특정 어셈블리의 내용이 변경되면 애플리케이션이 종료되지 않고서도 즉시 즉시 바로 바로 그 변경된 내용을 반영하는 애플리케이션이 있다면 유용하게 쓰일 곳이 많지 않을까? 음...막상 생각해보려 하니 웹 서버 외에 딱히 떠오르는 용도는 없다-_-;;

잠시 예제를 좀 더 이해하기 쉽도록 하기 위해서 예제에서 기본적으로 사용하고 있는 개념에 대해서 살펴보겠다. AppDomain이나 섀도우 복사에 대해서 알고 있는 사람들은 이 부분은 스킵해도 된다.

▶ ASP.NET 애플리케이션의 AppDomain

웹서버는 가상 디렉토리별로 하나씩의 AppDomain을 생성한다. AppDomain을 모르겠다면 이전 포스트를 참조한다. 동일한 서버에 두개의 ASP.NET 애플리케이션을 생성하는 경우를 생각해보자. 어떤 일이 일어나나?  특별한 설정을 하지 않는한 하나의 ASP.NET 작업 프로세스가 두 개의 애프리케이션을 호스팅한다. 이 프로세스 이름은 XP 또는 Win 2000에서는 aspnet_wp.exe이고 ASPNET 계정으로 실행된다. Win2k3에서는 w3wp.exe가 작업 프로세스가 되고 기본적으로 계정은 NETWORK SERVICE가 된다.

하나의 객체는 하나의 AppDomain에서만 살아간다. 각각의 ASP.NET 애플리케이션은 자신만의 전역 변수 객체를 가지고 있다. Cache, Application 그리고 Session 객체들은 다른 애플리케이션과 공유되지 않는다. 비록 같은 프로세스내에서 존재하더라도 isolation의 단위는 AppDomain이다.

▶ 섀도우 복사 및 재시작

그리고 예제를 이해하기 위해서는 섀도우 복사라는 것도 알고 있어야 한다. 이 내용도 앞의 포스트를 참조하면 설명되어 있다. 필자도 몰랐다. -_-;; 어셈블리가 일단 하나의 AppDomain으로 로딩되면, 그 AppDomain에서 그 어셈블리를 제거할 방법은 없다. 프로세스에서 AppDomain을 내리고(unloading) 나서 다시 새로운 AppDomain을 생성해야 새로운 버전의 어셈블리를 사용할 수 있게 된다.  근데 문제는 현재 프로세스에서 사용되고 있는 어셈블리는 파일 시스템상에서 덮어쓸 수가 없게 된다. 흔히 말하는 프로세스가 잡고(lock) 있는 상태가 되는 것이다.

AppDomain의 섀도우 복사 기능을 이용하면 파일 시스템상에서의 파일에 락을 걸지 않을 수 있다.  ASP.NET 애플리케이션용 AppDomain은 필요한 dll들을 우선 bin폴더에서 임시 디렉토리로 복사한다. 그런 다음 락을 걸고 나서 메모리로 로딩한다. 따라서 애플리케이션이 실행되고 있는 동안에도 bin 폴더로 파일들을 복사할 수 있는 것이다. 이런 복사를 섀도우 복사라 한다.

섀도우 복사는 AppDomain에서 지원하는 기능이다. 즉 AppDomain을 생성할때 섀도우 복사와 관련된 속성 즉 어느 곳에 있는 파일을 섀도우 복사할 것인가, 그리고 복사를 어디로 할 것인가만 설정하면 어셈블리를 로딩할때 자동으로 섀도우 복사를 하게 된다. 여기서 bin 폴더로 새로운 파일이 덮어씌여졌다는 것을 자동인식하는 기능은 AppDomain의 기능이 아니다. bin 폴더의 변경을 감지하고 기존의 AppDomain을 내리고 새로운 것을 올리는 작업은 ASP.NET 런타임이 하는 일이다.

■ 예제 제한

이 예제에서는 bin 폴더의 파일이 변경되는 것을 감지할 수 있는 기능은 구현하지 않고 있다. 예제에서는 웹 서버나 자동 감지 기능을 구현하려는 것이 아니라  Unity 컨테이너를 확장하는 방법을 보이려는 것이다. 파일 변경 자동 감지에 관심있는 사람은 FileSystemWatcher 클래스 같은 것을 사용해보길 바란다. 여기서는 파일 변경 감지가 이뤄졌다고 보고 해당 AppDomain을 코드상에서 수동으로 내렸다가 다시 올리고 있다. 만약 파일 변경 감지를 할 수 있는 프로그램을 만들어서 AppDomain을 수동으로 내렸다 올렸다 하는 부분을 대신한다면 좀 더 멋있는 예제가 될 것이다.

이번 포스트는 너무 길어질 것 같다. 그림도 그려야 제대로 설명할 수 있을 것 같고. 본격적인 예제 설명은 다음 포스트로 미뤄야겠다. 예제가 너무 궁금한 사람은 앞에서도 소개한 페이지를 보라.

오늘도 빨리 자야 내일 아침 수영할 수 있다. 다른 사람들, 계속 왕복하고 있는 동안 필자는 졸려서 걸어가는게 태반이다. 물속에서 눈 감고 걸어다니는 사람을 상상해봐라. 좀비같지 않나?

Posted by dalbong2

이번 포스트에서 Unity 컨테이너 확장에 대해서 포스팅을 할까 했는데 아무래도 ObjectBuilder에 대해서 좀 더 설명을 해야 할 것 같다. 코드를 보니까 바로 예제로 들어가기는 무리인듯 하다. 해서 ObjectBuilder가 객체를 생성하는 과정에 대해서 좀 더 자세히 알아보도록 하겠다. 다음은 바로 앞 포스트에서 보인 그림이다.

그림에서는 stage를 "Stage1", "Stage2"로 정의하고 있다.  그리고 각 stage별로 Strategy를 정의하고 있는데, 다음과 같다 : "Strategy1_1","Strategy1_2" 그리고 "Strategy2_1","Strategy2_2". ObjectBuilder의 Strategy는 IBuilderStrategy라는 인터페이스를 구현해야 한다. 이 인터페이스를 구현해야 ObjectBuilder가 그 Strategy를 인식할 수 있다.

IBuilderStrategy의 정의는 그림과 같다. IBuilderStrategy을 사용하면 객체가 생성되고 제거될때를 전 후해서 필요한 작업을 해 줄 수 있다.

PreBuildUp() 객체를 생성하기 전에 즉 Buildup하기 전에 Strategy에서 실행시켜야 하는 로직이 있다면 이 메소드를 구현하면된다.
PostBuildUp() 객체를 생성하고 나서 실행시켜야 하는 로직이 있다면 이 메소드를 구현한다.
PreTearDown() 객체를 컨테이너에서 제거하기전에 실행시켜야 하는 로직이 있다면 이 메소드를 구현한다.
PostTearDown() 객체를 제거한 후 실행시켜야 하는 로직이 있다면 이 메소드를 구현한다.

이 메소드들이 실행되는 순서는 다음 그림을 참조한다.

이 그림은 객체를 생성할때의 메소드가 호출되는 순서이다. 각 stage별로 포함된 Strategy의 PreBuildUp() 이 호출된 다음 그리고 나서 다시 반대순으로 PostBuildUp()이 호출된다. 중간 Stage에 Creation 단계가 있어야 할 것이다. 그림에 포함시기면 좋을텐데. 쯔읍...!

모든 Strategy의 PreBuildUp()과 PostBuildUp()이 호출되고 나면 해당 객체를 반환하게 된다.

객체를 컨테이너에서 제거하는 것도 유사한 절차를 밟는다. stage순서대로 PreTearDown()이 호출되고 다시 stage 역순으로 PostTearDown()이 호출된다.

오늘은 여기까지. 다음에 Unity 확장 예제를 알아보겠다. 근데, Unity 컨테이너를 확장하는 방법도 보니까 여러가지가 있는 듯하다. 이렇게 객체를 생성, 제거하는 과정에 참여하는 것도 있겠지만, 컨테이너 자체도 확장할 수 있는 듯 하고 또 어떤 확장이 있는지 모르겠다. 해서 확장하는 예제를 어떻게 해야 할지 좀 고민스럽다.

고민은 다음에 하고 오늘은 자자.

Posted by dalbong2

이제 Unity 컨테이너가 하는 일에 대해서 굵직한 것은 거의 모두 알아본 것이나 마찬가지다. 타입 매핑 정보를 등록하고 인스턴스를 얻는 API를 컨테이너가 제공해주고 있다는 것을 알았다. 설명하지 않은 API도 더 있긴 하지만. 그리고 config의 스키마에 대해서도 좀 더 자세히 알아봐야 한다. 그러나 구체적인 사항들에 대해서는 개념과 구조 설명을 먼저 마치고 뒤에 일괄적으로 정리해보도록 하겠다. 물론 장담은 못한다 -_-;; 시간이 없거나 힘들면 관련 웹 페이지에 대한 링크로 대신할 수도 있다. 내가 원하는 방법대로 하겠다. 내 블로그니까.

이번 포스트는 다음 링크의 문서들을 참조했다. 이 문서를 처음부터 읽어서 이해가 가는 독자라면 필자의 이번 포스트는 읽지 않아도 되겠다. 

ObjectBuilder의 아키텍쳐

그리고 영문이긴 하지만 다음 포스트를 읽어보기 바란다.

Decontructing ObjectBuilder


■ Unity 컨테이너

좀 더 심화 학습으로 나가기 전에 이즈음해서 Unity 컨테이너가 뭔지 함 정의를 찍고 가보자.

Unity 컨테이너는 마이크로소프트의 dependency injection이 가능하고 확장 가능한 컨테이너이다. 가릿?

근데 이 정의는 초보자를 위한 정의이다. 좀 더 심화된 정의라면 "ObjectBuilder"라는 용어가 Unity 컨테이너의 정의에 포함되어 있어야 한다. 다음은 한국 마이크로소프트의 장현춘 아키텍트분의 설명이다.

Unity 는 ObjectBuilder에 wrapper를 씌워 개발자의 편의성을 높여 범용 개발 프레임웍으로 사용할 수 있게 P&P 팀에서 내놓은 것으로 Dependency Injection을 제공하는 컨테이너라 할 수 있다.

Unity 컨테이너는 내부적으로 ObjectBuilder라는 것을 기반으로 하고 있다는 것이다. 앞에서 Unity 컨테이너의 개념을 설명하는 그림을 보였지만, 이것을 이제 좀 더 자세히 그려보자.


ObjectBuilder를 그대로 사용해서 지금의 Unity기능(DI 컨테이너)을 개발자가 매번 구현하는 것이 너무 힘들기에 마이크로소프트에서 Unity라는 이름으로 미리 만들어 제공하고 있는 것이다. ObjectBuilder 자체는는 컨테이너도 컨터이너 프레임워크도 아니라 단지 DI 컨테이너를 만들 수 있는 기반을 제공한다. 말 디따 어렵네. 내가 써 놓고도 다시 읽어 보니 어렵다. 누가 이렇게 말해놓고 날 보고 이해하라면 난 화내는데 -_-;; 쉽게 말하면 ObjectBuilder라는 것은 객체를 만들어 낼 수 있는 공장(object factory)인데 외부(개발자)에서 그 객체를 만들어내는 과정에 참여할 수 있다는 것이다. 외부에서 참여할 수 있다는 것이 바로 기존의 것을 확장한다는 것이다.

■ Unity 컨테이너를 확장한다.

확장한다는 것은 상황에 따라서 다른 의미를 가질 수 있고 그 개념을 구현하는 기술또한 달라질 수 있다. 앞의 포스트에서 코어 프레임워크에서 UserInfo라는 베이스 클래스를 만들고 그리고 사이트 프레임워크에서 그것을 상속해서 사이트에 맞게 구현하는 것도 일종의 확장이다. 그럼 "Unity 컨테이너를 확장"한다는 것은 무슨 말인가를 먼저 이해할 필요가 있다.

그러나 방금전에 말했듯이 Unity의 거의 모든 기능은 ObjectBuilder를 기반으로 하고 있다. 해서 "Unity를 확장한다는 것이 무엇인지"를 알아 보기 전에  ObjectBuilder의 구조와 메커니즘을 이해하는 과정이 먼저여야 한다. 즉 Unity 를 확장한다는 것은 ObjectBuilder를 확장한다는 말이기 때문이다( 맞나? 아님 말고). 근데 ObjectBuilder 구조와 메커니즘을 이해한다는 것은 아키텍쳐와 패턴에 자주 접하는 사람들에게는 익숙할지 모르지만 처음 접하는 사람들에게는 간단한 것만은 아니다.  혹시 AOP(Aspect Oriented Programming)를 경험해본 사람들이라면 기존의 .NET 프레임워크의 확장 방법을 연상하면 쉽게 이해가 갈 것이다. 

■ ObjectBuilder 아키텍쳐

ObjectBuilder가 객체를 생성하는 과정은 단순하지 않다. ObjectBuilder가 최종 객체를 생성해서 요청한 코드에 반환하기까지는 미리 정해진  단계의 과정을 거친다. 예를 들어 PreCreation, Creation, Initialization, PostInitialization과 같은 단계가 있을 수 있다. 각 단계를 stage라고 한다. 

각 스테이지에서는 또 여러 작업이 포함될 수 있다. 예를 들어 PreCreation 스테이지에서는 현재 요청한 객체의 실제 타입(concrete type)이 뭔지를 등록된 매핑 정보를 통해서 확인하는 작업, 클라이언트(여기서 클라이언트는 코드를 말한다)가 singleton객체를 원하는지를  확인하는 작업, 그리고 constructor 호출시 자동 dependency injection이 발생해야 하는지 확인하는 작업, 속성에 Dependency 어트리뷰트가 붙어있는지 확인하는 작업, 메소드에 InjectionMethod 어트리뷰트가 붙어 있는지 확인하는 작업등등이 포함될 수 있다. 이런 스테이지별 실행되는 각각의 작업을 바로 strategy라고 한다. 앞에서 예를 든 작업의 strategy는 각각 TypeMappingStrategy, SingletonStrategy, ContructorReflectionStrategy, PropertyReflectionStrategy, MethodReflectionStrategy같은 이름이 붙을 수 있다.

그리고 이 strategy에 어떤 정보를 건네주고 싶은때는  policy라는 것을 사용한다. PreCreation 스테이지의 ContructionReflectionStrategy에서 현재 객체를 생성할때 어떤 버전의 contructor를 사용할지를 확인했다. 즉 객체를 생성하는데 어떤 파라미터를 넘겨줘야 하는지를 확인하는 작업을 했다. 그럼 확인된 그 파라미터에 대한 정보를 Creation 스테이지의 CreationStrategy로 넘겨줘야 한다. ContructionReflectionStrategy에서는 policy 객체를 생성해서 공용 저장소(컨텍스트 객체)에 저장한다. 이때의 컨텍스트 객체는 해당 객체를 생성하는과정별로 하나씩 존재해서 앞 스테이지의 각 strategy에서 저장한 정보를 계속 누적해서 다음 단계의 strategy로 전달될 수 있다. Creation 스테이지의 CreationStrategy에서는 컨텍스트 객체에서 ConstructionReflectionStrategy에서 저장한 policy 객체를 가져와서 객체를 생성할 때 필요한 파라미터를 구해서 contructor 또는 Activator 클래스를 이용해서 객체를 생성한다.

ObjectBuilder가 객체를 생성하는데 사용하는 개념 3가지를 알아봤다. 이 개념은 중요하다. 바로 ObjectBuilder의 확장 즉 Unity의 확장은 이 개념을 알아야 하기때문이다. 지금까지 설명한 내용을 다시 도식화해보자. 앞의 그림에서 2,3단계의 과정을 다시 상세히 그린것으로 보면 된다.

역시 그림이 좋아. 단순 명료해지거든. 음....!

이것은 ObjectBuilder가 기본적으로 구현해 놓고 있는 모델이다.  ObjectBuilder의 각 스테이지의 strategy를 정리하면 다음과 같다. 절대 욀려고 하지 말라. 각 스테이지 및 stragegy를 욀려고 하지 마라. 지금은 그 개념을 이해하면 된다.

스테이지 strategy 동작 입력 출력
PreCreation TypeMapping Strategy 인터페이스나 베이스 클래스의 매핑 정보를 이용해서 실제 타입으로 변환한다. IType Mapping Policy  
  Singleton Strategy 지정된 형태의 객체가 이미 존재하는 경우엔 그것을 돌려주고, 존재하지 않는 경우엔 새롭게 생성한다 Locator  
  Constructor Reflection Strategy InjectionConstructorAttribute가 붙은 constructor를 찾고, ConstructorPolicy(ICreationPolicy를 계승)를 작성한다 리플렉션 ICreation Policy
  Property Reflection Strategy ParameterAttribute가 붙은 프로퍼티를 찾고 PropertySetterPolicy를 작성한다 리플렉션 IProperty Setter Policy
  Method Reflection Strategy InjectionMethodAttribute가 붙은 메소드를 찾고 MethodPolicy를 작성한다 리플렉션 IMethod Policy
Creation Creation Strategy constructor 또는 Activator 클래스를 사용해 객체를 생성한다. ISingletonPolicy가 있다면 Locator에 등록한다 ICreation Policy ISingleton Policy Locator
Initialization Property Setter Strategy IPropertySetterPolicy를 기본으로 프로퍼티에 값을 설정한다   IProperty Setter Policy
  Method Execution Strategy IMethodPolicy를 기본으로 메소드를 실행한다 IMethod Policy  
Post Initialization Builder Aware Strategy 객체 생성의 완료를 통지한다    

앞의 그림은 ObjctBuilder기준으로 그린 것이다. ObjectBuilder2에서 구현해 놓은 각 strategy 클래스는 이와는 조금 다른 듯하다.  Untiy 소스 코드를 훑어 보다가 UnityBuildStage라는 클래스를 보게 되었다. 이 클래스를 보면 Unity 컨테이너에서는 ObjectBuilder에서 정의한 4단계의 스테이지 대신에 7단계의 스테이지를 사용하고 있는 것을 알 수 있다.  또한 각 stage에 포함되는 strategy 리스트도 달라지고 있다. 지금은 버전간의 차이를 이야기하려는 것이 아니다. ObjectBuilder의 아키텍쳐에 집중하길 바란다.

Unity의 재정의된 스테이지는 다음 코드를 보면 알 수 있다.

namespace Microsoft.Practices.Unity.ObjectBuilder

{

    /// <summary>

    /// The build stages we use in the Unity container

    /// strategy pipeline.

    /// </summary>

    public enum UnityBuildStage

    {

        /// <summary>

        /// First stage. By default, nothing happens here.

        /// </summary>

        Setup,


        /// <summary>

        /// Second stage. Type mapping occurs here.

        /// </summary>

        TypeMapping,


        /// <summary>

        /// Third stage. lifetime managers are checked here,

        /// and if they're available the rest of the pipeline is skipped.

        /// </summary>

        Lifetime,


        /// <summary>

        /// Fourth stage. Reflection over constructors, properties, etc. is

        /// performed here.

        /// </summary>

        PreCreation,


        /// <summary>

        /// Fifth stage. Instance creation happens here.

        /// </summary>

        Creation,


        /// <summary>

        /// Sixth stage. Property sets and method injection happens here.

        /// </summary>

        Initialization,


        /// <summary>

        /// Seventh and final stage. By default, nothing happens here.

        /// </summary>

        PostInitialization

    }

}

그리고 Unity의 기존 strategy 목록은 다음 코드를 보면 알 수 있다.

/// <summary>

/// This extension installs the default strategies and policies into the container

/// to implement the standard behavior of the Unity container.

/// </summary>

public class UnityDefaultStrategiesExtension : UnityContainerExtension

{

    /// <summary>

    /// Add the default ObjectBuilder strategies &amp; policies to the container.

    /// </summary>

    protected override void Initialize()

    {

        //

        // Main strategy chain

        //

        Context.Strategies.AddNew<BuildKeyMappingStrategy>(UnityBuildStage.TypeMapping);

        Context.Strategies.AddNew<LifetimeStrategy>(UnityBuildStage.Lifetime);


        Context.Strategies.AddNew<BuildPlanStrategy>(UnityBuildStage.Creation);


        //

        // Build plan strategy chain

        //

        Context.BuildPlanStrategies.AddNew<DynamicMethodConstructorStrategy>(

            UnityBuildStage.Creation);

        Context.BuildPlanStrategies.AddNew<DynamicMethodPropertySetterStrategy>(

            UnityBuildStage.Initialization);

        Context.BuildPlanStrategies.AddNew<DynamicMethodCallStrategy>(

            UnityBuildStage.Initialization);

        //

        // Policies - mostly used by the build plan strategies

        //

        Context.Policies.SetDefault<IConstructorSelectorPolicy>(

            new DefaultUnityConstructorSelectorPolicy());

        Context.Policies.SetDefault<IPropertySelectorPolicy>(

            new DefaultUnityPropertySelectorPolicy());

        Context.Policies.SetDefault<IMethodSelectorPolicy>(

            new DefaultUnityMethodSelectorPolicy());


        Context.Policies.SetDefault<IBuildPlanCreatorPolicy>(

            new DynamicMethodBuildPlanCreatorPolicy(Context.BuildPlanStrategies));

    }

}

Unity에서 제공하는 기본적인 익스텐션은 다음과 같은 스테이지와 strategy 타입을 사용하고 있다는 것을 알 수 있다.

스테이지 strategy 타입
TypeMapping BuildKeyMappingStrategy
Lifetime LifetimeStrategy
Creation BuildPlanStrategy
Initialization DynamicMethodConstructorStrategy
DynamicMethodPropertySetterStrategy

스토리는 이렇다. ObjectBuilder( 또는 ObjectBuilder2 )는 스테이지와 strategy를 확장할 수 있는 구조를 가지고 있다. Unity에서는 앞의 코드에서 처럼 스테이지를 UnityBuildStage로 정의하고 있고 그리고 여러개의 기본적인 Strategy를 정의해놓고 있다.

Unity 컨테이너를 확장한다는 것은 이런 스테이지와 strategy 목록을 Unity의 사용자가 직접 추가, 제거할 수 있다는 것이다. 

그러나 스테이지까지 재정의할 일이 있을까하는 생각도 든다.
스테이지와 strategy와 함께 Policy로 만들어야 한다. 앞에서 말한 것처럼 파이프라인상에서 뒤에 실행될 스테이지의 Strategy로 원하는 정보를 전달해야 할때 Strategy 타입에 해당하는 Policy 객체도 정의할 필요가 있다.

다른 Application Block에서도 ObjectBuilder가 제공하는 기본적인 단계 및 strategy를 사용하지 않고 나름대로의 정의해서 사용하고 있다는 것을 보면 ObjectBuilder의 위치를 알 수 있다.  그림을 그리고 싶은데, 아. 배고프다. 빨리 끊고 나가야 겠다. -_-;;;;;

이 포스트의 내용이 모두 정확한지는 잘 모르겠다. ObjectBuilder2.0의 구조를 자세히 설명해 놓은 문서를 찾지 못해서, 버전 1.0을 설명해 놓은 해외 블로그를 많이 참조했다.

실제로 이 Strategy를 사용자 정의해서 확장해서 뭘 어떻게 할 수 있다는 건지 프레임워크 입장에서 설명하고 싶은데, 생각할 힘이 없다. 진짜 배고파서 진땀이 난다. 힝.

컨테이너 확장하는 샘플은 다음 포스트로 넘겨야 겠다. 

Posted by dalbong2

앞의 Construction InjectionSetter Property Injection 포스트를 읽었다면, Method Call Injection이라는 것도 메소드를 호출할때 메소드 파라미터를 ? Unity 컨테이너가? 하는 예상을 할 수도 있겠다. 그렇지 않은가? 아님 말고.

● InjectionMethod 어트리뷰트

InjectionMethod 어트리뷰트가 붙은 샘플 코드이다.

public class MyObject

{


    public IMyInterface depObjectA;

    public MyBaseClass depObjectB;


    [InjectionMethod]

    public void Initialize(IMyInterface interfaceObj, MyBaseClass baseObj)

    {

        depObjectA = interfaceObj;

        depObjectB = baseObj;

    }


}

"코드가 Initialize() 메소드를 호출할때 두개의 인자 IMyInterface, MyBaseClass 타입이 컨테이너에 등록되어 있는지를 확인하고 그것의 실제 타입의 인스턴스를 생성해서 파라미터로 inject시켜 호출해준다"라고 상상했는가? OK! 그러나 그것이 다는 아니다. InjectionMethod 어트리뷰트가 붙은 메소드는 컨테이너에서 자동으로 호출도 해준다. 코드가 MyObject 객체를 원할때 컨테이너는 이 객체를 생성한 후 InjectionMethod 어트리뷰트가 붙은 메소드도 자동으로 호출해준다. 이때 필요하다면 파라미터도 자동으로 생성해서 메소드 호출을 수행한다.

다음 코드를 보면 IMyInterface <-> FirstObject, MyBaseClass<-> SecondObject 타입 매핑 정보를 컨테이너에 등록하고 나서, 컨테이너의 Resolve<>()를 통해서 MyObject객체를 생성하고 있다.

IUnityContainer uContainer = new UnityContainer()

   .RegisterType<IMyInterface, FirstObject>()

   .RegisterType<MyBaseClass, SecondObject>();

MyObject myInstance = uContainer.Resolve<MyObject>();


// 이제 종속 객체에 대한 참조를 가지고 있는 공개 변수에 접근할 수 있다.

IMyInterface depObjA = myInstance.depObjectA;

MyBaseClass depObjB = myInstance.depObjectB;

그러고 나서 공개 변수 depObjectA와 depObjectB에 바로 접근하고 있다. 이렇게 해도 두 변수는 null을 반환하지 않는다는 것이다. MyObject 객체가 코드에 반환되기전에 즉 uContainer.Resolve<MyObject>() 내부에서 MyObject 객체를 반환하기 전에 그 객체의 InjectMethod 어트리뷰트가 붙은 Initialize() 메소드를 호출해서 두 변수를 초기화하고 나서이기 때문이다.  필요하다면 프레임워크 사용자가 다시 Initialize() 직접 호출할 수도 있을 것이다. 두번 호출되는 것이 마음에 안든다면 InjectionMethod 어트리뷰트를 제거해서 프레임워크의 사용자에게 직접 Initialize()를 호출하도록 할 수도 있다. 가릿? 선택의 폭은 이전보다 넓어졌다. 프레임워크 입장에서는 여러 가능성에 대비할 수 있는 능력이 있다는 것은 좋은 일이다. OK!

지금까지 Unity 컨테이너에서 사용하고 있는 세 종류의 Dependecy Injection에 대해서 알아봤다. 모두 반복인적 코드를 업무 개발자들이 아닌 프레임워크단에서 처리해준다는 면에서 필자의 마음에 꼬옥 드는 패턴이다. 지난 포스트에서 말한 것처럼 이제 다음 시간에는 Unity를 확장한다는 것이 무엇이고 확장을 하는 구체적인 방법을 알아보고 싶다. 그러나 이 부분에 대해서는 필자도 조금 공부를 해야 할 것 같다.

Posted by dalbong2

앞의 포스트에서 Unity 컨테이너가 사용하는 dependency injection중에서 constructor injection에 대해서 알아봤다. 이제 두번째로 Property Injection에 대해서 알아보자.

● Dependency 어트리뷰트 사용

property에 dependency injection 표시는 다음과 같다.

public class MyObject

{

    private SomeOtherObject _dependentObject;


    [Dependency]

    public SomeOtherObject DependentObject

    {

        get { return _dependentObject; }

        set { _dependentObject = value; }

    }

}

코드처럼 Property에 Dependency 어트리뷰트를 붙여두면 된다.  대상 클래스 MyObject의 DependentObject 속성은 외부 타입 SomeOtherObject에 의존하고 있다는 것을 보여주고 있다. Property에는 getter 메소드와 setter 메소드가 있다. 가릿? OK! 이중에서 Dependency 표시가 적용되는 것은 setter 메소드이다. Unity 컨테이너가 이 타입 MyObject 의 인스턴스를 생성할때(!!) Dependency 어트리뷰트가 붙어 있는 속성을 확인해서 해당 속성에서 사용하는 외부의 의존 타입의 객체를 미리 생성해서 setter 메소드를 통해서 내부에 참조를 저장해둔다. 그런 다음 대상 객체의 인스턴스를 반환한다.

이 상황을 다시 한번 느긋하게 생각해보자. Unity 컨테이너가 대상 객체에 대한 인스턴스를 코드로 반환할때는 이미 대상 객체는  필요로 하는 외부 객체들에 참조를 모두 갖추어서 초기화가 완전히 된 상태인 것이다. 대상 타입 MyObject 가 인스턴스가 될때는 이제 독립적인 상태가 되어 있는 것이다.  좀 더 독립적인 객체 지향적이라는 느낌이 오지 않나? 아님 말고.

이제 이 객체를 사용하는 코드를 보자.

IUnityContainer uContainer = new UnityContainer();

MyObject myInstance = uContainer.Resolve<MyObject>();


// 이제 종속 객체를 가지고 있는 속성에 접근한다.

SomeOtherObject depObj = myInstance.DependentObject;

내부의 _dependentyObject를 초기화한 코드는 없다. 컨테이너가 MyObject를 생성할때 Dependency 어트리뷰트가 붙어있는 속성을 확인하고 나서 그 속성에서 필요로 하는 SomeOtherObject 타입의 _dependentObject를 초기화해준다. 개발자는 이제 속성을 호출해서 SomeOtherObject 인스턴스를 사용하면 된다.

Dependency 어트리뷰트가 붙은 속성의 반환값의 타입이 인터페이스나 베이스 클래스일 수도 있다. 이 경우 Unity 컨테이너는 이 베이스 타입이 컨테이너에 등록되어있는지를 확인하고 그 실제 타입의 객체를 생성해서 inject한다. 가릿?

 

Posted by dalbong2

이전 포스트에서는 Unity 컨테이너는 자신에 등록된 타입들의 메소드(constructor, setter 속성, 일반 메소드)를 호출할때  "특별한 표시"가 있으면 dependency injection 메커니즘을 활성화 시킨다는 말을 했다. 그 특별한 표시는 어트리뷰트를 이용한다. 그 어트리뷰트는 메소드 타입 즉  constructor, setter 속성, 일반 메소드에 따라 다르다. 이제 이 포스트에서는 세개의 표시를 어떻게 하는지 그리고 그 표시들에 따라서 Unity 컨테이너는 어떻게 작동하는지 즉 dependency injection이 뭔지 좀 더 자세히 알아보자.

● Constructor Injection 표시

Constructor가 호출될때 DI 메커니즘을 활성시키는 방법에는 상황에 따라서 두가지가 있다.

▶ 정의된 Constructor가 하나밖에 없는 경우

이때는 아무 표시를 해 주지 않아도 자동으로 dependency 객체를 생성해서 injection시켜준다.  앞의 로거 예제에서 이미 본적이 있다.

public Biz01(ILogger logger)

{

    this.logger = logger;


    // 비즈니스 객체가 생성될때 로그를 남긴다.

    logger.Write("Biz01 생성자 호출" );

}

contructor의 파라미터로 ILogger 타입의 객체를 사용하고 있다. 이 타입의 인스턴스를 생성하는 코드는 다음과 같았다.

//Biz01 객체를 생성한다.

Biz01 biz1 = container.Resolve<Biz01>();

이 코드는 Biz01타입의 객체를 원한다는 것을 컨테이너에 알려주는 것이다. 그러나 코드상에서는 Biz01 constructor에서 사용하고 있는 ILogger 타입의 인스턴스를 건네주지는 않고 있다. 컨테이너는 ILogger 타입이 등록되었는지를 확인한다. ILogger 타입의 실제 타입으로 FSLogger가 등록되어 있다는 것을 확인하고 FSLogger 객체를 생성해서 Biz01 생성의 파라미터로 넘겨준다. 그렇게 생성된 Biz01 객체를 코드에 반환한다. 이것이 contructor  injection이다. 유가릿?

때로는 constructor도 여러개가 있을 수 있다. 이런 경우는 컨테이너에게 어떤 녀석을 호출할지를 알려줘야 한다.

InjectionContructorAttribute

public class MyObject

{


    public MyObject(SomeClassA objA, SomeClassB objB)

    {

        //...

    }


    [InjectionConstructor]

    public MyObject(DependentClassA depA, DependentClassB depB)

    {

        //...

    }

}

컨테이너에게 MyObject 객체를 요구하면 컨테이너는 [InjectionConstructor]가 표시된 constructor를 호출한다. 이 contructor에는 두 개의 종속된 타입 DependentClassA, DependentClassB가 존재한다. 컨테이너는 두 종속 타입의 인스턴스를 생성해서 대상 클래스의 생성에 inject 시킨다.

대상 클래스가 constructor를 여러개의 버전으로 정의하고 있는 경우, Unity는 InjectionConstructor 어트리뷰트가 표시되어 있는 버전을 호출한다. constructor가 여러개 있는데도 InjectionConstructor 어트리뷰트 표시가 없으면 Unity는 한번 더 고민을 하게된다. 그래서 contructor의 파라미터수가 가장 많은 것을 호출한다. 만약 가장 많은 수의 파라미터를가진 constructor가 여러개 존재하면 예외가 발생한다.

Posted by dalbong2

이번 포스트에서는 Unity Application Block에서 사용하고 있는 Dependency Injection 패턴에 대해서 알아본다. UAB에서 구현하고 있는 이 패턴에 대해서 잘 이해하면 프레임워크를 만들때 개발자가 해야 할 일을 프레임워크단에서 공통적으로 해결해 줄 수 있는 유연한 방법을 많이 찾을 수있을 것으로 보인다. 프로젝트가 한참 진행되는 도중이라면 새로운 고객의 요구사항을 수용하기가 참 힘들다. 고객은 단순히 메소드에 파라미터 하나만 더 추가해서 빌드하면 되지 않겠는냐는 식으로 간단히 말한다. 그러나 그 간단한 요구사항이 공통팀과 업무 개발자들에게는 쓰나미로 다가오는 경우가 있다. 이런 경우 Dependency Injection 패턴을 잘 활용하면 업무 개발자가 기존 코드의 수정을 하지 않고도 공통팀에서 프레임워크단을 수정하는 것만으로 쓰나미를 피해갈 수 있는 가능성이 많아질 수 있다는 것이다. 그렇다면 이 패턴이 뭔지 알아보자.

Dependency Injection? 종속되는 것을 injection? 삽입하다? 끼워넣다? 그렇다. 말 그대로하면 종속되는 것을 끼워넣는 것이다. 종속되는 것? 끼워넣어줘? 대체 뭔 말이여! 대체 누가 뭘 끼워넣어준다는 것이여!

미리 말하면, "어떤 객체에 필요한(종속되는) 객체를 컨테이너!가 개발자 대신에 생성해서 필요한 자리에 건네준다"는 것으로 표현할 수 있다. 즉 개발자는 객체(종속된 객체)를 대상 객체에 건네주지 않아도 컨테이너가 알아서 생성해서 대상 객체에 대신 건네준다는 것이다.

  이전 그림

  수정된 그림

잘못 설명한 부분 [대상 객체(TargetObject)의 어떤 "메소드"를 호출할때는 인자로 특정 타입의 인스턴스가 필요하다고 하자. 개발자는 이 대상 객체에 대한 참조를 new를 생성해서 얻는 것이 아니라 Unity 컨테이너에 요청해서 얻는다. 컨테이너는 이미 생성되어 있는 인스턴스를 또는 필요한 경우에는 새로 생성해서 개발자에게 전달해준다. 개발자는 대상 객체가 특정 타입의 인스턴스를 필요로 함에도 불구하고 그 객체를 넘겨주지 않고 그냥 호출한다.

그 호출은 바로 대상 객체로 넘어가는 것이 아니라 일단 Unity 컨테이너가 중간에 인터셉트한다. 그래서 대상 객체에서 특정 타입의 객체가 필요하다는 것을 확인하고나서는 그 객체를 개발자 대신에 생성해서 대상 객체의 "메소드"를 호출한다. 반환값이 있다면 일단 컨테이가 받고 나서 다시 개발자에게로 건네준다. 이것이 dependency injection(DI)이 일어나는 메커니즘이다. ]

이탤릭체로 된 부분은 이전에 설명한 부분이지만 잘못된 설명이다. 내가 뭔 소리를 해 놓은 건지 모르것다. .NET에서 지원하는 프락시를 이용하는 AOP 패턴과 혼동한 듯 싶다.

Unity 컨테이너에서는 객체를 생성할때 미리 메소드 호출에 필요한 파라미터 객체를 생성해둔다. 이어지는 포스트에서 설명하지만 dependency injection란 것이 파라미터를 필요로 하는 메소드를 코드상에서 파라미터를 건네주지 않고 호출할 수 있다는 것은 아니다.  dependency injection이 일어나는 것은 객체를 생성할때이다. 객체를 생성할때 필요한 해당 클래스의 외부 인스턴스를 미리 생성해서 객체를 초기화시켜주는 작업을 말한다. 뒤의 포스트에서 자세히 설명한다. 특히 ObjectBuilder 아키텍쳐를 설명하는 부분을 유용하게 참조할 수 있을 것이다. - 2008.06.15 일요일 오후 수정

컨테이너에게 이런 dependency injection 메커니즘이 작동해야 한다는 것을 알려주기 위해서 대상 객체의 메소드에 특별한 어트리뷰트를 이용해서 표시를 해줘야 한다. 이 어트리뷰트에 대해서는 뒤에서 설명한다. 앞에서는 "메소드"라고 했지만 사실 메소드는 우리가 알고 있는 일반 메소드뿐만 아니라 특수한 메소드 즉 생성자 그리고 속성 그중에서도 setter 속성도 여기에 해당된다. 즉 생성자, setter 속성, 일반 메소드에 특정 표시를 해 주면 컨테이너는 이 메소드들을 호출할때는 DI 메커니즘이 작동해야 한다는 것을 인식한다. 그래서 이 메소드들의 호출에 필요한 인스턴스를 개발자가 건네주지 않아도 자기가 스스로 생성해서 호출해주는 것이다.

그럼 그 표시 즉 어트리뷰트는 어떤 모습인가. 생성자, setter 속성, 일반 메소드 모양은 다르다.

다음 포스트에서는 세 종류의 injection에 대해서 예제를 통해서 알아보도록 하겠다. 너무 많은 것을 한꺼번에 하면 지친다. 필자도 지치고 읽는 사람도 지치고.

Posted by dalbong2

지난 포스트에서 말한 것처럼 이번에는 타입을 시작 프로그램에서 프로그램적으로 컨테이너에 등록했던 작업을 이제 Config로 분리해내는 작업을 하겠다. Unity 컨테이너의 완전한 Config 스키마는 새로운 것이 나올때마다 하나씩 알아보는 것으로 하겠다. 왜냐고? 지금은 필자도 다 모른다는 것이 가장 큰 이유이고 그리고 모든 것을 지금 설명하는 것은 시기상조일것이라는 생각이다. 

지난 포스트의 예제의 시작 프로그램의 코드를 다시 한번 더 보자.

namespace FSLoggerConsole

{

    class Program

    {

        static void Main(string[] args)

        {


            // 편의상 이전 로그 파일이 존재하면 삭제한다. 로그가 누적되면 테스트가 방해되잖아.

            if (System.IO.File.Exists(@"C:\FSLogger.log"))

                System.IO.File.Delete(@"C:\FSLogger.log");


            // 컨테이너 생성

            UnityContainer container = new UnityContainer();


            //DalbongAppContext에 현재 컨테이너 참조 저장

            DalbongAppContext.Current.Container = container;



            //ILogger와 FSLogger의 매핑 정보를 등록한다.

            container.RegisterType<ILogger, FSLogger>(new ContainerControlledLifetimeManager());


            SiteUserInfo userinfo = new SiteUserInfo("dalbong2");

            container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());


            //Biz01 객체를 생성한다.

            Biz01 biz1 = container.Resolve<Biz01>();

            //Biz01 biz2 = container.Resolve<Biz01>();



            biz1.Save();

            //biz2.Save();


            //로그 파일의 내용을 콘솔에 출력한다.

            Console.WriteLine(System.IO.File.ReadAllText(@"C:\FSLogger.log"));

            Console.Read();


        }

    }

}

좀 수고해서 애드인을 하나 설치했더니 코드가 예쁘게 나오는군. 좋아 좋아.

앞의 코드를 보면 ILogger<->FSLogger 매핑 정보를 등록하는 부분이 있다. 이 부분을 config로 분리하겠다. 

<configuration>

  <configSections>

    <section name="unity"

          type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,

                Microsoft.Practices.Unity.Configuration, Version=1.1.0.0,

                Culture=neutral, PublicKeyToken=31bf3856ad364e35" />


  </configSections>

  <unity>

    <typeAliases>

      <!-- LifetimeMager 타입 -->

      <typeAlias alias="singleton"

          type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,

            Microsoft.Practices.Unity" />


    </typeAliases>

    <containers>

      <container name="container01">

        <types>

          <!-- 타입 매핑 등록-->

          <type type="Dalbong.Framework.ILogger,Dalbong.Framework" mapTo="Site.Framework.FSLogger,Site.Framework">

            <lifetime type="singleton" />

          </type>

        </types>


      </container>

    </containers>

  </unity>

</configuration>

<configSections> 부분에는 "unity"라는 섹션을 해석할 수 있는 핸들러를 설정하고 있다. UnityConfigurationSection 이라는 녀석이 "unity"라는 섹션을 해석할 수 있다는 것이다. 이대로 추가해주면 된다.

아래에 <unity>라는 섹션이 있고 그 안에  <typeAliases> 부분과 <containers> 요소가 있다. 두 요소 이름을 보면 복수형"~s"로 끝나는 것을 보면 각각 <typeAlias>와 <container>가 여러개 포함될 수 있을 것이라는 것을 눈치챌 수 있다. <typeAlias>는 타입에 별칭을 매핑시키는 곳이다. 앞의 예제에서 "Microsoft.Practices.Unity.ContainerControlledLifetimeManager"가 쓰일 수 있는 곳에 "singleton"이 사용될 수 있다는 것이다. <container>요소 내부에는 <types>가 있고 역시 복수형 "~s"로 끝나는 것을 봐서는 여러개의 <type>이 포함될 수 있다는 것을 알 수 있다. 예제에서는 ILogger 타입을 실제(concrete) 타입 FSLogger로 매핑한다는 것을 보여주고 있다. 그리고 그 객체는 singleton 객체라는 것을 표시해주고 있다.

간단하지만 타입 매핑 등록 설정은 끝났다. 이제 이 설정을 코드상에서 컨테이너에 로딩하는 작업이 필요하다. 코드는 다음과 같다.

namespace FSLoggerConsole

{

    class Program

    {

        static void Main(string[] args)

        {



            // 컨테이너 생성

            IUnityContainer container = new UnityContainer();


  • //설정 내용을 컨테이너로 로딩한다.
  • UnityConfigurationSection section
  • = (UnityConfigurationSection)onfigurationManager.GetSection("unity");
  • section.Containers["container01"].Configure(container);

            //DalbongAppContext에 현재 컨테이너 참조 저장

            DalbongAppContext.Current.Container = container;


  • //ILogger와 FSLogger의 매핑 정보를 등록한다.
  • //container.RegisterType<ILogger, FSLogger>(new ContainerControlledLifetimeManager());

            SiteUserInfo userinfo = new SiteUserInfo("dalbong2");

            container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());


            ...

           

        }

    }

}

이제 실행을 하면 이전 결과와 동일한 효과가 있다. 그러나 그전에 다음 두개의 어셈블리에 대한 참조를 추가해야 한다.

Unity 컨테이너를 사용한 간단한 예제를 마친다. 다음 포스트부터는 Unity 블럭을 제작하는 데 사용한 Dependency Injection 패턴을  알아볼 것이고 Unity 블럭에서 그 패턴을 어떻게 적용되고 있는지 알아보겠다.  근데, Dependency Injection 패턴도 일반 다른 패턴들처럼 설명하고 정리하기가 필자에게는 상당히 힘든 일이다. 이 블로그에는 분명 패턴과 관련된 카테고리가 있음에도 아직 관련 포스트를 제대로 올리지 못한 것은 관심은 있으나 아직 정리할 실력이 되지 못한다는 것이다. 이 부분을 정리하는데 상당한 시간이 소요될 것 같다. 그리고 Unity 컨테이너를 확장하는 방법에 대해서도 알아보면서 Unity 컨테이너를 좀 더 상세히 알아보겠다.

Posted by dalbong2

앞 포스트에서 UAB를 사용해서 개발 프레임워크 구조와 유사하게 로깅 프로그램을 구현했다. 프로젝트가 한참 진행되어 개발 진척도는 50%를 넘어가고 있다. 그런데 어느날 고객이 다음과 같이 무심코 내뱉는다.

"현재 비즈니스 메소드를 호출할때 로그를 남기고 있는데, 로그로 사용자 아이디와 이름도 남기고 싶습니다".

업무 프로그램에서 고객 아이디와 이름만 로깅 메소드 Write()로 넘겨주면 되겠지하는 생각이 먼저 떠오르는가?  그럼 불쌍한 개발자는 다시 모든 프로그램을 수정해야 하는가? 그리고  다른 프로젝트에서는 사용자의 아이디, 이름뿐만 아니라 부서 정보도 남겨달라고 하면 어떻게 해야 하나. 그때는 프레임워크의 코드를 또 수정해야 하나?

두 번째 문제부터 해결해보자.  이를 위해서 필자는 사용자 정보 클래스를 다음과 같이 설계하겠다. 

UserInfo 클래스는 코어 프레임워크쪽에서 사용하는 타입니다. 그리고 SiteUserInfo는 사이트별로 변경될 수 있는 클래스이다.

사용자라면 최소한 아이디와 이름은 있을 것이다.  ID 속성의 실제 값이 사원 번호인 경우도 있을 것이다. 필자는 두 속성을 어떤 사이트의 사용자 정보 클래스에 포함될 수 있는 필수 속성으로 보았다. 그리고 코어 프레임워크에서는 대부분 비즈니스 로직과 관련된 정보는 사용하지 않는다. 따라서 다음과 데이터를 구성했다.

namespace Dalbong.Framework
{
    public class UserInfo
    {
        private string _id = "";
        private string _name = "";

        public UserInfo(string id )
        {
            _id = id;

            // id를 통해서 이름을 DB에서 조회해온다. 
            _name = "달봉이";
        }

        public virtual string ID
        {
            get
            {
                return _id;   
            }
        }
        public virtual string Name
        {
            get
            {
                return _name;
            }
        }
    }
}

다음은 사이트별로 필요한 속성을 추가하는 SiteUserInfo를 다음과 같이 정의했다. 부서 코드를 받을 수 있는 속성을 추가했다. 생성자의 인자로 사용자 아이디를 건네받아서 필요한 정보를 생성자에서 채우도록 하고 있다.

namespace Site.Framework
{
    public class SiteUserInfo : UserInfo
    {
        // 부서코드
        string _deptCode = "";

        public SiteUserInfo(string id)
            : base(id)
        {
            // id를 통해서 부서코드를 DB에서 조회홰온다.
            _deptCode = "HR";
        }
        public string DeptCode
        {
            get
            {
                return _deptCode;
            }
            set
            {
                _deptCode = value;
            }
        }
    }
}

이제 첫번째 문제를 알아보자. 새로운 로깅을 위해서 로깅에 필요한 정보를 로거 객체의 Write()의 인자로 넘겨주도록 코드를 수정할 수는 없다. 결국 전역적 성격을 띄는 변수를 만들어서 로거 객체 내부에서 접근할 수 있도록 해야 한다. 이렇게 하면 공통팀에서만 수정해주면 모든 업무 개발자들이 수정을 하지 않도록 할 수 있다.

사용자 정보를 전역 변수로 만드는 방법에는 여러가지가 있다. AppDomain 객체를 사용할 수도 있고, CallContext라는 것을 사용해서도 해결할 수 있다.  필자는 코어 프레임워크 프로젝트에 컨텍스트 클래스를 하나 만들었다. 그래서 이 클래스의 정적 멤버에 사용자 정보를 저장해두고 애플리케이션의 어디에서든지 사용자 정보에 접근할 수 있도록 했다.  컨텍스트 클래스명을 DalbongAppContext라고 해서 다음과 같이 정의했다.

namespace Dalbong.Framework
{
    public class DalbongAppContext
    {
        static DalbongAppContext _self = null;
        static Microsoft.Practices.Unity.UnityContainer _container = null;
        private DalbongAppContext()
        {
            // 공개적인 생성을 막는다.
        }
        public static DalbongAppContext Current
        {
            get
            {
                if (_self == null)
                    _self = new DalbongAppContext();
                return _self;
            }
        }
        public Microsoft.Practices.Unity.UnityContainer Container
        {
            get
            {
                return _container;
            }
            set
            {
                _container = value;
            }
        }
    }
}


외부의 코드에서는 정적 멤버인 Current 속성을 통해서만 이 객체에 접근할 수 있다. 생성자는 private 버전만 정의해서 외부에서 공개적으로 생성하는 것을 막고 있다. 생성자를 private로 하는 것은 자주 접할 수 있는 것은 아니라서 무슨 말인지 갸웃하는 개발자도 있을 듯 싶다. 생성자를 생략하면 기본적으로 public 생성자를 사용할 수 있지만 생성자를 정의했는데, private 생성자만 있으면 외부에서 new같은 것을 통해서 생성할 수 없다는 것을 말한다.

Current 속성이 static이므로 DalbongAppContext.Current와 같이 접근할 수 있다. 이 속성이 호출되었을때 그 내부에서는 null인지를 확인해서 그렇다면 객체를 생성해서 반환한다. 아니라면 이전의 객체를 반환한다. 이렇게 하면 애플리케이션 전체에서 인스턴스 하나만 존재하게 된다. 이것이 바로 Singleton 패턴이다.

컨텍스트 객체에는 Container 속성이 정의되어 있는데 이것은 Program.cs에서 정의한 UnityContainer 객체에 대한 참조를 저장해서 애플리케이션 어디에서든지 접근할 수 있도록 하기 위해서이다. 사용자 정보도 이 컨테이너에 인스턴스가 저장될 것이다. 그래서 로거 객체에서도 DalbongAppContext의 Container 속성을 통해서 Unity 컨테이너에 저장된 사용자 정보 객체에 접근할 것이다.

다음은 새로운 타입을 정의하고 있는 파일을 추가한 개발 구조이다.

Program.cs 파일의 Main 메소드 내용을 보면 다음과 같다.

namespace FSLoggerConsole
{
    class Program
    {
        static void Main(string[] args)
        {

            // 편의상 이전 로그 파일이 존재하면 삭제한다. 로그가 누적되면 테스트가 방해되잖아.
            if (System.IO.File.Exists(@"C:\FSLogger.log"))
                System.IO.File.Delete(@"C:\FSLogger.log");

            // 컨테이너 생성
            UnityContainer container = new UnityContainer();

            //DalbongAppContext에 현재 컨테이너 참조 저장
            DalbongAppContext.Current.Container = container;

           //UserInfo 매핑 정보를 등록한다. sigleton 패턴 사용
            //container.RegisterType<UserInfo, SiteUserInfo>(new ContainerControlledLifetimeManager());

            //ILogger와 FSLogger의 매핑 정보를 등록한다.
            container.RegisterType<ILogger, FSLogger>(new ContainerControlledLifetimeManager());

            SiteUserInfo userinfo = new SiteUserInfo("dalbong2");
            container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());

            //Biz01 객체를 생성한다.
            Biz01 biz1 = container.Resolve<Biz01>();
            //Biz01 biz2 = container.Resolve<Biz01>();

            biz1.Save();
            //biz2.Save();

            //로그 파일의 내용을 콘솔에 출력한다.
            Console.WriteLine(System.IO.File.ReadAllText(@"C:\FSLogger.log"));
            Console.Read();

        }
    }
}

사용자 정보 객체는 보통 사용자가 로그인하고 나서 애플리케이션이 시작할때 생성된다. 앞의 코드중에서 다음과 같은 부분이 있다.

SiteUserInfo userinfo = new SiteUserInfo("dalbong2");
container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());

SiteUserInfo 객체를 생성(UserInfo 객체가 아니다)해서 컨테이너에 객체를 등록하고 있다.

이 코드를 보면 RegisterType<>() 메소드를 사용하지 않고, UnityContainer의 RegisterInstance<>() 메소드를 사용해서 이미 생성되어 있는 인스턴스를 등록하고 있다. UnityContainer는 타입뿐만 아니라 이렇게 인스턴스를 직접 등록할 수도 있다는 것이다. 이후 UnityContainer의 Resolve<UserInfo>()를 통해서 그 인스턴스를 반환받을 수 있다. RegisterInstance<>()에서 Singleton 패턴을 표시하는 ContainerControlledLifetimeManager 객체를 넘겨주고 있으므로 사용자 정보 객체는 하나만 생성되어 컨테이너에 보관된다.

사실 처음에는 이 코드 대신에 초록색으로 되어 있는 부분을 사용하려고 했다. 근데, 에러였다. 무엇때문인지는 모르겠다. 찾다 그만 뒀다. 내일 출근해야해.....흑!  닝기리!!

그림은 컨테이너가 타입 뿐만 아니라 그림처럼 인스턴스도 직접 포함시킬 수 있다는 것을 보여주고 있다.

그리고 나중에 알아보겠지만(언젤지는...) 컨테이너의 익스텐션이란 것도 만들어서 등록해서 사용할 수 있다. 이것은 Unity 컨테이너의 기능을 확장할 수 있는 사용자 정의 방법이다.

이제 비즈니스 객체의 메소드를 호출할때 사용자 정보를 남기도록 하자. 누가 한다고요? 개발자, 공통팀? 공통팀!

FSLogger 클래스의 Write() 메소드를 다음과 같이 수정했다.

public void Write(string message )
{
    SiteUserInfo userinfo =(SiteUserInfo) DalbongAppContext.Current.Container.Resolve<UserInfo>();
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("ID:{0}, 이름:{1}, 부서:{2}, 메세지:{3}", userinfo.ID, userinfo.Name, userinfo.DeptCode,message);
    //sb.Append(message);
    System.IO.File.AppendAllText(@"C:\FSLogger.log", sb.ToString() + Environment.NewLine);
}

DalbongAppContext.Current.Container속성을 통해서 Unity 컨테이너로 부터 사용자 정보 객체를 얻어오고 있다. 그리고 나서 SiteUserInfo 로 캐스팅한다. 이렇게 하면 사이트별로 추가 수정한 사용자 정보 객체를 사용할 수 있게 되는 것이다.

뭔가 의미있는 결론으로 글을 마쳐야 하는데, 항상 결론을 정리할때쯤 되면 체력이 부족하다. 오늘도 역시나 피곤하다.  간결한 결론을 내릴 힘이 없다.

그리고 이런 구조의 프레임워크 설계대신에 자신이 생각하고 있는 구조가 있다면 필자에게 좀 알려주길 바란다.


시작 프로젝트 FSLoggerConsole 부분은 프로젝트의 공통팀에서 수정한다.  이 부분의 코드는 대부분 등록하고 애플리케이션을 초기화하는 코드가 주로 있게 된다. 타입이나 인스턴스 그리고 컨테이너의 익스텐션을 등록하는 부분은 config 설정으로 빼내는 것이 코드가 간결해 질 수 있다. 이 부분은 다음 포스트로 넘겨서 간단히 정리하도록 하겠다. 

만들어 놓고 보니까 구조가 좀 어색한것 같다. 쓰으~~

Posted by dalbong2
TAG singleton