본문 바로가기

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

개발 프레임워크 만들기 대장정 03- 인터페이스, 상속, 가상 메소드

Unity Applicaion Block에서는 컨테이너라는 개념을 사용하고 있다. 그 컨테이너를 Unity라고 부르고 있다. 컨테이너라는 것이 무엇을 담고(contain), 그것이 뭘 하는지를 생각해보자.  그러나 그 이전에 잠시 다른 문제에 시간을 할애해야 할 것 같다. 처음에는 바로 컨테이너 개념 설명으로 가려 했으나 이것을 설명하기 전에 인터페이스, 베이스 클래스 그리고 상속을 먼저 생각해봐야 할 것 같다.

 

앞의 포스트에서 확장성 얘기를 하면서 로깅 예를 들었다.  로깅 저장소가 어디냐에 따라서 로깅하는 로직 구현은 달라질 것이다. 데이터베이스에 로깅하는 넘은 DBLogger라고 하고 파일 시스템에 로깅을 하는 넘을 FSLogger라고 이름을 붙이자. DBLogger는 데이터베이스를 연결해서 그곳에 insert 쿼리문을 날려야 할 것이고 FSLogger는 파일 시스템에 파일을 생성하고 파일에 텍스트를 쓰는 일을 해야 한다.

 

현장의 사이트에서는  어떤 Logger를 사용해야 할지 결정해야 한다. 그 결정을 프레임워크에 알려줘야 한다. 요즘은 그런 결정을 프레임워크에 알려주는 방식으로 config 파일을 주로 사용한다. config 파일에 어떤 Logger를 사용할지 설정하고 개발 프레임워크는 런타임시에 그 설정을 읽어서 프레임워크 내의 객체가 Logger를 요청할때 동적으로 실질적인 Logger(Real Logger)를 생성해서 사용한다. 이런 실제 Logger에 대한 정보는 config를 사용하지 않고 직접 프레임워크에 알려주는 방식도 제공한다. 즉 프레임워크의 API를 통해서 프로그램적으로 설정할 수도 있다.

 

이제 인터페이스에 대한 얘기를 할때가 된 것 같다. 

 

개발 프레임워크 입장에서 확실한 것은 로그를 써야 한다는 액션 그 자체뿐이다.  상황은 이렇다. "로그를 써야 하는 공통적인 액션 이 있기는 하지만 그 액션이 실제로 어떻게 구현되어야 하는지는 아직 알 수 없다 ". 즉 개발 프레임워크 입장에서는 그 액션이 실제로 어떻게 구현되는지는 알 바가 아니다.  개발 프레임워크에서는 로그를 남기도록 요청할 수 있는 메소드만 제공해주면 되는 것이다. 즉 "내(개발 프레임워크)가 요청을 하면 실제로 어떤 넘인지에 따라서  원하는 대로 로깅을 하면 된다"는 것이다. 개발 프레임워크가 바라보는 넘과 실제로 로깅을 하는 넘이 다르다는 것을 말하고 있는 것이다. 개발 프레임워크가 바라보는 넘은 단지 요청을 전달하는 역할만을 한다. 요청을 실제로 처리하는 넘은 따로 있다. 지금 상황을 그림으로 표현하면 다음과 같다.

 

 

개발 프레임워크 내의 객체는 요청 전달자만 알 수 있다. 실제로 어떤 처리자로 전달될지는 모른다. 전달받은 요청을 어떤 전달자로 전달해야 하는지는 런타임이 되어서야 알 수 있다.  런타임시에 요청 전달자가 가지고 있는 실제 처리자에 대한 참조가 가리키고 있는 인스턴스를 찾아가서 그 인스턴스에게 전달하는 것이다.

 

이런 시나리오에서의 요청 전달자를 객체 지향적인 언어로 표현하면 인터페이스(interface)라고 한다( 지금은 인터페이스를 타입으로 말하는 것이 아니라 실제의 인스턴스 참조를 갖는 객체를 의미한다). interface라는 영단어는 두 물체가 서로 맞닿아 있는 부분이라고 한다. 서비스를 요청하는 객체(여기서는 프레임워크내 객체)와 실제 서비스를 제공하는 객체 사이의 경계에 존재한다. 추상(abstract)과 실제(concrete)의 경계를 인터페이스가 연결해주고 있는 것이다. 

 

인터페이스는 프레임워크 구현에서 자주 사용되는 방식으로서 훌륭한 객체 지향 기술이다. 유연하고 확장성있는 프레임워크를 만들어내기위해서는 인터페이스 개념을 잘 이해하고 잘 활용할 수 있어야 한다. 앞에서 이야기한대로, 사용자(앞에서의 예라면 개발 프레임워크내의 객체가 되겠다)의 입장에서는 어떤  기능이 있을 것이라는 것만은 확실하지만 그 기능을 실제 객체에서는 구체적으로 어떻게 구현하는지는 알 수 없는 경우 또는 그 구현 방법이 다양할 수 있는 경우라면 인터페이스를 사용할 수 있는지를 먼저 생각해봐야 한다.

 

앞의 시나리오에서 요청 전달자를 Logger로 표현하고 있는데, 이 녀석이 인터페이스이다. 개발 프레임워크내에서는 이 녀석만 바라보면 되는 것이고 실제로 기능의 구현은 이 녀석을 상속한 다른 넘들이 한다. 인터페이스의 문법적 표현은 익히 아는 바와 같다.

public interface ILogger
{
    void Write(string message);
}

프레임워크내에서 참조할 ILogger(I를 접두어로 붙이는 것이 마이크로소프트에서 권장하는 인터페이스에 대한 명명규칙이다)는 Write()라는 공통 기능만을 표시하고 있으면 된다. Write()를 실제로 구현(implement)하는 것은 이 인터페이스를 상속하는 실제 클래스들이다. 다음은 ILogger를 실제로 구현하는 FSLogger를 간단히 흉내냈다.

 

public class FSLogger : ILogger
{
    public void Write(string message)
    {
        System.IO.File.AppendAllText("C:\Log.log", message );
    }
}

부모 타입이 인터페이스인 경우는 "구현한다(implement)"라고 표현하지만 부모 타입이 클래스인 경우는 "상속한다(inherit)"라고 표현한다. 그러나 그 의미는 동일하다. 이렇게 정의하고 나면 다음과 같은 코드가 가능하다는 것은 익히 알고 있다.

 

ILogger logger = new FSLogger();

 

메모리는 다음과 같은 상태에 있게 된다.

 

 

logger에는 FSLogger에 대한 객체에 대한 참조가 할당된다. 인터페이스에 포함된 메소드는 모두 가상 메소드(virtual method)이다. 일반 메소드와 가상 메소드가 어떻게 다른지 알아보도록 하자. 인터페이스와 함께 가상 메소드의 상속은 중요한 개념이다. 다음 코드가 실행된다고 해 보자.

 

logger.Write();

 

런타임은 우선  logger객체로 이동해서 Write()를 실행하려고 한다. 그러나 그 메소드가 가상 메소드인 것을 확인한다. 현재 호출된 메소드가 가상 메소드라는 것이 확인되면 런타임은 logger가 가리키고 있는 실제의 인스턴스로 이동한다. 그런 다음 그 인스턴의 Write() 메소드를 실행한다. 그림에서는 FSLogger 인스턴스의 Write()가 실행되는 것이다.

 

이처럼 가상 메소드가 호출될때는 현재 실제의 인스턴스를 확인해서 그 인스턴스의 메소드를 실행하는 것이다.  가상 메소드는 분명 일반 메소드가 호출되는 경로가 다르다. 참고로 런타임이 메소드를 가상 메소드로 인식하는 경우는 몇 가지 경우가 있습니다. 앞에서처럼 인터페이스에 포함된 메소드는 아무 표시가 없더라도 모두 가상 메소드가 된다. 그리고 일반 타입의 메소드에 virtual이 붙게 되면 가상 메소드가 된다. 그런 메소드는 자식 타입에서 override를 붙여서 메소드를 재정의할 수 있고 이렇게 오버라이드한 메소드를 다시 자식 타입에서 오버라이드할 수 있다. 이렇게 오버라이드로 재정의한 메소드도 가상 메소드가 된다.

 

만약 개발 프레임워크내에서 다음과 같은 타입의 객체가 사용되고 있다고 해 보자.

 

class A
{
    ILogger logger = null;

    public ILogger Logger
    {
        set
        {
        logger = value;
        }
    }

    public void operate()
    {

         ...

         logger.Write();

        ...

    }

}

특정 메소드에서 ILogger 타입의 logger객체를 사용하고 있다.  프레임워크 외부에서는 실제로 어떤 타입의 로거 객체를 사용할 것인가에 대한 정보를 프레임워크에 알려준다. 프레임워크 외부에서 실제 타입을 알려준다는 것은 예를 들어 다음과 같은 config 설정을 이용하는 것을 말한다.

<types>
          <type type="Dalbong.Framework.ILogger,Dalbong.Framework"
                 mapTo="Site.Framework.FSLogger,Site.Framework">
                <lifetime type="singleton" />
          </type>
</types>

그럼 logger에 실제(concrete)의 인스턴스를 생성하고 할당하는 작업은 프레임워크에서 담당할 것이다.  이렇게 인터페이스를 이용한 패턴의 코드를 프레임워크에 두면  앞에서 설명한 처럼 프레임워크에서는 실제 어떤 로거를 사용하는지에 대해서는 몰라도 된다.

 

외부에서 제공하는 실제 로거 타입에 대한 정보를 받는 것이 Unity Application Block 프레임워크에서는 Unity라 불리는 녀석이다. Unity는 실제 타입을 받아서 실제로 객체를 생성시켜 A 클래스의 logger 객체에 할당해준다. 그러자면 A 클래스는 로거 인스턴스를 할당할 수 있는 setter 속성이 정의되어 있어야 할 것이다. Unity가 실제 타입을 생성하는 것에 대해서는 다음 아니 그 다음 포스트에서 알아보겠다. 다음 포스트에서는 베이스 클래스와 인터페이스를 잠깐 비교해보도록 하겠다. 이 포스트에서 모두 설명하려 했는데 너무 길어지는 듯해서 다음 포스트로 넘기겠다.