본문 바로가기

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

ClickOnce 어플리케이션의 파일 관리(Ⅱ)

1.1 어플리케이션 파일 관리

ClickOnce 어플리케이션은 앞에서 말한대로 여러 종류의 파일들로 구성되어 있습니다 : 실행파일, 설정 파일, 리소스 파일, 데이터 파일, 참조 어셈블리 파일등. 먼저 어플리케이션을 구성하는 참조 어셈블리들외에 일반 파일들을 어떻게 배포에 포함시키는지를 알아봅니다. 그리고 어플리케이션의 코드상에서 직접 파일들을 그룹별로 다운로드해서 사용하는 방법 등을 알아봅니다.

1.1.1 ClickOnce 배포에 파일 포함시키기

ClickOnce로 배포되기 위해서는 해당 파일을 일단 배포 대상에 포함시켜야 할 것입니다. 그런 후에그 파일을 데이터 파일로 설정할 것인지 아니면 일반 어플리케이션 구성 파일로 설정할 것인지가 결정될 것입니다.

어플리케이션을 작성하다보면 참조되는 어셈블리 파일외에도 XML 파일, 텍스트 파일등 기타 필요한 파일들을 어플리케이션의일부로 사용하는 경우가 있습니다. 여기서는 이런 파일들을 어떻게 ClickOnce 배포에 포함시키는지를 알아보겠습니다.

■ 프로젝트에 포함시키기

어플리케이션을 배포 서버로 게시할 때 Visual Studio를 사용하는 경우, 파일을 배포에 포함시키고자 한다면 그 파일은 반드시 Visual Studio의 프로젝트에 포함되어 있어야 합니다. 파일을 Visual Studio에 포함시키려면 다음 절차대로 합니다.

1. 솔루션 탐색기에서 프로젝트 오른쪽 클릭-> 추가->기존 항목을 선택합니다.

2. 파일 대화창에서 추가하고자 하는 파일을 선택합니다.

3. 대화창의 추가 버튼을 클릭합니다.

1373990461

그림 프로젝트에 파일 추가

프로젝트에는 추가된 파일과 소스 파일이나 프로젝트 파일등이 함께 혼재되어 있지만, 최종 게시 결과물에는 소스 파일이나 프로젝트 파일은 포함되지 않고 추가된 파일만이 포함됩니다.

■ 빌드 작업 속성

그러나 프로젝트 일부로 추가된 파일들이 게시 최종 결과물에도 포함되기 위해서는 반드시 설정해줘야 하는 속성값이 있습니다. 바로 “빌드 작업(Build Action)” 속성입니다. 처음에 필자도 이 속성값을 제대로 설정하지 않아서 파일이 왜 포함되지 않는지 원인을 찾지 못해 헤매던 기억이 납니다.

빌드 작업 속성에서 선택할 수 있는 값은 다음과 같습니다: 없음(None), 컴파일(Compile), 내용(Content), 포함 리소스(Embedded Resource).

프로젝트에 파일을 추가하면 Visual Studio는 파일 확장자를 기반으로 해서 적절한 “빌드 작업” 속성 값을 결정합니다. 이 중에서 빌드 작업값을 “내용” 또는 “컴포넌트”로 설정한 파일은 “게시 상태” 그리고 “다운로드 그룹” 속성값들을 편집할 수 있습니다.

“내용”을 선택하게 되면 추가된 파일이 게시되는 파일목록에도 포함되게 됩니다. 만약 파일을 프로젝트에 추가했는데도 “빌드 작업” 속성값이 “없음”으로 선택되었다면, 이 상태로 게시를 해도 게시 결과물에는 포함되지 않게 됩니다.

소스 코드 파일에 대한 빌드작업 속성값은 “컴파일”로 선택됩니다. “컴파일”로 선택된 파일의 내용은컴파일러에 의해서 컴파일이 된 다음 어셈블리에 포함되게 됩니다.

“포함 리소스”로 선택된 파일도 빌드 후 출력되는 결과 어셈블리에 포함되지만 컴파일은 되지 않습니다. “컴파일” 또는 “포함 리소스”로 선택된 파일들 자체는 게시 결과물에 포함되지는 않지만 이 파일들이 포함된 어셈블리는 게시 결과물에 포함됩니다. 파일을 어셈블리에 리소스로 포함시키는 것에 대해서는 뒤에서 다시 보게 됩니다.

어셈블리 파일을 참조로 추가하는 것이 아니라 “항목추가”를 이용해서 일반 파일로서 프로젝트에 추가하면 기본적으로 빌드 작업값이 “컴포넌트”로 선택됩니다. 이 값으로 선택된 상태에서 게시를 하면 배포 목록에는 포함되지만 게시 결과물에는 파일이 직접 포함되지 않게 됩니다. 이 값은 플러그인 프로그램을 ClickOnce 배포에 포함하려는 경우 사용할 수 있습니다. 플러그인 프로그램을 배포하는 것은 뒤에서 알아봅니다.

1.1.2 계층 구조 배포

만약 계층 구조를 포함하고 있는 어플리케이션을 배포하면 클라이언트에서는 어떻게 캐싱되는지 알아보도록 하겠습니다. 예를 들어 그림처럼 여러 종류의 파일을 디렉토리별로 구분해서 관리하고 있는 실행 프로젝트가 있다고 해 보겠습니다.

1306206572

그림 계층 구조 프로젝트

게시 탭 디자이너에서 어플리케이션 구성 파일 편집하는 창을 띄워보면 “게시 상태” 속성을 모두 “포함(자동)”으로 선택하도록 합니다.

1392589266

그림 계층구조 파일의 게시 상태 설정

이 설정대로 게시를 한 어플리케이션을 사용자가 설치를 하게되면 프로젝트에서 구성되었던 구조 그대로가 로컬의 디렉토리로 배포가 됩니다.

1323062058

그림 계층 구조 배포 결과

ClickOnce 어플리케이션은 이처럼 클라이언트로 배포되어서도 그대로 계층 구조를 유지하고 있기 때문에 파일을 접근하기 위해서 개발시 상대 경로를 기반으로 코드가 작성되어 있다면 그 코드는 클라이언트로 배포되어서도 문제없이 실행될 수 있게 되는 것입니다. 상대 경로라고 했는데 그럼 무엇을 기준으로 하는 상대경로일까요. 현재 어플리케이션이 실행되는 그 디렉토리가 기준이 된다는 것이 너무 당연한 것일까요. 기준 디렉토리는 .NET 프레임워크에서 아주 중요한 기본 개념중의 하나입니다. 간단히 내릴 수 있는 정의는 아니고 바로 이어서 관련된 다른 개념들과 더불어 살펴봅니다.

1.1.3 어플리케이션 도메인 및 디렉토리

어플리케이션 파일들은 ClickOnce 어플리케이션에만 포함되는 것이 아니라 다른 일반 .NET 어플리케이션도 이런 파일로 구성됩니다. 사실 어플리케이션을 파일 관리는 .NET 프레임워크의 일반적인 주제입니다. ClickOnce의 어플리케이션 파일 관리 기능은 많은 부분을 .NET의 기본 개념에 의존하고 있습니다. 따라서 여기서 소개하는 .NET의 파일 관리에 필요한 기본적인 개념은 ClickOnce 배포와는 직접적인 관련이 없을 지 모르지만 ClickOnce 어플리케이션 파일 관리 구조를 이해하고 응용하기 위해서는 필요한 개념들입니다.

■ 어플리케이션 도메인

.NET의 기본 개념으로 “어플리케이션 기본 디렉토리(application base directory)”라는 개념이 있습니다. ClickOnce가 파일을 관리하는 것을 이해하기 위해서는 이 개념을 알고 있어야 합니다. 그러나 어플리케이션 디렉토리라는 개념을 알기 위해서는 또 어플리케이션 도메인(Application Domain)이라는 개념을 이해해야 합니다.

어플리케이션 도메인이나 어플리케이션 디렉토리 개념은 ClickOnce 어플리케이션에 국한되는 개념이 아니라 .NET 어플리케이션이라면 모두에 적용되는 개념입니다. 따라서 한번 잘 이해해 두면 다른 형태의 .NET 어플리케이션을 이해하는데도 많은 도움이 됩니다.

어플리케이션 도메인에 대한 개념을 이해하기 위해서 머리속에서 상상을 하나 해 보겠습니다. .NET 어플리케이션이 시작되면 공간이 하나 생성된다고 생각하십시요. 그리고 그곳에서 어플리케이션이 실행되는 것입니다. 상상을 한다는 것은 다른 말로 하면 그 공간이 논리적인 공간이라는 의미입니다. 이런 공간을 어플리케이션 도메인(Application Domain)이라고 합니다. .NET의 CLR(Common Language Runtime )은 .NET 어플리케이션을 실행시키기전에 기본적으로 하나씩의 어플리케이션 도메인을 생성하고 그곳에서 어플리케이션을 로딩시킵니다. 이런 어플리케이션 실행 공간은 그림처럼 하나의 OS 프로세스내에 여러 개 생성될 수 있습니다. 개발자가 직접 하나의 어플케이션 도메인에서 다른 어플리케이션 도메인을 생성해서 그곳에 다른 프로그램을 로딩할 수도 있습니다.

1111300478

그림 어플리케이션 도메인

어플리케이션 도메인은 어플리케이션마다 할당되는 독립적인 공간입니다. 어플리케이션 도메인의 독립성은 어플리케이션 도메인이 무엇인지를 잘 설명해주는 특징중의 하나입니다. 두 어플리케이션이 동일한 어셈블리를 참조하고 있다고 생각해보겠습니다. 어플리케이션은 각자의 어플리케이션 도메인으로 어셈블리를 로딩하고 객체도 각각의 도메인 안에서 별도로 생성합니다. 그림처럼 프로세스 A에 있는 도메인A와 도메인 B에는 동일한 타입의 객체들이 독립적으로 존재하게 되는 것입니다. 어플리케이션 도메인간에는 객체의 인스턴스 멤버뿐만 아니라 정적 멤버도 공유가 되지 않습니다. 즉 완전히 독립된 실행 공간입니다. 어플리케이션 도메인은 어플리케이션의 논리적인 독립된 실행 공간이라고 정의할 수 있겠습니다. 이 공간은 .NET에서 AppDomain이라는 클래스로 표현됩니다.

어플리케이션 도메인에는 어플리케이션에 필요한 속성들이 기본적으로 설정됩니다:어플리케이션 이름, 설정 파일(config) 위치, 참조되는 다른 어셈블리들이나 다른 파일들을 검색하기 위한 기준 디렉토리 등등. 그리고 어플리케이션 설정 파일(.config)을 통해서 사용자가 정의한 내용들도 이곳에 로딩됩니다. 어플리케이션 도메인의 기본 속성중에서 방금 언급한 “파일을 검색하기 위한 기준 디렉토리” 이것이 바로 어플리케이션 디렉토리(Application Directory)입니다.

OS 프로세스 하나에는 앞에서 본대로 여러 개의 AppDomain 인스턴스가 존재할 수 있습니다. 그러나 CLR 인스턴스는 OS 프로세스에 하나만 존재할 수 있습니다. 이 사실이 이슈가 될 수 있는 경우는 하나의 머신에 버전이 다른 .NET 프레임워크가 같이 설치되어 있는 경우입니다.

v1.1로 만들어진 어플리케이션에서 v2.0으로 만들어진 어셈블리를 사용하려면 문제가 됩니다. v1.1 어플리케이션이 실행되면 그 어플리케이션용 프로세스에는 v1.1의 CLR이 로딩되어 있습니다. 그런 상황에서 v2.0의 어셈블리를 어플리케이션에서 로딩하려고 하면 v2.0의 CLR은 로딩될 수 없고 결과 v1.1의 CLR에서 생성한 AppDomain 객체에서 v2.0의 어셈블리를 사용해야 하는데 이것은 .NET에서 지원하지 않고 있는 호환성입니다.

그러나 만약 v2.0에서 만들어진 어플리케이션에서 v1.1로 만들어진 어셈블리를 사용하려고 한다면 이것은 가능합니다. v2.0의 AppDomain에서 v1.1로 만들어진 어셈블리는 정상적으로 실행될 수 있는데 이것은 .NET에서 지원하는 호환성입니다. 정리하면 이렇습니다.

구버전(v1.1)의 어플리케이션 -> 최신버전(v2.0) 어셈블리 사용 ( 불가 )

최신 버전(v2.0) 어플리케이션 -> 구버전(v1.1) 어셈블리 사용 ( 가능 )

이것은 하나의 OS 프로세스에 하나의 CLR 버전만이 로딩될 수 있다는 사실을 알고 있다면 쉽게 이해할 수 있는 문제입니다.

■ 어플리케이션 (기본)디렉토리(Application Directory)

앞에서 ClickOnce 어플리케이션이 설치되는 디렉토리는 사전에 알 수 없다고 했습니다. 이것은 ClickOnce 어플리케이션 개발자는 특정 파일에 접근하기 위해서 드라이브 문자로 시작하는 절대 경로를 사용해서 개발할 수 없다는 의미입니다. 그러나 다운로드된 어떤 파일을 어플리케이션의 코드상에서 직접 검색해서 접근해야 한다면 현재 실행되고 있는 어플리케이션이 기준으로 삼고 있는 디렉토리가 있으면 가능할 것입니다. 그 어플리케이션이 디렉토리 검색을 할 때 사용할 수 있는 기준 디렉토리가 있고 어플리케이션에서는 그 값을 기준으로 해서 상대적인 검색을 하게 되면 특정 파일에 접근할 수 있게 되는 것입니다. 이때 사용하는 기준 디렉토리를 어플리케이션 기본 디렉토리(application base directory)라고 합니다. .NET 프레임워크에서는 설정 파일(.config)이 있는 디렉토리를 기본 디렉토리로 정의하고 있습니다. 만약 설정 파일이 없는 경우는 실행 파일(exe)이 있는 디렉토리가 있는 위치가 됩니다. ClickOnce에서는 .NET이 제공하는 이 값을 사용해서 파일을 관리하게 됩니다.

1071327086

그림 어플리케이션 기본 디렉토리

그림처럼 어플리케이션 기본 디렉토리는 어플리케이션의 논리적인 공간이 어플리케이션 도메인과 어플리케이션의 실제 물리적인 공간을 연결시켜주는 값이라고 볼 수 있습니다. 이 값은 AppDomain 클래스에는 BaseDirectory라는 정적 속성으로 표현됩니다.

AppDomain.CurrentDomain.BaseDirectory

이 API를 이용하면 상당히 프로그램적으로 파일을 관리하는 프로그램을 만들 수 있게 됩니다. 다음과 같은 코드를 보겠습니다.

Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + "MyAssembly.dll");

assembly.LoadFrom() 메소드는 어셈블리에 대한 경로를 받아서 코드상에서 동적으로 어셈블리를 로딩할 수 있는 메소드입니다. ClickOnce 어플리케이션에서도 동적으로 파일을 로딩해야 하는 경우가 있을 수 있습니다. 그러나 불행히도 개발자는 어플리케이션 디렉토리가 어떤 이름으로 생성될 지 모릅니다. 이런 경우 BaseDirectory 속성 값을 이용하면 된다는 것입니다. 이 코드는 재 사용성에서 아주 유연한 코드가 될 수 있습니다.

이처럼 어플리리케이션 디렉토리를 기준으로 하면 ClickOnce 어플리케이션이 실제로 설치되는 물리적인 경로는 모르더라도 코드상에서도 파일 시스템상의 파일들을 자유롭게 관리할 수 있게 됩니다.

■ 어플리케이션 시작 디렉토리(Application Startup Directory)

어플리케이션 디렉토리와 비슷한 개념으로 어플리케이션 시작 디렉토리(startup directory)라는 것이 있습니다. 이것은 어플리케이션 디렉토리와 약간 다른 개념입니다. ClickOnce 어플리케이션이 부분적으로만 신뢰할 수 있는 경우라면 프로그램 AppLaunch.exe의 호스팅을 받아서 실행된다고 했습니다. 부분 신뢰 어플리케이션의 시작 프로그램은 AppLaunch.exe이고 실행 어플리케이션은 해당 ClickOnce 실행 프로그램이 되는 것입니다. 이때 이 ClickOnce 어플리케이션의 시작 디렉토리는 AppLaunch.exe가 있는 .NET 프레임워크가 설치된 경로가 됩니다. 그러나 어플리케이션 기본 디렉토리는 ClickOnce 어플리케이션에 대한 경로가 됩니다. 다음을 보면 부분 신뢰 어플리케이션의 시작 디렉토리와 기본 디렉토리가 서로 다르다는 것을 알 수 있습니다.

Application.StartupPath 값

- C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727

AppDomain.CurrentDomain.BaseDirectory 값

- C:\Documents and Settings\ighwang\Local Settings\Apps\2.0\

34EZGTJ6.BGC\57XMAOKQ.7XK\dalb..tion_e6a5b0bc5547f078_0001.0000_d4a576a93baa50b1\

그러나 완전 신뢰 어플리케이션에서는 시작 디렉토리와 어플리케이션의 기본 디렉토리는 동일하게 ClickOnce 어플리케이션을 가리키는 경로가 됩니다.

정리하면 어플리케이션 기본 디렉토리는 현재 어플리케이션의 실행 디렉토리(working directory)이고 시작 디렉토리는 OS 프로세스가 어플리케이션을 시작했던 디렉토리(original, startup directory)라고 할 수 있겠습니다.

부분 신뢰 프로그램에서 AppDomain.CurrentDomain.BaseDirectory 속성을 사용할 수 있으려면 FileIOPermission 권한이 필요합니다.

1.1.4 그룹별 다운로드하기

어떤 경우에는 사용자의 요청에 따라(on-demand), 어플리케이션 구성 파일을 서버측에서 실시간으로 다운로드해서 실행하는 구조를 택해야 할 수도 있습니다. 이런 경우 어플리케이션 구성 파일 속성 편집 창에서 보았던 “다운로드 그룹” 속성을 이용해서 파일을 그룹별로 다운로드해서 사용할 수 있습니다. 다음은 사용자 정의 “다운로드 그룹”을 만드는 절차입니다.

1. Visual Studio에서 배포할 어플리케이션의 프로젝트를 오픈합니다.

2. 프로젝트 디자이너에서 “게시”탭을 선택합니다.

3. “응용 프로그램 파일” 버튼을 클릭해서 구성 파일 속성 편집 창을 띄웁니다.

4. 그룹별로 다운로드하기를 원하는 파일의 “다운로드 그룹” 컬럼의 출력되는 리스트 항목중에서 “(새로 만들기…)”항목을 선택해서 그룹명으로 “My Images”라고 입력합니다.

5. 다른 파일을 선택해서 “다운로드 그룹” 속성을 선택하면 이제 선택 리스트 항목들중의 하나로 “My Images”가 포함되어 있습니다. 이 항목을 선택합니다.

이제 두 파일은 “My Images”그룹으로 묶이게 되었습니다. 이렇게 사용자 정의의 그룹으로 묶인 파일들은 게시를 하게 되면 그 결과물에는 포함됩니다. 그러나 사용자가 자신의 머신에 초기 설치를 하거나 업데이트 버전을 설치해도 사용자 정의 그룹으로 묶인 파일은 다운로드되지 않습니다. 직접 ClickOnce 배포 API를 사용해서 프로그램적으로 다운로드 및 로딩을 구현해야 합니다. 필요한 API들은 네임스페이스 System.Deployment.Application에 포함되어 있습니다.

코드 그룹별 다운로드하기

private void btnGroupDownlad_Click(object sender, EventArgs e)

{

//ClickOnce 어플리케이션인지를 확인합니다.

if (ApplicationDeployment.IsNetworkDeployed)

{

//현재 배포 객체에 대한 참조를 구합니다.

ApplicationDeployment current = ApplicationDeployment.CurrentDeployment;

//원하는 그룹의 파일들이 아직 다운로드되지 않았는지를 확인합니다.

if (!current.IsFileGroupDownloaded("My Images"))

{

//1. 동기적으로 파일을 다운로드하는 코드입니다.

//1.1 아직 다운로드 되지 않았다면 파일을 먼저 다운로드 합니다.

current.DownloadFileGroup("My Images");

//1.2 파일이 다운로드되면 파일을 사용합니다.

UsingMyImages();

// //2. 비동기적으로 파일을 다운로드하는 코드입니다.

// //2.1 파일 다운로드 완료를 나타내는 이벤트에 핸들러를 등록합니다.

// current.DownloadFileGroupCompleted += new

// DownloadFileGroupCompletedEventHandler(current_DownloadFileGroupCompleted);

// //2.2 다운로드를 비동기적으로 시작합니다.

// current.DownloadFileGroupAsync("My Images");

}

else

{

//해당 그룹을 이미 다운로드했다면

UsingMyImages();

}

}

else

{

// ClickOnce 어플리케이션이 아닌 경우

UsingMyImages();

}

}

void current_DownloadFileGroupCompleted(object sender, DownloadFileGroupCompletedEventArgs e)

{

//그룹의 파일들이 비동기적으로 다운로드가 완료되고 나면 파일을 이용합니다.

UsingMyImages();

}

//다운로드된 이미지를 사용합니다.

private void UsingMyImages()

{

string[] arrImages = System.IO.Directory.GetFiles(@".\My Images", "*.bmp");

for(int i = 0; i< arrImages.Length; i++) // string file in arrImages)

{

Bitmap myImage = new Bitmap(arrImages[i]);

PictureBox pbImage = new PictureBox();

pbImage.Image = myImage;

pbImage.Size = new Size(100,100);

pbImage.SizeMode = PictureBoxSizeMode.Zoom;

pbImage.Location = new Point(100*i, 0);

pnlImages.Controls.Add( pbImage);

}

}

샘플 코드는 버튼을 클릭했을 때 “My Images”그룹에 포함된 이미지들을 직접 다운로드해서 로딩하는 코드입니다. 먼저 IsNetworkDeployed 속성을 이용해서 어플리케이션이 ClickOnce로 배포된 어플리케이션인지를 확인합니다. 만약 ClickOnce를 이용해서 배포된 어플리케이션이 아니라면 예를 들어 개발중에 로컬에서 실행시키는 경우에는 배포용 API를 사용해서 이미지를 로딩할 수 없습니다. 이런 경우는 다운로드하는 과정없이 어플리케이션 디렉토리에서 바로 이미지를 로딩해야 합니다.

일단 ClickOnce로 배포된 어플리케이션으로 확인된 경우는 현재 “My Images”그룹의 파일이 다운로드되어 있는지를 확인합니다. 이때 사용하는 API가 IsFileGroupDownloaded() 메소드입니다. 이때 그룹명을 메소드의 인자로 전달합니다. 그룹의 파일 목록이 변경되었다면 게시 페이지에서 다운로드 그룹의 목록을 변경한 다음 파일어플리케이션을 새로운 버전으로 다시 게시해야 합니다. 이렇게 하고 나서 새로운 버전의 어플리케이션을 설치하고 나서 실행시키면 IsFileGroupDownloaded()는 false를 반환합니다.

만약 아직 해당 그룹의 파일이 다운로드되지 않았다면 실제로 서버에 접근해서 파일을 다운로드 합니다. 파일 다운로드는 동기적으로도 가능하고 비동기적으로도 가능합니다. 동기적으로 파일을 다운로드한다면 그룹의 파일을 모두 다운로드해서 해당 파일을 이용한 작업이 모두 끝날때까지 다른 작업 예를 들어 윈도우상에서 프로그램을 이동시킨다거나 하는 작업을 할 수 없게 됩니다. 따라서 동기적인 작업이 꼭 필요한 경우가 아니라면 비동기적으로 파일을 다운로드하는 방식이 권장됩니다. 동기적으로 파일을 다운로드하려면 DownloadFileGroup() 메소드를 그리고 비동기적으로 파일을 다운로드하고 싶다면 DownloadFileGroupAsync() 메소드를 사용합니다.

동기 메소드 DownloadFileGroup()을 이용하면 바로 뒤에서 다운로드된 파일을 사용하는 코드 UsingMyImages()를 호출하면 됩니다. 그러나 비동기 메소드 DownloadFileGroupAsync()를 호출하때는 파일 그룹의 다운로드가 완료되었을 알려주는 이벤트 DownloadFileGroupCompletedEvnetHandler를 이용해야 합니다. 코드에서는 파일 그룹의 다운로드가 완료되면 current_DownloadFileGroupCompleted() 메소드를 호출하도록 설정하고 있습니다.

이제 핸들러 메소드에서 파일을 이용하는 코드를 호출합니다. 다음은 파일을 그룹별로 다운로드하고 이용하는 전체 코드입니다.

1067175491

그림  프로그램적으로 파일 다운로드하기

“My Images 로드” 버튼을 클릭하면 “My Images” 그룹에 포함된 파일을 다운로드해서 첫번째 Panel 컨트롤에 로딩합니다. UsingMyImages() 메소드에서는 다운로드된 이미지 파일들을 Directory.GetFiles() 메소드를 이용해서 접근하고 있습니다. 이 메소드는 파일들이 포함되어 있는 폴더 “My Images”를 현재 디렉토리에 대한 상대 경로로 표현하고 있습니다. 이 현재 디렉토리란 바로 어플리케이션이 실행 되고 있는 디렉토리로서 어플리케이션 디렉토리(Application Directory)로 표현됩니다.

참고로 이미지 파일을 프로젝트에 포함시키면 기본적으로 “빌드 작업”은 “내용”으로 선택됩니다. 그래서 게시를 하면 게시 결과물에 포함됩니다. 그러나 이미지 파일에 대한 “출력 디렉토리로 복사” 속성이 기본적으로 “복사 안 함”으로 선택되어져 있습니다.

1329825048

그림  출력 디렉토리로 복사 속성

이 속성이 “복사 안 함”으로 선택되면 빌드를 해도 파일이 빌드 폴더로 복사되지 않습니다. 따라서 어플리케이션을 개발 당시 로컬에서 실행키시면 어플리케이션 디렉토리에 파일을 찾을 수 없어서 에러가 발생하게 됩니다. 이 속성을 “항상 복사” 선택해서 해당 파일이 빌드 디렉토리로 복사되도록 설정하면 됩니다.

1.1.5 ClickOnce와 NTD 배포 결합

기업에서 사용하는 ERP 시스템은 그 구성 어셈블리 파일의 수가 수백이 되고 그 어셈블리에 포함된 화면들의 수는 모두 합치면 수천개가 되는 경우도 있습니다. 각 화면들은 팀별로 개발이 되고 개발 도중에는 계속해서 화면이 수정되고 어셈블리도 추가, 제거를 반복하게 됩니다. 따라서 이런 경우는 그 어셈블리를 모두 실행 프로젝트에 추가시켜 게시하는 것은 바람직하지 못한 구조라 하겠습니다.

차라리 이런 경우는 내용이 없는 실행 프로그램(EXE) 하나만 작성해서 ClickOnce로 클라이언트 머신에 설치하고 그 내용이 되는 실제 업무 화면이 포함되어 있는 어셈블리는 사용자가 메뉴를 클릭하는 순간 실시간(on-demand 방식)으로 배포 서버에서 다운로드해서 해당 업무 화면을 실행 프로그램의 내용으로 로딩하는 구조로 설계하는 것이 효율적입니다. 업무 개발 팀에서는 언제든지 해당 어셈블리를 수정해서 배포 서버에 있는 이전 버전의 어셈블리를 덮어쓰기만 하면 사용자는 항상 최신 버전의 업무 화면을 볼 수 있게 됩니다.

화면 수가 많은 프로젝트에서는 실행 프로그램에 출력되는 메뉴 목록 자체도 고정시킬 수가없습니다. 데이터베이스에 저장시켜두고 실행프로그램이 로딩될 때 그곳에서 읽어와서 메뉴를 출력시켜 주는 구조가 될 것입니다.

1104107713

그림 7‑12 ClickOnce와 NTD결합 어플리케이션

실행 프로그램(EXE) 자체는 ClickOnce로 배포되어 클라이언트에 설치됩니다. 실행 프로그램이 구동되면서 메뉴 목록을 데이터 소스로부터 조회해서 출력합니다. 최종 사용자는 출력된 메뉴 목록중에서 하나를 선택합니다. 사용자가 메뉴를 선택하면 화면에 해당하는 클래스 및 포함 어셈블리에 대한 정보가 실행 프로그램으로 건네져야 합니다. 프로그램은 메뉴에 해당하는 화면이 포함된 어셈블리를 파일 서버에 요청(On-Demand)해서 다운로드합니다. 그러고 나서 동적으로 해당 화면의 인스턴스를 생성해서 적당한 위치에 출력하게 됩니다.

업무 화면들이 포함된 어셈블리를 동적으로 다운로드할 수 있는 API는 ClickOnce의 배포 API를 사용하지 않습니다. .NET 프레임워의 Assembly 클래스에는 v2.0이전서부터 LoadFrom(), LoadFile()처럼 동적으로 어셈블리를 로딩할 수 있는 메소드들이 제공되고 있었습니다. 이런 메소드를 사용해서 어플리케이션 어셈블리를 다운로드하는 것은 .NET 프레임워크의 NTD(No-Touch Deployment) 메커니즘을 이용합니다.

이런 경우는 실행 프로그램(EXE)는 ClickOnce로 배포하고 사용자가 메뉴를 클릭했을 때 메뉴에 해당하는 업무 화면과 그 화면이 포함된 어셈블리들은 NTD 방식으로 배포하는 혼합된 구조라고 할 수 있습니다.

앞에서 본 “OnDemandDownload” 프로젝트의 실행 폼을 보면 “LoadFrom” 버튼이 있습니다. LoadFrom() 메소드에 전달되는 인자로는 어셈블리에 대한 전체 경로와 인스턴스를 생성하고 싶은 타입명이 필요합니다. 예제 프로젝트에는 “LoadFromAssembly”가 있습니다. 여기서 빌드된 어셈블리를 호출해서 테스트해 볼 수 있습니다.

어셈블리 경로

- D:\ClickOnce원고\최종코드\07\리소스배포\LoadFromAssembly\bin\Debug\LoadFromAssembly.dll

클래스명

- LoadFromAssembly.LoadFromForm

어셈블리경로란에는 LoadFromAssembly.dll에 대한 적절한 경로를 변경해서 입력합니다. 클래스명은 네임스페이스를 포함한 완전한 이름을 입력합니다. 이제 “LoadFrom” 버튼을 클릭하면 다음 코드가 실행됩니다.

코드 동적으로 인스턴스 생성하기- LoadFrom()이용

private void btnLoadFrom_Click(object sender, EventArgs e)

{

//원격 서버상의 어셈블리가 포함된 디렉토리 경로(HTTP, UNC 형식도 가능)

string strAssemblyPath = txtAssemblyPath.Text;

//클래스명( 네임스페이스 포함)

string strClassName = txtClassName.Text;

//어셈블리 로딩

Assembly assembly = Assembly.LoadFrom(strAssemblyPath);

//어셈블리내 타입의 인스턴스 생성

object obj = assembly.CreateInstance(strClassName, true);

// 인스턴스 타입 캐스팅

Form f = obj as Form;

//이후, f 인스턴스를 사용하면 됩니다.

f.Show();

}

어셈블리에 대한 경로는 로컬 디스크 경로뿐만 아니라 원격 배포 서버에 대한 HTTP 형식(http://서버/경로/어셈블리명 ) 또는 UNC형식(\서버\경로\어셈블리명)의 경로도 가능합니다. 그리고 타입명을 입력할때는 타입의 네임스페이스를 포함한 전체명을 입력합니다.

두 인자를 이용해서 동적으로 어셈블리를 로딩하고 타입에 해당하는 인스턴스를 생성한 뒤에는 new연산자를 이용해서 생성한 일반 인스턴스처럼 필요한대로 사용하면 됩니다. 코드에서는 단순히 보여주기만 하고 있습니다.

1130954473

그림 7‑13 LoadFrom()으로 로딩된 폼

LoadFrom()으로 다운로드된 어셈블리는 ClickOnce 캐시에 저장되는 것이 아니라 NTD에 의해 다운로드되는 파일들이 저장되는 별도의 공간이 있습니다. 윈도우 탐색기를 이용하면 다운된 어셈블리를 볼 수 있습니다. “시작->실행”에 “assembly”를 입력하고 실행합니다. 출력되는 파일 탐색기에서 assembly 폴더의 아래에 Download 폴더가 보이는데 이곳을 보면 다운로드된 어셈블리를 볼 수 있습니다.

1098048945

그림 7‑14 NTD 어셈블리 캐시 상태

때로는 이렇게 ClickOnce 배포와 NTD 배포를 적절히 혼합하게 되면 더욱더 유연한 배포 방식으로 구현할 수 있습니다.

1.1.6 플러그인(Plug-In) 어플리케이션

플러그인(Plug-In)은 이미 존재하는 어플리케이션의 기능을 확장하거나 또는 사용자 정의 기능을 추가할 수 있도록 허용하는 어플리케이션에서 자주 사용하는 방법입니다. 어플리케이션을 컴파일할때는 플러그인에 대한 구체적인 정보는 알지 못합니다. 다만 플러그인으로 사용될 컴포넌트의 타입만을 알 수 있습니다. 이런 플러그인 어플리케이션의 구조는 그림과 유사합니다.

1325403059

그림 7‑15 플러그인 어플리케이션 구조

플러그인 어플리케이션이 구동되면서 현재 사용할 수 있는 플러그인 프로그램 목록을 출력시킵니다. 사용자가 그 목록중에서 하나를 선택하면 어플리케이션은 해당 프로그램을 파일 서버에 요청해서 실행시킵니다.

이미 이런 구조를 구현하는 .NET의 두가지 방법에 대해서 앞에서 알아봤습니다. 그룹별로 어셈블리를 다운로드해서 사용하는 예와 ClickOnce와 NTD 배포 방식을 결합해서 어플리케이션을 작성하는 예가 이런 구조였습니다.

두 가지 구현의 구체적인 면에서는 차이가 있습니다. 코드 그룹별로 파일을 다운로드하는 방식은 ClickOnce의 배포 API(IsFileGroupDownloaded(), DownloadFileGroupAsync(), DownloadFileGroupCompletedEvnetHandler)를 이용하고 있고 두 번째 배포 방식의 결합 어플리케이션에서는 .NET 프레임워크에서 제공하는 동적 어셈블리 로딩 메소드(LoadFrom(), Load(), LoadFile())를 이용하고 있습니다. 메소드의 차이뿐만 아니라 프로그램 파일을 로딩하는 방법도 다릅니다. ClickOnce 배포 API를 사용하게 되면 클라이언트 머신에 캐싱된 어셈블리 버전을 사용할 수가 있어서 성능상에 있어서는 더 효과적입니다. 그러나 그룹별로 다운로드하는 경우는 해당 그룹의 파일 목록이 변경되면 어플리케이션의 버전을 변경해서 다시 게시해야 합니다. 그래야 클라이언트에서 IsFileGroupDownloaded()에서 false를 반환해서 다시 그룹의 파일 목록을 다운로드받을 수가 있습니다. 그러나 어셈블리 로딩 메소드는 어플리케이션 자체를 자주 업데이트할 필요가 없습니다. 대신에 변경된 어셈블리만 파일 서버로 복사하기만 하면 됩니다.

1.1.7 어플리케이션 어셈블리 배포

ClickOnce 어플리케이션은 간단히는 하나의 실행 파일과 설정 파일(.exe.config) 그리고 매니페스트 파일들로 구성될 수 있습니다. 그러나 어플리케이션에서 참조하고 있는 다른 어셈블리들이 있을 수 있습니다. 이런 어셈블리들은 “로컬 복사” 속성이 true로 된 상태에서 게시가 되면 기본적으로 게시된 결과 파일에 포함되게 됩니다. 만약 현재 참조하고 있는 어셈블리가 GAC에 등록되어 있다면, 클라이언트 머신에 어플리케이션과 함께 캐싱되는 로컬 어셈블리로 배포할지 아니면 GAC에 등록되는 공용 어셈블리로 배포할지를 결정해야 합니다.

만약 참조되는 어셈블리를 로컬 어셈블리로 배포하겠다고 하면 사전에 클라이언트 머신에 배포해야 하는 복잡한 절차가 생략되겠지만 어떤 경우의 어셈블리는 반드시 GAC에 등록이 되어야 하는 경우도 있을 수 있습니다. 예를 들어 어플리케이션은 부분 신뢰의 어플리케이션으로 배포되더라도 그 참조되는 어셈블리는 AllowPartiallyTrustedCallersAttribute(APTCA)어트리뷰트를 가지고 GAC에 등록되어 있어야 하는 시나리오에서는 반드시 참조되는 어셈블리가 GAC에 등록되어야 합니다. GAC에 등록된 어셈블리는 권한에 제한없이 모든 작업을 할 수 있습니다. 그 중에서 APTCA 어트리뷰트를 갖는 어셈블리들은 부분 신뢰의 환경에서 구동되는 어플리케이션에서도 사용을 할 수가 있습니다. 따라서 APTCA 어트리뷰트를 갖는 어셈블리는 부분 신뢰의 환경에서 구동되는 어플리케이션이 자신에게 주어진 보안 박스 범위를 벗어나는 작업을 할 수 있는 방법이 됩니다.

어떤 경우 상용 컨트롤용 어셈블리도 비슷한 이유 때문에 사전에 미리 클라이언트 머신의 GAC에 설치되어 있어야 하는 경우도 있습니다. 컨트롤의 설명서를 참조해서 로컬 어셈블리로 배포해도 되는 건지 아니면 반드시 GAC로 등록되어야 하는지를 확인할 필요가 있습니다.

사전에 GAC에 등록할 필요가 없다는 점에서 로컬 어셈블리로 배포하는 것이 편리하기는 하지만 그렇다고 이 방식이 꼭 좋은 것만은 아닙니다. 예를 들어 어플리케이션에서 참조하고 있는 상용 컨트롤용 어셈블리와 기타 어셈블리들이 몇 백개가 된다고 해 보겠습니다. 그런 상황에서 서버에 게시된 어플리케이션의 버전 번호가 변경되어서 업데이트를 수행해야 한다고 했을 때 클라이언트측 어셈블리와 서버측 어셈블리의 버전을 비교 확인하는 작업에서 무시못할 정도의 시간이 걸립니다. 만약 어플리케이션의 업데이트가 자주 일어나야 하는 경우라면 차라리 상용 컨트롤용 어셈블리들은 최초 한번만 클라이언트의 GAC에 등록하고 배포 목록에서는 제외하는 것이 바람직할 수도 있습니다. 참조 어셈블리를 배포 목록에서 제외하려면 “로컬 복사” 속성을 false로 설정합니다.

정리하면 사전에 GAC에 등록해야 하는 어셈블리들이 있다면 부트스트래퍼용 패키지를 작성해서 사전 필수 프로그램으로서 ClickOnce 어플리케이션이 시작하기 전에 클라이언트 머신에 설치되도록 해야 합니다.