Template Method

Template Method Pattern이란?

템플릿 메서드 패턴Template Method Pattern은 알고리즘의 구조를 정의하면서, 그 일부 단계를 서브클래스에서 구현하도록 하는 행동 패턴 중 하나입니다. 이 패턴을 통해 상위 클래스에서 알고리즘의 기본 골격을 정의하고, 하위 클래스에서 특정 단계의 세부 구현을 제공할 수 있습니다. 이를 통해 코드의 중복을 줄이고, 알고리즘의 구조를 재사용하며, 확장성을 높일 수 있습니다.

Template Method Pattern의 필요성

소프트웨어 개발 과정에서는 여러 작업이 비슷한 흐름을 따르지만, 그 중 일부 단계가 다르게 구현되어야 하는 경우가 자주 발생합니다. 예를 들어, 도서관 관리 시스템에서 다양한 형태의 보고서를 생성할 때, 보고서 생성의 흐름은 같지만 데이터 처리나 출력 형식이 다를 수 있습니다. 템플릿 메서드 패턴을 사용하면, 공통된 알고리즘 흐름을 상위 클래스에서 정의하고, 서브클래스에서 각 단계의 세부 구현을 제공하여 다양한 변형을 쉽게 관리할 수 있습니다.

Template Method Pattern 구조

D2 Diagram

  • 클라이언트 → ConcreteClassA / ConcreteClassB
    • 클라이언트는 ConcreteClassA 또는 ConcreteClassB의 인스턴스를 생성하고 TemplateMethod()를 호출합니다.
  • AbstractClass
    • TemplateMethod()는 알고리즘의 전체 뼈대를 정의하며, 내부에서 AbstractStep1()AbstractStep2()를 호출합니다.
    • AbstractStep1()AbstractStep2()는 추상 메서드로 선언되어 있어, 하위 클래스에서 구현됩니다.
  • ConcreteClassA / ConcreteClassB
    • 이들 클래스는 AbstractClass를 상속하고, AbstractStep1()AbstractStep2()를 구체적으로 구현합니다.
    • 이를 통해 알고리즘의 구조는 유지하면서도, 각 클래스별로 세부 동작을 다르게 정의할 수 있습니다.

Template Method Pattern의 구성 요소

추상 클래스Abstract Class

알고리즘의 기본 골격을 정의하고, 변해야 할 부분을 추상 메서드로 선언합니다. 이 클래스는 공통적인 알고리즘 구조를 제공하며, 서브클래스에서 구현해야 하는 메서드를 포함합니다.

구체적인 서브클래스Concrete Subclasses

상위 클래스에서 정의된 알고리즘의 골격을 따르면서, 각 단계에서 필요한 구체적인 구현을 제공합니다. 서브클래스는 상위 클래스에서 정의된 추상 메서드를 구현하여 특정 작업을 수행합니다.

템플릿 메서드Template Method

알고리즘의 여러 단계를 호출하여 전체 작업을 수행하는 메서드입니다. 이 메서드는 상위 클래스에서 구현되며, 서브클래스에서 오버라이드하지 않습니다. 템플릿 메서드는 알고리즘의 구조를 고정하고, 세부적인 구현은 서브클래스에 위임합니다.

Template Method Pattern 적용

잘못된 알고리즘 구현 방식

템플릿 메서드 패턴을 사용하지 않고, 알고리즘을 각 클래스에서 독립적으로 구현하는 경우, 공통된 로직이 반복되거나 코드가 중복될 수 있습니다. 아래는 도서관 관리 시스템에서 두 가지 유형의 보고서를 생성하는 예시입니다.

// 템플릿 메서드 패턴을 적용하지 않은 경우
public class TextReportGenerator
{
    public void GenerateReport()
    {
        string header = "도서관 보고서";
        string content = "텍스트 형식의 보고서 내용입니다.";
        string footer = "끝.";
        Console.WriteLine(header);
        Console.WriteLine(content);
        Console.WriteLine(footer);
    }
}
public class HtmlReportGenerator
{
    public void GenerateReport()
    {
        string header = "<h1>도서관 보고서</h1>";
        string content = "<p>HTML 형식의 보고서 내용입니다.</p>";
        string footer = "<footer>끝.</footer>";
        Console.WriteLine(header);
        Console.WriteLine(content);
        Console.WriteLine(footer);
    }
}

중복된 코드

TextReportGeneratorHtmlReportGenerator 클래스는 공통된 로직(헤더, 본문, 푸터 출력)을 반복적으로 구현하고 있습니다. 이로 인해 코드가 중복되며, 유지보수가 어려워집니다.

확장성 부족

새로운 보고서 형식을 추가하려면 기존 클래스에서 공통적인 로직을 다시 구현해야 합니다. 이로 인해 코드의 재사용성이 낮아지고, 새로운 형식의 보고서를 추가할 때마다 많은 수정이 필요합니다.

Template Method Pattern 적용 예시

템플릿 메서드 패턴을 사용하면, 공통된 알고리즘을 상위 클래스에서 정의하고, 세부적인 구현은 서브클래스에서 처리할 수 있습니다. 이를 통해 코드 중복을 줄이고, 새로운 형식을 쉽게 추가할 수 있습니다.

// 추상 클래스: 보고서 생성 알고리즘의 기본 골격 정의
public abstract class ReportGenerator
{
    public void GenerateReport()
    {
        PrintHeader();
        PrintContent();
        PrintFooter();
    }
    protected abstract void PrintHeader();
    protected abstract void PrintContent();
    protected abstract void PrintFooter();
}
// 구체적인 서브클래스: 텍스트 형식의 보고서 생성
public class TextReportGenerator : ReportGenerator
{
    protected override void PrintHeader()
    {
        Console.WriteLine("도서관 보고서");
    }
    protected override void PrintContent()
    {
        Console.WriteLine("텍스트 형식의 보고서 내용입니다.");
    }
    protected override void PrintFooter()
    {
        Console.WriteLine("끝.");
    }
}
// 구체적인 서브클래스: HTML 형식의 보고서 생성
public class HtmlReportGenerator : ReportGenerator
{
    protected override void PrintHeader()
    {
        Console.WriteLine("<h1>도서관 보고서</h1>");
    }
    protected override void PrintContent()
    {
        Console.WriteLine("<p>HTML 형식의 보고서 내용입니다.</p>");
    }
    protected override void PrintFooter()
    {
        Console.WriteLine("<footer>끝.</footer>");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        ReportGenerator textReport = new TextReportGenerator();
        textReport.GenerateReport();
        ReportGenerator htmlReport = new HtmlReportGenerator();
        htmlReport.GenerateReport();
    }
}

ReportGenerator 클래스

템플릿 메서드 GenerateReport()를 정의하고, 보고서 생성의 기본 구조를 제공합니다. PrintHeader(), PrintContent(), PrintFooter() 메서드는 서브클래스에서 구현해야 하는 추상 메서드로, 구체적인 보고서 형식에 따라 다른 방식으로 구현됩니다.

TextReportGenerator 클래스

ReportGenerator의 서브클래스로, 텍스트 형식의 보고서를 생성합니다. 각 단계에서 텍스트 형식에 맞게 출력합니다.

HtmlReportGenerator 클래스

ReportGenerator의 서브클래스로, HTML 형식의 보고서를 생성합니다. 각 단계에서 HTML 형식에 맞게 출력합니다.

클라이언트 코드

ReportGenerator 객체를 생성하고, 템플릿 메서드를 호출하여 보고서를 생성합니다. 각 객체는 자신에게 맞는 형식으로 보고서를 출력합니다.

Template Method Pattern 장단점

장점

코드 중복 제거

템플릿 메서드 패턴을 사용하면, 알고리즘의 공통된 부분을 상위 클래스에서 정의할 수 있어, 코드 중복이 줄어듭니다.

유연성 증가

알고리즘의 세부 구현을 서브클래스에서 제공할 수 있으므로, 다양한 방식으로 알고리즘을 확장하거나 변경할 수 있습니다. 이를 통해 새로운 요구사항에 쉽게 대응할 수 있습니다.

유지보수성 향상

공통된 알고리즘 구조를 상위 클래스에 정의함으로써, 알고리즘의 변경이 필요할 때 모든 서브클래스를 수정할 필요 없이 상위 클래스만 수정하면 됩니다. 이는 코드의 유지보수성을 크게 향상시킵니다.

단점

서브클래스 수 증가

템플릿 메서드 패턴을 적용하면, 알고리즘의 각 변형을 처리하기 위해 서브클래스가 많이 늘어날 수 있습니다. 이는 클래스의 수를 증가시키고, 코드의 복잡성을 높일 수 있습니다.

알고리즘 유연성 제한

알고리즘의 기본 구조가 상위 클래스에 고정되므로, 서브클래스에서 알고리즘의 전체 흐름을 변경하기는 어렵습니다. 이는 경우에 따라 알고리즘의 유연성을 제한할 수 있습니다.

테스트 어려움

개선 방안

중첩 클래스 사용

서브클래스가 한 곳에서만 사용되는 경우, 해당 서브클래스를 중첩 클래스(내부 클래스)로 정의하여 코드의 구조를 간결하게 유지할 수 있습니다. 이를 통해 클래스의 수를 줄이고, 관련 클래스를 그룹화 할 수 있습니다.

public abstract class ReportGenerator
{
    public void GenerateReport()
    {
        PrintHeader();
        PrintContent();
        PrintFooter();
    }
    protected abstract void PrintHeader();
    protected abstract void PrintContent();
    protected abstract void PrintFooter();
    // 중첩 클래스로 서브클래스 정의
    public class TextReportGenerator : ReportGenerator
    {
        protected override void PrintHeader() => Console.WriteLine("도서관 보고서");
        protected override void PrintContent() => Console.WriteLine("텍스트 형식의 보고서 내용입니다.");
        protected override void PrintFooter() => Console.WriteLine("끝.");
    }
}

서브클래스의 통합

서브클래스가 많아질 경우, 유사한 기능을 가진 서브클래스를 통합하여 관리할 수 있습니다. 예를 들어, 특정 메서드의 구현만 다른 서브클래스들이 있다면, 이를 통합하고 필요에 따라 메서드를 오버라이드하여 사용할 수 있습니다.

public class UnifiedReportGenerator : ReportGenerator
{
    private readonly string _header;
    private readonly string _content;
    private readonly string _footer;
    public UnifiedReportGenerator(string header, string content, string footer)
    {
        _header = header;
        _content = content;
        _footer = footer;
    }
    protected override void PrintHeader() => Console.WriteLine(_header);
    protected override void PrintContent() => Console.WriteLine(_content);
    protected override void PrintFooter() => Console.WriteLine(_footer);
}

Factory Pattern 결합

서브클래스를 생성할 때 팩토리 패턴을 사용하여 필요한 서브클래스를 동적으로 생성하고 관리할 수 있습니다. 이를 통해 클래스의 수를 줄이고, 서브클래스 생성 로직을 중앙에서 관리할 수 있습니다.

public class ReportGeneratorFactory
{
    public static ReportGenerator CreateReportGenerator(string format)
    {
        return format switch
        {
            "text" => new TextReportGenerator(),
            "html" => new HtmlReportGenerator(),
            _ => throw new ArgumentException("Unknown format"),
        };
    }
}

구성Composition 사용

서브클래스의 수를 줄이기 위해, 서브클래스 대신 컴포지션을 활용할 수 있습니다. 특정 행동을 여러 클래스에 걸쳐 재사용할 수 있도록, 해당 행동을 별도의 클래스로 분리하고, 상위 클래스가 이 클래스를 활용하도록 설계합니다. 이렇게 하면 서브클래스의 수를 줄이면서도 유연성을 유지할 수 있습니다.

public interface IReportPart
{
    void Print();
}
public class HeaderPart : IReportPart
{
    public void Print() => Console.WriteLine("도서관 보고서");
}
public class ContentPart : IReportPart
{
    public void Print() => Console.WriteLine("보고서 내용입니다.");
}
public class FooterPart : IReportPart
{
    public void Print() => Console.WriteLine("끝.");
}
public abstract class ReportGenerator
{
    private readonly IReportPart _header;
    private readonly IReportPart _content;
    private readonly IReportPart _footer;
    protected ReportGenerator(IReportPart header, IReportPart content, IReportPart footer)
    {
        _header = header;
        _content = content;
        _footer = footer;
    }
    public void GenerateReport()
    {
        _header.Print();
        _content.Print();
        _footer.Print();
    }
}

맺음말

템플릿 메서드 패턴은 알고리즘의 공통된 구조를 상위 클래스에 정의하고, 세부적인 구현은 서브클래스에서 처리할 수 있도록 하여, 코드 중복을 줄이고 유지보수성을 향상시킬 수 있는 강력한 패턴입니다. 도서관 관리 시스템과 같은 애플리케이션에서 다양한 형식의 보고서를 생성하거나, 공통된 작업 흐름을 따르는 여러 작업을 처리할 때 유용하게 사용할 수 있습니다. 다만, 서브클래스의 수가 증가할 수 있으며, 알고리즘의 유연성이 제한될 수 있으므로, 상황에 맞게 적절히 활용하는 것이 중요합니다.

심화 학습

Template Method와 Strategy

두 패턴은 행동을 캡슐화 하거나 재사용 가능한 알고리즘을 제공한다는 점에서 유사한 목표를 가지고 있습니다. 두 패턴을 결합하면 기본 알고리즘의 큰 틀은 고정하면서, 세부적인 동작은 전략 객체를 통해 런타임에 유연하게 변경할 수 있습니다. 템플릿 메서드 패턴만 사용할 경우, 알고리즘의 세부 동작을 변경하려면 새로운 서브클래스를 만들어야 하므로 유연성이 제한됩니다.

public interface IContentStrategy
{
    void PrintContent();
}
public class TextContentStrategy : IContentStrategy
{
    public void PrintContent() => Console.WriteLine("텍스트 형식의 보고서 내용입니다.");
}
public class HtmlContentStrategy : IContentStrategy
{
    public void PrintContent() => Console.WriteLine("<p>HTML 형식의 보고서 내용입니다.</p>");
}
public abstract class ReportGenerator
{
    private readonly IContentStrategy _contentStrategy;
    protected ReportGenerator(IContentStrategy contentStrategy)
    {
        _contentStrategy = contentStrategy;
    }
    public void GenerateReport()
    {
        PrintHeader();
        _contentStrategy.PrintContent();
        PrintFooter();
    }
    protected abstract void PrintHeader();
    protected abstract void PrintFooter();
}

훅 메서드 사용

템플릿 메서드에서 호출될 수 있는 선택적인 메서드를 추가하여, 서브클래스에서 알고리즘의 일부를 변경할 수 있도록 합니다. 이 훅 메서드Hook Method는 서브클래스에서 필요에 따라 오버라이드할 수 있으며, 기본 구현을 제공하여 서브클래스가 특정 단계에서 동작을 변경할 수 있는 유연성을 제공합니다.

public abstract class ReportGenerator
{
    public void GenerateReport()
    {
        PrintHeader();
        if (ShouldPrintContent())
        {
            PrintContent();
        }
        PrintFooter();
    }
    protected abstract void PrintHeader();
    protected abstract void PrintContent();
    protected abstract void PrintFooter();
    // 훅 메서드로 유연성 제공
    protected virtual bool ShouldPrintContent() => true;
}

테스트 방법 개선

의존성 주입DI 사용

테스트 시에 서브클래스에 의존하는 대신, 인터페이스나 전략 패턴을 통해 주입된 의존성을 사용하여 테스트를 쉽게 할 수 있습니다. 이를 통해 서브클래스가 아닌 의존성을 목Mock으로 대체하여 테스트할 수 있습니다.

public class TestableReportGenerator : ReportGenerator
{
    private readonly IContentStrategy _contentStrategy;
    public TestableReportGenerator(IContentStrategy contentStrategy) 
        : base(contentStrategy)
    {
        _contentStrategy = contentStrategy;
    }
    protected override void PrintHeader() => Console.WriteLine("Test Header");
    protected override void PrintFooter() => Console.WriteLine("Test Footer");
}
// 테스트 코드
var mockContentStrategy = new Mock<IContentStrategy>();
var generator = new TestableReportGenerator(mockContentStrategy.Object);
generator.GenerateReport();

상위 클래스의 메서드 모킹

상위 클래스의 메서드를 가상 메서드로 정의하고, 테스트 시 서브클래스에서 특정 메서드를 오버라이드하여 모킹Mock할 수 있도록 합니다. 이를 통해 특정 단계의 동작을 쉽게 검증할 수 있습니다.

public class TestableReportGenerator : ReportGenerator
{
    protected override void PrintHeader() => Console.WriteLine("Mock Header");
    protected override void PrintFooter() => Console.WriteLine("Mock Footer");
}
// 테스트 코드
var generator = new TestableReportGenerator();
generator.GenerateReport();