웹써핑중에 라면 만들기 강좌를 활용한 좋은 예가 있어 소개해드릴까 합니다.
원래 라면 만들기 강좌 다음 버전은 어렵지 않으면서도 활용하기 좋은 다른 패턴을 쉽게 풀어보려고 했는데
한 놈만 제대로 이해한다면 다른 패턴들을 공부할때 쉽게 수긍 할 수 있는 부분이 많다고 봤습니다.
어째든.. 그냥 긁어다 붙이면 너무 성의가 없으니 기사를 적절히 끊어서 설명하겠습니다.
Continuing with this idea of showing alternatives using the same example, I thought it might be interesting to show the Decorator Pattern which can be used when the Policy Injection Application Block and PostSharp may be overkill for your application.
In the two examples mentioned above I created a Stopwatch Attribute that timed the ILogger.Write Method used in our console application:
public interface ILogger
{
void Write(string message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message)
{
Console.WriteLine();
Console.WriteLine("*** In Logger ***");
Console.WriteLine(string.Format
("message : {0}", message));
}
}
참고로 이 기사를 쓴 분은 MS의 log 관련 application block을 쓰는 모양인데
저는 log4net을 씁니다. 그래서 관련 코드에대해서는 설명이 부족할 수 있습니다.
음.. 어째든 위 소스를 보니 ConsoleLogger는 ILogger라는 추상화된 컴퍼넌트를 구현하고 있군요.
아직 데코레이터 클래스는 나오지 않았습니다. 아마 이 놈이 라면이 될 모양입니다.
이 ConsoleLogger에 Write()라는 기능을 나중에 동적으로 추가시키려고 하는것이겠죠.
그런데 라면 만들기때는 abstract class를 사용했습니다. 왜냐하면 공통된 구현이 많아서
최상위 클래스에서 일부 구현을 해주는게 편했기 때문이죠. 어떤 객체를 추상화의 수준을
interface냐 abstract class냐는 상황에 따라 다릅니다. 거의 대부분이 Interface를 쓰지만요.
Creating these attributes is pretty simple for use in the Policy Injection Application Block and PostSharp. However, we can avoid the use of these 3rd party libraries by creating a StopwatchLoggerDecorator Class that will achieve the same results:
public class StopwatchLoggerDecorator : ILogger
{
private readonly ILogger _inner;
public StopwatchLoggerDecorator(ILogger inner)
{
_inner = inner;
}
public void Write(string message)
{
Stopwatch sw = new Stopwatch();
sw.Start();
_inner.Write(message);
sw.Stop();
Console.WriteLine();
Console.WriteLine(string.Format("Time Elapsed: {0} ms", sw.ElapsedMilliseconds));
}
}
아.. 바로 데코레이터가 만들어졌군요. 라면 만들기때와는 달리 데코레이터의 추상화는 없는것 같습니다.
이 경우를 단일 데코레이터라고 합니다. 그런데 라면 만들기때는 재료(데코레이터)가 다양했습니다.
다음과 같이 아무리 감싸도 결국 라면이되는 표현 new 재료2(new 재료1(new 라면()))..
이렇게 되기위해서 데코레이터는 자기가 감싸야 될 클래스의 추상화 클래스를 구현할 필요가 있었습니다.
c# 코드상에서 재료 as 라면과 같은 표현이 가능해야 된다고도 했었죠.
라면 만들기에서 재료라는 데코레이터는 다양했었고 각 재료들마다의 개별적인 메서드들이
있을수 있었기 때문에 재료라는 데코레이터의 추상화를 만들어주는것이 필요했습니다.
그리고 각 재료들은 그 추상화된 객체를 구현하도록 했었지요.
이것을 다중 데코레이터라고 합니다.
소스로 돌아가서.. 역시 이번 데코레이터도 ConsoleLogger가 구현한 ILogger를 구현했군요.
이건 공식에 가깝습니다. 이 패턴에서는 자기가 감싸게 될 놈의 추상화 객체를 구현해야만 합니다.
이 데코레이터는 Write()가 수행되는 시간을 계산하는 기능이 추가되어있습니다.
핵심은 역시 생성자에서 ILogger라는 자기가 감싸게 될 추상화 객체를 인자로 받는 것이죠.
이런식으로 내부적으로 다른 객체를 생성하지 않고 관계를 맺는것이 모든 패턴들의 특징입니다.
(이런 관계를 is-a 관계라고 하고 반대로 객체안에서 객체를 생성하게 되는것을 has-a관계라고 합니다.)
우리는 Write() 기능이 데코레이터 되었다는걸 명확히 알 수 있습니다.
ILogger를 구현한 클래스의 Write()를 재사용하면서 그 기능에 스톱와치 클래스의 기능까지 추가했습니다.
그런데 Stopwatch라는 클래스는 우리가 소스를 건들일수 없는 상용 컴퍼넌트였다면..
생각해보세요. Log에 그 기능을 추가하기 위해선 applicatoin block의 소스를 뜯어 고치던가 했어야 했던지
합치지 못하고 분리해서 호출했어야 할지도 모르죠. 데코레이터 패턴을 안다면 그런 생각자체를 못하겠지만..
어째든 이름도 잘 지었네요. 이 데코레이터의 이름은 그래서 StopwatchLoggerDecorator 이군요.
이런식으로 네이밍 하는것은 권장되는 사항입니다. 코드만 봐도 그 의도가 드러나기 때문에
따로 주석을 쓸 이유도 없습니다.
아래에 이 데코레이터를 적용한 예가 있습니다.
The Decorator Class implements the same ILogger Interface and takes the "real" ILogger in its constructor, ConsoleLogger. Inevitably it passes the Write Call onto the real logger, but before doing so starts a Stopwatch. After the call to ConsoleLogger is finished ( _inner.Write ), it stops the Stopwatch and displays the elapsed time in the console.
In keeping with the examples mentioned above, one can register the ILogger in your IoC Container / Dependency Injection Tool of choice. Here is an example using Unity:
IUnityContainer container = new UnityContainer();
// Register Logger
ILogger consoleLogger = new ConsoleLogger();
ILogger logger = new StopwatchLoggerDecorator(consoleLogger);
container.RegisterInstance<ILogger>(logger,
new ContainerControlledLifetimeManager());
// Use Logger
var registeredLogger = container.Resolve<ILogger>();
registeredLogger.Write("Hello!");
Console.ReadLine();
The results are as follows:
위 소스에서 데코레이터를 적용한 부분은 아래와 같습니다.
ILogger consoleLogger = new ConsoleLogger();
ILogger logger = new StopwatchLoggerDecorator(consoleLogger);
이 부분만 보세요. 라면 만들기때와 마찬가지로 라면을 재료로 요리했군요.
간단히 logger.Write(); 써주면 결과화면처럼 나오게 됩니다.
(Write()의 호출이 원문과 다르긴 하지만.. 실제로 저렇게 써도 되는거니 넘어가죠. -_-)
이 기사를 쓴 목적이 ConsoleLogger를 변경하지 않고도 새로운 기능을 추가하는것이였는데 성공적입니다.
그런데 우리의 고객이 이왕 출력되는거 html 출력으로도 보고 싶어합니다.
여러분은 어쩌시겠습니까? Write() 메서드를 오버로딩하시겠다고요? 왜이러시나요..
(한번 해보세요.. 되는지.. -_-)
Write() 메서드에 인자를 받게 만들어 내부에서 if를 써서 분기를 하시겠다고요? 역시 안됩니다.. -_-
HTMLLoggerDecorator를 만드셔야죠. 그리곤 사용은 아래와 같이 쓸수있어야 합니다.
new HTMLLoggerDecorator(consoleLogger);
또는 new HTMLLoggerDecorator(new StopwatchLoggerDecorator(consoleLogger))
이 HTMLLoggerDecorator는 어떻게 구현해야 할까요? (html 태그를 어렵게 적용시키려 하지 마세요-_-)
참고로.. new StopwatchLoggerDecorator(new HTMLLoggerDecorator(consoleLogger))와 같이 쓰게되면
이상한 출력결과가 나올수 있습니다. 감싸는 순서가 HTMLLoggerDecorator를 만든 의도와는 달리 사용이
되었기 때문인데요. 이런 문제들은 팩토리나 빌더같은 다른 생성패턴들과 결합해서 써야 될 필요성이
있습니다. 팩토리나 빌더는 다음에 좋은 예가 떠오르면 글을 올려보도록 하겠습니다.
이 글이 패턴을 이해하는데 조금이나마 도움이 되었기를 바랍니다.
전체 기사 링크
http://codebetter.com/blogs/david.hayden/archive/2008/09/30/decorator-pattern.aspx
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=18&MAEULNO=8&no=1635&page=1