본문 바로가기

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

개발 프레임워크 만들기 대장정 07- Hello 프로그램 with UAB

오늘은 UAB(앞으로는 Unity Application Block을 이렇게 줄이겠다)용 Hello 프로그램을 만들어보고자 한다. 앞에서 계속 얘기했던 FSLogger를 구현해서 사이트별 확장 구조의 로깅 프로그램을 만들 것이다. 다음은 Visual Studio.NET의 개발 구조이다.

 

 

디렉토리 구조Hello 프로그램치고 너무 복잡한가? "프로레임워크 만들기 대장정"인만큼 Hello 프로그램 구조도 좀 그럴싸하게 만들어서 현장에서 사용하는 것과 유사하게 만들려고 했다. 그러나 뒤에서 코드를 보면 알겠지만 내용은 암것도 없다. 디렉토리 구조를 먼저 설명한다.

솔루션 폴더 "01 DabongFramework"이 개발 프레임워크 관련 프로젝트들을 포함시킬 부분이다.  뒤에 나올 사이트별 확장 프레임워크와 구분할 필요가 있는 경우는 이 프레임워크를 코어(Core) 프레임워크라고 표현하자. 이곳에 포함되는 프로젝트는 현장에 나가서 수정되지 않는다. 이곳에 포함된 프로젝트는 어셈블리 형태로만 현장에 배포된다고 생각하면 된다. 물론 개발 프레임워크 부분에 버그가 없고 설계가 제대로 되어있다는 가정이 필요하긴 하지만.

현재 이 폴더에는 "Dalbong.Framework"라는 라이브러리 프로젝트만 포함되어 있고 ILogger.cs 하나만 있다. 그렇지만 핵심이 되는 여러 프로젝트가 포함될 수 있을 것이다. 예외 처리, 트랜잭션 처리, 데이터 액세스관련 추상 타입(인터페이스, 베이스 클래스)등등.

솔루션 폴더 "02 SiteFramework"에 있는 프로젝트들은 개발 프레임워크는 사이트별로 현장에서 필요한대로 제작하는 코드들이다. 이 예제에서는 FSLogger가 SiteFramework로 분류되어 있지만 파일 시스템에 로그를 남기는 일은 어느 사이트에서나 흔히 있는 일이고 따라서 FSLogger 정도의 로거는 사실 미리 만들어서 코어 프레임워크쪽에 포함시키는 것이 대부분이다. 그렇지만 앞에서부터 계속 로거를 예로 들어서 설명하고 있으므로 이 포스트의 예제도 이것으로 가겠다.

좀더 적절한 예를 들라면 아마 사용자 정보 클래스같은 것이 있을 수 있다. 사용자 정보 클래스같은 경우는 사이트마다 그 구성 변수들이 달라질 수 있다. 어떤 곳에서는 단순히 사용자 아이디만을 그 멤버 변수로 가질 수도 있고, 어떤 사이트에서는 사용자아이디, 부서코드, 전화번호등 더 많은 정보들을 포함하도록 설계할 수도 있을 것이다. 이런 경우 01 폴더에 아이디만 포함하는 베이스 클래스만 하나 만들어놓고( 코어 프레임워크에서는 사용자의 아이디만이 필요한 경우가 대부분이다), 그리고 사이트에서 이것을 상속해서 필요한 대로 추가하면 될 것이다.

현장 프로젝트에 참여해본 경험이 있는 개발자라면 "공통팀" 또는 "공통개발팀" 또는 "통합팀"등으로 불리는 개발팀이 있다는 것을 알 것이다. 업무 개발보다는 주로 전체 프레임워크나 또는 공통 모듈의 개발을 담당하는 팀이다. "02 SiteFramework" 폴더에 있는 코드는 바로 이 팀에서 작업할 부분이다. 현실적으로보면 01 폴더와 02 폴더의 구분이 없고 "공통팀"에서 함께 작업하는 경우가 대부분이다. 그러나 같은 사람이 작업하더라도 01과 02를 구분해서 작업을 하자는 것이 개인적인 의견이다. 

솔루션 폴더 "03 BusinessTask"에 업무 개발자들이 작업하는 프로젝트들이 포함됩니다. 3티어 구조로 간다면 하나의 단위 업무(Job01)에 대부분 3개의 프로젝트가 포함될 것이다. UI 프로젝트, 비즈니스 로직 프로젝트, 데이터 액세스 프로젝트. 이 예제에서는 그것을 다 구현하지는 않았다. 그 중에서 비즈니스 로직을 대표하는 프로젝트 하나만을 포함하고 있다. 개발자들은 주로 02 폴더에 포함된 사이트별 코드를 사용한다. 확장되지 않은 부분은 코어 부분도 사용하게 될 것이다.

마지막으로 시작 프로젝트로 "FSLoggerConsole"이라는 콘솔 프로젝트가 있다. 이곳에 Application Block 관련 참조가 2개 추가되어 있다.

 

Microsoft.Practices.ObjectBuilder2.dll

Microsoft.Practices.Uinity.dll

 

코드 살피기

 

이제 UAB를 사용하는 코드를 직접 보도록 하자. 코아 프레임워크에 있는 ILogger 인터페이스이다. ILogger.cs 파일을 보면 된다. 더 설명할 부분이 엄따. 코드는 알겠는데, 이 인터페이스가 무슨 의미인지, 왜 이런 인터페이스가 필요한지, 주변 시츄에이션이 이해가 되지 않으면 앞의포스트를 읽어 보기 바란다. 

 

namespace Dalbong.Framework
{
    public interface ILogger
    {
        void Write(string message);
    }
}

 

이제 이것을 사이트에서 실제로 구현하는 단계이다. 그 코드는 Site.Framework 프로젝트의 FSLogger.cs 파일을 보면 된다.  코어 프레임워크의 인터페이스를 사이트에서 확장하는 코드이다.  FSLogger 객체가 생성되는 부분에서 로그를 남기고 있다. 미리 말하면 여러번 생성해도 한번만 호출할 수도 있다는 것이다. 즉 singleton 패턴으로 설정할 수 있다는 것이다.

 

using System.IO;
using Dalbong.Framework;
namespace Site.Framework
{
    public class FSLogger : ILogger
    {
        public FSLogger()
        {  
            System.IO.File.AppendAllText(@"C:\FSLogger.log","FSLogger 생성자 호출" + Environment.NewLine);
        }
        public void Write(string message)
        {
            System.IO.File.AppendAllText(@"C:\FSLogger.log", message + Environment.NewLine);
        }
    }
}

 

다음 코드는 비즈니스 로직을 수행하는 객체를 흉내내고 있는 클래스이다. 주목할 부분은 Biz01 객체의 생성자의 인자로 ILogger 객체를 받는 부분이다. 왜 주목해야 하냐고? 나중에 보면 알겠지만, Biz01 객체를 생성할때 파라미터를 넘기지 않는뜨아. 그리고 앞에서처럼 생성자에서 생성된다는 로그를 남기고 있다.

 

using Dalbong.Framework;
using Site.Framework;
namespace Site.Job01
{
    public class Biz01
    {
        private ILogger logger;
        public Biz01(ILogger logger)
        {
            this.logger = logger;
            // 비즈니스 객체가 생성될때 로그를 남긴다.
            logger.Write("Biz01 생성자 호출" );
        }
        public void Save()
        {
            logger.Write("Save() 호출");
        }
    }
}

 

이제 이런 비즈니스 객체를 실행시켜 보자. 주목해야 할 주석이 //주목 1, //주목2, //주목 3 으로 표시되어 있다.

 

using Dalbong.Framework;
using Site.Framework;
using Site.Job01;
using Microsoft.Practices.Unity;
namespace FSLoggerConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            UnityContainer container = new UnityContainer();
           

            // 편의상 이전 로그 파일이 존재하면 삭제한다. 로그가 누적되면 테스트가 방해되잖아.
            if (System.IO.File.Exists(@"C:\FSLogger.log"))
                System.IO.File.Delete(@"C:\FSLogger.log");
           

 

            //주목 1 : ILogger와 FSLogger의 매핑 정보를 등록한다.
            container.RegisterType<ILogger, FSLogger>(new ContainerControlledLifetimeManager());
           

 

            //주목 2 : 아래 주석을 해제할때를 일러주겠다.
            //container.RegisterType< Biz01>(new ContainerControlledLifetimeManager());
            //Biz01 객체를 생성한다.
            Biz01 biz1 = container.Resolve<Biz01>();
            Biz01 biz2 = container.Resolve<Biz01>();
           

 

            // 주목 3
            //Biz01 biz1 = new Biz01();
            //Biz01 biz2 = new Biz01();
            biz1.Save();
            biz2.Save();
           

           

           //로그 파일의 내용을 콘솔에 출력한다.
            Console.WriteLine(System.IO.File.ReadAllText(@"C:\FSLogger.log"));
            Console.Read();
        }
    }
}

 

컨테이너를 하나 생성하고 있다. 그리고 로그가 누적되면 보기가 불편해서 프로그램을 실행할때마다 로그 파일을 삭제한다. 다음으로 //주목 1 부분을 보면 ILogger를 등록하고 있는데, FSLogger와 매핑된다는 것을 컨테이너에 알려주고 있다. 컨테이너에 ILogger 객체를 요구하면 이제 FSLogger 객체가 반환되어 사용될 것이다. OK? 유가릿?

 

그 다음 Biz01 타입의 객체를 생성한다고 주석을 달아놓고서는 제너릭 메소드 Resolve<Biz01>() 메소드를 호출하고 있다. 제너릭이 나온지도 꽤 오래되었고, 이제 이것에 대해서도 익숙해져야 할 것 같다.  그런 의미에서 언제 함 시간내서 정리가 필요할 것 같은데. 모르겠다. 언제가 될지. UAB 소스는 제너릭을 지원하지 않는 버전의 .NET을 지원하기 위해서 제너릭 타입을 사용하지 않는 버전의 메소드도 지원하고 있지만 최신 버전의 .NET을 위한 제너릭 버전의 메소드도 지원해주고 있다.

 

각설하고 객체를 생성하는 코드가 복잡하다. 객체를 생성하는데 new를 사용하지 않고 컨테이너의 Resolve<Type>() 메소드를 사용하고 있다. 코드가 길어진만큼 뭔가 장점이 있어야 하지 않겠는가? 당근도 없는데 편한길을 포기할 당나귀는 없다. 좀 뒤에 이 사연을 보여주겠다. 그런 다음 이제 비즈니스 객체의 메소드를 호출하고 있다. 이제 일단 실행시켜서 결과를 보자.

 

 

결과를 좀 자세히 살펴보자. FSLogger 객체가 생성되는 부분이 언제인가. 기억나는가? Biz01객체의 생성자에서 생성된다. 근데, 결과를 보면 Biz01객체는 두번 생성되었는데, FSLogger 객체는 1번만 생성되었다. 처음 Biz01 객체를 생성할때 한번만 FSLogger 객체를 생성하고서는 그 객체를 보유하고 있다가 다음번 FSLogger 객체에 요청이 오면 컨테이너는 기존의 객체를 반환해주기때문이다. 바로 컨테이너가 sigleton 패턴을 우리 대신에 구현해놓고 있다. 우리는 단지 singleton 패턴을 사용하겠다는 "표시"만 해주면 된다. 이 표시를 어떻게 하냐고?

 

container.RegisterType<ILogger, FSLogger>(new ContainerControlledLifetimeManager());

 

RegisterType() 메소드로 ILogger 타입을 등록할때 메소드 인자로 ContainerControlledLifetimeManager()의 인스턴스를 하나 생성해서 같이 넘겨주고 있다. 이것이 바로 ILogger 객체(즉 FSLogger 객체)를 sigleton으로 사용하겠다는 우리의 생각을 컨테이너에게 전달해주는 부분이다. 타입의 이름을 그대로 해석하면 컨테이너가 객체의 수명을 제어하겠다는 것인데, 컨테이너는 객체를 sigleton으로 제어하는 것이 기본으로 생각하고 있나보다.

 

여튼 타입을 등록할때 LifetimeManager란 것을 지정해서 그 타입의 객체들에 대한 수명을 제어할 수 있다는 것이다. 이런 LifetiemManger는 Unity에서 두 가지를 제공하고 있는데 앞에서 본 ContainerControlledLifetimeManager 이것이 하나이고 다른 것은 ExternallyControlledLifetimeManager이다. 이것에 대해서는 나중에 설명하겠다.

 

만약 singleton 패턴으로 사용하고 싶지 않다면. 즉 사용할때마다 생성해서 사용하고 나면 없애고 싶은 경우는? LifetimeManager없이 타입을 등록하면 된다.

 

container.RegisterType<ILogger, FSLogger>(/*new ContainerControlledLifetimeManager()*/);

 

 

Biz01객체를 생성할때 컨테이너의 Resolve<Biz01>()를 호출하고 있다. 그러나 Biz01 타입은 컨테이너에 등록하지 않았다. 컨테이너는 등록된 타입중에서 Biz01이 없다는 것을 알고 기본(?) 조건으로 이 객체를 생성한다. 기본 조건이란 것은 sigleton 패턴을 적용하지 않는다는 것이다. 그럼 new를 사용할때와 뭐가 차이가 있는가. 대단한 차이가 있다.

 

container.Resolve<Biz01>();

new Biz01();

 

일단 new사용해서 앞에서처럼 하면 빌드가 되지 않는다. 인자로 ILogger 객체를 넘겨주지 않았기 때문에. 그럼 Resolve<Biz01>()은 ? OK다! 이 메소드를 호출하면 일단 컨테이너는 생성자의 인자 목록에 포함된 타입들을 등록된 타입중에서 찾는다. 거기에서 찾게 되면 그 타입을 이용해서 인스턴스를 알아서 자동으로 생성해서 생성자를 다시 호출해준다.

 

이게 일명 "Automatic Contructor injection"이라고 부른다. 생성자를 호출할때 필요한 인자에 대한 객체를 생성해서 알아서 끼워 넣어(? inject)서 다시 생성자를 호출해준다는 것이다. 유가릿?

 

이 예제에서는 콘솔 애플리케이션의 클라이언트단에서 Unity Application Block을 사용했지만, Win, Web 애플리케이션에서도 사용할 수 있고 그리고 UI(User Interface)단에서뿐만 아니라 서버측의 서비스단 객체들의 컨테이너로도 사용할 수 있을 것이다. 해 봤냐고? 모찌롱 안해봤쪄. 그러나  안될게 뭐가 있겠어.

 

이 대장정을 계속하다보면 언제간 Enterprise Library 객체들도 이 컨테이너로 관리하는 날이 오지 않겠어. 물론 그렇게 하는 것이 타당하다는 분석이 먼저겠지만. 이미 Enterprise Library가 내부적으로 UAB를 사용하고 있다든지 하면, 또 관리할 필요가 없겠찌어.

 

이제 다음 포스트부터는 Unity Application Block에서 사용하고 있는 패턴 및 기술을 설명할 생각이다. Dependency Injection, LifetimeManager, 컨테이너를 확장하는 방법, 필요하다면 제너릭도 할까... 제너릭은 내가 정리해둔것이 없어서, 시간이 많이 걸릴것 같은데. 다음 포스트는 아직 확실히 정해지지 않았지만 되도록이면 우선 순위를 Unity 관련 부분에 두겠다.