현재 참여하고 있는 프로젝트는 ClickOnce NTD배포를 혼합해서 사용하고 있다. 다음은 어제밤에 NTD에게서 당한 린치 사건이다. 12까지 퇴근을 못하고 택시비로 몇 만원을 강탈당했다. 그 사건에 대한 내용을 지금부터 기록해 보려고 한다.


1.
사건 개요

현재 참여하고 있는 기업에서는 처음에 배포 서버에 HTTP핸들러를 하나 제작해서 사용하고 있었다. 그 녀석이 하는 일은 두 가지였다.
하나는, 배포 서버로 퍼블리시(publish)[각주:1] 되는 어셈블리를 디렉토리별로 구분하여 관리할 수 있도록 할 수 있게 하는 역할을 한다.  HTTP핸들러는 또한 위성 어셈블리의 반복 요청에 대한 최적화 방안으로 사용될 수 있다.
그러나 HTTP핸들러의 존재는 시스템을 설치하는 입장에서는 귀찮은 존재다. .dll 파일에 대한 요청이 들어오면 ASP.NET에게 요청을 건네주도록 하는 IIS 설정도 해야 하고, 또한 핸들러를 지정해주는 web.config 파일도 관리해야 한다..[각주:2]
현재 참여하고 있는 프로젝트에서는 HTTP핸들러를 사용할만큼 디렉토리 구조가 복잡하지도 않았고 그래서 IIS 서버의 "디렉토리 검색"을 선택함으로써 첫번째 디렉토리 구조화 역할을 대신할 수 있다.  IIS 관리자에서 원하는 디렉토리 오른쪽 클릭->속성->디렉토리탭->디렉토리 검색 체크를 하면 해당 디렉토리로 파일 요청이 들어오면 하위 디렉토리를 모두 검색할 수 있게 된다. 그러나 사실 현재 프로젝트에서는 하위 디렉토리가 없어서 이런 설정도 필요없게 되었다.


그리고 HTTP핸들러가 담당하는 위성 어셈블리 반복 요청에 대한 최적화 방안도 클라이언트측에서 구동되는 스마트클라이언트 애플리케이션의 컬쳐 정보를 없애버리는 방안[각주:3]으로 대체하기로 결정했다.

이렇게 해서 HTTP 핸들러가 하는 역할들이 모두 다른 대안으로 대체되었고 이제 HTTP핸들러가 있을 필요가 없어지게 되었다. HTTP 핸들러를 제거할 만반의 준비를 갖췄다. 다음 그림은 HTTP핸들러를 제거하는 장면이다.
1165766805

DLL 요청 매핑 제거

그리고 web.config를 삭제하면 된다.
1222482475

web.config 삭제

드뎌 제거 완료!
달봉이는 떨리는 손으로 스마트클라이언트 시스템을 다시 구동시켜 어셈블리 요청 버튼을 클릭했다. NTD에 의해 완벽하게 다운이 되고 또한 로딩도 완벽했다. 모든것이 정상적으로 작동했다. 작업 완료를 하고, 커피를 한잔 마시면서 느긋하게 작업 과정에 대한 문서 작성에 대한 구상을 하고 있었다.

얼마후 업무팀에서 화면이 뜨지 않는 다는 연락이 왔다. 에러는 "어셈블리를 찾을 수 없다"는 것이였다. 아무 생각없이 Fiddler[각주:4]를 구동시켰다. 습관처럼 서버측에서 해당 어셈블리가 제대로 다운되는지를 확인하기 위해서였다.

스마트클라이언트 시스템을 구동시켰다. 그리고 메뉴를 클릭했다. 이 메뉴를 클릭하면 내부적으로 Activator.CreateInstanceFrom(url)이 호출된다. 달봉이는 생각했다. '이 메소드가 호출되면 서버로 해당 어셈블리에 대한 HTTP 요청이 갈 것이고 서버에는 어셈블리에 대한 MIME 타입이 등록되어 있기 때문에 정상적으로 내려보내줄것이다.' 달봉이는 게슴츠레한 눈으로 무심하게 자판을 두드렸다.

그런데 자판 소리는 점점 달봉이의 심장을 두드리기 시작했다. 불길한 조짐을 느낄 수 있었다.  Fiddler는 정확히 해당 어셈블리가 다운되고 있다는 것을 보여주고 있었다. 그러나 화면은 출력되지 않았다. "어셈블리를 찾을 수 없다"는 메세지가 떳다. 후다닥 시작->실행에 "assembly"를 입력하고 어셈블리 다운로드 캐시를 확인했다.

없었다 !!  >>~~~

없었다. 해당 어셈블리가 NTD로 다운이 되었다는 것까지는 확인했는데, 어셈블리가 캐시에 없었다 !! 결국 작동이 되는 PC가 있었고, 작동이 되지 않는 PC가 있다는 것이다. 다운은 되나 어셈블리 캐시로 저장을 못하고 그리고 로딩이 되지 못하는 PC가 있었다.

제일 먼저 떠올랐던 것은
MIME 필터였다.

3.
달봉이 KB

이런 현상을 이해하기 위한 달봉이기 가지고 있는 관련 지식 리소스로는 우선 MIME 타입을 알고 있다는 것이다. 달봉이는 MIME 타입에 대해서 다음처럼 이해하고 있다.
'IIS서버는 파일 확장자 별로 MIME 타입이라는 것을 등록해두고 있다. 이 목록에 없는 요청을 하면 서버는 그 요청에 정상적인 응답을 하지 않을 것이다. IIS는 클라이언트로 요청 처리 결과를 내려보낼 때 MIME 타입도 첨부해서 보낸다. 그리고 클라이언트는 그 MIME 타입을 판별해서 어떻게 처리할 지를 결정한다. MIME 타입에 해당하는 MIME 필터가 존재하면 제어권을 그 필터에게로 넘긴다는 것이다.'

달봉이는 서버에서 다운된 어셈블리가 특별히 다른 일반 스트림과 구분되어서 .NET 프레임워크에서 처리되도록 하기 위해서는 클라이언트 PC의 레지스트리에 다음과 같은 MIME Type에 대한 MIME 필터가 등록되어 있어야 한다는 것도 알고 있었다.
1000310438

클라이언트 PC에 등록된 MIME 타입 및 필터


닷넷 프레임워크가 설치되면 application/octet-stream, application/x-msdownload, application/x-complus MIME타입에 대한 MIME 필터가 등록된다.

즉 달봉이는 .NET의 어셈블리가 서버에서 다운될 수 있기 위해서는 IIS에 확장자 .dll .exe에 대한 MIME 타입이 등록되어 있어야 하고, 또는 클라이언트 PC에도 해당 MIME 타입별 MIME 필터가 등록되어 있어야 한다는 것까지는 알고 있었다.
그러나 현재 발생하고 있는 문제, 클라이언트로 어셈블리 스트림이 내려오긴 했으나 로딩은 되지 않는 경우는 달봉이의 이해 수준을 넘어서는 문제였다.

이제 어셈블리가 서버에서 다운되어 클라이언트 PC에서 로딩되는 과정을 좀더 자세히 들여다 보기 위한 달봉이의 삽질이 시작된다.


4. 사건
추적

NTD 의한 어셈블리의 배포는 IE 브라우저와 IIS서버 그리고 HTTP 프로토콜에 대한 이해가

필요하다. NTD IE IIS를 이용해서 어셈블리 배포가 이뤄지기 때문이다.

1310302593
IE IIS에 의한 NTD배포


그림을 보면 클라이언트측의 스마트클라이언트 애플리케이션에서 서버측에 있는 어셈블리를 요청하면 그 요청은 Fusion이라는 엔진으로 전달이 된다. 이 엔진은 해당 어셈블리를 다운받기 위해서 IE를 이용한다. IE는 어셈블리에 대한 요청이라고 해서 특별한 처리를 하는 것이 아니다요청을 받은 IE입장에서는 어셈블리에 대한 요청이나 일반 jpg 이미지 파일과 같은 요청이나 다를 것이 없다. 보통처럼 HTTP 요청을 만들어서 IIS서버로  보낸다.
IIS
에서의 처리를 보기 전에 잠시 NTD배포의 스마트클라이언트에서 IIS 어셈블리 요청을 보내는 보내는 경우는 2가지 있다

1)
애플리케이션 엔트리 포인트를 가지고 있는 어셈블리를 요청하는 경우
2)
참조되는 어셈블리를 요청하는 경우

스마트클라이언트 애플리케이션을 구동하기 위해서 엔트리 포인트를 가지고 있는 어셈블리를 요청하는 경우가 첫번째인데, 엔트리 어셈블리를 요청하는 방법은 스마트클라이언트 애플리케이션의 타입에 따라 다르다. 시작 프로그램으로 IE 브라우저를 사용한다면 브라우저

페이지에서 <object> 태그를 사용해서 최초 어셈블리를 요청하게 된다.

<OBJECT id="컨트롤ID" classid="어셈블리경로명(dll 확장자포함)#로딩할화면의클래스명" >

EXE 타입의 애플리케이션은 바로 URL을 통해서 구동이 된다[각주:5]

http://localhost/XXX.EXE


두번째, 참조되는 어셈블리를 요청하는 경우는 정적 참조뿐만 아니라 Assembly.Load 또는 Assembly.LoadFrom같은 동적으로 참조되는 어셈블리를 요청할때도 HTTP요청이 보내진다.
이런 HTTP요청은 다음과 같이 GET방식으로 의 IIS서버에 보내진다

GET /XXX.DLL HTTP/1.1
GET /XXX.EXE HTTP/1.1


서버측에서 이 파일을 내려보내주느냐 마느냐는 .NET 프레임워크와는 상관없는 웹 서버가 결정할 문제이다. IIS서버는 MIME 타입이 등록된 파일만 내려보내는데, .dll .exe 다음과 같은 MIME 타입으로 등록되어 있다.

.dll - application/octet-stream
.exe - application/x-msdownload

이제 클라이언트측에서 HTTP요청이 오면 IIS서버는 요청한 파일을 내려보내줄 준비가 되어 있다. IIS HTTP요청의 헤더에 포함된 정보를 통해서 클라이언트에 캐싱된 파일이 아직 만료 시간상 유효한지를 체크해서 파일을 내려보낼지에 대한 여부를 결정한다. 만약 클라이언트측 파일이 아직 유효하다고 판단되면(보통 만료일자를 통해 비교한다) 서버측에서는 새로운 파일을 내려보내지 않는다. 대신에 304(Not Modified) 코드를 내려보낸다. 브라우저와 IIS의 파일 다운로드는 이런 식이기 때문에 NTD에 의한 어셈블리의 다운로드도 브라우저와 웹서버의 캐시 설정 등에 영향을 받는다.
하여튼
클라이언트까지 요청한 파일을 다운받는 것은 IE가 하는 일이다. 파일이 다운되면 IE에 포함된 URL Moniker라는 엔진은 외부에서 들어오는 모든 HTTP 응답 스트림을 필터링해서 MIME 타입을 기반으로해서 레지스트리에 MIME 필터가 등록되어 있는지를 확인한다. 만약 현재 응답의 MIME타입에 대응하는 MIME 필터가 등록되있다는것을 확인하면 응답에 대한 제어권을 해당 MIME 필터로 넘긴다. MIME 필터는 특정 MIME 타입별로 응답 내용을 변경하는 기능을 제공한다. 예를 들어서 XML문서가 브라우저에 표시될때 XML요소가 트리뷰처럼 보이도록 하는 것은 text/xml MIME타입을 위한 MIME 필터가 제공해주는 기능이다. MIME 필터는 앞의 그림에서 본 것처럼 HKEY_CLASSES_ROOT\PROTOCOLS\Filter키의 하위에 MIME타입별로 등록되어 있다.

[
지금부터는 달봉이의 추리이다.]
이제 HTTP응답을 받은 클라이언트측에서는 .dll 어셈블리의 MIME 타입에 대한 MIME 필터를 확인하면 MIME 필터가 응답에 제어권을 넘겨받을 것이다이 녀석이 무엇을 하는지는 아직 알려진 바가 없지만 하여튼 주어진 역할을 할 것이다. MIME 필터가 담당하는지는 정확히는 모르겠지만 확실한 것은, 정상적인 로딩이 일어나는 경우는
어셈블리가 로딩되기 전에 반드시 어셈블리 다운로드 캐시에 캐싱이 되어 있다는 것이다.

테스트를 통한 MIME 필터의 역할을 추측해 본다면,
1) <object>에 의해서 다운되었다면 IEHost.dll IE 프로세스내에서 구동시키던지
2) url
에 의해 다운된 exe 어셈블리인 경우라면 IEExec.exe 프로세스가 구동되든지,
3) 참조에 의한 다운로드라면 Fusion으로 제어권을 넘겨주던지 할 것이다.


지금 달봉이가 직면한 문제는 세번째 경우에 해당한다. 즉 정상적이라면 fusion은 제어권을 넘겨받고 어셈블리 확인을 거치고, CAS 정책을 통과해서 로딩이 되어야 할 것이다. 그러나 어셈블리가 서버로부터 내려오기는 했지만 어셈블리 다운로드 캐시에는 저장이 되지 않았다는 것이다. 또한 어떤 PC에서는 제대로 캐싱이 되고 어떤 PC에서는 작동이 되지 않았다.

사실 달봉이는 처음에 로딩이 되지 않은 것을 MIME 타입이 잘못된 것으로 알았다. 그래서 서버측에서 HTTP 핸들러를 제작해서, .dll에 대한 요청에 대해서 수작업으로 MIME 타입을 “application/x-msdownload, aspplication/octet-stream, application/x-complus”을 번갈아 서 설정을 하면서 테스트를 해 봤다. 그러나 결과는 마찬가지였다.

달봉이는 진정을 취하고 차분히 생각할 시간을 가졌다. 일단 클라이언트까지는 다운이 되었다. 그럼 바인딩 단계를 거칠 것이다. 그렇다면 에러나는 단계가 바인딩 단계일 것이고 당연히 이 단계에서의 에러는 fuslogvw.exe[각주:6]를 통해서 알아볼 수 있을 것이다. 당황한 나머지 당연한 생각을 그제서야 할 수 있게 된 것이다.

Fuslogvw.exe
를 구동시키고 작동이 되지 않은 메뉴를 클릭했다. 그런 다음 fustlogvw에서 해당 어셈블리 바인딩에 대한 항목을 더블 클릭해서 상세 내용을 살펴보았다.

*** 어셈블리 바인더 로그 엔트리  (2006-05-31 @ 오후 5:43:01) ***

작업을 수행하지 못했습니다.

바인딩 결과: hr = 0x80070002. 지정된 파일을 찾을 수 없습니다.

다음 위치에서 어셈블리 관리자 로드:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll

다음 실행 파일에서 실행:  C:\Documents and Settings\dalbong70\Local Settings\Apps\2.0\YW9HQRK5.98C\MPV5Z7KK.ZQY\lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712\LemContainer.exe

--- 자세한 오류 로그가 아래에 표시됩니다.

=== Pre-bind state information ===

LOG: User = VM-COMPUTER\dalbong70

LOG: Where-ref bind. Location = http://deploy.lem.com/lem/SmartControls/Lem.Win.Cons.Construction.LabEquMgmt.dll

LOG: Appbase = file:///C:/Documents and Settings/dalbong70/Local Settings/Apps/2.0/YW9HQRK5.98C/MPV5Z7KK.ZQY/lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712/

LOG: Initial PrivatePath = NULL

LOG: Dynamic Base = NULL

LOG: Cache Base = NULL

LOG: AppName = LemContainer.exe

Calling assembly : (Unknown).

===

LOG: This bind starts in LoadFrom load context.

WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().

LOG: Using application configuration file: C:\Documents and Settings\dalbong70\Local Settings\Apps\2.0\YW9HQRK5.98C\MPV5Z7KK.ZQY\lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712\LemContainer.exe.config

LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.

LOG: Attempting download of new URL http://deploy.lem.com/lem/SmartControls/Lem.Win.Cons.Construction.LabEquMgmt.dll.

오류: 다운로드된 파일이 catch되지 않았습니다. 웹 서버가 콘텐츠를 즉시 만료하도록 구성된 것 같습니다.

로그: 모든 URL을 검색하려고 했지만 실패했습니다.



마지막에서 두번째줄을 보고 원인이 되는 부분을 바로 추측할 수 있었다. 다음 그림은 현재 스마트클라이언트 애플리케이션의 IIS 디렉토리 구조를 보여주고 있다.

1040182137

업무 어셈블리의 만료 정책


현재 스마트클라이언트 시스템에서는 업무팀의 UI 어셈블리는 SmartClients 디렉토리에 모아두고 있다. 이 디렉토리는 캐시 정책으로 속성->HTTP Headers ->Enable content expiration 섹션의 “Expire immediately(즉시 만료)”를 선택하고 있다. 이 선택을 택한 이유는 개발 당시는 업무팀의 UI 작업은 계속 수정될 것이고 따라서 다음 요청시는 바로 바로 수정된 어셈블리가 클라이언트로 내려갈 수 있도록 하겠다는 의도 때문이었다.


그러나 바인딩 로그를 보면 이 설정이 클라이언트에서는 문제가 되었다는 얘기다. IE로 다운된 후 어셈블리 다운로드 캐시로 가기전에 바로 사라져버릴 것이라는 추측을 했다. 그래서 IIS에서의 설정을 “Expire after 1 day(s)”로 수정을 해 봤다. “~~이었다.


그러나 문제는 또 있었다. (day)단위로 캐싱시킬 수는 없었다. “Expire immediately”를 포기할 수는 없었던 것이다. 개발시에는 하루에도 여러번 화면이 수정될 것이고, 클라이언트에서는 즉시 그 수정된 결과를 볼 수 있어야 했다. 그래서 또 한번의 삽질을 시도했다. HTTP RFC를 뒤지면서 Cache-Control이라는 지시어를 주목하게 되었다. 이 지시어와 즉시 만료를 같이 사용하면 클라이언트에서 일단은 그것을 사용(fuslogview의 상세 내용에 나오는 용어로 하자면 “catch”)할 수 있을 지도 모른다는 생각이었다.

Expire Immediately

Cache-Control : Private


1404552924
IIS
커스텀 헤더 설정

두 헤더 지시어를 동시에 사용하고  재 시도를 했다. 모든 PC에서 정상적으로 로딩되었다. 그리고 의도한 대로 이후의 요청에 의해서도 클라이언트측에 캐싱된 것을 사용하는 것이 아니라 계속해서 서버측 어셈블리가 다운되는 것을 확인 할 수 있었다.


5. 달봉이의 수사 결론

많은 삽질이 그렇지만 이번에도 결론은 한 줄로 끝난 셈이다.

Cache-Control : Private


그러나 정확히 이 설정이 MIME 필터와 Fusion에 어떤 영향을 미치는지는 확인할 수 없었다. 또한 IIS->IE(URL 모니커)->MIME 필터->[IEExec.exe, IEHost.dll, Fusion]중 하나로 제어권이 이동되지만 각각에서 정확히 무엇을 하는지도 아직 알 수 없었다.

달봉이는 공개수사를 요청하는 바이다.



참조문서
HOW TO: Use the IEHost Log to Debug .NET Object Hosting in Internet Explorer
http://support.microsoft.com/default.aspx?scid=kb;en-us;313892

INFO: Internet Explorer가 .NET Framework 어셈블리에 대한 권한을 확인하는 방법
http://support.microsoft.com/kb/311301/
  1. 달봉이는 클라이언트 PC로 어셈블리가 내려가는 것을 배포(deployment)라 하는 것과 구분해서, 배포 관리자가 빌드된 어셈블리를 배포 웹 서버로 복사시키는 것을 퍼블리시한다는 것으로 표현하겠다. [본문으로]
  2. 구체적인 HTTP핸들러 제작 및 핸들러 설치는 위성 어셈블리의 반복 요청을 최적화하는 방안을 설명하는 곳을 참조한다 [본문으로]
  3. 위성 어셈블리의 반복 요청에 대한 최적화 방안을 참조한다. [본문으로]
  4. 자세한 내용은 스마트클라이언트 디버깅 툴을 참고한다. [본문으로]
  5. ClickOnce에서도 EXE 어셈블리를 요청하지만 이런식의 URL 명령을 사용하지는 않는다 [본문으로]
  6. 스마트클라이언트 디버깅을 참조한다. .NET 프레임워크를 설치하면 함께 제공되는 명령 프로프트에서 fuslogvw.exe를 실행하면 된다. [본문으로]
Posted by dalbong2

스마트클라이언트 애플리케이션은 디버깅하기가 상당히 까다로운 면이 있다. 특히 IE 브라우저 임베딩 타입의 스마트클라이언트 애플리케이션의 경우는 디버깅에 상당한 어려움을 느끼는 경우가 많다. 이제 기본적인 디버깅 툴들을 소개한다. 이런 툴들을 언제 사용해야 할지를 알고 적절한 시기에 적절한 툴을 사용할 수 있도록 해야 할 것이다.

■ 어셈블리 다운로드 캐시 뷰어

외부의 배포 서버에서 다운된 어셈블리는 어셈블리 다운로드 캐시(assembly download cache)에 저장이 된다. 그 저장 결과를 그림처럼 윈도우 탐색기를 이용해서 볼 수 있다. 다음 명령을 실행하면 뷰어가 구동된다.

시작->실행->”assembly”

어셈블리 다운 여부에 대한 결과만을 확인하고 싶다면 윈도우 탐색기를 이용해서 어셈블리 다운로드 캐시(assembly download cache)를 확인해봐서도 알 수 있다.

1121090377

어셈블리 다운로드 캐시 확인

그런데 어셈블리 다운로드와 관련해서 어셈블리 다운로드 캐시 뷰어만으로는 해결할 수 없는 문제들이 많이 있다.

■ HTTP요청/응답 캡쳐 툴들

어셈블리 다운로드와 관련해서 구체적인 에러 내용을 알고 싶은 경우 사용할 수 있는 툴들이 있다. 다음 툴들은 클라이언트 PC에서 나가고 들어오는 모든 HTTP 트래픽을 캡쳐해서 분석해준다. 이런 툴들을 사용하면 스마트클라이언트 애플리케이션 특히 NTD기반의 애플리케이션을 제작할 때 아주 편리하다.

Fiddler(http://www.fiddlertool.com/fiddler/)- 무료판
HTTP Analyzer(http://www.ieinspector.com/) – 시험판

이런 툴들을 사용하면 원하는 어셈블리가 다운되었는지를 확인할 수 있음은 물론 다운이 안되는 구체적인 에러 원인이 무엇인지를 알려줄 수 있다. 특정 HTTP 요청을 더블클릭하면 상세 내용을 출력해주는데, 특히 HTTP 요청 및 응답의 헤더 부분의 내용은 아주 유용하게 사용되는 경우가 많다. 정상적으로 끝나지 않은 HTTP 요청을 클릭하면 그 원인 또는 추적의 단서를 제공해준다. 무료 또는 시험판은 앞의 URL에서 다운로드할 수 있다.

다음은 Fiddler.exe의 실행모습니다. Fiddler는 구동을 시키면 바로 HTTP 트래픽 캡션를 시작한다.
1213950792

Fiddler 실행모습

다음은 HTTP Analyzer 실행모습이다. 이것은 구동 후 캡쳐 시작 버튼 또는 중지 버튼을 클릭해서 트래픽 캡쳐를 제어할 수 있다.
1089419117

HTTP Analyzer 실행 모습

달봉이는 이 도구들의 도움을 톡톡히 본 경험이 있다. NTD 배포의 애플리케이션을 제작할 때 이 툴들은 클라이언트 PC로의 다운은 정상적으로 이뤄졌다는 내용을 출력해주는데도, 어셈블리 다운로드 캐시에서는 다운된 어셈블리가 보이지 않았다. 결국에 가서는 클라이언트로의 다운로드는 이뤄졌으나 캐시에는 저장되지 않은 것이 정상적인 거동이었고, 이것은 IIS 서버의 만료 정책을 “즉시 만료(Expire immediately)”로 선택해놔서 이런 결과가 나왔다는 것을 발견할 수 있었다. 만약 이런 툴들이 없었다면 IIS서버로부터 다운조차도 되지 않았다고 추측하고 그쪽으로만 디버깅을 고수하였을 것이다.
하여튼 이런 툴들을 사용해서 클라이언트 PC로의 다운은 되었으나 캐싱이 되지 않는다는 것을 인식하고는 바로 바인딩시에 로그를 남겼을지도 모른다는 추측을 했다. 그래서 다음에 소개하는 어셈블리 바인딩 로그 뷰어를 사용해서 그 원인이 IIS의 만료 정책 때문이라는 것을 알게 되었던 것이다.

■ 어셈블리 바인딩 로그 뷰어(Fuslogvw.exe)- 바인딩 로그 뷰어

이 툴을 사용하면 어셈블리가 다운된 후 바인딩하는 단계 즉 참조되는 어셈블리를 찾아서 어셈블리 파일을 확보하고 바인딩하는 과정에서 단계별로 남겨지는 모든 로그를 볼 수 있다. 바인딩 과정에서 발생하는 에러 또한 남겨진 로그를 통해서 그 원인을 파악할 수 있다. 어셈블리 바인딩은 모든 .NET 애플리케이션의 실행에서 일어나는 과정으로 따라서 이 툴은 스마트클라이언트 애플리케이션의 디버깅에 국한되는 툴은 아니다. 바인딩 과정에 대해서는 다른 포스트를 참조하기 바란다.

.NET과 함께 제공되는 프롬프트 명령창에서 다음 명령어를 실행시키면 바인딩로그뷰어가구동된다.
1206731669

바인딩 로그 뷰어 실행 명령

1245459083

바인딩 로그 뷰어 구동 모습

이 창에는 바인딩이 실패한 모든 건들이 출력된다. 바인딩 실패에 대해 자세한 내용을 보고 싶다면 원하는 레코드를 더블클릭하거나 선택하고 오른쪽의 View Log 버튼을 클릭하면 상세 내용을 출력하는 창이 뜬다.

“항목 삭제” 버튼은 특정 레코드를 삭제할 때 사용하고, “모두 삭제” 버튼은 기존의 모든 로그를 삭제한다. 삭제 후 .NET 애플리케이션을 실행시키고 나서 “새로 고침” 버튼을 클릭하면 현재 로깅된 내용을 새로 불러와서 출력하는 버튼이다. “설정” 버튼을 클릭하면 로깅과 관련해서 설정할 수 있는 옵션들을 보여주는 창이 뜬다.  
1099874183

로그 설정 창

바인딩시의 모든 로그를 남기고 싶다면 “모든 바인딩을 디스크에”라는 옵션을 선택하면 된다. 또한 바인딩시 실패한 경우만 로그를 남기고 싶다면 “바인딩 실패를 디스크에” 옵션을 선택하면 된다. 또한 바인딩 로그 뷰어가 사용하는 기본적인 로깅 경로대신에 사용자가 정의한 경로에 로그 파일을 남기고 싶은 경우 “사용자 지정 로그 경로 사용”체크 박스를 선택하고 경로를 지정하고 그리고 로그 뷰어에서 로그 위치를 선택하는 부분에서 “기본값” 대신에 “사용자 지정”을 선택하면 된다. 다음은 바인딩 로그의 상세 내용의 예이다.

*** 어셈블리 바인더 로그 엔트리  (2006-05-31 @ 오후 5:43:01) ***
작업을 수행하지 못했습니다.
바인딩 결과: hr = 0x80070002. 지정된 파일을 찾을 수 없습니다.
다음 위치에서 어셈블리 관리자 로드:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
다음 실행 파일에서 실행:  C:\Documents and Settings\dalbong70\Local Settings\Apps\2.0\YW9HQRK5.98C\MPV5Z7KK.ZQY\lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712\LemContainer.exe
--- 자세한 오류 로그가 아래에 표시됩니다.
=== Pre-bind state information ===
LOG: User = VM-COMPUTER\dalbong70
LOG: Where-ref bind. Location = http://***/SmartControls/Lem.Win.Cons.Construction.LabEquMgmt.dll
LOG: Appbase = file:///C:/Documents and Settings/dalbong70/Local Settings/Apps/2.0/YW9HQRK5.98C/MPV5Z7KK.ZQY
/lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = LemContainer.exe
Calling assembly : (Unknown).
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: Using application configuration file: C:\Documents and Settings\dalbong70\Local Settings\Apps\2.0\YW9HQRK5.98C\MPV5Z7KK.ZQY\lemc..tion_e57baca1538944b1_07d6.0005_ef2b483355832712\LemContainer.exe.config
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: Attempting download of new URL http://***/SmartControls/Lem.Win.Cons.Construction.LabEquMgmt.dll.
오류: 다운로드된 파일이 catch되지 않았습니다. 웹 서버가 콘텐츠를 즉시 만료하도록 구성된 것 같습니다.
로그: 모든 URL을 검색하려고 했지만 실패했습니다.

이 로그들을 제대로 해석하기 위해서는 APPBASE, 버전 정책, 설정 파일 등 .NET의 많은 개념들에 대한 이해가 필요하다. 하여튼 이 바인딩 로그는 원하는 어셈블리가 http://***/SmartControls/Lem.Win.Cons.Construction.LabEquMgmt.dll이지만 결국에 가서는 기술된 내용과 오류로 인해서 바인딩이 성공하지 못했다는 내용을 알려주고 있다.

다음 블로그를 보면 로딩시 발생할 수 있는 예외에 대해서 자세히 설명되어 있다.
Debugging Assembly Loading Failures - Suzanne Cook's blog
http://blogs.msdn.com/suzcook/archive/2003/05/29/57120.aspx


다음 블로그도 좋은 내용을 가지고 있다.
Fusion Log Viewer
http://detritus.blogs.com/lycangeek/2005/03/in_previous_pos.html

■ IEHost 로그

바인딩 과정을 무사히 통과하고 로딩단계에서 에러가 발생할 수 있다. 만약 클라이언트측 호스트 프로그램으로 IE 브라우저를 사용하면, 내부적으로 IEHost.dll이라는 관리형 코드(managed code)가 스마트클라이언트 컨트롤을 호스팅하게 된다. 이 어셈블리가 스마트클라이언트 컨트롤을 로딩할 때 로그를 남기도록 설정할 수 있는데, 이 로그를 통해서 로딩시의 에러에 대한 단서를 찾을 수 있다. 로딩시의 에러에 대한 예를 들면, 어셈블리의 권한이 부족하여 로딩이 되지 않는다든가 또는 객체의 초기화 단계에서 에러가 발생할 수도 있다. 로딩시의 에러를 로깅하기 위해 다음과 같은 순의 설정이 필요하다.

1) 시작->실행->regedit.exe
2) HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework
3)오른쪽 클릭->새로만들기->DWORD값->"DebugIEHost" 입력
4) "DebugIEHost" 오른쪽클릭->수정-> 값데이터 : 1 입력
5)오른쪽 클릭->새로만들기-문자열 값->"IEHostLogFile"입력
6) "IEHostLogFile"오른쪽클릭->수정-> 값데이터 : "c:\IEDebug.log" 입력
1081700950

IEHost 로그 설정

이렇게 설정하고 브라우저를 실행시키고 나면 다음과 설정한 경로에 다음처럼 로그 파일이 남는다. 노트 패드같은 텍스트 편집기로 볼 수 있다.
1266656773

IEHost 로그 파일 설정

앞에서 소개한 툴들의 사용법을 익히는 것도 중요하지만 및 이 툴들을 어느 순간 사용해야 할지에 대한 판단을 내리는 것이 더 중요하다. 즉 서버측에서 발생하는 에러인지 어셈블리가 클라이언트로 다운이 되지 않아서 생기는 에러인지 아니면 클라이언트로 다운은 되었으나 어셈블리가 바인딩되는 도중에 발생한것인지, 아니면 로딩하다가 발생한 판단이 정확히 서야 한다는 것이다. 이런 에러에 대한 상황 판단은 많은 부분이 .NET의 어셈블리 버전 정책(versioning policy)과 관련한 이론을 정확히 이해하는 것에 달려있다.
Posted by dalbong2

NTD 애플리케이션의 초기설정

애플리케이션이 구동되기 위해서 미리 설치되어 있어야 하는 필수 프로그램들이 있다. 아직까지 NTD나 ClickOnce는 배포만을 다루는 기술이다. 스마트클라이언트 애플리케이션이 실행되기 위한 초기설정을 위해서는 배포와는 다른 작업으로 생각해야 한다.
초기설정단계에서 해야 할 일중의 하나는 스마트클라이언트 애플리케이션이 구동되기 전에 필요한 필수 프로그램이 미리 설치되어 있어야 한다. ClickOnce에서는 부트스트래퍼(BootStrapper) 기술을 이용해서 애플리케이션이 시작되기 전에 필수 프로그램을 설치할 수 있다. 그러나 NTD 애플리케이션에서는 필수 프로그램을 설치하는 프로그램을 직접 코딩해야 할 것이다.
또한 NTD 애플리케이션의 경우는 또한 필요한 CAS 권한 설정도 이 단계에서 해야 하는 경우도 있을 것이다. 여기서는 IE기반의 NTD 애플리케이션을 위한 초기설정에 대해서 알아본다.

NTD용 애플리케이션의 클라이언트 PC의 초기 설정 내용을 보면 보통 다음과 같다.

1) .NET Framework 설치
2) 기타 다른 프로그램 설치 예) 리포트 뷰어 설치
3) 필요한 전역 어셈블리 GAC에 등록 예) COM interop용 dll 등록
4) 클라이언트 보안 설정(Code Access Security)

이런 초기 설정 작업은 여러 방식으로 할 수 있을 것이다. 다음 그림은 그런 시나리오중의 하나를 보여주고 있다.
1083017939

NTD 애플리케이션용 클라이언트 PC 초기설정

그림을 보면 초기 설정용 “셋업 서버”와 스마트클라이언트 어셈블리 배포용 “배포 서버”가 분리되어 있다. 물론 그림은 개념적인 내용이고 실제로는 같은 서버를 사용할 수도 있다.

■ .NET 프레임워크 설치 여부 판단

사용자는 먼저 클라이언트 PC의 초기 설정 작업을 수행하기 위해서 CheckClient.aspx를 호출한다. 이 페이지에서는 클라이언트 PC에 .NET 프레임워크가 설치되었는지를 확인하게 된다. 이 작업이 가능한 이유는 클라이언트가 HTTP 요청을 보낼 때 요청의 헤더부분에 .NET 프레임워크의 설치 여부를 알리는 정보를 UserAgent 문자열과 함께 보낸다.
1217593502

HTTP요청의 헤더

그림을 보면 클라이언트 PC에 설치된 .NET 프레임워크의 모든 버전들이 UserAgent 헤더값에 표시되어 있다. 이것을 CheckClient.aspx에서 캐치해서 판단하게 되는 것이다.
ASP.NET에서는 이것을 해석해서 알려주는 속성을 정의하고 있다. HttpBrowserapabilities 클래스의 ClrVersion 속성이 그 일을 해 준다.

string clrVersion = Request.Browser.ClrVersion;

ClrVersion 속성은 System.Version 타입의 값을 반환하는데, 이 타입은 버전 번호에 해당하는 Major, Minor, Build, Revision속성을 통해서 클라이언트에 설치되어 있는 CLR 버전을 알려준다. 만약 CLR이 설치되어 있지 않으면 버전번호 0, 0,-1,-1로 반환한다. 만약 CLR의 버전이 다른 것이 하나 이상 설치되어 있다면 최신 버전 번호를 리턴한다. 클라이언트에 .NET이 설치되었는지는 이 정보들을 통해서 얻을 수 있을 것이다.

■ 수동 설치

만약 .NET 프레임워크가 설치되어 있지 않다면 우선 .NET 프레임워크를 설치해야 할 것이다. 그리고 이 버전에서는 필요하다면 CAS 설정도 한다. CAS 설정을 MSI로 배포하는 방법은 “[연재] 7. 보안정책 배포”를 참고한다( http://dalbong2.net/65 ). 그리고 전역 어셈블리를 GAC에 등록하는 작업 또는 클라이언트 PC의 브라우저 세팅이 필요하다면 이곳에 할 수 있을 것이다. 설치 프로그램을 작성하면 될 것이다.

클라이언트측에 .NET 프레임워크가 설치되어 있다면 기타 필요한 다른 프로그램을 선택할 수 있는 뷰를 보여주면 될 것이다. 각각의 셋업 프로그램을 선택하도록 할 것인지 전체를 한번에 설치하도록 할 것인지는 상황에 따라 적절하게 선택하면 될 것이다.

■ NTD용 부트스트래퍼 제작

1067921513

NTD용 부트스트래퍼

직접 NTD 애플리케이션용 부트스트래퍼 역할을 하는 프로그램 제작할 수도 있을 것이다. 일단 부트스트래퍼가 제작되면 이 프로그램을 모든 사용자들이 한번은 자신들의 PC에 설치한다.  그리고 이 프로그램은 PC가 부팅될 때 자동 실행되도록 시작 프로그램으로 등록한다. 이 프로그램에서는 그림처럼 필요한 체크를 한 후 만약 설정이 되지 않은 부분이 있다면 서버에 요청을 해서 자동으로 세팅하도록 할 수 있다. 

■ 다른 방법?

기타 다른 방법도 있을 것이다.

Posted by dalbong2

http://support.microsoft.com/default.aspx?scid=kb;en-us;313892

이 문서는 IE가 어셈블리를 로딩하는 과정에서 발생하는 에러를 로컬 PC에 로깅할 수 있는 방법을 보여주고 있다. 이 방법은 달봉이가 디버깅 툴을 소개하면서 함께 설명한 적이 있다.
이 문서에는 더불어서 IE에서 어셈블리가 로딩되는 과정을 설명하고 있는데, 이것이 더 중요한 정보이다.
MIME 필터가 들어오는 스트림의 MIE 타입을 모리터링하다가 .NET 어셈블리라고 판단되면 IEHost.dll을 로딩하고 요청 인스턴스를 생성하게 된다는 내용이 있다.

http://msdn2.microsoft.com/en-us/library/ms775147.aspx

이 문서는 IE 브라우저가 MIME 타입을 결정하고 MIME 타입을 핸들링하는 것에 대해서 설명하고 있다.

Posted by dalbong2
IE 임베딩 방식에서의 애플리케이션 도메인의 중복 생성 ??

현재 달봉이가 참여하고 있는 L 기업의 프로젝트에서는 Plumtree라는 EP 솔루션이 들어오기로 되어 있다. 시스템의 메뉴와 권한 관리는 이 EP 솔루션이 담당하고 업무 화면은 스마트클라이언트로 구현하겠다는 것이 대세가 되고 있는 분위기이다.

서버측 기술인 Plumtree 솔루션과 클라이언트측 기술인 스마트클라이언트를 같이 사용한다는 것 자체가 달갑지는 않다. 그러나 도입되는 솔루션과 기술을 결정하는 것은 다분히 기업들간의 영업적 성격이 강한 문제들이다. 이런 상황을 겪고 있자면 가끔 그런 유머가 생각난다. 펜대만 돌리는 경영자님께서 그랬단다. "왜 우리 회사는 오라클을 버리고 자바를 도입하지 않는거야? 통합성도 좋다는데!!"

하여튼 그런 식으로 간다면 가는 것이다. 결국 웹 페이지 하나에 스마트클라이언트 스마트클라이언트 컨트롤 하나씩을 임베딩 시키는 방식이 된다. 즉 최소한 메뉴 수만큼의 임베딩 페이지가 존재하게 되는 것이다.

그런데 어느 순간 달봉이의 뇌리를 스치는 question mark가 있었다. 끔찍한 생각이었다. 우선 그 끔찍한 의문이 나오기까지의 근거가 된 달봉이의 KB를 말한다.


■ 달봉이의 KB(Knowledge Base)

스마크클라이언트 컨트롤을 로딩하기 위한 <object> 태그의 형식은 다들 알고 있을 것이다

<OBJECT id="SmartClientControl"
classid="어셈블리명(경로포함)#네임스페이스를 포함한 클래스명" >

스마트클라이언트 컨트롤을 가지고 있는 웹 페이지가 호출되면 클라이언트측에서는 하나의 "애플리케이션 도메인(application domain)"이 생성된다는 것은 이야기 한적이 있다( 없나? ). 애플리케이션 도메인이 생성되고 나서 <object> 태그에서 지정한 어셈블리를 그곳으로 로딩시킨다. 그리고 나서 마지막으로 컨트롤의 인스턴스를 생성하는 것이다.

그리고 IE 임베딩 방식의 스마트클라이언트 애플리케이션에서는 생성된 애플리케이션 도메인은 IE 브라우저가 닫히기 전에는 언로드 되지 않는다. 즉 IE 프로세스가 끝나야 그곳에 포함된 애플리케이션 도메인도 없어진다.

또 한가지, 하나의 OS 프로세스에는 여러 개의 .NET 애플리케이션 도메인이 생성될 수 있다. 애플리케이션 도메인들은 각각 독립된 공간으로서 다른 도메인의 어셈블리에는 접근할 수 없다(다른 도메인의 어셈블리에 접근하는 것이 조치를 취하면 불가능한 것은 아니지만 이것이 기본적인 내용이다).

달봉이가 가지고 있는 KB를 좀 더 자세히 보고 싶다면 지난번 애플리케이션 도메인과 베이스 디렉토리(http://www.dalbong2.net/18)에 대해 올린 포스트를 읽어 보기 바란다.

Question

그렇다면 <object> 태그가 호출될때마다 애플리케이션 도메인이 생성되면 어떻게 되나? 그럼 동일한 메뉴를 클릭할 때마다 다른 애플리케이션 도메인이 생성되고 그리고 이미 동일한 어셈블리가 다른 도메인에 로딩되어 있는데도 다시 새로운 도메인으로 로딩되는 것은 아닐까? 그럼 메모리 사용량은? 순간 끔찍했다. 즐거운 토요일이었으나 바로 테스트를 해 봐야 했다.
다음은 간단한 테스트 내용이다. 정확히 하려면 애플리케이션이 생성될때 마다 로그를 남겨서 확인하는 방법을 택할 수도 있었지만 밖에서는 식구들이 쇼핑을 갈 준비를 하고 있었다. 애플리케이션 도메인 생성시 참여할 수 있는 방법에 대해서는 다음 포스트에서 다룰 생각이다.

테스트 1

1223161795

테스트 #1

 

두개의 프레임으로 구성되어 있다. 좌측에는 두 개의 메뉴가 있다. 메뉴 1을 클릭해서 winform1.aspx를 호출했다. 이 aspx 페이지에는 그림처럼 두 개의 컨트롤이 각각의 <object> 태그로 로딩되어 있다. 그리고 위쪽 컨트롤의 "Set Value" 버튼을 클릭하면 다음과 같은 코드가 수행된다.

private void button1_Click(object sender, System.EventArgs e)
{
AppDomain.CurrentDomain.SetData("test", "value from domain1");
}

그리고 아래 컨트롤의 "Get Value" 버튼의 클릭 핸들러는 다음과 같다.

private void button1_Click(object sender, System.EventArgs e)
{
object o =AppDomain.CurrentDomain.GetData("test" );
if( o != null )
MessageBox.Show( o.ToString() );
else
MessageBox.Show( "원하는 값이 없습니다.");
}

"Set Value" 버튼을 클릭하면 CurrentDomain에 "test"라는 이름의 속성에 문자열 "value from domain1"이 값으로 저장된다. "Get Value" 버튼을 클릭하면 CurrentDomain의 "test" 속성값을 가져와서 출력한다. 만약 두 개의 컨트롤이 다른 애플리케이션 도메인에서 생성되어 있는 상황이라면 "Get Value" 버튼 핸들러가 제대로 작동하지 않아야 한다. "Set Value"버튼을 클릭했다. 그러나 제대로 출력되었다.

1124504831

테스트결과#1

그렇다면 두 개의 <object> 태그로 어셈블리를 로딩해도 같은 도메인으로 로딩된 것이다.

메뉴 2를 클릭해서 winform2.aspx를 호출했다. 출력된 컨트롤의 "Get Value" 버튼을 클릭해도 도메인 저장소에서 값을 구할 수 있었다. 달봉이는 여기서 잠시 고민을 했다.

클라이언트측의 .NET 프레임워크에서는 어떻게 새로운 애플리케이션 도메인을 생성할지 말지를 결정하는 것일까? 조금 후 애플리케이션의 베이스 디렉토리값이 도메인의 고유 아이디로 사용될 수 있을 것이라는 추측을 하게 되었다. 즉 aspx에 설정된 베이스 디렉토리가 다르다면 어떻게 될까라는 생각을 하게 되었다. 추측대로라면 에러가 발생해야 한다. 테스트를 다시 수행했다.

테스트 2

1224001611

테스트 #2

메뉴 2에 연결된 winform2.aspx에서는 베이스 디렉토리 값을 달리 줬다.

winform1.aspx 에서는
<link
rel=Configuration
href="http://localhost/AppDomainTestWeb/base1/controls.xml"/>

winform2.aspx 에서는
<link
rel=Configuration
href="http://localhost/AppDomainTestWeb/base2/controls.xml"/>

그런 다음 winform1.aspx의 "Set Value" 버튼을 클릭해서 문자열을 도메인의 공용 저장소에 저장하고 그리고 다시 winform2.aspx에서 "Get Value" 버튼을 이용해서 해당 이름의 값을 구하려고 했다. 그런 값을 찾을 수 없다는 에러였다.

 1107500897

테스트결과 #2


이 결과를 다시 한번 더 확인하기 위해서 IEHost.dll이 남긴 로그를 검토해봤다. IEHost.dll가 로그를 남기도록 하는 설정은 디버깅 툴(http://www.dalbong2.net/14)을 소개하면서 같이 설명했다.

다음은 winform1.aspx를 호출하면서 남긴 로그의 일부분이다. 먼저 WindowsControlLibrary1.UserControl1을 생성하면서 남긴 로그이다.

Microsoft.IE.SecureFactory:
Locating domain for http://localhost/AppDomainTestWeb/base1/
Microsoft.IE.IDKey: Created key
Microsoft.IE.Manager: The domain does not exist.
...
Microsoft.IE.SecureFactory:
Application base: http://localhost/AppDomainTestWeb/base1/
...
Microsoft.IE.SecureFactory:
Trying to create instance of type
http://localhost/AppDomainTestWeb/WindowsControlLibrary1.DLL
#WindowsControlLibrary1.UserControl1

베이스 디렉토리값 "http://localhost/AppDomainTestWeb/base1/" 에 해당하는 도메인을 찾을 수 없어 도메인을 생성한다는 내용이 있다.

다음은 WindowsControlLibrary2.UserControl1 컨트롤을 생성하면서 남긴 로그의 부분이다.

Microsoft.IE.SecureFactory:
Locating domain for http://localhost/AppDomainTestWeb/base1/
...
Microsoft.IE.SecureFactory: Do not have to create new domain

"http://localhost/AppDomainTestWeb/base1/" 에 해당하는 도메인이 이미 생성되어 있다는 내용이다.

다음은 베이스 디렉토리를 다르게 설정한 winform2.aspx에서 WindowsControlLibrary2.UserControl1를 생성하면서 남긴 로그의 부분이다.

Microsoft.IE.SecureFactory:
Locating domain for http://localhost/AppDomainTestWeb/base2/
Microsoft.IE.IDKey: Created key
Microsoft.IE.Manager: The domain does not exist.
Microsoft.IE.IDKey: Created key
Microsoft.IE.Manager: The domain does not exist.

베이스 디렉토리 값 "http://localhost/AppDomainTestWeb/base2/"에 해당하는 도메인이 없기에 새롭게 생성한다는 내용이다.

테스트 결과

정리하면 모든 웹 페이지의 베이스 디렉토리 설정만 같다면 웹 페이지 하나에 컨트롤 하나를 로딩시키는 구조로 가더라도 메뉴를 클릭할때 마다 애플리케이션 도메인을 생성하고 어셈블리를 도메인별로 중복해서 로딩시킬 일은 없을 것이다. 다행한 일이다.

그러나 이렇게 메뉴가 웹 페이지에 있는 구조가 마음에 든다는 것은 아니다. 이런 구조는 스마트클라이언트 컨트롤에서 웹 페이지를 제어하는 일 예를 들어 특정 메뉴를 디자인적으로 토글시킨다든가 하는 일은 여전히 힘들다.

하여튼 달봉이는 느긋한 마음으로 식구들과 함께 쇼핑을 하러 갈 수 있었다. 므흣~

Posted by dalbong2
지난 포스트에서, 웹 페이지 하나에 스마트클라이언트 컨트롤 하나씩을 로딩하는 IE 임베딩 방식의 구조에서도 애플리케이션 도메인은 하나만 생성된다고 했었다. 단 모든 웹 페이지에 설정된 베이스 디렉토리 값은 같아야 한다는 것이다.

모든 웹 페이지의 스마트클라이언트 컨트롤들이 동일한 애플리케이션 도메인으로 로딩된다는 것은 도메인의 속성을 모든 컨트롤에서 공유할 수 있다는 의미이다. 이 속성을 공유 저장소로 이용하면 페이지에서 페이지로의 데이터 전달에 사용할 수 있다는 것이다. 이미 지난 포스트에서 테스트를 하면서도 이 방식을 사용하고 있었다.  
Posted by dalbong2

1. 문제 제기

NTD타입의 스마트클라이언트 애플리케이션을 개발하면서, 서버로 어셈블리를 요청하는 과정을 모니터링하다 보면 다음과 같은 화면을 볼 수 있다.

리소스 어셈블리반복 요청

어셈블리를 하나 요청하면 그것과 관련해서 추가적인 요청이 여러번 반복된다. 이런 추가적인 요청이 로컬 PC에서만 일어난다면 모를까 원격에 있는 서버에 어셈블리를 요청할때마다 반복적으로 일어난다면 성능에 영향을 끼칠 수 있다. 이번에는 이런 현상이 왜 일어나는지 알아보고 방지하거나 최적화하는 방안에 대해 생각해 볼 것이다. 이런 현상은 .NET이 리소스를 관리하는 방식때문인데, 리소스 관리는 애플리케이션의 Globalizaion&Localization과 관련된 주제다.

2. Globalization(전역화) & Localization(지역화)

전역화(Globalization)는 여러 컬쳐(언어와 지역)에 있는 사용자를 위해 지역화된 사용자 인터페이스와 국가별 데이터를 지원하는 응용 프로그램을 디자인하고 개발하는 과정이다. 즉 동일한 애플리케이션이 영어권에 가서는 영어로, 프랑스어권에 가서는 프랑스어로 한국에서는 한국어로 UI가 출력되도록 한다는 것이다. 애플리케이션 자체를 영어, 프랑스어, 한국어 세가지로 개발하는 것이 아니라 응용프로그램 자체는 동일하고 UI에 출력되는 이미지나 문자열(라벨 컨트롤에 출력되는 텍스트를 생각한다) 그리고 날짜, 시간, 통화 같은 데이터의 포맷만 각 지역과 언어에 맞게만 변경해서 출력해 준다.
지역화(Localization)은 응용 프로그램 리소스를 설정된 현재의 컬쳐에 맞는 지역화된 버전으로 번역하는 과정이다. 예를 들어 현재의 컬쳐가 한국이라면 애플리케이션의 로고 이미지도 한글로 된 것으로 출력하고 조회 버튼의 텍스트도 "조회"출력한다. 또한 날짜도 yyyy/mm/dd" 포맷으로 변경하여 출력한다. 이것이 애플리케이션의 지역화이다.

애플리케이션의 지역화를 고려해서 개발자가 모든 컬쳐를 고려하는 식의 코딩을 해야 하는 것은 아니다.

if(영어권)
Logo = 영어로고이미지
else if(한국)
Logo = 한글로고이미지
...

개발자는 코딩은 동일하게 해도 된다. 다만 컬쳐에 맞는 UI 디자인이 변경될뿐이다. 코드상에서는 다음과 같이만 하면 된다.

Logo = 로고이미지

.NET의 리소스관리자는 이런 코드를 만나면 우선 현재 설정된 컬쳐를 보게 되고 그 컬쳐에 맞는 검색 경로에서 이미지를 검색해와서 출력해준다. 개발자는 여러 컬쳐에 맞는 이미지만 준비해서 리소스 관리자가 찾을 수 있는 위치에 두면된다.

3. 컬쳐 표현

컬쳐를 표현하는 방식은 다음과 같다.

언어코드[-국가(지역)코드]

예를 들면, "en-US"는 U.S 영어 컬쳐를 "en-AU"는 오스트레일이아의 영어 컬쳐를 말한다. 코드는 보통 2글자이다. 디폴트로 애플리케이션의 현재 컬쳐는 사용자 PC에 설정된 것을 따른다.
만약 프로그램상에서 특정 컬쳐에 대한 정보를 를 변경하고 싶다면 CultureInfo 인스턴스를 생성할 필요가 있다.

Thread.CurrentThread.CurrentUICulture = new CultureInfo("ko-KR");
Thread.CurrentThread.CurrentCulture = new CultureInfo("ko-KR");

첫번째 코드의 CurrentUICulture 속성은, 애플리케이션의 UI를 특정 컬쳐에 맞는 이미지를 출력하려고 할때 사용하는 설정이다. 두번째 코드의 CurrentCulture는 시간, 날짜, 통화를 특정 컬쳐에 맞게 출력하고 싶을때 사용하는 설정이다. 여기서는 리소스 관련 컬쳐 설정이 주제이므로 첫번째 설정과 관련이 있다.

4. .NET의 리소스 관리 모델

애플리케이션이 개발이 완료되고 배포될때,  설계, 개발시에는 예상치 못했던 특정 컬쳐에서도 서비스가 제공되어야 하는 경우가 있을 수 있다. 그렇다고 코딩을 다시 해서 재컴파일하는 작업을 할 수는 없는 일이다. 이런 경우 .NET에서 제공하는 리소스 관리 모델을 사용하면 쉽게 특정 컬쳐에 해당하는 리소스만 추가함으로써 문제를 해결할 수 있다. 이런 모델이 적용되려면 처음부터 지역화를 염두에 두고 비즈니스 로직을 담은 어셈블리와 리소스만 가지고 있는 어셈블리를 분리해서 개발해야 한다. .NET의 리소스 관리자는 현재 설정된 컬쳐에 해당하는 리소스 어셈블리를 찾아서 사용하게 된다. 이런 리소스 어셈블리를 위성 어셈블리(satellite assembly)라고 하는데, 코드는 없고 특정 컬쳐와 관련된 문자열이나 이미지 같은 리소스만 있다.

위성 어셈블리는 이름이 주는 느낌 그대로의 역할을 한다. 혼자서는 다른 로직을 수행할 수 없고 어떤 기본 어셈블리[각주:1]에서 필요로하는 자원을 공급해주는 역할을 한다. 이런 모델을 허브-스포크(hub-spoke) 모델이라고 한다.

허브-스포크 모델

그림에서 메인 어셈블리에는 주로 개발자가 작성하는 코드가 있다. 그리고 메인 어셈블리에도 리소스가 포함될 수 있는데, 컬쳐와는 상관없는 리소스들이다. 이 메인 어셈블리는 설정된 컬쳐의 위성 어셈블리가 없는 경우 최종적으로 리소스를 얻기위해서 소스를 검색하는 어셈블리로서 기본 리소스(default resources)라고 한다. 즉 "kr-KR"의 컬쳐가 적용되고 있는 애플리케이션이 실행되고 있을때, 리소스 관리자가 지정된 이미지를 찾기 위해서 "ko-KR" 컬쳐 관련 어셈블리를 지정된 위치에서 찾았지만 그 어셈블리를 발견하지 못했다면 또는 어셈블리는 찾았는데 원하는 이미지를 가지고 있지 않다면 최종적으로 메인 어셈블리에서 그 이미지를 검색한다.

앞에서 말한대로 이런 허브-스포크 모델을 사용하면 애플리케이션의 개발이 완료되고 배포까지 하고 나서도 새로운 컬쳐의 리소스가 필요하다면 그 컬쳐의 위성 어셈블리만 새로 만들어서 메인 어셈블리의 재컴파일없이도 배포할 수 있다는 것이다. 애플리케이션은 현재의 컬쳐에 적당한 위성 어셈블리만 로딩 또는 다운로딩해서 사용할 수 있다. 그러나 모든 컬쳐의 위성 어셈블리를 테스트하기위해서는 컬쳐별로 테스트환경을 구성해야 한다는 단점도 있다.

위성 어셈블리의 분리

허브-스포크 모델을 다시 그려보면 그림처럼 될 수 있다. 만약 ko-KR 관련 리소스만 있는 어셈블리, en-US 관련 리소스만 있는 어셈블리를 따로 만들어두고 컬쳐명과 동일한 이름의 디렉토리밑에 위성 어셈블리를 배포해두면 CLR이 필요한 어셈블리를 찾아서 로딩한다. 어셈블리를 두면 우리나라에서 애플리케이션을 실행시킬때와 미국에서 애플리케이션을 실행시킬때를 구분해서 해당 리소스 어셈블리를 선택적으로 사용할 수 있다는 것이다.

5. 특정 컬쳐의 리소스(Culture-specific resource) 관리

이제 Visual Studio.NET를 이용해서 특정 컬쳐의 위성 어셈블리를 만드는 방법을 알아본다. Visual Studio.NET의 프로젝트에서 윈폼을 하나 추가하면 기본적으로 하나의 Form1.resx 이라는 파일도 같이 추가된다.

기본 .resx 파일

이 .resx 파일에는 디자인 모드에서 폼에 추가되는 모든 자원에 대한 정보와 데이터가 기록된다. 이 파일은 XML 형식으로 되어 있으며 notepad.exe같은 텍스트 편집기로 열어보면 모든 기록이 텍스트로 되어 있는 것을 볼 수 있다. 이미지 같은 객체들의 내용은 바이너리 텍스트로 되어 있다.


<data name="button1.Locked"
type="System.Boolean, mscorlib,
Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
  <value>False</value>
</data>
<data name="button1.DefaultModifiers"
type="System.CodeDom.MemberAttributes, …">
  <value>Private</value>
</data>
<data name="button1.BackgroundImage"
type="System.Drawing.Bitmap, System.Drawing,
Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a"
mimetype="application/x-microsoft.net.object.bytearray.base64">
  <value>
       Qk1CBQAAAAAAADYAAAAoAAAAFgAAABMAAAABABgAAAAAAA
AAAADEDgAAxA4AAAAAAAAAAAAA/f39t7e3…
  </value>
  </data>

Form1.resx 파일은 컴파일을 하게 되면 메인 어셈블리(뒤에 설명한다. 지금은 우리가 코드를 작성해서 빌드하면 생성되는 보통의 어셈블리라고 생각하자)에 포함되는 기본 리소스를 가지고 있다. 폼에 기본 컬쳐(특정 컬쳐의 리소스를 찾을 수 없을 경우 사용하는 컬쳐를 말한다. 뒤에서 다시 설명한다)에 해당하는 UI 디자인을 한다.

기본 컬쳐의 윈폼

이제 ko-KR 컬쳐를 위한 UI를 만들어보자.

특정 컬쳐 리소스 추가

Form1 속성창에서 다음처럼 설정을 변경한다.

Localized->True
Language->한국어(대한민국)

이렇게 선택하면 오른쪽 그림처럼 ko-KR 컬쳐를 위한 리소스 정보가 저장될 .resx파일이 생성된다. 이때 국가(지역)코드가 없고 언어 코드만 있는 리소스 파일도 함께 생성된다. "한국어(대한민국)"을 선택한 다음부터의 디자인 정보는 이제 Form1.ko.resx,

Form1.ko-KR.resx 파일에 저장된다. 만약 "한국어(대한민국)" 대신 "한국어"를 선택했다면 ko에 해당하는 resx 파일만 생성한다.

ko-KR 컬쳐의 윈폼

이제 프로젝트를 빌드하면 다음과 같은 컬쳐별로 디렉토리가 생성되고 어셈블리가 하나씩 들어가 있는데 이것이 .resx 파일에 들어있던 데이터를 가지고 있는 위성 어셈블리이다.

메인 어셈블리, 위성 어셈블리

ResourceProject.exe에는 기본 컬쳐를 위한 Form1.resx 리소스가 임베딩되어 있다. 그리고 ko, ko-KR 폴더가 생성되어 있고 각각의 폴더에는 ResourceProject.resources.dll이 하나씩 들어있는데, 각 컬쳐에 해당하는 리소스가 들어있는 어셈블리이다.

여기에 폼 Form2를 하나더 추가해서 ko 컬쳐를 선택하면 Form2.ko.resx, Form2.ko.resx 파일이 생성된다.

지역화된 폼 추가

이것을 빌드하면 debug/ko/ResourceProject.resources.dll 및 debug/ResourceProject.exe 새로운 리소스가 추가된다. 위성 어셈블리는 컬쳐별로 관리되는 반면에 리소스 자체는 네임스페이스별로 관리된다. 만들어진 위성 어셈블리 파일명을 이루는 ResourceProject는 리소스가 사용된 클래스의 네임스페이스이다.

이제 애플리케이션 개발이 완료되고 이미 배포까지 마친 상태에서 기존의 폼에 새로운 컬쳐의 UI를 보여줄 필요가 있다면 해당 컬쳐의 .resx파일을 추가하고 빌드해서 기존 위성 어셈블리에 리소스를 추가한 다음 배포 디렉토리에 해당 컬쳐명과 동일한 폴더를 만든 후 그곳에 넣어두면 된다. 리소스 관리자는 지역화된 윈폼이 호출되면 우선 해당 위성 어셈블리를 로딩하고 필요한 리스소를 사용하게 될것이다.

이제 리소스 관리자(Resource manager)가 어떻게 이미지를 출력하기 위해 위성 어셈블리를 검색하는지 그 메커니즘을 알아보도록 하자.

6. 위성 어셈블리 반복 요청 배경

현재 컬쳐에서 사용할 수 있는 리소스는 복수개의 위성 어셈블리에 있을 수 있다. ko-KR 컬쳐에서 실행되는 애플리케이션은 하나의 리소스(버튼의 Text 속성 값)가 ko-KR 위성 어셈블리, ko 위성 어셈블리 그리고 메인 어셈블리에 모두 있을 수 있다. 이런 경우 리소스 관리자는 가장 구체적인 컬쳐의 위성 어셈블리부터 검색한다. ko-KR 위성 어셈블리에서 원하는 리소스를 검색하고 찾지 못하면 다시 ko 위성 어셈블리에서 찾는다. 거기서도 찾지 못하면 마지막으로 컬쳐 중립적인 기본 리소스를 검색한다.

리소스 관리자는 ResourceManager라는 클래스로 구현되는데, 리소스 관리자는 애플리케이션이 실행되는 현재 쓰레드의 CurrentUICulture 속성값을 기준으로 해서 리소스의 위성 어셈블리를 검색한다. 다음 예제 코드는 메인 어셈블리 LocalizedAssem.dll가 "ko-KR" 컬쳐의 위성 어셈블리를 검색하는 예이다.

AppBase\ko-KR\LocalizedAssem.resources.dll
AppBase\ko-KR\LocalizedAssem.resources\ LocalizedAssem.dll
AppBase\ko-KR\privatePath1\ LocalizedAssem.resources.dll
AppBase\ko-KR\privatePath1\ LocalizedAssem.resources \LocalizedAssem.resources.dll
AppBase\ko-KR\privatePath2\ LocalizedAssem.resources.dll
AppBase\ko-KR\privatePath2\ LocalizedAssem.resources\ LocalizedAssem.resources.dll

.exe 확장자를 갖는 어셈블리에 대해서 동일한 검색이 이뤄진다.

리소스가 사용된 어셈블리의 기본 네임스페이스(LocalizedAssem)를 근거로 위성어셈블리 파일명을 만들고 몇 단계의 검색을 거친다. 각 단계에서 적절한 리소스가 발견되면 그것을 사용하고 바로 검색을 멈춘다. 그렇지 않으면 다음 단계를 계속 진행한다. 리소스 관리자는 현재 첫번째는 현재 컬쳐명과 동일한 이름을 갖는 디렉토리에서 리소스 파일을 찾는다. 거기서 못찾게되면 두번째 줄처럼 리소스 파일이름과 동일한 이름의 디렉토리에서 찾는다. 계속 ~

여러분이 애플리케이션을 직접 제작한 경우 실제 검색 경로는 상황에 따라서 앞에서와는 다를 수 있다. 설정 파일 .config이 없는 경우는 Appbase 밑의 bin 폴더를 검색하는 경로가 출력될 수 있다. 또는 지역 코드가 없는 경우는 다른 검색 경로가 나올 지 모른다.

여기서 AppBase는 애플리케이션의 베이스 디렉토리[각주:2]값이다. ko-KR 하위 폴더를 검색하고 다시 ko 하위폴더를 검색하게 되는데, 검색할 수 있는 모든 경로를 찾지 못한다면 결국 메인 어셈블리의 기본 리소스를 참조하게 된다. 여기서도 찾지 못하면 어쩔 수 없이 이미지는 출력되지 못한다.

이런 식의 리소스 검색은 어떤 경우는 성능상의 문제가 될 수 있다. 특히 애플리케이션의 베이스 디렉토리가 원격 서버상의 경로(http://server/~~)로 설정되는 스마트클라이언트 애플리케이션의 경우는 메인 어셈블리를 요청할때마다 그 위성 어셈블리를 찾기 위해서 원격 서버로의 왕복이 계속해서 일어나게 된다. 따라서 위성 어셈블리에 대한 반복 요청을 최소화할 수 있는 방안이 필요하다.

7. 위성 어셈블리 반복 요청의 최적화 방안

이런 반복적인 요청을 어떻게 처리할 것인지에 대한 3가지의 방안이 있을 수 있다.

1) 아무 조치도 취하지 않는다.

위성어셈블리 반복 요청은 .NET 프레임워크의 의도된 거동이다. 한만디로 그 무시 무시한 "By design"이라는 말이다. 애플리케이션이 제 기능을 발휘하는 데는 아무 문제가 없다. 다만 성능이 안 좋게 나올뿐이다.

2). 사용자 정의 HTTP핸들러를 제작한다.

원격 서버에 DLL 파일 요청을 처리하는 사용자 정의 HTTP핸들러를 만든다. 핸들러에서는 리소스 파일에 대한 요청임을 확인하면 예를 들어 .resources.dll 끝나면 파일에 대한 요청인 경우는 0바이트의 더미 파일을 내려보냄으로써 더이상의 HTTP 요청이 올라오지 않도록 한다. 이 방안은 각 메인 어셈블리별로 리소스 파일에 대한 최소한 1번의 요청은 올라오게 된다.
이 방안의 단점이라면 추가적인 IIS 세팅과 web.config 파일 설정 그리고 HTTP핸들러이 구현된 어셈블리를 관리해야 한다는 것이다.

3) 애플리케이션의 컬쳐 정보 및 메인 어셈블리의 컬쳐 정보를 조정한다.

메인 어셈블리는 비록 컬쳐와는 상관없는 코드와 리소스만 있지만, 특정 컬쳐로 표시하게 되면 리소스 관리자는 이 설정에 따라 불필요한 어셈블리 검색을 하지 않게 된다.  이때 사용하는 어트리뷰트가 NeutralResourcesLanguageAttribute이다.

using System.Resources;
[assembly: NeutralResourcesLanguageAttribute("ko-KR")]

이 코드는 메인 어셈블리를 컬쳐 ko-KR로 표시하고 있다. 만약 현재 애플리케이션이 실행되는 쓰레드의 CurrentUICulture가 ko-KR로 메인 어셈블리의 설정된 컬쳐와 동일하다면 리소스 검색없이 바로 바로 메인 어셈블리의 리소스를 사용하게 된다.

using System.Resources;
[assembly: NeutralResourcesLanguageAttribute("ko")]

이렇게 국가(지역) 코드가 없는 컬쳐로 메인 어셈블리를 표시하면, 현재 애플리케이션이 실행되는 쓰레드의 CurrentUICulture가 "ko-KR"처럼 특정 지역이 표시된 경우만 디렉토리 검색을 하게 되고 국가 "ko"경우는 검색을 멈추게 된다.

현재 애플리케이션이 실행되는 쓰레드의 CurrentUICulture를 아예 다음처럼 없애버리는 방법도 있다.

Thread.CurrentThread.CurrentUICulture = new CultureInfo("");

이렇게 해 버리면 리소스 관리자의 위성 어셈블리 검색에 대한 시도가 아예 원천 봉쇄되버린다. 검색에 사용할 기준 경로를 만들어 낸 수가 없는 것이다.

4) 장단점

어셈블리나 애플리케이션의 어셈블리의 컬쳐를 조정하는 방법은 간단하긴 하지만 Globalization관점에서는 단점을 가진다. 특정 컬쳐로 고정되거나 또는 아예 컬쳐 지원 메커니즘을 사용할 수 없게 되어 버린다.

반면에 HTTP 핸들러를 사용하면 나중에 해외 지사를 확장한다든지 했을때 좀더 유연하게 대처할 수 있는 여지가 있다. 그러나 앞에서 말한 것처럼 관리와 설정에 대한 번거로움이 있다.

8. 결론

위성 어셈블리에 대한 반복적인 요청은 스마트클라이언트 애플리케이션에서는 성능에 큰 영향을 줄 수 있다. 정확히 말하면 애플리케이션의 베이스 클래스가 원격 서버상의 경로를 가리키는 경우에 문제가 된다. 따라서 ClickOnce는 문제가 되지 않는다. NTD와 혼합형(ClickOnce+NTD)의 배포 방식을 이용하는 애플리케이션에서 문제가 될 수 있다. 이런 경우 상황에 맞는 적절한 방안을 적용해야 한다.

  1. 메인어셈블리라고도 불린다 [본문으로]
  2. 스마트클라이언트 애플리케이션에서 애플리케이션 도메인(application domain)과 베이스 디렉토리(application base directory)에 대한 이해는 매우 중요하다. 이것에 대한 포스트는 곧 올리도록 할 것이다. [본문으로]
Posted by dalbong2