SimpleIsBest.NET

유경상의 닷넷 블로그
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.

스마트 클라이언트 시리즈 아홉 번째 글입니다. 이 시리즈 시작한 지가 어언 9개월이 다 되어 가네요. 2-3 달 안에 끝내려고 했는데 어찌 어찌 하다 보니 쥘쥘쥘... 마무리는 아직도 먼데... 아흐...


시리즈 목차

Smart Client (IX) : Assembly Deployment (2)

지난 포스트에서는 스마트 클라이언트에서 OBJECT 태그 혹은 링크에 의해 최초의 어셈블리가 어떻게 다운로드 되는가에 대해 살펴보았다. 간략히 정리해 보자면, 스마트 클라이언트 시나리오에서 최초로 로드 되는 어셈블리는 브라우저 캐시에 의해 다운로드 여부가 결정되며, 서버 상에 존재하는 DLL/EXE의 날짜와 브라우저 캐시 상에 존재하는 DLL/EXE의 날짜를 비교하여 새(?) 파일이 다운로드 되거나 브라우저 캐시 상의 어셈블리가 사용되게 된다.

이번 포스트에 알아 볼 내용은 이렇다. 스마트 클라이언트의 첫 번째 어셈블리가 다운로드 되었건 브라우저 캐시의 어셈블리가 사용되었건, 메모리에 로드 된 스마트 클라이언트 어셈블리가 참조하는 다른 어셈블리들은 어떻게 될 것인가에 대한 것이다. 필자가 이렇게 이야기 한다는 것은 벌써 '첫 번째' 어셈블리와 다르기 때문이겠지?

지난 포스트에서도 언급한 바와 같이, 스마트 클라이언트의 최초의 어셈블리 만이 날짜에 의해 다운로드 여부가 결정되고 그 이후에는 닷넷의 기본 어셈블리 바인딩 규칙을 사용하게 된다. 따라서 독자들은 MSDN에 아주 잘 설명된 다음 글을 읽어 보면 되겠다.

이 글에서 설명하는 것은 ASP.NET을 비롯한 모든 닷넷 어플리케이션에 적용되는 규칙이다. 스마트 클라이언트도 닷넷 어플리케이션이므로 당연히 이 규칙에 적용된다. 따라서 이 글을 잘 읽어 보면 스마트 클라이언트 시나리오에서 어셈블리가 로드 되는 규칙을 알 수 있을 것이다. 이상...

Assembly Reference Basic

요렇게만 끝나면 독자들의 불만이 하늘을 찌를 듯 할 것이니... 부연 설명을 조금(?)만 하겠다. 닷넷 런타임이 어셈블리를 바인딩 하는 규칙을 이해하기 위해서 먼저 어셈블리를 참조하는 것에서 몇 가지 알아 두어야 할 사항이 있다. 첫 번째는 전체 참조(full reference)와 부분 참조(partial reference) 란 것이다. 전체 참조는 어셈블리의 이름, 버전, culture, public key token 을 모두 명시하여 참조하는 것을 말하며 부분 참조는 이들 중 일부만을 사용하여 참조하는 것을 말한다. 다음은 전체 참조와 부분 참조의 예시 이다.

  • Full reference example
    • StrongAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
    • WeakAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
  • Partial reference example
    • MyAssembly
    • MyAssembly, Version=1.0.0.0
    • MyAssembly, Version=1.0.0.0, Culture=neutral
    • MyAssembly, PublicKeyToken=b77a5c561934e089

여기서 주의할 점은 강력한 이름이 없는 어셈블리 일지라도 전체 참조를 사용할 수 있으며, 강력한 이름이 있더라도 부분 참조가 가능하다는 점이다. 다시 말해 전체 참조/부분 참조는 어셈블리를 참조하는 방법으로서 사용하는 것이지 어셈블리에 강력한 이름이 있는지 없는지는 상관이 없다는 것이다. 요것에 주의를 하자.

두 번째로 알아두어야 할 사항은 정적 참조와 동적 참조이다. 정적 참조는 우리가 컴파일러에서 /r 옵션으로 참조를 추가하거나 Visual Studio 에서 일반적으로 수행하는 '참조'를 수행하면 생성되는 참조를 말한다. 좀 더 짜증나게 설명하자면 정적 참조는 어셈블리가 정적으로 참조하는 어셈블리에 대한 참조로서 의 매니페스트(manifest)에 기록되는 참조를 말한다. -_-; 쉽게 말하자면... 독자들이 Visual Studio에서 수행하는 대부분의 참조가 정적 참조가 되겠다. 동적 참조는 코드에 의해 어셈블리를 참조하는 것을 말하며 System.Reflection.Assembly.Load 와 같은 메쏘드 호출에 의해 어셈블리를 참조할 때 동적 참조라고 한다.

How the Runtime Locates Assembly

개략적인 기초를 닦았으니 닷넷 런타임(CLR)이 어떻게 어셈블리를 찾아 로드 하는지 살펴보자. 이미 관련 문서를 제시했으니 상세한 내용은 꼭 읽어 보기 바란다. 이렇게 신신 당부를 했음에도 불구하고 눈치 없이 질문하는 독자들이 꼭 있기 마련이다. 이런 독자들은 잽싸게 화장실에 들어가 숏 잡고 반성하기 바란다. 지금 설명하는 부분은 이미 스마트 클라이언트, 그것을 알려주마 (IV) : App Base Directory & Configuration 글에서 한번 언급한 내용이다.

1. Examining the Configuration Files

닷넷 런타임이 어셈블리를 로드 해야 할 시기가 오면(그것이 어셈블리에 기록된 정적 참조이건 Load 메쏘들 호출에 의한 동적 참조이건 상관없이) Configuration 파일을 확인하여 로드 할 어셈블리의 이름, 버전, culture, PublicKeyToken을 확정한다. 이는 configuration 파일에 기록된 어셈블리 redirection을 확인한다는 얘기가 되겠다. Configuration에는 어셈블리의 특정 버전이 주어지면 해당 버전이 아닌 더 높은 버전을 사용하도록 한다던가( 요소), 부분 참조를 전체 참조로 바꾼다던가( 요소) 등의 작업을 통해 실제로 로드 할 어셈블리를 확정하는 작업이 되겠다. 이에 대해서는 이 글에서 언급하지 않겠다. 그렇지 않아도 긴 글이기에... -_-;

이는 내가 Visual Studio 에서 MyAssembly, Version=1.0.0.0 을 참조했다 할지라도 configuration에 의해 MyAssembly, Version=2.0.0.0 이 실제로 로드 될 수 있다는 것이다. 이렇게 Configuration에 의해 실제로 로드 할 어셈블리를 확정 짓는 작업이 닷넷 런타임이 최초로 수행하는 작업이다.

2. Checking for Previously Referenced Assemblies

요 단계는 쉽다. 로드 할 실제 어셈블리의 이름, 버전(전체 참조) 등이 확정되었다면, 다음에 닷넷 런타임이 수행하는 작업은 그 어셈블리가 이미 로드 된 것인가 확인한다. 만약에 이미 로드 되었다면 어셈블리 로드 작업은 종료된다. 그렇지 않다면 다음 단계를 수행한다. 졸라 간단하고 쉽지 않은가?

3. Checking the Global Assembly Cache

요거 좀 조심해야 할 단계이다. 닷넷 런타임은 로드 할 어셈블리의 참조가 강력한 이름(Strong Name)의 참조라면 GAC을 찾는다. GAC에서 해당 어셈블리를 찾는다면 그것을 로드 할 것이고 찾지 못했다면 당연히 다음 단계로 넘어가게 된다.

이 단계에서 주의할 점이란, 닷넷 런타임은 반드시 강력한 이름을 가진 전체 참조(full reference: 벌써 까먹었는가? 30초는 기억해야지... 바로 전에 설명한 거다 -_-)가 주어져야만 GAC을 뒤진다는 점이다. 요 점에 주의에 또 주의를 하자. 이 부분은 스마트 클라이언트의 배포에서 상당히 중요하게 작용하기 때문이다.

4. Locating the Assembly through Codebase or Probing

GAC 에서도 못 찾았으니 이제 조뺑이 치면서 어셈블리들을 여러 디렉터리들에서 말 그대로 검색해 봐야 한다. PATH 환경 변수가 지정하는 디렉터리들을 뒤질까? 말도 안 되는 소리이다. Windows 시스템에서 DLL을 찾을 때 작업 디렉터리(working directory), PATH, SYSTEM32 디렉터리 순서로 뒤졌었다. 이렇게 뒤지는 동안 DLL의 버전 문제로 인해 DLL 지옥(dll hell) 문제가 유발되었던 것을 기억하자(무슨 반공 포스터가 연상되는... -_-). 닷넷에서는 현재 어플리케이션 도메인 베이스 디렉터리(base directory)를 기준으로 어셈블리를 찾는다. 이렇게 어플리케이션 도메인을 기준으로 어셈블리를 찾는 과정을 조사(probing)라 부른다(조사란 용어로는 대략 글이 짜증나게 바뀌므로 걍 probing으로 영문 표기하겠다 -_-). Probing에 대한 구체적인 예제는 꼭 MSDN에 가서 예제를 살펴보는 것이 실력 향상에 커~다란 도움이 될 것이다.

어셈블리를 찾기 위해 probing을 하기 전에, 앞서 1 번째 단계에서 어셈블리를 configuration에 명시할 때 codebase 가 주어져 있다면 이 codebase를 먼저 검사한다. 그리고 이 codebase에서 어셈블리를 찾지 못한다면 곧바로 예외(exception)과 함께 어셈블리 바인딩은 종료한다.

5. LoadFrom() Method

LoadFrom() 메쏘드는 일반적인 Load() 메쏘드와는 매우 다른 방식을 취한다. Load() 메쏘드 호출이나 어셈블리 참조에 의해 어셈블리를 로드 하는 경우, 기준은 해당 어셈블리의 이름이다. 하지만 LoadFrom() 메쏘드는 어셈블리의 이름이 아닌 어셈블리 파일명에 의해서 어셈블리를 로드 한다. 다시 한번 말한다. LoadFrom() 메쏘드는 어셈블리 파일명에 의해 어셈블리를 로드 한다.

어셈블리가 파일명에 의해 로드 되므로 어셈블리의 버전이 뭔지 PublicKeyToken 이 어떻게 되는지 미리 알아낼 방법이 전혀 없다. 이런 이유로 LoadFrom 메쏘드를 사용하면 어셈블리 이름에 의해 configuration을 참조한다던가 GAC을 뒤진다던가 하는 일은 결코 발생하지 않는다. 다시 한번 강조한다. LoadFrom() 을 사용하는 경우, GAC을 참조하는 일은 없다.

SmartClient & Assembly Resolution

지금까지 어셈블리 바인딩에 대한 기본적인 사항들을 알아 봤으니 이 기본(!)적인 사항을 스마트 클라이언트에 대입하여 보자.

Assembly Download Cache

약간은 쌩뚱 맞게 등장하는 우리의 어셈블리 다운로드 캐시... 이 녀석은 닷넷이 인터넷에서 다운로드 받은 어셈블리들에 대한 목록을 기록해 두는 캐시 역할을 수행한다. 어셈블리 다운로드 캐시는 gacutil.exe에 /ldl 옵션을 주거나 윈도우 탐색기를 이용하여 C:\Windows\Assembly\Download 폴더를 브라우징 함으로써 어떤 어셈블리들이 다운로드 캐시에 존재하는가 확인할 수 있다(화면1).


화면1. 다운로드 캐시

어셈블리 다운로드 캐시에 존재하는 어셈블리들은 스마트 클라이언트 시나리오에 의해 다운로드 받은 어셈블리들 이거나 LoadFrom 메쏘드에 의해 로컬 파일 시스템이 아닌 네트워크 폴더 경로 혹은 URL이 주어졌을 때 다운로드 된 어셈블리 목록들이 나타난다.

이들 어셈블리는 전용(private) 어셈블리와 공용(public) 어셈블리로 나뉘어 지는데, 어셈블리에 강력한 이름이 존재하면 공용 어셈블리요 그렇지 않다면 전용 어셈블리가 된다. 공용 어셈블리와 전용 어셈블리는 스마트 클라이언트의 배포 시나리오에서 매우 중요한 의미를 갖는다. 어셈블리 다운로드 캐시에 존재하는 공용 어셈블리들은 GAC의 일부로 간주되기 때문이다. 이에 대해서는 곧 상세히 다시 설명하도록 하겠다. 잠시만 기다리기 바란다.

Typical Example

대개의 경우 달랑 하나의 어셈블리 만으로 스마트 클라이언트를 구성하는 경우는 드물다. 공통 모듈도 있을 것이고, 상용 컨트롤도 사용해야 할 것이다. 이들은 대개 별도의 어셈블리로 구성되어 있으므로 이것을 스마트 클라이언트 어셈블리가 '참조'를 해야만 할 것이다. 그림1은 이런 상황을 보여주고 있다.


그림1. 스마트 클라이언트에서 어셈블리 참조 예제

어셈블리 A가 B와 C를 참조하고 있고 B는 강력한 이름을 갖고 있고 C는 강력한 이름이 없다. 그림1의 예제가 브라우저 임베디드 스마트 클라이언트이므로 A는 OBJECT 태그에 의해 로드 될 것이다. 어셈블리 A는 스마트 클라이언트의 "최초" 어셈블리이므로 버전에 상관없이 날짜에 의해 다운로드 여부가 결정됨은 여러 번 이야기한 바 있다. A 가 B를 참조하기 때문에 B 역시 다운로드 될 것이다. B가 다운로드 된 후에 B는 어셈블리 다운로드 캐시에 공용 어셈블리로서 등록된다는 점에 유의하자. A가 C 역시 참조하기 때문에 C 역시 다운로드 되며 어셈블리 다운로드 캐시에 전용 어셈블리로서 등록된다.

이렇게 일단 A, B, C 가 다운로드 된 이후, 브라우저를 완전히 종료하고 나중에 이 스마트 클라이언트를 다시 구동하면 어떻게 될까? 어셈블리 A는 OBJECT 태그에 의해 참조되므로 브라우저의 캐시에 존재하는 DLL의 날짜와 서버에 존재하는 DLL의 날짜를 비교하여 다운로드 하는 절차를 거치게 될 것이다. A가 B를 참조하므로 B 역시 다시 로드 되어야 한다. 이제 앞서 언급한 어셈블리 바인딩 규칙을 적용해 보자. Configuration 설정은 없다고 가정할 때, A가 B를 단순히 "참조" 했으므로 A는 B에 대해 전체 참조(full reference) 이름으로 참조되어 있다. 그리고 어셈블리 B가 강력한 이름을 가지므로 닷넷 런타임은 GAC을 뒤지게 된다. 앞서 공용 어셈블리는 GAC의 일부로 간주되므로 닷넷 런타임은 어셈블리 다운로드 캐시 상에 존재하는 어셈블리 B를 곧바로 로드 하게 된다. 런타임이 어셈블리 B를 다운로드 하기 위해 웹 서버에 접근하는 일은 발생하지 않는다는 점이 포인트 되겠다.

그렇다. 이 경우 닷넷 런타임은 웹 서버에 어셈블리 B에 대해 어떤 HTTP 메시지도 전송하지 않는다. 날짜 비교도 없으며 오로지 어셈블리의 전체 참조 이름에 의해서만 어셈블리 다운로드 캐시에서 어셈블리를 찾아 버린 것이다. 따라서 서버상에 존재하는 어셈블리 B의 날짜가 바뀌어도 어셈블리 B는 결코 다운로드 되지 않는다. 또한 서버 상에 존재하는 어셈블리 B의 버전이 바뀌어도 A 가 바뀐 새 버전의 어셈블리 B를 참조하지 않고 이전 버전의 어셈블리 B를 참조하고 있어도 서버 상의 어셈블리는 클라이언트로 다운로드 되지 않는다. 왜? 어셈블리 다운로드 캐시에 이미 존재하니까...

어셈블리 C는 강력한 이름을 갖지 않기 때문에 닷넷 런타임이 GAC을 검사하지 않는다. 따라서 항상 어셈블리 probing 과정을 거치게 되고 이는 곧 웹 서버에 어셈블리 다운로드를 위한 HTTP GET 요청이 날아가게 될 것이다. HTTP GET 요청에 따라 웹 서버는 웹 서버 상의 어셈블리 C의 날짜와 브라우저 캐시 상에 존재하는 어셈블리 C의 날짜를 비교하여 어셈블리 C를 다운로드 하거나 304 NOT MODIFIED 결과를 반환하게 된다.

그렇다면, 새로운 버전의 어셈블리 B가 다운로드 되도록 하는 방법은 무엇일까? 어셈블리 B의 버전을 올리고(1.0.0.0 에서 2.0.0.0 으로 올렸다고 가정해 보자) 이것을 컴파일 한 후, B를 참조하는 어셈블리 A가 새로운 버전의 어셈블리 B를 "참조"하도록 재 컴파일 해야만 한다. 스마트 클라이언트 상에서 A가 B를 참조할 때 전체 참조 이름을 닷넷 런타임에 넘기므로 런타임은 2.0.0.0을 로드 하려고 시도할 것이다. 어셈블리 다운로드 캐시에는 1.0.0.0 이 존재하므로 GAC에서 어셈블리를 찾는 것은 실패하고 어셈블리 probing이 발생하며 웹 서버에서 어셈블리 B를 다운로드 하게 된다. 새로이 다운로드 된 어셈블리 B는 다시 어셈블리 다운로드 캐시에 공용 어셈블리로 등록됨은 말할 것도 없다.

Trouble shooting

새로이 업데이트 되어 컴파일 된 어셈블리의 다운로드 여부는 그 어셈블리가 강력한 이름을 가지고 있는가 여부에 따라서 조금 다르게 작동한다. 물론 이 규칙은 앞서 설명한 기본적인 닷넷 런타임의 어셈블리 바인딩 규칙에 의해 결정됨에도 유의하자.

새로운 버전의 어셈블리가 다운로드 되지 않거나 어셈블리가 변화되지 않았음에도 지속적으로 다운로드 되는 현상이 발생한다면 곧바로 Fiddler를 구동시키고 어떤 어셈블리가 다운로드 되는지 살펴본다. 어셈블리 다운로드에 대한 HTTP 요청이 발생하는지 전혀 발생하지 않는지를 잘 살펴보아야 할 것이다. 그리고 Fuslogvw.exe 와 같은 유틸리티를 이용하여 어떤 어셈블리가 어떤 어셈블리를 참조하려고 했는지 역시 잘 살펴 보는 것이 좋다.

그리고 나서 어셈블리에 강력한 이름이 있는지, 어플리케이션 베이스 디렉터리는 어떤지, 브라우저 및 웹 서버 상의 캐시 설정이 어떻게 되어 있는지, 그리고 어셈블리 다운로드 캐시에는 어떤 어셈블리들이 있는지 등을 살펴보고 종합적으로 문제가 어디에 있는지 살펴보아야 한다.

스마트 클라이언트는 결코 쉬운 기술 요소가 아니다. 만만하게 보았다간 정신건강에 크게 해로울 것이다.

What's Next

다음 포스트에서는 비주얼 스튜디오 내에서 '참조' 하는 것 같은 정적 참조가 아닌 Load 혹은 LoadFrom을 이용하여 어셈블리들을 동적으로 참조하는 것에 대해 설명하도록 하겠다. 응용 프로그램이 매우 간단하지 않다면 몇 개의 어셈블리들에 여러 화면이 분산되어 존재할 것이고, 메뉴에서 이들 어셈블리들을 동적으로 로드 해야 하는 경우가 많이 발생한다. 스마트 클라이언트 시나리오에서 Load 혹은 LoadFrom을 사용할 때 어떤 것을 주의해야 하는지에 대해 이야기 할 것이다.

스마트 클라이언트 이야기는 이어진다. 쭈~욱~



Comments (read-only)
#re: 스마트 클라이언트, 그것을 알려주마 (IX) : Assembly Deployment (2) / 어흥이 / 2006-11-29 오후 1:06:00
스마트 클라이언트 다시 연재하시는군요^^
좋아하실분들 많겠네요~
저 역시 포함해서...

감사합니다.^^.
#re: 스마트 클라이언트, 그것을 알려주마 (IX) : Assembly Deployment (2) / 이방은 / 2006-11-29 오후 2:13:00
이 파트만 해서 책한권 쓰셔도 될듯 합니다..@.@
#아하.. / 조조 / 2006-12-14 오후 5:22:00
아하.. LoadFrom 이 gac를 전혀 참조하지 않는 거였군요. (가만 생각하니 이전에 이 메소드 쓰면서 분명 이런 내용을 봤을텐데..당시엔 무시했었나..)
요즘은 클릭원스 를 보고 있는데.. 이건 게시버전에 의해 다운로드가 발생하더군요.
이상하게 요즘 2.0에 와서.. 서버를 셋팅하기에 따라 어셈블리 다운과 임베디드가 잘 안되는 서버가 발생하여...
(이유는 잘 모르겠지만 IIS와 .NET을 밀고 새로 하면 되더군요)
그럴 경우 이미 운영되는 서버를 IIS를 밀고 다시 셋팅하기가 힘들어서 검토할만하더군요..