본문 바로가기

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

개발 프레임워크 만들기 대장정 08- 사용자 정보, 애플리케이션 컨텍스트 객체 추가하기

앞 포스트에서 UAB를 사용해서 개발 프레임워크 구조와 유사하게 로깅 프로그램을 구현했다. 프로젝트가 한참 진행되어 개발 진척도는 50%를 넘어가고 있다. 그런데 어느날 고객이 다음과 같이 무심코 내뱉는다.

"현재 비즈니스 메소드를 호출할때 로그를 남기고 있는데, 로그로 사용자 아이디와 이름도 남기고 싶습니다".

업무 프로그램에서 고객 아이디와 이름만 로깅 메소드 Write()로 넘겨주면 되겠지하는 생각이 먼저 떠오르는가?  그럼 불쌍한 개발자는 다시 모든 프로그램을 수정해야 하는가? 그리고  다른 프로젝트에서는 사용자의 아이디, 이름뿐만 아니라 부서 정보도 남겨달라고 하면 어떻게 해야 하나. 그때는 프레임워크의 코드를 또 수정해야 하나?

두 번째 문제부터 해결해보자.  이를 위해서 필자는 사용자 정보 클래스를 다음과 같이 설계하겠다. 

UserInfo 클래스는 코어 프레임워크쪽에서 사용하는 타입니다. 그리고 SiteUserInfo는 사이트별로 변경될 수 있는 클래스이다.

사용자라면 최소한 아이디와 이름은 있을 것이다.  ID 속성의 실제 값이 사원 번호인 경우도 있을 것이다. 필자는 두 속성을 어떤 사이트의 사용자 정보 클래스에 포함될 수 있는 필수 속성으로 보았다. 그리고 코어 프레임워크에서는 대부분 비즈니스 로직과 관련된 정보는 사용하지 않는다. 따라서 다음과 데이터를 구성했다.

namespace Dalbong.Framework
{
    public class UserInfo
    {
        private string _id = "";
        private string _name = "";

        public UserInfo(string id )
        {
            _id = id;

            // id를 통해서 이름을 DB에서 조회해온다. 
            _name = "달봉이";
        }

        public virtual string ID
        {
            get
            {
                return _id;   
            }
        }
        public virtual string Name
        {
            get
            {
                return _name;
            }
        }
    }
}

다음은 사이트별로 필요한 속성을 추가하는 SiteUserInfo를 다음과 같이 정의했다. 부서 코드를 받을 수 있는 속성을 추가했다. 생성자의 인자로 사용자 아이디를 건네받아서 필요한 정보를 생성자에서 채우도록 하고 있다.

namespace Site.Framework
{
    public class SiteUserInfo : UserInfo
    {
        // 부서코드
        string _deptCode = "";

        public SiteUserInfo(string id)
            : base(id)
        {
            // id를 통해서 부서코드를 DB에서 조회홰온다.
            _deptCode = "HR";
        }
        public string DeptCode
        {
            get
            {
                return _deptCode;
            }
            set
            {
                _deptCode = value;
            }
        }
    }
}

이제 첫번째 문제를 알아보자. 새로운 로깅을 위해서 로깅에 필요한 정보를 로거 객체의 Write()의 인자로 넘겨주도록 코드를 수정할 수는 없다. 결국 전역적 성격을 띄는 변수를 만들어서 로거 객체 내부에서 접근할 수 있도록 해야 한다. 이렇게 하면 공통팀에서만 수정해주면 모든 업무 개발자들이 수정을 하지 않도록 할 수 있다.

사용자 정보를 전역 변수로 만드는 방법에는 여러가지가 있다. AppDomain 객체를 사용할 수도 있고, CallContext라는 것을 사용해서도 해결할 수 있다.  필자는 코어 프레임워크 프로젝트에 컨텍스트 클래스를 하나 만들었다. 그래서 이 클래스의 정적 멤버에 사용자 정보를 저장해두고 애플리케이션의 어디에서든지 사용자 정보에 접근할 수 있도록 했다.  컨텍스트 클래스명을 DalbongAppContext라고 해서 다음과 같이 정의했다.

namespace Dalbong.Framework
{
    public class DalbongAppContext
    {
        static DalbongAppContext _self = null;
        static Microsoft.Practices.Unity.UnityContainer _container = null;
        private DalbongAppContext()
        {
            // 공개적인 생성을 막는다.
        }
        public static DalbongAppContext Current
        {
            get
            {
                if (_self == null)
                    _self = new DalbongAppContext();
                return _self;
            }
        }
        public Microsoft.Practices.Unity.UnityContainer Container
        {
            get
            {
                return _container;
            }
            set
            {
                _container = value;
            }
        }
    }
}


외부의 코드에서는 정적 멤버인 Current 속성을 통해서만 이 객체에 접근할 수 있다. 생성자는 private 버전만 정의해서 외부에서 공개적으로 생성하는 것을 막고 있다. 생성자를 private로 하는 것은 자주 접할 수 있는 것은 아니라서 무슨 말인지 갸웃하는 개발자도 있을 듯 싶다. 생성자를 생략하면 기본적으로 public 생성자를 사용할 수 있지만 생성자를 정의했는데, private 생성자만 있으면 외부에서 new같은 것을 통해서 생성할 수 없다는 것을 말한다.

Current 속성이 static이므로 DalbongAppContext.Current와 같이 접근할 수 있다. 이 속성이 호출되었을때 그 내부에서는 null인지를 확인해서 그렇다면 객체를 생성해서 반환한다. 아니라면 이전의 객체를 반환한다. 이렇게 하면 애플리케이션 전체에서 인스턴스 하나만 존재하게 된다. 이것이 바로 Singleton 패턴이다.

컨텍스트 객체에는 Container 속성이 정의되어 있는데 이것은 Program.cs에서 정의한 UnityContainer 객체에 대한 참조를 저장해서 애플리케이션 어디에서든지 접근할 수 있도록 하기 위해서이다. 사용자 정보도 이 컨테이너에 인스턴스가 저장될 것이다. 그래서 로거 객체에서도 DalbongAppContext의 Container 속성을 통해서 Unity 컨테이너에 저장된 사용자 정보 객체에 접근할 것이다.

다음은 새로운 타입을 정의하고 있는 파일을 추가한 개발 구조이다.

Program.cs 파일의 Main 메소드 내용을 보면 다음과 같다.

namespace FSLoggerConsole
{
    class Program
    {
        static void Main(string[] args)
        {

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

            // 컨테이너 생성
            UnityContainer container = new UnityContainer();

            //DalbongAppContext에 현재 컨테이너 참조 저장
            DalbongAppContext.Current.Container = container;

           //UserInfo 매핑 정보를 등록한다. sigleton 패턴 사용
            //container.RegisterType<UserInfo, SiteUserInfo>(new ContainerControlledLifetimeManager());

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

            SiteUserInfo userinfo = new SiteUserInfo("dalbong2");
            container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());

            //Biz01 객체를 생성한다.
            Biz01 biz1 = container.Resolve<Biz01>();
            //Biz01 biz2 = container.Resolve<Biz01>();

            biz1.Save();
            //biz2.Save();

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

        }
    }
}

사용자 정보 객체는 보통 사용자가 로그인하고 나서 애플리케이션이 시작할때 생성된다. 앞의 코드중에서 다음과 같은 부분이 있다.

SiteUserInfo userinfo = new SiteUserInfo("dalbong2");
container.RegisterInstance<UserInfo>(userinfo, new ContainerControlledLifetimeManager());

SiteUserInfo 객체를 생성(UserInfo 객체가 아니다)해서 컨테이너에 객체를 등록하고 있다.

이 코드를 보면 RegisterType<>() 메소드를 사용하지 않고, UnityContainer의 RegisterInstance<>() 메소드를 사용해서 이미 생성되어 있는 인스턴스를 등록하고 있다. UnityContainer는 타입뿐만 아니라 이렇게 인스턴스를 직접 등록할 수도 있다는 것이다. 이후 UnityContainer의 Resolve<UserInfo>()를 통해서 그 인스턴스를 반환받을 수 있다. RegisterInstance<>()에서 Singleton 패턴을 표시하는 ContainerControlledLifetimeManager 객체를 넘겨주고 있으므로 사용자 정보 객체는 하나만 생성되어 컨테이너에 보관된다.

사실 처음에는 이 코드 대신에 초록색으로 되어 있는 부분을 사용하려고 했다. 근데, 에러였다. 무엇때문인지는 모르겠다. 찾다 그만 뒀다. 내일 출근해야해.....흑!  닝기리!!

그림은 컨테이너가 타입 뿐만 아니라 그림처럼 인스턴스도 직접 포함시킬 수 있다는 것을 보여주고 있다.

그리고 나중에 알아보겠지만(언젤지는...) 컨테이너의 익스텐션이란 것도 만들어서 등록해서 사용할 수 있다. 이것은 Unity 컨테이너의 기능을 확장할 수 있는 사용자 정의 방법이다.

이제 비즈니스 객체의 메소드를 호출할때 사용자 정보를 남기도록 하자. 누가 한다고요? 개발자, 공통팀? 공통팀!

FSLogger 클래스의 Write() 메소드를 다음과 같이 수정했다.

public void Write(string message )
{
    SiteUserInfo userinfo =(SiteUserInfo) DalbongAppContext.Current.Container.Resolve<UserInfo>();
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("ID:{0}, 이름:{1}, 부서:{2}, 메세지:{3}", userinfo.ID, userinfo.Name, userinfo.DeptCode,message);
    //sb.Append(message);
    System.IO.File.AppendAllText(@"C:\FSLogger.log", sb.ToString() + Environment.NewLine);
}

DalbongAppContext.Current.Container속성을 통해서 Unity 컨테이너로 부터 사용자 정보 객체를 얻어오고 있다. 그리고 나서 SiteUserInfo 로 캐스팅한다. 이렇게 하면 사이트별로 추가 수정한 사용자 정보 객체를 사용할 수 있게 되는 것이다.

뭔가 의미있는 결론으로 글을 마쳐야 하는데, 항상 결론을 정리할때쯤 되면 체력이 부족하다. 오늘도 역시나 피곤하다.  간결한 결론을 내릴 힘이 없다.

그리고 이런 구조의 프레임워크 설계대신에 자신이 생각하고 있는 구조가 있다면 필자에게 좀 알려주길 바란다.


시작 프로젝트 FSLoggerConsole 부분은 프로젝트의 공통팀에서 수정한다.  이 부분의 코드는 대부분 등록하고 애플리케이션을 초기화하는 코드가 주로 있게 된다. 타입이나 인스턴스 그리고 컨테이너의 익스텐션을 등록하는 부분은 config 설정으로 빼내는 것이 코드가 간결해 질 수 있다. 이 부분은 다음 포스트로 넘겨서 간단히 정리하도록 하겠다. 

만들어 놓고 보니까 구조가 좀 어색한것 같다. 쓰으~~