티스토리 뷰

반응형

안녕하세요, 견고하고 유연한 소프트웨어 아키텍처를 꿈꾸는 모든 개발자 여러분! 오늘은 객체 지향 설계의 두 기둥이자, 코드의 재사용성과 유연성을 극대화하는 템플릿 메서드 패턴(Template Method Pattern)전략 패턴(Strategy Pattern)을 깊이 있게 탐구하는 시간을 가져보겠습니다. 이 두 패턴은 각각 '상속(Inheritance)'과 '위임(Delegation, 또는 구성/Composition)'이라는 객체 지향의 근본적인 메커니즘을 활용하여 변화에 강한 코드를 구축하는 방법을 제시합니다.

이번 글에서는 단순한 정의를 넘어, 두 패턴의 작동 원리, 핵심적인 차이점, 그리고 실제 개발 시나리오에서 어떤 패턴을 선택해야 할지 명확한 가이드를 제시해 드릴 것입니다. 객체 지향 프로그래밍에 대한 기본적인 이해를 가진 개발자나 컴퓨터 공학 전공 학생이라면, 이 글을 통해 디자인 패턴 학습의 깊이를 더하고, 실무에서 마주하는 복잡한 문제들을 우아하게 해결할 수 있는 통찰력을 얻게 되실 것입니다. 지금부터 저와 함께 객체 지향 설계의 지평을 넓혀나갈 준비가 되셨나요?


객체 지향 설계와 디자인 패턴의 중요성: SOLID 원칙과 효율적인 개발

우리가 개발하는 소프트웨어는 끊임없이 변화하고 진화합니다. 새로운 요구사항, 기능 수정, 성능 개선 등 변화의 물결 속에서 코드가 복잡하고 엉망진창이 된다면, 작은 수정 하나가 시스템 전체에 예상치 못한 부작용을 일으키고 개발 생산성은 저하될 것입니다.

여기서 바로 객체 지향 설계(Object-Oriented Design, OOD)의 중요성이 부각됩니다. 객체 지향 설계는 소프트웨어를 유연하고, 재사용 가능하며, 확장 가능하고, 유지보수하기 쉽게 만들기 위한 방법론입니다. 캡슐화, 상속, 다형성, 추상화와 같은 객체 지향의 4대 원리를 통해 이러한 목표를 달성하고자 합니다.

하지만 이러한 원리만으로는 때때로 충분치 않습니다. 수많은 개발자가 오랜 시간 동안 다양한 문제를 해결하며 공통적으로 발견한 "반복적으로 발생하는 문제"와 그에 대한 "검증된 해결책"이 있습니다. 이것이 바로 디자인 패턴(Design Pattern)입니다. 디자인 패턴은 특정 컨텍스트 내에서 반복되는 설계 문제에 대한 일반화된 해결책으로, GoF(Gang of Four)에 의해 널리 알려진 23가지 패턴이 대표적입니다.

디자인 패턴 학습은 단순히 코드 조각을 외우는 것을 넘어, 객체 지향 설계 원칙(예: SOLID 원칙)을 실제 코드에 어떻게 적용할 것인지에 대한 실질적인 가이드를 얻는 과정입니다. 특히 개방-폐쇄 원칙(Open/Closed Principle, OCP)은 "확장에는 열려 있고, 변경에는 닫혀 있어야 한다"는 원칙으로, 디자인 패턴의 많은 부분이 이 원칙을 준수하도록 돕습니다. 즉, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있도록 하는 것이죠.

디자인 패턴은 다음과 같은 이점을 제공합니다:

  • 공통 언어 제공: 개발자 간의 의사소통을 원활하게 합니다. "여기에는 전략 패턴을 적용하면 좋겠어!"처럼 복잡한 설계 아이디어를 한 문장으로 명확히 전달할 수 있습니다.
  • 검증된 해결책 적용: 이미 수많은 프로젝트에서 검증된 효과적인 설계 방식을 적용하여 시행착오를 줄이고 코드 품질을 향상시킵니다.
  • 유연하고 확장 가능한 코드 작성: 변화에 강한 코드를 작성하여 장기적인 유지보수 비용을 절감합니다.
  • 객체 지향 원칙의 이해 증진: 패턴을 학습하는 과정에서 자연스럽게 캡슐화, 상속, 다형성 등의 객체 지향 핵심 개념을 심화 학습하게 됩니다.

결론적으로, 디자인 패턴은 복잡한 소프트웨어 시스템을 설계하고 구현하는 데 있어 강력한 도구이자 지혜의 보고입니다. 이제 이 중요한 배경 지식을 바탕으로, 템플릿 메서드 패턴과 전략 패턴의 세계로 깊이 들어가 보겠습니다.


템플릿 메서드 패턴 심층 분석: 상속으로 구현하는 고정된 흐름

어떤 작업을 수행할 때, 그 작업의 '전체적인 흐름'은 변하지 않지만, 중간 중간의 '세부적인 단계'는 상황에 따라 달라져야 하는 경우가 있습니다. 예를 들어, 음료를 만드는 과정은 정해진 순서가 있지만, 재료와 끓이는 방식은 음료 종류에 따라 달라질 수 있습니다. 템플릿 메서드 패턴은 바로 이러한 시나리오에 완벽하게 들어맞는 디자인 패턴입니다.

템플릿 메서드 패턴의 정의와 핵심

템플릿 메서드 패턴알고리즘의 골격(template)을 정의하고, 알고리즘의 일부 단계를 서브클래스에서 구현하도록 하는 패턴입니다. 이 패턴을 사용하면 알고리즘의 구조는 변경하지 않으면서 서브클래스에서 특정 단계를 재정의할 수 있습니다. 즉, 변하지 않는 부분은 상위 클래스에 고정하고, 변하는 부분은 하위 클래스에 맡기는 것이 핵심입니다.

여기서 가장 중요한 메커니즘은 바로 상속(Inheritance)입니다. 상위(부모) 클래스는 전체 알고리즘의 템플릿(골격)을 정의하는 '템플릿 메서드'를 가지며, 이 메서드는 일련의 추상(abstract) 또는 오버라이드 가능한(hook) 메서드를 호출합니다. 하위(자식) 클래스는 이 추상 메서드들을 구체적으로 구현함으로써, 전체 알고리즘의 흐름은 유지하되 자신만의 특정한 행동을 주입하게 됩니다.

템플릿 메서드 패턴의 구조

템플릿 메서드 패턴은 주로 다음과 같은 두 가지 주요 구성 요소로 이루어집니다.

  1. 추상 클래스 (Abstract Class):
    • 템플릿 메서드 (Template Method): 알고리즘의 골격을 정의하는 메서드입니다. 일반적으로 final로 선언하여 서브클래스에서 오버라이드할 수 없도록 하여, 알고리즘의 고정된 흐름을 보장합니다. 이 메서드 내에서 추상 메서드와 구체 메서드를 순서대로 호출합니다.
    • 추상 메서드 (Abstract Methods, Primitive Operations): 서브클래스에서 반드시 구현해야 할 알고리즘의 가변적인 단계들을 정의합니다.
    • 구체 메서드 (Concrete Methods): 모든 서브클래스에서 공통적으로 사용되는, 변하지 않는 단계들을 구현합니다.
    • 훅 메서드 (Hook Methods): 서브클래스에서 선택적으로 오버라이드할 수 있는 메서드입니다. 기본적으로 아무것도 하지 않거나 기본 동작을 제공하여, 서브클래스가 필요할 때만 기능을 확장할 수 있도록 합니다.
  2. 구체 클래스 (Concrete Class):
    • 추상 클래스를 상속받아 추상 메서드들을 구체적으로 구현합니다. 이를 통해 각 구체 클래스는 고정된 알고리즘 흐름 속에서 자신만의 특정 행동을 정의합니다.
반응형

템플릿 메서드 패턴 구조

템플릿 메서드 패턴의 동작 방식

템플릿 메서드는 상위 클래스에 정의된 일련의 작업을 순서대로 실행합니다. 이 작업들 중 일부는 상위 클래스에서 이미 구현되어 고정되어 있고, 다른 일부는 추상 메서드로 선언되어 있어 하위 클래스에서 오버라이드(재정의)하도록 강제됩니다. 훅(Hook) 메서드는 하위 클래스가 특정 지점에서 코드를 삽입할 수 있는 선택적 기회를 제공합니다.

예를 들어, 음료를 만드는 BeverageMaker 추상 클래스가 있다고 가정해 봅시다. prepareBeverage()라는 템플릿 메서드 안에서 boilWater(), brew(), pourInCup(), addCondiments()와 같은 메서드들을 순서대로 호출합니다. boilWater()pourInCup()은 모든 음료에 공통이므로 BeverageMaker에서 구현할 수 있습니다. 하지만 brew()addCondiments()는 음료 종류에 따라 달라지므로 추상 메서드로 선언하여 CoffeeMakerTeaMaker 같은 하위 클래스에서 구현하게 하는 것이죠.

예시 코드 (Java)

이해를 돕기 위해 간단한 게임 플레이 과정을 모델링하는 예시 코드를 살펴보겠습니다. 모든 게임은 시작, 플레이, 종료라는 기본적인 흐름을 가지지만, 각 단계의 세부 구현은 게임마다 다를 수 있습니다.

// 추상 클래스: 게임의 골격을 정의합니다.
abstract class Game {
    // 템플릿 메서드: 게임의 전체적인 흐름을 정의합니다.
    // 이 메서드는 final로 선언하여 서브클래스에서 오버라이드할 수 없도록 합니다.
    public final void play() {
        initialize();     // 게임 초기화 (변하는 부분)
        startPlay();      // 게임 시작 (변하는 부분)
        endPlay();        // 게임 종료 (변하는 부분)
        displayWinner();  // 승자 표시 (훅 메서드)
    }

    // 추상 메서드: 서브클래스에서 반드시 구현해야 하는 변하는 부분입니다.
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();

    // 훅 메서드: 서브클래스에서 선택적으로 오버라이드할 수 있는 부분입니다.
    // 기본적으로 아무것도 하지 않거나 기본 동작을 제공합니다.
    void displayWinner() {
        System.out.println("게임이 종료되었습니다. 승자를 표시합니다.");
    }
}

// 구체 클래스 1: 축구 게임
class Football extends Game {
    @Override
    void initialize() {
        System.out.println("축구 게임을 초기화합니다. 선수들을 필드에 배치합니다.");
    }

    @Override
    void startPlay() {
        System.out.println("축구 게임을 시작합니다. 킥오프!");
    }

    @Override
    void endPlay() {
        System.out.println("축구 게임을 종료합니다. 경기 결과 집계.");
    }

    // 훅 메서드를 오버라이드하여 특정 게임에 맞는 동작을 추가할 수 있습니다.
    @Override
    void displayWinner() {
        System.out.println("축구 게임의 우승팀을 발표합니다!");
    }
}

// 구체 클래스 2: 농구 게임
class Basketball extends Game {
    @Override
    void initialize() {
        System.out.println("농구 게임을 초기화합니다. 선수들을 코트에 배치합니다.");
    }

    @Override
    void startPlay() {
        System.out.println("농구 게임을 시작합니다. 점프볼!");
    }

    @Override
    void endPlay() {
        System.out.println("농구 게임을 종료합니다. 경기 결과 집계.");
    }
}

// 클라이언트 코드
public class TemplateMethodPatternDemo {
    public static void main(String[] args) {
        System.out.println("--- 축구 게임 시작 ---");
        Game footballGame = new Football();
        footballGame.play(); // 템플릿 메서드 호출

        System.out.println("\n--- 농구 게임 시작 ---");
        Game basketballGame = new Basketball();
        basketballGame.play(); // 템플릿 메서드 호출
    }
}

코드 설명:

  • Game 추상 클래스는 play()라는 템플릿 메서드를 final로 정의하여 게임의 전반적인 흐름을 고정합니다.
  • initialize(), startPlay(), endPlay()는 각 게임에 따라 달라지는 부분으로 추상 메서드로 선언됩니다.
  • displayWinner()는 훅 메서드로, 기본 동작을 제공하지만 Football 클래스처럼 특정 게임에서 필요하면 오버라이드할 수 있습니다.
  • FootballBasketball 클래스는 Game을 상속받아 추상 메서드들을 각 게임에 맞게 구현합니다.
  • 클라이언트 코드에서는 Game 타입으로 객체를 생성하고 play() 메서드를 호출함으로써, 각 게임의 고유한 로직을 실행하면서도 Game 클래스가 정의한 틀 안에서 일관된 방식으로 동작하게 합니다.

템플릿 메서드 패턴의 장점과 단점

장점:

  • 코드 재사용성: 변하지 않는 공통 코드를 상위 클래스에 두어 중복을 제거하고 재사용성을 높입니다.
  • 일관된 알고리즘 유지: 알고리즘의 전체 구조를 상위 클래스에서 관리하므로, 모든 서브클래스에서 일관된 흐름을 보장합니다.
  • 유지보수 용이: 알고리즘의 특정 단계를 수정해야 할 때, 상위 클래스 한 곳만 수정하면 되므로 유지보수가 쉽습니다.
  • 확장성: 새로운 기능을 추가할 때, 상위 클래스의 템플릿 메서드를 건드리지 않고 새로운 서브클래스를 만들어 추상 메서드를 구현하기만 하면 됩니다 (OCP 준수).

단점:

  • 유연성 제한: 템플릿 메서드가 final로 선언되면 알고리즘의 흐름을 바꿀 수 없습니다. 흐름 자체를 변경해야 한다면 다른 패턴을 고려해야 합니다.
  • 상속의 제약: 상속을 기반으로 하므로, 상위 클래스와 하위 클래스 간의 결합도가 높아집니다. 상위 클래스의 변경이 하위 클래스에 영향을 줄 수 있습니다.
  • 클래스 증가: 작은 차이를 위해 여러 서브클래스를 생성해야 할 수 있어 클래스 수가 늘어날 수 있습니다.

템플릿 메서드 패턴은 "알고리즘의 뼈대는 고정하되, 세부 구현은 달라져야 할 때" 매우 유용합니다. 상속을 통해 강력한 상위-하위 관계를 형성하며, 공통된 작업을 효율적으로 관리하는 데 탁월합니다.


전략 패턴 완벽 가이드: 위임으로 교체하는 유연한 행동

템플릿 메서드 패턴이 "알고리즘의 골격은 고정하고 단계는 다양하게"였다면, 전략 패턴"하나의 행동에 대해 여러 알고리즘을 정의하고 런타임에 유연하게 교체"하는 것에 초점을 맞춥니다. 마치 게임 캐릭터가 싸움 도중에 어떤 무기를 들지(칼, 활, 마법 지팡이) 상황에 따라 실시간으로 바꾸는 것과 같습니다. 각 무기는 공격 방식(전략)이 다르지만, 캐릭터(컨텍스트)는 무기(전략)를 바꿔 낄 수 있습니다.

전략 패턴의 정의와 핵심

전략 패턴(Strategy Pattern)동일한 계열의 알고리즘들을 정의하고 각각을 캡슐화한 뒤, 런타임에 이 알고리즘들을 상호 교환할 수 있도록 하는 패턴입니다. 이를 통해 클라이언트(알고리즘을 사용하는 객체)는 구체적인 알고리즘 구현에 묶이지 않고, 필요한 시점에 동적으로 알고리즘을 선택하여 사용할 수 있습니다.

이 패턴의 핵심 메커니즘은 바로 위임(Delegation) 또는 객체 조합(Composition)입니다. 행동을 수행하는 객체(컨텍스트)가 직접 그 행동을 구현하는 대신, 행동을 정의하는 별도의 인터페이스(전략)를 통해 구현된 다른 객체(구체적인 전략)에 그 책임을 위임합니다. 이는 컨텍스트와 특정 알고리즘 간의 강한 결합을 피하고, 런타임에 행동을 쉽게 변경할 수 있게 합니다.

전략 패턴의 구조

전략 패턴은 주로 다음과 같은 세 가지 주요 구성 요소로 이루어집니다.

  1. 전략 인터페이스 (Strategy Interface):
    • 모든 전략 클래스가 구현해야 할 공통 메서드를 정의합니다. 클라이언트는 이 인터페이스를 통해 구체적인 전략에 접근하므로, 구체적인 전략 클래스의 변경에 영향을 받지 않습니다.
  2. 구체적인 전략 클래스 (Concrete Strategy):
    • 전략 인터페이스를 구현하며, 실제 알고리즘(또는 행동)을 정의합니다. 각 구체 전략 클래스는 동일한 문제를 해결하는 다른 방식을 캡슐화합니다.
  3. 컨텍스트 클래스 (Context Class):
    • 클라이언트에게 전략 객체를 노출하고, 실제 작업을 수행하는 클래스입니다.
    • 전략 인터페이스 타입의 객체(인스턴스)를 유지(참조)하며, 클라이언트의 요청이 들어오면 이 전략 객체에게 실제 작업을 위임(delegate)합니다.
    • 컨텍스트는 어떤 구체적인 전략이 사용되는지 알 필요가 없습니다. 단지 전략 인터페이스를 통해 작업을 요청할 뿐입니다.
    • 클라이언트는 컨텍스트를 통해 어떤 전략을 사용할지 설정할 수 있습니다.

전략 패턴 구조

전략 패턴의 동작 방식

클라이언트는 먼저 사용할 구체적인 전략 객체를 생성합니다. 그리고 이 전략 객체를 컨텍스트 객체에 주입(Injection)합니다. 컨텍스트 객체는 이제 이 전략 객체를 사용하여 필요한 행동을 수행합니다. 만약 행동을 변경해야 한다면, 클라이언트는 단순히 다른 전략 객체를 생성하여 컨텍스트에 주입하기만 하면 됩니다. 컨텍스트의 핵심 로직은 전혀 변경되지 않습니다.

예를 들어, 결제 시스템을 생각해보겠습니다. PaymentStrategy 인터페이스는 pay(amount) 메서드를 정의합니다. CreditCardPayment, PayPalPayment, KakaoPayPayment와 같은 구체적인 전략 클래스들은 이 인터페이스를 구현하여 각자의 결제 방식을 제공합니다. ShoppingCart 컨텍스트 클래스는 PaymentStrategy 타입의 객체를 가지고 있으며, checkout(amount) 메서드를 호출하면 내부의 paymentStrategy.pay(amount)를 호출하여 실제 결제 로직을 위임합니다. 사용자는 쇼핑 카트에서 결제 방식을 선택함으로써 런타임에 결제 전략을 변경할 수 있습니다.

예시 코드 (Java)

결제 방식을 예시로 전략 패턴을 구현해 보겠습니다.

// 1. 전략 인터페이스: 모든 결제 전략의 공통 인터페이스를 정의합니다.
interface PaymentStrategy {
    void pay(int amount);
}

// 2. 구체적인 전략 클래스 1: 신용카드 결제
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String name;

    public CreditCardPayment(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원(을) 신용카드 (" + cardNumber + ", " + name + ")로 결제했습니다.");
    }
}

// 2. 구체적인 전략 클래스 2: 페이팔 결제
class PayPalPayment implements PaymentStrategy {
    private String emailId;

    public PayPalPayment(String emailId) {
        this.emailId = emailId;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원(을) 페이팔 (" + emailId + ")로 결제했습니다.");
    }
}

// 2. 구체적인 전략 클래스 3: 카카오페이 결제 (새로운 전략 추가)
class KakaoPayPayment implements PaymentStrategy {
    private String phoneNumber;

    public KakaoPayPayment(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원(을) 카카오페이 (" + phoneNumber + ")로 결제했습니다.");
    }
}


// 3. 컨텍스트 클래스: 전략 객체를 사용하여 작업을 수행합니다.
class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    private int totalAmount;

    public ShoppingCart() {
        this.totalAmount = 0;
    }

    public void addItem(int price) {
        this.totalAmount += price;
    }

    // 클라이언트가 런타임에 결제 전략을 설정할 수 있도록 합니다.
    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    // 실제 결제 요청은 설정된 전략 객체에게 위임합니다.
    public void checkout() {
        if (paymentStrategy == null) {
            System.out.println("결제 수단이 선택되지 않았습니다.");
            return;
        }
        paymentStrategy.pay(totalAmount);
        this.totalAmount = 0; // 결제 후 초기화
    }
}

// 클라이언트 코드
public class StrategyPatternDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(10000);
        cart.addItem(5000);
        System.out.println("총 결제 금액: " + cart.totalAmount + "원\n");

        // 신용카드 전략으로 결제
        System.out.println("--- 신용카드 결제 ---");
        PaymentStrategy creditCard = new CreditCardPayment("1234-5678-9012-3456", "홍길동");
        cart.setPaymentStrategy(creditCard);
        cart.checkout();
        System.out.println();

        // 페이팔 전략으로 결제 (런타임에 전략 변경)
        System.out.println("--- 페이팔 결제 ---");
        cart.addItem(20000); // 다시 아이템 추가
        System.out.println("총 결제 금액: " + cart.totalAmount + "원");
        PaymentStrategy payPal = new PayPalPayment("hong.gildong@example.com");
        cart.setPaymentStrategy(payPal);
        cart.checkout();
        System.out.println();

        // 카카오페이 전략으로 결제 (런타임에 전략 변경, 새로운 전략)
        System.out.println("--- 카카오페이 결제 ---");
        cart.addItem(7000); // 다시 아이템 추가
        System.out.println("총 결제 금액: " + cart.totalAmount + "원");
        PaymentStrategy kakaoPay = new KakaoPayPayment("010-1234-5678");
        cart.setPaymentStrategy(kakaoPay);
        cart.checkout();
    }
}

코드 설명:

  • PaymentStrategy 인터페이스는 pay() 메서드를 정의하여 모든 결제 전략의 공통 계약을 만듭니다.
  • CreditCardPayment, PayPalPayment, KakaoPayPayment 클래스는 PaymentStrategy를 구현하여 각각의 결제 로직을 캡슐화합니다.
  • ShoppingCart 클래스는 PaymentStrategy 타입의 paymentStrategy 멤버 변수를 가지고, setPaymentStrategy()를 통해 런타임에 어떤 구체적인 전략 객체를 사용할지 설정할 수 있습니다.
  • checkout() 메서드에서는 실제 결제 로직을 paymentStrategy.pay(totalAmount)를 호출하여 위임합니다.
  • 클라이언트 코드에서는 ShoppingCart 객체에 다양한 PaymentStrategy 객체를 주입함으로써, 런타임에 유연하게 결제 방식을 변경할 수 있습니다. 새로운 결제 방식(예: 네이버페이)이 추가되어도 ShoppingCart 클래스의 코드를 전혀 수정할 필요 없이, PaymentStrategy를 구현하는 새로운 클래스만 추가하면 됩니다. 이는 개방-폐쇄 원칙(OCP)을 완벽하게 준수하는 예시입니다.

전략 패턴의 장점과 단점

장점:

  • 높은 유연성: 런타임에 객체의 행동을 동적으로 변경할 수 있습니다.
  • OCP(개방-폐쇄 원칙) 준수: 새로운 전략을 추가하더라도 컨텍스트 클래스의 코드를 수정할 필요가 없습니다. 확장에 열려 있고, 변경에 닫혀 있습니다.
  • SRP(단일 책임 원칙) 준수: 각 전략 클래스는 오직 하나의 알고리즘 구현에 대한 책임만을 가집니다. 컨텍스트 클래스는 전략을 사용하는 책임만을 가집니다.
  • 강한 결합 방지: 컨텍스트와 구체적인 전략 구현체 간의 의존성을 줄여 결합도를 낮춥니다.
  • 알고리즘 분리: 알고리즘 구현을 컨텍스트 클래스에서 분리하여 코드의 가독성과 유지보수성을 높입니다.

단점:

  • 클래스 수 증가: 각기 다른 전략을 위해 별도의 클래스를 생성해야 하므로, 클래스 수가 늘어날 수 있습니다. 이는 시스템의 복잡성을 증가시킬 수 있습니다.
  • 클라이언트의 부담: 클라이언트가 어떤 전략을 언제 사용할지 알아야 하고, 적절한 전략 객체를 생성하여 컨텍스트에 주입해야 합니다.
  • 성능 오버헤드: 작은 차이의 알고리즘에 전략 패턴을 적용할 경우, 인터페이스와 클래스 생성에 따른 약간의 성능 오버헤드가 발생할 수 있습니다.

전략 패턴은 행동 자체를 교체할 수 있는 유연성이 필요하거나, 특정 알고리즘의 다양한 변형이 존재할 때 매우 강력한 도구로 활용됩니다.


템플릿 메서드 vs 전략 패턴: 핵심 차이점과 선택 가이드

템플릿 메서드 패턴과 전략 패턴은 모두 "변하는 것과 변하지 않는 것을 분리"하여 유연성과 재사용성을 높인다는 공통 목표를 가집니다. 하지만 이 목표를 달성하는 방식과 주된 메커니즘에서 큰 차이를 보이며, 따라서 적합한 사용 시나리오도 다릅니다. 이 두 패턴의 핵심적인 차이점을 명확히 이해하는 것은 올바른 디자인 선택을 위한 필수적인 과정입니다.

가장 근본적인 차이는 두 패턴이 각각 상속(Inheritance)위임(Delegation, 또는 Composition)이라는 객체 지향의 두 가지 강력한 메커니즘 중 어느 것을 활용하는지에 있습니다.

핵심 비교표

구분 템플릿 메서드 패턴 전략 패턴
핵심 메커니즘 상속(Inheritance) 위임(Delegation)/조합(Composition)
제어의 주체 상위(추상) 클래스 (템플릿 메서드) 컨텍스트 클래스 (위임 대상 결정) 및 클라이언트 (어떤 전략을 사용할지 결정)
변화의 대상 알고리즘의 고정된 골격 내의 일부 단계 행동(알고리즘) 자체
유연성 상대적으로 낮음: 컴파일 타임에 알고리즘 흐름이 고정됨 매우 높음: 런타임에 행동(알고리즘)을 동적으로 교체 가능
재사용성 변하지 않는 공통 로직을 상위 클래스에서 재사용 여러 컨텍스트에서 동일한 전략을 재사용 가능
확장성 새로운 하위 클래스를 추가하여 특정 단계 구현 새로운 전략 클래스를 추가하여 행동 확장
결합도 강함: 상위-하위 클래스 간의 상속 관계 때문에 강하게 결합됨 약함: 컨텍스트는 전략 인터페이스에만 의존하므로 결합도가 낮음
OCP 준수 일부 준수: 새로운 단계 구현 시 기존 상위 클래스 수정 없이 확장 가능 매우 잘 준수: 새로운 전략 추가 시 컨텍스트 수정 없이 확장 가능
주요 해결 문제 알고리즘의 단계 순서가 고정되어 있고, 각 단계의 세부 구현만 달라지는 경우 객체의 행동 자체를 런타임에 변경해야 하거나, 동일한 행동에 대해 여러 알고리즘이 필요한 경우
예시 시나리오 음료 만들기, 게임 플레이, 문서 처리 파이프라인 결제 방식, 정렬 알고리즘, 세금 계산 방식, 알림 서비스

언제 템플릿 메서드 패턴을 사용할까?

템플릿 메서드 패턴은 다음과 같은 상황에서 효과적입니다.

  • 알고리즘의 흐름(순서)이 고정되어야 할 때: 모든 서브클래스가 일관된 방식으로 작업을 수행해야 할 때, 템플릿 메서드가 그 순서를 강제하여 실수를 방지하고 코드의 안정성을 높입니다.
  • 공통된 로직을 한 곳에서 관리하고 싶을 때: 여러 클래스에서 반복되는 중복 코드를 상위 클래스의 템플릿 메서드로 추출하여 코드의 중복을 제거하고 유지보수성을 향상시킵니다.
  • 부분적인 구현 변경만 허용하고 싶을 때: 알고리즘의 전체적인 구조는 상위 클래스가 관리하되, 특정 단계만 하위 클래스에서 자유롭게 변경할 수 있도록 할 때 유용합니다.
  • 제어의 역전(Inversion of Control, IoC)을 활용하여 하위 클래스가 아닌 상위 클래스가 전체 흐름을 제어하고자 할 때 적합합니다.

언제 전략 패턴을 사용할까?

전략 패턴은 다음과 같은 상황에서 강력한 해결책을 제공합니다.

  • 런타임에 객체의 행동을 동적으로 변경해야 할 때: 클라이언트가 어떤 행동을 사용할지 런타임에 결정해야 할 때, 컨텍스트의 핵심 로직을 건드리지 않고 유연하게 행동을 교체할 수 있습니다.
  • 동일한 행동에 대해 여러 알고리즘이 존재하고, 이를 클라이언트가 선택해야 할 때: 예를 들어, 데이터를 정렬하는 다양한 방법(버블 정렬, 퀵 정렬, 병합 정렬)이 있고, 어떤 방법을 사용할지 사용자가 선택하게 해야 할 때 유용합니다.
  • if-else 또는 switch-case 문이 너무 많아질 때: 특정 조건에 따라 다른 알고리즘을 선택하는 로직이 복잡해질 때, 각 알고리즘을 별도의 전략 클래스로 분리하여 코드의 복잡성을 줄이고 가독성을 높입니다.
  • 클라이언트가 구체적인 알고리즘 구현으로부터 분리되기를 원할 때: 컨텍스트는 오직 전략 인터페이스에만 의존하므로, 구체적인 전략 구현체가 변경되더라도 컨텍스트나 클라이언트 코드는 영향을 받지 않습니다.
  • 객체 간의 강한 결합을 피하고 싶을 때: 상속 대신 위임을 통해 객체 간의 관계를 느슨하게 만들고, 유연성을 극대화합니다.

결론적으로

  • "흐름은 고정, 세부 단계는 변화"템플릿 메서드 패턴 (상속 기반)
  • "행동 자체가 변화, 런타임에 교체 가능"전략 패턴 (위임 기반)

두 패턴 모두 객체 지향의 핵심 원칙을 준수하며 코드의 품질을 높이지만, 해결하고자 하는 문제의 본질이 다름을 기억해야 합니다. 문제의 특성을 정확히 파악하여 적절한 패턴을 선택하는 것이 성공적인 설계의 열쇠입니다.


실전 적용 가이드: 템플릿 메서드와 전략 패턴, 언제 어떻게 사용할까?

지금까지 템플릿 메서드 패턴과 전략 패턴의 정의, 구조, 동작 방식, 그리고 핵심적인 차이점을 살펴보았습니다. 이제 이 지식을 바탕으로 실제 개발 프로젝트에서 어떤 패턴을 선택해야 할지 구체적인 판단 기준과 시나리오를 통해 알아보겠습니다. 실무에서 맞닥뜨릴 수 있는 문제에 두 패턴을 어떻게 적용할 수 있을까요?

패턴 선택을 위한 질문 리스트

패턴을 선택하기 전에 스스로에게 다음과 같은 질문을 던져보는 것이 좋습니다.

  • 작업의 순서흐름이 고정되어 변하지 않는가?
    • "그렇다"면, 템플릿 메서드 패턴을 고려할 수 있습니다.
    • "아니다, 순서 자체가 유동적으로 변할 수 있다"면, 다른 패턴(예: Builder, Command)을 고려하거나 전략 패턴의 조합을 생각해야 합니다.
  • 작업의 일부 단계만 다르고, 나머지 단계는 모든 경우에 동일한가?
    • "그렇다"면, 템플릿 메서드 패턴이 적합합니다. 공통 단계를 상위 클래스에 두고, 변하는 단계만 하위 클래스에서 오버라이드할 수 있습니다.
  • 객체의 전체적인 행동 자체가 런타임에 유연하게 변경되어야 하는가?
    • "그렇다"면, 전략 패턴을 고려하세요. 객체가 수행하는 알고리즘을 상황에 따라 교체할 수 있습니다.
  • 새로운 행동이나 알고리즘이 추가될 때, 기존 코드를 수정하고 싶지 않은가?
    • "그렇다"면, 전략 패턴이 더 강력한 OCP 준수를 제공합니다. 새로운 전략 클래스를 추가하는 것만으로 확장이 가능합니다. (템플릿 메서드도 확장 가능하지만, 이는 알고리즘의 단계에 대한 확장입니다.)
  • 상속 계층 구조를 통해 공통 로직을 공유하고 싶고, 강력한 타입 관계가 필요한가?
    • "그렇다"면, 템플릿 메서드 패턴이 적합합니다.
  • 객체 간의 결합도를 최소화하고, 유연한 객체 조합을 통해 행동을 구성하고 싶은가?
    • "그렇다"면, 전략 패턴이 더 적합합니다.

실제 개발 시나리오 예시

시나리오 1: 데이터 처리 파이프라인 (템플릿 메서드 패턴)

문제: 웹 애플리케이션에서 사용자로부터 업로드된 다양한 형식(CSV, XML, JSON)의 데이터를 처리해야 합니다. 어떤 형식의 데이터든 "파일 읽기 -> 데이터 파싱 -> 데이터 유효성 검사 -> 데이터베이스 저장"이라는 고정된 단계를 거쳐야 합니다. 단, 각 단계의 구체적인 구현은 파일 형식에 따라 달라집니다.

해결: 이 경우, 템플릿 메서드 패턴이 완벽하게 들어맞습니다.

  • AbstractDataProcessor라는 추상 클래스를 정의하고, processData()라는 템플릿 메서드 안에 고정된 단계(읽기, 파싱, 검사, 저장)를 순서대로 호출하도록 합니다.
  • readData(), parseData(), validateData(), saveData()와 같은 메서드들을 추상 메서드로 선언합니다.
  • CsvDataProcessor, XmlDataProcessor, JsonDataProcessor와 같은 구체 클래스들을 AbstractDataProcessor를 상속받아 각자의 파일 형식에 맞는 구현을 제공합니다.
  • 새로운 데이터 형식(예: Excel)이 추가되어도, 기존 AbstractDataProcessor나 다른 구체 클래스들을 수정할 필요 없이, ExcelDataProcessor라는 새로운 클래스만 추가하여 확장할 수 있습니다.
// 예시: 데이터 처리 파이프라인 (템플릿 메서드)
abstract class DataProcessor {
    public final void processData() { // 템플릿 메서드
        readData();
        parseData();
        validateData();
        saveData();
        sendNotification(); // 훅 메서드
    }

    abstract void readData();
    abstract void parseData();
    abstract void validateData();
    abstract void saveData();

    // 훅 메서드: 선택적으로 구현
    void sendNotification() {
        System.out.println("데이터 처리가 완료되었습니다. (기본 알림)");
    }
}

class CsvDataProcessor extends DataProcessor {
    @Override void readData() { System.out.println("CSV 파일 읽기"); }
    @Override void parseData() { System.out.println("CSV 데이터 파싱"); }
    @Override void validateData() { System.out.println("CSV 데이터 유효성 검사"); }
    @Override void saveData() { System.out.println("CSV 데이터 DB 저장"); }
    @Override void sendNotification() { System.out.println("CSV 처리 완료 알림 전송!"); }
}

class XmlDataProcessor extends DataProcessor {
    @Override void readData() { System.out.println("XML 파일 읽기"); }
    @Override void parseData() { System.out.println("XML 데이터 파싱"); }
    @Override void validateData() { System.out.println("XML 데이터 유효성 검사"); }
    @Override void saveData() { System.out.println("XML 데이터 DB 저장"); }
}

// 사용 예시 (클라이언트 코드)
public class DataProcessorDemo {
    public static void main(String[] args) {
        System.out.println("--- CSV 데이터 처리 ---");
        DataProcessor csvProcessor = new CsvDataProcessor();
        csvProcessor.processData();

        System.out.println("\n--- XML 데이터 처리 ---");
        DataProcessor xmlProcessor = new XmlDataProcessor();
        xmlProcessor.processData();
    }
}

이 시나리오에서는 데이터 처리의 '흐름'은 고정되어 있고, 각 '단계의 구현'만 달라지므로 템플릿 메서드 패턴이 매우 효과적입니다.

시나리오 2: 알림 서비스 (전략 패턴)

문제: 사용자에게 중요한 정보를 알리는 서비스가 있습니다. 알림 방식은 처음에는 이메일과 SMS만 지원했지만, 앞으로 카카오톡, 푸시 알림, 디스코드 등 다양한 채널이 추가될 예정입니다. 서비스는 어떤 채널로 알림을 보낼지 런타임에 유연하게 선택하고 싶습니다.

해결: 이 경우, 전략 패턴이 이상적인 해결책입니다.

  • NotificationStrategy라는 인터페이스를 정의하고 send(message) 메서드를 선언합니다.
  • EmailNotification, SmsNotification, KakaoTalkNotification, PushNotification과 같은 구체적인 전략 클래스들이 이 인터페이스를 구현하여 각 알림 채널에 맞는 전송 로직을 캡슐화합니다.
  • Notifier라는 컨텍스트 클래스를 정의하고, NotificationStrategy 타입의 객체를 멤버 변수로 가집니다. setStrategy() 메서드를 통해 런타임에 어떤 알림 전략을 사용할지 설정할 수 있도록 합니다. notify(message) 메서드가 호출되면, 내부의 strategy.send(message)를 호출하여 알림을 위임합니다.
  • 새로운 알림 채널(예: Discord)이 추가되어도, Notifier 클래스의 코드를 수정할 필요 없이 DiscordNotification이라는 새로운 전략 클래스만 추가하면 됩니다.
// 예시: 알림 서비스 (전략 패턴)
interface NotificationStrategy {
    void send(String message);
}

class EmailNotification implements NotificationStrategy {
    @Override public void send(String message) { System.out.println("이메일로 알림: " + message); }
}

class SmsNotification implements NotificationStrategy {
    @Override public void send(String message) { System.out.println("SMS로 알림: " + message); }
}

class KakaoTalkNotification implements NotificationStrategy {
    @Override public void send(String message) { System.out.println("카카오톡으로 알림: " + message); }
}

class Notifier { // 컨텍스트
    private NotificationStrategy strategy;

    public void setStrategy(NotificationStrategy strategy) {
        this.strategy = strategy;
    }

    public void notifyUser(String message) {
        if (strategy != null) {
            strategy.send(message);
        } else {
            System.out.println("알림 전략이 설정되지 않았습니다.");
        }
    }
}

// 사용 예시 (클라이언트 코드)
public class NotificationDemo {
    public static void main(String[] args) {
        Notifier notifier = new Notifier();

        System.out.println("--- 이메일 알림 ---");
        notifier.setStrategy(new EmailNotification());
        notifier.notifyUser("주문이 완료되었습니다.");

        System.out.println("\n--- 카카오톡 알림 ---");
        notifier.setStrategy(new KakaoTalkNotification());
        notifier.notifyUser("배송이 시작되었습니다.");

        System.out.println("\n--- SMS 알림 ---");
        notifier.setStrategy(new SmsNotification());
        notifier.notifyUser("새로운 공지가 있습니다.");
    }
}

이 시나리오에서는 '알림을 보낸다'는 행동 자체에 대해 여러 알고리즘이 존재하고, 이를 런타임에 선택적으로 교체해야 하므로 전략 패턴이 매우 적합합니다.

올바른 패턴 선택이 코드 품질에 미치는 영향

적절한 디자인 패턴의 선택은 다음과 같은 긍정적인 영향을 미칩니다.

  • 높은 응집도(High Cohesion): 각 클래스가 단일 책임(SRP)을 가지도록 도와주어, 코드가 변경될 때 미치는 영향을 최소화합니다.
  • 낮은 결합도(Low Coupling): 객체들 간의 의존성을 줄여서, 한 객체의 변경이 다른 객체에 미치는 파급 효과를 줄입니다. 이는 코드의 유연성과 재사용성을 높이는 핵심 요소입니다.
  • 유지보수성 향상: 코드가 예측 가능하고 일관된 구조를 가지므로, 버그를 찾고 수정하기가 쉬워집니다.
  • 확장성 증대: 새로운 요구사항이 발생했을 때 기존 코드를 최소한으로 변경하거나 전혀 변경하지 않고 기능을 확장할 수 있습니다 (OCP).
  • 재사용성 극대화: 잘 설계된 패턴은 코드의 특정 부분을 다른 프로젝트나 다른 맥락에서 쉽게 재사용할 수 있도록 합니다.

디자인 패턴은 단순히 코드를 아름답게 만드는 것을 넘어, 소프트웨어의 생명 주기를 연장하고 팀의 개발 효율성을 높이는 실질적인 도구입니다. 복잡한 시스템을 설계할 때는 항상 "변하는 것은 무엇이고, 변하지 않는 것은 무엇인가?"라는 질문을 던져보고, 그 답에 따라 적절한 디자인 패턴을 적용하는 습관을 들이는 것이 중요합니다.


마무리: 더 나은 객체 지향 설계를 위한 지름길

지금까지 우리는 객체 지향 설계의 중요한 두 가지 디자인 패턴, 템플릿 메서드 패턴전략 패턴을 심도 있게 탐구했습니다. 두 패턴 모두 "변하는 것과 변하지 않는 것을 분리"하여 코드의 유연성과 재사용성을 높인다는 공통 목표를 가집니다. 하지만 템플릿 메서드 패턴이 상속을 통해 '알고리즘의 고정된 흐름 속에서 일부 단계를 변화'시키는 데 집중한다면, 전략 패턴은 위임(조합)을 통해 '알고리즘(행동) 자체를 런타임에 유연하게 교체'하는 데 초점을 맞춘다는 점을 명확히 이해하셨기를 바랍니다.

이 두 패턴은 각각 if-else 또는 switch-case 문으로 인해 코드가 복잡해지는 것을 방지하고, 특정 알고리즘의 모든 변형을 한 곳에 집중시키는 대신, 각 변형을 별도의 클래스로 분리하여 단일 책임 원칙(SRP)을 준수하도록 돕습니다. 특히 개방-폐쇄 원칙(OCP)을 효과적으로 적용하여, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있는 견고하고 확장 가능한 아키텍처를 구축하는 데 필수적인 역할을 합니다.

디자인 패턴 학습은 단순히 패턴의 이름을 외우거나 코드를 복사하는 것이 아닙니다. 그것은 문제 해결의 지혜를 얻고, 객체 지향 원칙을 실제 설계에 적용하는 방법을 배우며, 더 나은 코드를 작성하는 사고방식을 기르는 과정입니다. 템플릿 메서드 패턴과 전략 패턴은 이러한 여정에서 여러분이 마주할 수많은 설계 문제에 대한 강력한 해결책을 제시해 줄 것입니다.

소프트웨어 개발은 끊임없는 학습의 연속입니다. 오늘 다룬 두 패턴 외에도 수많은 디자인 패턴들이 존재하며, 각 패턴은 특정 상황과 문제에 대한 최적의 해결책을 제시합니다. 이 글을 통해 얻은 지식을 바탕으로, 더 많은 디자인 패턴을 탐구하고 여러분의 프로젝트에 현명하게 적용하여 더욱 견고하고 유연한 소프트웨어를 구축하시길 바랍니다.

여러분의 코드가 오늘보다 내일 더 아름다워지기를 응원합니다!


이미지 생성 프롬프트 제안:
"추상적인 개념을 시각화: 템플릿 메서드 패턴과 전략 패턴을 상징하는 두 개의 다른 메커니즘이 복잡한 소프트웨어 아키텍처 다이어그램 위에 배치되어 상속(inheritance)과 위임(delegation) 관계를 명확하게 보여주는 이미지. 깔끔하고 전문적인 그래픽 디자인 스타일. 배경은 소프트웨어 코드 블록의 추상화된 형태로 표현."

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함
반응형