본문 바로가기

IT 살이/04. 기술 - 프로그래밍

NTD 배포 및 어셈블리 로딩 그리고 IIS 설정

어셈블리 로딩과정을 알고 싶다면 바로 아래 링크를 참조한다.

2015/10/15 - [04.기술-APP/.NET InDepth] - 어셈블리 바인딩(최종)




현재 참여하고 있는 프로젝트는 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핸들러를 제거하는 장면이다.

DLL 요청 매핑 제거

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

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 필터가 등록되어 있어야 한다는 것도 알고 있었다.

클라이언트 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를 이용해서 어셈블리 배포가 이뤄지기 때문이다.


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 디렉토리 구조를 보여주고 있다.

업무 어셈블리의 만료 정책


현재 스마트클라이언트 시스템에서는 업무팀의 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



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를 실행하면 된다. [본문으로]