🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Template Method Pattern 최신 기능 - 슬라이드 1/13
A

AI Generated

2025. 11. 4. · 14 Views

Template Method Pattern 완벽 가이드

알고리즘의 골격을 정의하고 세부 구현을 서브클래스에 위임하는 Template Method Pattern을 실무 예제와 함께 깊이 있게 알아봅니다. 코드 중복을 제거하고 유지보수성을 높이는 실전 노하우를 제공합니다.


목차

  1. Template_Method_Pattern_기본_개념
  2. 추상_클래스와_Hook_메서드
  3. 실전_예제_데이터_처리_파이프라인
  4. Hollywood_Principle
  5. Template_Method_vs_Strategy_Pattern
  6. 실무_활용_테스트_프레임워크_구조
  7. Hook_메서드_활용_전략
  8. 불변성과_final_메서드

1. Template Method Pattern 기본 개념

시작하며

여러분이 여러 종류의 문서를 처리하는 시스템을 개발한다고 생각해보세요. PDF, Excel, CSV 파일 모두 "파일 열기 → 데이터 읽기 → 검증 → 가공 → 저장" 같은 비슷한 단계를 거칩니다.

하지만 각 파일 형식마다 세부 구현은 다르죠. 이런 상황에서 각 파일 처리 클래스마다 전체 로직을 반복해서 작성하면 어떻게 될까요?

코드 중복이 심해지고, 공통 로직에 버그가 있으면 모든 곳을 수정해야 합니다. 또한 새로운 단계를 추가하려면 모든 클래스를 일일이 수정해야 하는 악몽 같은 상황이 펼쳐집니다.

바로 이럴 때 필요한 것이 Template Method Pattern입니다. 이 패턴은 알고리즘의 전체 구조는 부모 클래스에서 정의하고, 각 단계의 구체적인 구현만 자식 클래스에 맡기는 방식입니다.

이렇게 하면 공통 로직은 한 곳에서 관리하고, 각 파일 형식의 특성만 개별적으로 구현할 수 있습니다. 유지보수가 훨씬 쉬워지고 새로운 파일 형식 추가도 간단해지죠.

개요

간단히 말해서, Template Method Pattern은 알고리즘의 뼈대를 부모 클래스에 정의하고, 구체적인 구현은 자식 클래스가 결정하도록 하는 디자인 패턴입니다. 실무에서 이 패턴이 빛을 발하는 순간은 여러 클래스가 비슷한 절차를 따르지만 세부 동작이 다를 때입니다.

예를 들어, 다양한 결제 수단(신용카드, 계좌이체, 간편결제)을 처리하는 시스템에서 "인증 → 금액 확인 → 결제 처리 → 영수증 발행"이라는 공통 흐름은 같지만, 각 결제 수단의 구체적인 처리 방식은 다릅니다. 기존에는 각 결제 클래스마다 전체 로직을 중복해서 작성했다면, Template Method Pattern을 사용하면 공통 흐름은 한 번만 정의하고 각 결제 수단의 특성만 개별적으로 구현할 수 있습니다.

이 패턴의 핵심 특징은 세 가지입니다. 첫째, 알고리즘의 구조를 변경하지 않고 특정 단계만 재정의할 수 있습니다.

둘째, 공통 코드를 부모 클래스에 집중시켜 중복을 제거합니다. 셋째, 서브클래스의 확장 포인트를 명확하게 제어할 수 있습니다.

이러한 특징들이 코드의 일관성을 유지하면서도 유연한 확장을 가능하게 만듭니다.

코드 예제

abstract class DataProcessor {
  // Template Method: 전체 알고리즘의 골격을 정의
  public process(): void {
    this.readData();
    this.validateData();
    this.transformData();
    this.saveData();
    this.cleanup();
  }

  // 추상 메서드: 서브클래스가 반드시 구현해야 함
  protected abstract readData(): void;
  protected abstract transformData(): void;

  // 구체 메서드: 공통 로직은 부모 클래스에서 구현
  protected validateData(): void {
    console.log("공통 검증 로직 수행");
  }

  protected saveData(): void {
    console.log("데이터 저장 완료");
  }

  // Hook 메서드: 선택적으로 오버라이드 가능
  protected cleanup(): void {}
}

설명

이것이 하는 일: Template Method Pattern은 상속을 활용하여 알고리즘의 구조와 구현을 분리합니다. 부모 클래스는 메서드 호출 순서를 제어하고, 자식 클래스는 각 단계의 구체적인 동작을 구현합니다.

첫 번째로, process() 메서드가 템플릿 메서드 역할을 합니다. 이 메서드는 전체 알고리즘의 흐름을 정의하며, 각 단계를 순서대로 호출합니다.

이 메서드는 final로 선언되어야 하며(TypeScript에서는 관례적으로 오버라이드하지 않음), 서브클래스가 전체 흐름을 임의로 변경할 수 없도록 보호합니다. 그 다음으로, 추상 메서드인 readData()transformData()는 서브클래스가 반드시 구현해야 하는 부분입니다.

이들은 각 데이터 형식마다 달라지는 핵심 로직을 담당합니다. 예를 들어 CSV 처리 클래스는 쉼표로 구분된 데이터를 읽고, JSON 처리 클래스는 JSON 파싱을 수행하는 식으로 각자의 방식을 구현합니다.

세 번째로, validateData()saveData() 같은 구체 메서드는 모든 서브클래스에서 공통으로 사용하는 로직입니다. 이런 메서드들을 부모 클래스에 구현함으로써 코드 중복을 제거하고, 공통 로직을 한 곳에서 관리할 수 있습니다.

만약 검증 로직에 버그가 발견되면 부모 클래스만 수정하면 모든 서브클래스에 자동으로 반영됩니다. 마지막으로, cleanup() 같은 Hook 메서드는 선택적으로 오버라이드할 수 있는 확장 포인트입니다.

기본 구현은 아무것도 하지 않지만, 특정 서브클래스에서 필요하다면 오버라이드하여 추가 동작을 수행할 수 있습니다. 예를 들어 파일 처리 클래스는 임시 파일을 삭제하는 로직을 추가할 수 있습니다.

여러분이 이 패턴을 사용하면 새로운 데이터 형식을 추가할 때 추상 메서드만 구현하면 되므로 확장이 매우 쉽습니다. 또한 알고리즘의 전체 구조가 한눈에 보이므로 코드 이해도가 높아지고, 공통 로직의 변경이 필요할 때 한 곳만 수정하면 되므로 유지보수가 편리합니다.

무엇보다 서브클래스가 전체 흐름을 망가뜨릴 수 없도록 제어할 수 있다는 점이 큰 장점입니다.

실전 팁

💡 템플릿 메서드는 public으로, 하위 단계 메서드들은 protected로 선언하여 외부에서는 템플릿 메서드만 호출하도록 제한하세요. 이렇게 하면 알고리즘의 무결성을 보장할 수 있습니다.

💡 추상 메서드의 개수를 최소화하세요. 너무 많은 추상 메서드는 서브클래스 구현을 복잡하게 만듭니다. 정말 필수적인 변형 포인트만 추상 메서드로 만들고, 나머지는 Hook 메서드나 구체 메서드로 제공하세요.

💡 템플릿 메서드 내에서 this를 통해 메서드를 호출하면 다형성이 작동합니다. 부모 클래스에서 자식 클래스의 오버라이드된 메서드가 자동으로 호출되므로, 이 메커니즘을 잘 이해하고 활용하세요.

💡 서브클래스가 실수로 템플릿 메서드를 오버라이드하지 않도록 문서화하거나 final 키워드를 사용하세요(Java의 경우). TypeScript에서는 주석으로 명시하거나 private로 만들고 public wrapper를 제공하는 방법도 있습니다.

💡 단위 테스트 작성 시 Mock 서브클래스를 만들어 각 단계를 독립적으로 테스트할 수 있습니다. 추상 메서드를 간단히 구현한 테스트용 서브클래스를 만들면 템플릿 메서드의 흐름을 검증하기 쉽습니다.


2. 추상 클래스와 Hook 메서드

시작하며

여러분이 Template Method Pattern을 구현할 때 가장 고민되는 부분은 무엇일까요? "이 메서드는 서브클래스가 반드시 구현해야 할까, 아니면 선택적으로 구현할 수 있게 할까?"입니다.

모든 메서드를 추상 메서드로 만들면 서브클래스의 부담이 너무 커지고, 모두 구체 메서드로 만들면 확장성이 떨어집니다. 실제 프로젝트에서 이런 설계 결정은 매우 중요합니다.

잘못 설계하면 서브클래스마다 불필요한 보일러플레이트 코드가 반복되거나, 반대로 확장이 필요한 부분을 확장할 수 없는 경직된 구조가 만들어집니다. 바로 이럴 때 필요한 것이 Hook 메서드입니다.

Hook 메서드는 기본 구현을 제공하되, 필요한 경우에만 서브클래스가 오버라이드할 수 있는 확장 포인트입니다. 이렇게 추상 메서드와 Hook 메서드를 적절히 조합하면 필수 구현과 선택적 구현을 명확히 구분할 수 있고, 서브클래스는 정말 필요한 부분만 구현하면 되므로 개발이 훨씬 수월해집니다.

개요

간단히 말해서, 추상 메서드는 서브클래스가 반드시 구현해야 하는 필수 메서드이고, Hook 메서드는 선택적으로 오버라이드할 수 있는 확장 포인트입니다. 실무에서 이 구분이 중요한 이유는 명확합니다.

예를 들어, 게임 캐릭터의 공격 패턴을 구현한다고 해봅시다. "공격 준비 → 공격 실행 → 공격 후 처리"라는 흐름에서 "공격 실행"은 캐릭터마다 완전히 다르므로 추상 메서드로 만들어야 합니다.

하지만 "공격 후 처리"는 대부분 캐릭터는 아무것도 안 하지만, 특정 캐릭터(예: 버서커)만 추가 효과가 있다면 Hook 메서드로 만드는 것이 적절합니다. 기존에는 "구현 강제"와 "유연한 확장" 사이에서 선택해야 했다면, 이제는 추상 메서드로 필수 동작을 강제하고 Hook 메서드로 선택적 확장을 허용하여 두 가지 장점을 모두 가져갈 수 있습니다.

이 접근법의 핵심 특징은 세 가지입니다. 첫째, 추상 메서드를 통해 컴파일 타임에 필수 구현을 강제합니다.

둘째, Hook 메서드로 기본 동작을 제공하면서도 확장 가능성을 열어둡니다. 셋째, 서브클래스의 복잡도를 최소화하여 개발 생산성을 높입니다.

이러한 특징들이 유연하면서도 안전한 설계를 가능하게 만듭니다.

코드 예제

abstract class GameCharacter {
  // Template Method
  public attack(): void {
    this.prepareAttack();
    const damage = this.calculateDamage();
    this.executeAttack(damage);
    this.afterAttack(); // Hook 메서드
    this.playSound(); // Hook 메서드
  }

  // 추상 메서드: 모든 캐릭터가 반드시 구현
  protected abstract calculateDamage(): number;
  protected abstract executeAttack(damage: number): void;

  // 구체 메서드: 공통 로직
  protected prepareAttack(): void {
    console.log("공격 준비 중...");
  }

  // Hook 메서드: 기본 구현 제공, 필요시 오버라이드
  protected afterAttack(): void {
    // 기본적으로는 아무것도 하지 않음
  }

  // Hook 메서드: 선택적 기능
  protected playSound(): void {
    console.log("기본 공격 사운드");
  }
}

class Warrior extends GameCharacter {
  protected calculateDamage(): number {
    return 100;
  }

  protected executeAttack(damage: number): void {
    console.log(`전사가 ${damage}의 물리 데미지를 입혔습니다!`);
  }

  // Hook 메서드 오버라이드: 전사만의 특수 효과
  protected afterAttack(): void {
    console.log("방어력 버프 획득!");
  }
}

class Berserker extends GameCharacter {
  protected calculateDamage(): number {
    return 150;
  }

  protected executeAttack(damage: number): void {
    console.log(`버서커가 ${damage}의 강력한 데미지를 입혔습니다!`);
  }

  // Hook 메서드 오버라이드: 버서커만의 특수 효과
  protected afterAttack(): void {
    console.log("체력을 소모하여 공격력 증가!");
  }

  protected playSound(): void {
    console.log("광전사의 함성!");
  }
}

설명

이것이 하는 일: 추상 메서드와 Hook 메서드를 구분하여 서브클래스의 구현 부담을 최소화하면서도 필요한 확장성은 확보합니다. 각 메서드 타입은 명확한 목적과 역할을 가지고 있습니다.

첫 번째로, calculateDamage()executeAttack() 같은 추상 메서드는 각 캐릭터의 정체성을 결정하는 핵심 동작입니다. 이들을 추상 메서드로 선언함으로써 "새로운 캐릭터를 만들려면 반드시 공격 방식을 정의해야 한다"는 규칙을 코드 레벨에서 강제합니다.

TypeScript 컴파일러가 이를 검증하므로 개발자가 실수로 누락할 수 없습니다. 그 다음으로, afterAttack() 같은 Hook 메서드는 빈 구현이나 기본 구현을 제공합니다.

대부분의 캐릭터는 공격 후 특별한 동작이 없으므로 이 메서드를 오버라이드하지 않아도 됩니다. 하지만 Warrior나 Berserker처럼 특수 효과가 있는 캐릭터는 이 Hook 메서드를 오버라이드하여 자신만의 동작을 추가합니다.

세 번째로, prepareAttack() 같은 구체 메서드는 모든 캐릭터에게 공통적인 로직을 담당합니다. 이런 메서드는 오버라이드할 필요가 없으며, 만약 공통 로직이 변경되어도 부모 클래스 한 곳만 수정하면 됩니다.

필요하다면 일부 서브클래스에서 오버라이드할 수도 있지만, 일반적으로는 그대로 사용합니다. 마지막으로, playSound() 같은 Hook 메서드는 기본 동작을 제공하면서도 커스터마이징을 허용합니다.

기본 캐릭터는 "기본 공격 사운드"를 재생하지만, Berserker는 더 강렬한 "광전사의 함성!"을 재생합니다. 이렇게 기본값을 제공하면 서브클래스는 필요한 경우에만 오버라이드하면 되므로 보일러플레이트 코드가 줄어듭니다.

여러분이 이 패턴을 사용하면 새로운 캐릭터를 추가할 때 핵심 메서드(추상 메서드)만 집중해서 구현하면 되고, 특수 효과가 필요한 경우에만 Hook 메서드를 오버라이드하면 됩니다. 코드 리뷰 시에도 "이 캐릭터의 고유한 특성은 무엇인가?"가 명확히 드러나므로 이해하기 쉽습니다.

또한 나중에 모든 캐릭터에 새로운 기능을 추가하고 싶다면 부모 클래스에 새로운 Hook 메서드를 추가하기만 하면 되므로 확장이 매우 용이합니다.

실전 팁

💡 Hook 메서드의 이름은 "before", "after", "on" 같은 접두사를 사용하여 확장 포인트임을 명확히 표현하세요. 예: beforeValidation(), afterSave(), onError(). 이렇게 하면 다른 개발자가 코드를 볼 때 "아, 이건 선택적으로 오버라이드할 수 있구나"를 즉시 파악할 수 있습니다.

💡 Hook 메서드는 빈 구현({}) 또는 의미 있는 기본 구현을 제공하세요. 절대 예외를 던지거나 NotImplementedError를 던지지 마세요. Hook 메서드는 오버라이드하지 않아도 동작해야 합니다.

💡 추상 메서드의 개수는 5개 이하로 유지하는 것이 좋습니다. 그 이상이면 클래스의 책임이 너무 크거나 추상화 수준이 적절하지 않다는 신호입니다. 이 경우 클래스를 분리하거나 일부를 Hook 메서드로 전환하는 것을 고려하세요.

💡 Hook 메서드에 주석으로 "언제 오버라이드해야 하는지"를 문서화하세요. 예: // Override this method if your character has special effects after attacking. 이런 가이드가 있으면 서브클래스 작성자가 훨씬 쉽게 확장할 수 있습니다.

💡 Template Method 내에서 Hook 메서드를 여러 개 호출할 때는 순서에 의미를 부여하세요. 예를 들어 beforeX()doX()afterX() 같은 일관된 패턴을 유지하면 코드의 가독성이 높아지고 예측 가능성이 증가합니다.


3. 실전 예제 데이터 처리 파이프라인

시작하며

여러분이 다양한 소스(API, 데이터베이스, 파일)에서 데이터를 가져와 ETL(Extract, Transform, Load) 작업을 수행하는 시스템을 만든다고 생각해보세요. 각 데이터 소스마다 "연결 → 추출 → 변환 → 검증 → 적재 → 연결 종료"라는 동일한 파이프라인을 거치지만, 각 단계의 구체적인 구현은 완전히 다릅니다.

이런 상황에서 각 데이터 소스마다 전체 파이프라인 로직을 복사해서 붙여넣으면 어떻게 될까요? 파이프라인에 로깅, 에러 핸들링, 성능 모니터링을 추가하려면 모든 클래스를 일일이 수정해야 합니다.

한 곳을 빠뜨리면 불일치가 발생하고 버그의 온상이 됩니다. 바로 이럴 때 Template Method Pattern이 진가를 발휘합니다.

파이프라인의 전체 흐름과 공통 로직(로깅, 에러 핸들링 등)은 부모 클래스에서 관리하고, 각 데이터 소스의 특성(API 호출, SQL 쿼리, 파일 읽기 등)만 서브클래스에서 구현하는 것입니다. 이렇게 하면 새로운 데이터 소스를 추가할 때 핵심 로직만 구현하면 되고, 파이프라인의 공통 기능은 자동으로 상속받습니다.

유지보수도 훨씬 쉬워지고 코드 품질도 일관되게 유지할 수 있습니다.

개요

간단히 말해서, 데이터 처리 파이프라인에 Template Method Pattern을 적용하면 ETL 프로세스의 골격은 한 곳에서 정의하고, 각 데이터 소스의 특성만 개별적으로 구현할 수 있습니다. 실무에서 이 접근법이 특히 유용한 이유는 데이터 파이프라인이 점진적으로 진화하기 때문입니다.

처음에는 단순히 데이터를 가져와 저장하는 것으로 시작하지만, 시간이 지나면서 "실패 시 재시도", "처리 시간 측정", "데이터 품질 검증", "알림 전송" 같은 공통 요구사항이 추가됩니다. Template Method Pattern을 사용하면 이런 공통 기능을 부모 클래스에 추가하기만 하면 모든 데이터 소스에 자동으로 적용됩니다.

기존에는 각 데이터 소스 처리 클래스를 독립적으로 작성했다면, 이제는 DataPipeline 추상 클래스를 상속받아 필수 메서드만 구현하면 됩니다. 파이프라인의 실행 순서, 에러 핸들링, 로깅 같은 복잡한 부분은 부모 클래스가 알아서 처리해줍니다.

이 패턴의 핵심 장점은 세 가지입니다. 첫째, 모든 데이터 소스가 동일한 파이프라인 구조를 따르므로 일관성이 보장됩니다.

둘째, 공통 기능(로깅, 모니터링 등)을 한 곳에서 관리하므로 유지보수가 편리합니다. 셋째, 새로운 데이터 소스 추가가 매우 간단해집니다.

이러한 장점들이 대규모 데이터 처리 시스템의 복잡도를 크게 낮춰줍니다.

코드 예제

abstract class DataPipeline<T> {
  // Template Method: ETL 파이프라인 정의
  public async execute(): Promise<void> {
    try {
      console.log(`[${this.getName()}] 파이프라인 시작`);
      const startTime = Date.now();

      await this.connect();
      const rawData = await this.extract();
      console.log(`추출된 데이터: ${rawData.length}건`);

      const transformedData = await this.transform(rawData);
      await this.validate(transformedData);
      await this.load(transformedData);

      const duration = Date.now() - startTime;
      console.log(`[${this.getName()}] 파이프라인 완료 (${duration}ms)`);

      await this.onSuccess(); // Hook
    } catch (error) {
      console.error(`[${this.getName()}] 파이프라인 실패:`, error);
      await this.onError(error); // Hook
      throw error;
    } finally {
      await this.disconnect();
    }
  }

  // 추상 메서드: 각 데이터 소스가 반드시 구현
  protected abstract getName(): string;
  protected abstract connect(): Promise<void>;
  protected abstract extract(): Promise<T[]>;
  protected abstract transform(data: T[]): Promise<T[]>;
  protected abstract disconnect(): Promise<void>;

  // 구체 메서드: 공통 검증 로직
  protected async validate(data: T[]): Promise<void> {
    if (data.length === 0) {
      throw new Error("변환된 데이터가 비어있습니다");
    }
    console.log("데이터 검증 통과");
  }

  // 구체 메서드: 공통 적재 로직
  protected async load(data: T[]): Promise<void> {
    console.log(`${data.length}건의 데이터를 데이터베이스에 저장`);
    // 실제 저장 로직...
  }

  // Hook 메서드: 선택적 확장
  protected async onSuccess(): Promise<void> {}
  protected async onError(error: any): Promise<void> {}
}

// API 데이터 소스 구현
class ApiDataPipeline extends DataPipeline<any> {
  constructor(private apiUrl: string) {
    super();
  }

  protected getName(): string {
    return "API Pipeline";
  }

  protected async connect(): Promise<void> {
    console.log(`API 연결: ${this.apiUrl}`);
  }

  protected async extract(): Promise<any[]> {
    // 실제로는 fetch나 axios 사용
    return [{ id: 1, name: "데이터1" }, { id: 2, name: "데이터2" }];
  }

  protected async transform(data: any[]): Promise<any[]> {
    // API 응답을 내부 형식으로 변환
    return data.map(item => ({
      id: item.id,
      name: item.name.toUpperCase(),
      source: "API"
    }));
  }

  protected async disconnect(): Promise<void> {
    console.log("API 연결 종료");
  }

  protected async onSuccess(): Promise<void> {
    console.log("성공 알림을 Slack으로 전송");
  }
}

// 파일 데이터 소스 구현
class FileDataPipeline extends DataPipeline<string> {
  constructor(private filePath: string) {
    super();
  }

  protected getName(): string {
    return "File Pipeline";
  }

  protected async connect(): Promise<void> {
    console.log(`파일 열기: ${this.filePath}`);
  }

  protected async extract(): Promise<string[]> {
    // 실제로는 fs.readFile 사용
    return ["line1", "line2", "line3"];
  }

  protected async transform(data: string[]): Promise<string[]> {
    // 각 라인을 정제
    return data.map(line => line.trim().toLowerCase());
  }

  protected async disconnect(): Promise<void> {
    console.log("파일 닫기");
  }
}

설명

이것이 하는 일: 데이터 처리 파이프라인을 Template Method Pattern으로 구조화하여 모든 데이터 소스가 일관된 방식으로 처리되도록 보장합니다. 공통 로직은 재사용하고 특화된 부분만 개별 구현합니다.

첫 번째로, execute() 템플릿 메서드가 전체 ETL 프로세스의 흐름을 제어합니다. 이 메서드는 try-catch-finally를 사용하여 에러 핸들링과 리소스 정리를 보장하며, 실행 시간을 측정하고 로그를 남깁니다.

이런 공통 로직이 부모 클래스에 한 번만 구현되어 있으므로, 모든 서브클래스는 자동으로 동일한 에러 핸들링과 로깅 기능을 갖게 됩니다. 그 다음으로, extract()transform() 같은 추상 메서드는 각 데이터 소스의 정체성을 결정합니다.

ApiDataPipeline은 HTTP 요청으로 데이터를 추출하고 JSON을 파싱하는 반면, FileDataPipeline은 파일 시스템에서 읽고 텍스트를 처리합니다. 이렇게 데이터 소스마다 완전히 다른 추출 로직을 구현하지만, 전체 파이프라인의 흐름은 동일하게 유지됩니다.

세 번째로, validate()load() 같은 구체 메서드는 모든 데이터 소스에 공통적으로 적용되는 로직입니다. 예를 들어 데이터가 비어있는지 검증하는 로직은 데이터 소스와 관계없이 항상 필요하므로 부모 클래스에 구현되어 있습니다.

만약 나중에 "데이터 중복 검사"를 추가하고 싶다면 validate() 메서드만 수정하면 모든 파이프라인에 자동으로 적용됩니다. 마지막으로, onSuccess()onError() 같은 Hook 메서드는 선택적 확장 포인트를 제공합니다.

대부분의 파이프라인은 이 메서드들을 오버라이드하지 않지만, ApiDataPipeline처럼 성공 시 알림을 보내야 하는 경우에만 onSuccess()를 오버라이드합니다. 이렇게 필요한 경우에만 추가 동작을 구현할 수 있어 유연성이 높습니다.

여러분이 이 구조를 사용하면 새로운 데이터 소스를 추가할 때 핵심 메서드 4-5개만 구현하면 됩니다. 에러 핸들링, 로깅, 시간 측정, 리소스 정리 같은 복잡한 부분은 이미 부모 클래스에 구현되어 있으므로 신경 쓸 필요가 없습니다.

또한 모든 파이프라인이 동일한 구조를 따르므로 코드 리뷰나 디버깅이 훨씬 쉬워집니다. 나중에 "재시도 로직"이나 "성능 모니터링"을 추가하고 싶다면 부모 클래스의 execute() 메서드만 수정하면 모든 파이프라인에 즉시 반영됩니다.

실전 팁

💡 제네릭 타입(<T>)을 활용하여 다양한 데이터 타입을 처리할 수 있도록 만드세요. 이렇게 하면 API는 JSON 객체를, 파일은 문자열을, 데이터베이스는 엔티티 객체를 다루는 식으로 유연하게 대응할 수 있습니다.

💡 템플릿 메서드에 성능 측정 코드를 넣으면 모든 파이프라인의 실행 시간을 자동으로 추적할 수 있습니다. 이 데이터를 모니터링 시스템에 전송하면 어느 파이프라인이 느린지 즉시 파악할 수 있습니다.

💡 connect()disconnect()를 추상 메서드로 만들어 리소스 관리를 강제하세요. 이렇게 하면 서브클래스가 실수로 연결을 닫지 않는 것을 방지할 수 있습니다. finally 블록에서 disconnect()를 호출하면 에러가 발생해도 리소스가 정리됩니다.

💡 파이프라인의 각 단계에서 중간 결과를 로깅하면 디버깅이 훨씬 쉬워집니다. 특히 "추출된 데이터: N건", "변환 완료: N건" 같은 정량적인 정보를 남기면 데이터가 어디서 사라지거나 변형되는지 추적할 수 있습니다.

💡 onError() Hook에서 에러 타입별로 다른 처리를 할 수 있습니다. 예를 들어 네트워크 에러는 재시도하고, 데이터 검증 에러는 알림만 보내는 식으로 세분화된 에러 핸들링을 구현하세요.


4. Hollywood Principle

시작하며

여러분이 Template Method Pattern을 처음 접하면 이상한 느낌을 받을 수 있습니다. "부모 클래스가 자식 클래스의 메서드를 호출한다고?" 일반적인 객체지향 프로그래밍에서는 자식 클래스가 부모 클래스의 메서드를 호출하는 것에 익숙한데, 이 패턴은 정반대로 작동합니다.

이런 역전된 제어 흐름은 처음에는 혼란스럽지만, 실제로는 매우 강력한 설계 원칙입니다. 전통적인 방식에서는 각 서브클래스가 "언제 어떤 순서로 메서드를 호출할지"를 스스로 결정해야 했습니다.

이렇게 되면 서브클래스마다 호출 순서가 달라질 수 있고, 공통 로직을 빼먹는 실수가 발생할 수 있습니다. 바로 이럴 때 적용되는 것이 Hollywood Principle입니다.

"Don't call us, we'll call you"(우리에게 연락하지 마세요, 우리가 연락할게요)라는 이 원칙은 제어권을 부모 클래스가 가지도록 합니다. 이렇게 하면 알고리즘의 전체 흐름은 부모 클래스가 제어하고, 서브클래스는 호출될 때까지 기다리기만 하면 됩니다.

서브클래스는 "어떻게 동작할지"만 정의하고 "언제 동작할지"는 부모에게 맡기는 것이죠. 이로써 일관성과 안정성이 크게 향상됩니다.

개요

간단히 말해서, Hollywood Principle은 상위 컴포넌트가 하위 컴포넌트를 호출하는 제어 역전(Inversion of Control) 원칙이며, Template Method Pattern이 이 원칙을 구현하는 대표적인 예입니다. 실무에서 이 원칙이 중요한 이유는 복잡한 알고리즘의 일관성을 보장하기 때문입니다.

예를 들어, 결제 시스템에서 "인증 → 검증 → 결제 → 영수증 발행 → 알림"이라는 순서가 정해져 있다면, 이 순서를 모든 결제 수단에서 지켜야 합니다. 만약 각 결제 클래스가 스스로 이 순서를 관리한다면 누군가는 실수로 인증을 빼먹거나 순서를 바꿀 수 있습니다.

기존에는 각 서브클래스가 "부모의 메서드를 정확한 순서로 호출"해야 했다면, Hollywood Principle을 적용하면 부모 클래스가 "자식의 메서드를 정확한 순서로 호출"하므로 서브클래스는 순서를 신경 쓸 필요가 없습니다. 이 원칙의 핵심 장점은 세 가지입니다.

첫째, 알고리즘의 구조를 한 곳에서 정의하므로 일관성이 보장됩니다. 둘째, 서브클래스의 복잡도가 낮아져 구현이 쉬워집니다.

셋째, 알고리즘의 흐름을 변경할 때 부모 클래스만 수정하면 되므로 유지보수가 편리합니다. 이러한 장점들이 대규모 시스템에서 코드 품질을 높이는 핵심 요소가 됩니다.

코드 예제

// Hollywood Principle: "Don't call us, we'll call you"
abstract class PaymentProcessor {
  // 부모가 제어 흐름을 완전히 관리
  public processPayment(amount: number): void {
    console.log("=== 결제 프로세스 시작 ===");

    // 부모가 자식의 메서드를 호출 (제어 역전)
    if (!this.authenticate()) {
      throw new Error("인증 실패");
    }

    if (!this.validate(amount)) {
      throw new Error("검증 실패");
    }

    const transactionId = this.executePayment(amount);
    console.log(`거래 ID: ${transactionId}`);

    this.issueReceipt(transactionId, amount);
    this.sendNotification(transactionId);

    console.log("=== 결제 프로세스 완료 ===");
  }

  // 서브클래스는 "어떻게"만 정의, "언제"는 부모가 결정
  protected abstract authenticate(): boolean;
  protected abstract executePayment(amount: number): string;

  // 공통 로직은 부모가 제공
  protected validate(amount: number): boolean {
    return amount > 0 && amount < 1000000;
  }

  protected issueReceipt(transactionId: string, amount: number): void {
    console.log(`영수증 발행: ${transactionId}, ${amount}원`);
  }

  protected sendNotification(transactionId: string): void {
    console.log(`알림 전송: ${transactionId}`);
  }
}

class CreditCardPayment extends PaymentProcessor {
  // "어떻게 인증할지"만 구현
  protected authenticate(): boolean {
    console.log("신용카드 인증 중...");
    return true;
  }

  // "어떻게 결제할지"만 구현
  protected executePayment(amount: number): string {
    console.log(`신용카드로 ${amount}원 결제`);
    return `CC-${Date.now()}`;
  }
}

// 잘못된 방식 (Hollywood Principle 위반)
class BadPaymentProcessor {
  // 서브클래스가 스스로 순서를 관리 (위험!)
  public processPayment(amount: number): void {
    // 실수로 authenticate()를 빼먹을 수 있음
    this.validate(amount);
    this.executePayment(amount);
    // 영수증 발행을 잊어버릴 수 있음
  }

  protected validate(amount: number): boolean { return true; }
  protected executePayment(amount: number): void {}
}

설명

이것이 하는 일: Hollywood Principle은 상위 수준 컴포넌트(부모 클래스)가 하위 수준 컴포넌트(서브클래스)를 언제 어떻게 호출할지 결정하는 설계 원칙입니다. 이를 통해 알고리즘의 제어 흐름을 중앙화하고 일관성을 유지합니다.

첫 번째로, processPayment() 템플릿 메서드가 전체 결제 프로세스의 흐름을 완벽하게 제어합니다. 이 메서드는 인증 → 검증 → 결제 → 영수증 → 알림이라는 정확한 순서를 강제하며, 서브클래스는 이 순서를 변경할 수 없습니다.

이것이 Hollywood Principle의 핵심입니다: "우리(부모)가 너(자식)를 언제 호출할지 결정할게." 그 다음으로, 서브클래스는 authenticate()executePayment() 같은 추상 메서드의 구현만 제공합니다. CreditCardPayment 클래스는 "신용카드로 어떻게 인증하고 결제하는지"만 알면 되고, "언제 인증하고 언제 결제하는지"는 전혀 신경 쓰지 않습니다.

부모가 적절한 타이밍에 이 메서드들을 호출해줄 것이라고 신뢰하는 것입니다. 세 번째로, 공통 로직인 validate(), issueReceipt(), sendNotification() 같은 메서드들도 부모가 적절한 시점에 호출합니다.

서브클래스는 이런 메서드들의 존재조차 알 필요가 없습니다. 부모가 알아서 처리해주기 때문이죠.

이렇게 하면 나중에 "결제 전에 사기 탐지를 추가하자"는 요구사항이 생겨도 부모의 processPayment() 메서드만 수정하면 모든 결제 수단에 자동으로 적용됩니다. 마지막으로, "잘못된 방식" 예제에서 볼 수 있듯이 서브클래스가 스스로 흐름을 제어하면 실수가 발생하기 쉽습니다.

인증을 빼먹거나 영수증 발행을 잊어버리거나 순서를 바꾸는 등의 실수가 각 서브클래스마다 다르게 나타날 수 있습니다. Hollywood Principle을 따르면 이런 실수가 원천적으로 불가능합니다.

여러분이 이 원칙을 적용하면 새로운 결제 수단을 추가할 때 핵심 비즈니스 로직(인증, 결제)에만 집중할 수 있습니다. 전체 프로세스의 흐름, 에러 핸들링, 로깅, 영수증 발행 같은 부가 기능은 부모가 자동으로 제공하므로 개발 속도가 빨라지고 버그도 줄어듭니다.

또한 코드 리뷰 시 "이 결제 수단의 고유한 특성은 무엇인가?"만 집중해서 보면 되므로 리뷰 품질도 향상됩니다.

실전 팁

💡 템플릿 메서드를 final로 만들어 서브클래스가 오버라이드하지 못하도록 보호하세요(TypeScript에서는 주석으로 명시). 이렇게 하면 제어 흐름의 무결성이 보장됩니다.

💡 템플릿 메서드 내에서 각 단계의 실행 여부를 로깅하면 디버깅이 쉬워집니다. "인증 시작", "인증 완료", "검증 시작" 같은 로그를 남기면 어느 단계에서 문제가 발생했는지 즉시 파악할 수 있습니다.

💡 서브클래스가 실수로 부모의 메서드를 직접 호출하지 않도록 주의하세요. 예를 들어 super.validate()를 호출하는 것은 괜찮지만, super.processPayment()를 호출하는 것은 제어 흐름을 망가뜨립니다.

💡 템플릿 메서드에 인자를 전달할 때는 서브클래스가 필요로 하는 모든 정보를 함께 전달하세요. 서브클래스가 외부 상태에 의존하지 않도록 하면 테스트가 쉬워집니다.

💡 복잡한 알고리즘의 경우 템플릿 메서드를 여러 개로 나눌 수 있습니다. 예를 들어 processPayment()handleRefund()처럼 서로 다른 흐름을 별도의 템플릿 메서드로 정의하세요.


5. Template Method vs Strategy Pattern

시작하며

여러분이 디자인 패턴을 공부하다 보면 Template Method Pattern과 Strategy Pattern이 비슷해 보여서 혼란스러울 수 있습니다. 둘 다 "알고리즘의 변형"을 다루고, 둘 다 다형성을 활용하며, 둘 다 확장 가능한 구조를 만들기 때문이죠.

실제 프로젝트에서 "이 상황에서 어떤 패턴을 써야 하지?"라는 질문에 직면하면 고민이 깊어집니다. 잘못된 선택을 하면 나중에 구조를 바꾸기 어려워지고, 코드가 불필요하게 복잡해질 수 있습니다.

바로 이럴 때 필요한 것이 두 패턴의 차이점과 선택 기준을 명확히 이해하는 것입니다. Template Method는 "상속"을 통해 알고리즘의 골격을 공유하고, Strategy는 "구성"을 통해 알고리즘 전체를 교체합니다.

이 차이를 이해하면 "전체 흐름은 같은데 일부 단계만 다른가?"(Template Method) 또는 "알고리즘 전체가 완전히 다른가?"(Strategy) 같은 질문으로 적절한 패턴을 선택할 수 있습니다. 올바른 패턴 선택은 코드의 유지보수성과 확장성에 큰 영향을 미칩니다.

개요

간단히 말해서, Template Method는 상속을 사용하여 알고리즘의 일부 단계를 변경하고, Strategy는 구성을 사용하여 알고리즘 전체를 교체합니다. 실무에서 이 차이가 중요한 이유는 문제의 성격에 따라 적합한 패턴이 다르기 때문입니다.

예를 들어, 다양한 파일 형식(CSV, JSON, XML)을 파싱하는 시스템을 만든다고 해봅시다. "파일 열기 → 파싱 → 검증 → 변환"이라는 공통 흐름이 있고 파싱 방식만 다르다면 Template Method가 적합합니다.

반면 압축 알고리즘(ZIP, GZIP, BZIP2)을 선택하는 시스템이라면 알고리즘 전체가 완전히 다르므로 Strategy가 적합합니다. 기존에는 "알고리즘이 비슷하면 무조건 Template Method"라고 생각했다면, 이제는 "공통 흐름이 있는가?", "런타임에 알고리즘을 바꿔야 하는가?", "상속 계층이 적절한가?" 같은 질문으로 선택할 수 있습니다.

두 패턴의 핵심 차이는 세 가지입니다. 첫째, Template Method는 컴파일 타임에 알고리즘이 결정되고 Strategy는 런타임에 결정됩니다.

둘째, Template Method는 IS-A 관계(상속)를 사용하고 Strategy는 HAS-A 관계(구성)를 사용합니다. 셋째, Template Method는 알고리즘의 골격을 공유하고 Strategy는 알고리즘을 완전히 캡슐화합니다.

이러한 차이들이 각 패턴의 적용 시나리오를 결정합니다.

코드 예제

// Template Method: 상속 기반, 알고리즘 골격 공유
abstract class ReportGenerator {
  // 알고리즘의 골격 정의
  public generate(): string {
    const header = this.createHeader();
    const data = this.fetchData();
    const body = this.formatData(data);
    const footer = this.createFooter();
    return `${header}\n${body}\n${footer}`;
  }

  protected abstract formatData(data: any[]): string;

  protected createHeader(): string {
    return "=== 리포트 ===";
  }

  protected fetchData(): any[] {
    return [{ id: 1 }, { id: 2 }];
  }

  protected createFooter(): string {
    return "=== 끝 ===";
  }
}

class PdfReport extends ReportGenerator {
  protected formatData(data: any[]): string {
    return `PDF 형식: ${data.length}건`;
  }
}

class ExcelReport extends ReportGenerator {
  protected formatData(data: any[]): string {
    return `Excel 형식: ${data.length}건`;
  }
}

// Strategy: 구성 기반, 알고리즘 전체 교체
interface CompressionStrategy {
  compress(data: string): string;
}

class ZipCompression implements CompressionStrategy {
  compress(data: string): string {
    return `[ZIP] ${data}`;
  }
}

class GzipCompression implements CompressionStrategy {
  compress(data: string): string {
    return `[GZIP] ${data}`;
  }
}

class FileCompressor {
  private strategy: CompressionStrategy;

  constructor(strategy: CompressionStrategy) {
    this.strategy = strategy;
  }

  // 런타임에 전략 변경 가능
  setStrategy(strategy: CompressionStrategy): void {
    this.strategy = strategy;
  }

  compress(data: string): string {
    return this.strategy.compress(data);
  }
}

// 사용 비교
function demonstrateDifference() {
  // Template Method: 컴파일 타임에 결정
  const pdfReport = new PdfReport();
  console.log(pdfReport.generate());

  // Strategy: 런타임에 변경 가능
  const compressor = new FileCompressor(new ZipCompression());
  console.log(compressor.compress("데이터"));

  compressor.setStrategy(new GzipCompression()); // 알고리즘 교체
  console.log(compressor.compress("데이터"));
}

설명

이것이 하는 일: 두 패턴은 알고리즘의 변형을 다루지만, 서로 다른 메커니즘과 목적을 가지고 있습니다. 각 패턴의 강점과 약점을 이해하면 상황에 맞는 최적의 선택을 할 수 있습니다.

첫 번째로, Template Method는 상속 계층을 만들어 알고리즘의 골격을 공유합니다. ReportGeneratorgenerate() 메서드는 "헤더 → 데이터 가져오기 → 데이터 포맷팅 → 푸터"라는 공통 흐름을 정의하며, 서브클래스는 formatData()만 오버라이드하여 자신만의 포맷을 제공합니다.

이 방식은 공통 로직이 많고 변형 포인트가 명확할 때 코드 중복을 크게 줄여줍니다. 그 다음으로, Strategy는 인터페이스와 구성을 사용하여 알고리즘 전체를 캡슐화합니다.

CompressionStrategy 인터페이스를 구현하는 각 클래스는 완전히 독립적인 압축 알고리즘을 제공하며, FileCompressor는 런타임에 전략을 교체할 수 있습니다. 이 방식은 알고리즘 간 공통점이 적고 런타임에 동적으로 변경해야 할 때 유용합니다.

세 번째로, 선택 기준을 명확히 정리하면 이렇습니다. Template Method를 선택하는 경우: (1) 알고리즘의 대부분이 동일하고 일부만 다름, (2) 공통 로직을 재사용하고 싶음, (3) 서브클래스의 변형 포인트를 제한하고 싶음, (4) IS-A 관계가 자연스러움(PdfReport IS-A ReportGenerator).

Strategy를 선택하는 경우: (1) 알고리즘 전체가 완전히 다름, (2) 런타임에 알고리즘을 변경해야 함, (3) 상속 계층을 피하고 싶음, (4) 알고리즘을 독립적으로 테스트하고 싶음. 마지막으로, 실무에서는 두 패턴을 혼합하여 사용할 수도 있습니다.

예를 들어 Template Method로 전체 흐름을 정의하되, 특정 단계에서 Strategy를 사용하여 알고리즘을 선택하는 식입니다. ReportGeneratorformatData() 메서드 내부에서 FormattingStrategy를 사용하면 더욱 유연한 구조를 만들 수 있습니다.

여러분이 패턴을 선택할 때는 "공통 흐름이 있는가?"를 먼저 확인하세요. 공통 흐름이 명확하다면 Template Method가 적합하고, 알고리즘이 완전히 독립적이라면 Strategy가 적합합니다.

또한 "런타임에 변경이 필요한가?"도 중요한 기준입니다. 한 번 정해지면 바뀌지 않는다면 Template Method의 단순함이 장점이고, 자주 바뀌어야 한다면 Strategy의 유연함이 필요합니다.

실전 팁

💡 "IS-A vs HAS-A" 테스트를 해보세요. "PdfReport IS-A ReportGenerator"가 자연스럽다면 Template Method, "FileCompressor HAS-A CompressionStrategy"가 자연스럽다면 Strategy를 선택하세요.

💡 Template Method는 상속 계층을 깊게 만들 수 있으므로 최대 2-3단계로 제한하세요. 그 이상 깊어지면 이해하기 어려워지고 Strategy로 전환하는 것이 좋습니다.

💡 Strategy 패턴은 전략 객체 생성 비용이 있으므로, 전략이 stateless라면 싱글톤이나 플라이웨이트 패턴을 함께 사용하여 객체를 재사용하세요.

💡 TypeScript에서는 Strategy를 함수로 간단히 구현할 수 있습니다. type CompressionStrategy = (data: string) => string; 같은 함수 타입을 사용하면 클래스를 만들 필요가 없어 코드가 간결해집니다.

💡 두 패턴을 혼합할 때는 책임을 명확히 분리하세요. Template Method는 "전체 흐름 제어"를, Strategy는 "특정 알고리즘 구현"을 담당하도록 역할을 구분하면 코드가 깔끔해집니다.


6. 실무 활용 테스트 프레임워크 구조

시작하며

여러분이 프로젝트에서 수백 개의 테스트를 작성하다 보면 공통 패턴을 발견하게 됩니다. "테스트 환경 설정 → 테스트 실행 → 결과 검증 → 환경 정리"라는 동일한 구조가 모든 테스트에 반복되는 것이죠.

매번 데이터베이스 연결을 설정하고 정리하는 코드를 복사해서 붙여넣다 보면 코드 중복이 심해집니다. 더 큰 문제는 나중에 테스트 환경을 변경할 때 발생합니다.

예를 들어 "모든 테스트에 실행 시간 측정을 추가하자"는 요구사항이 생기면 수백 개의 테스트를 일일이 수정해야 합니다. 한 곳이라도 빠뜨리면 일관성이 깨지고, 이런 수작업은 실수하기 쉽습니다.

바로 이럴 때 Template Method Pattern이 테스트 프레임워크의 기반이 됩니다. JUnit, Jest, pytest 같은 유명 테스트 프레임워크들이 모두 이 패턴을 사용하는 이유가 여기 있습니다.

테스트의 공통 구조(setup → test → teardown)는 부모 클래스가 정의하고, 각 테스트의 구체적인 로직만 서브클래스가 구현하는 것입니다. 이렇게 하면 새로운 테스트를 추가할 때 핵심 로직만 작성하면 되고, 환경 설정이나 정리는 자동으로 처리됩니다.

개요

간단히 말해서, 테스트 프레임워크에서 Template Method Pattern을 사용하면 테스트 실행의 생명주기(setup → execute → teardown)를 표준화하고, 각 테스트는 고유한 검증 로직만 구현하면 됩니다. 실무에서 이 패턴이 테스트 코드 품질을 크게 향상시키는 이유는 명확합니다.

통합 테스트에서 데이터베이스 연결, 트랜잭션 시작/롤백, 테스트 데이터 생성/삭제 같은 공통 작업을 매번 작성하는 것은 번거롭고 오류가 발생하기 쉽습니다. 베이스 테스트 클래스가 이런 공통 작업을 처리하면 개발자는 "무엇을 테스트할 것인가?"에만 집중할 수 있습니다.

기존에는 각 테스트 파일마다 beforeEach, afterEach 같은 설정 코드를 반복했다면, Template Method를 사용하면 한 번만 정의하고 모든 테스트가 상속받아 사용할 수 있습니다. 테스트 실행 시간 측정, 로깅, 에러 리포팅 같은 공통 기능도 부모 클래스에 추가하면 모든 테스트에 자동으로 적용됩니다.

이 접근법의 핵심 장점은 세 가지입니다. 첫째, 테스트 코드의 중복이 크게 줄어들어 유지보수가 쉬워집니다.

둘째, 모든 테스트가 동일한 방식으로 환경을 설정/정리하므로 일관성이 보장됩니다. 셋째, 새로운 테스트 작성이 매우 간단해져 개발 생산성이 향상됩니다.

이러한 장점들이 대규모 프로젝트에서 테스트 작성의 허들을 낮추고 테스트 커버리지를 높이는 데 기여합니다.

코드 예제

// 테스트 베이스 클래스: Template Method Pattern 적용
abstract class IntegrationTest {
  protected db: any;
  protected transaction: any;

  // Template Method: 테스트 실행 생명주기 정의
  public async run(): Promise<void> {
    const testName = this.getTestName();
    console.log(`\n[${testName}] 테스트 시작`);
    const startTime = Date.now();

    try {
      await this.setup();
      await this.beforeTest(); // Hook
      await this.executeTest(); // 추상 메서드
      await this.afterTest(); // Hook

      const duration = Date.now() - startTime;
      console.log(`[${testName}] 성공 (${duration}ms)`);
    } catch (error) {
      console.error(`[${testName}] 실패:`, error);
      throw error;
    } finally {
      await this.teardown();
    }
  }

  // 공통 setup: 모든 테스트에 적용
  protected async setup(): Promise<void> {
    console.log("  → 데이터베이스 연결");
    this.db = await this.connectDatabase();

    console.log("  → 트랜잭션 시작");
    this.transaction = await this.db.beginTransaction();
  }

  // 공통 teardown: 자동으로 정리
  protected async teardown(): Promise<void> {
    console.log("  → 트랜잭션 롤백");
    await this.transaction.rollback();

    console.log("  → 데이터베이스 연결 종료");
    await this.db.close();
  }

  // 추상 메서드: 각 테스트가 반드시 구현
  protected abstract getTestName(): string;
  protected abstract executeTest(): Promise<void>;

  // Hook 메서드: 필요한 경우에만 오버라이드
  protected async beforeTest(): Promise<void> {}
  protected async afterTest(): Promise<void> {}

  // 유틸리티 메서드: 모든 테스트가 공유
  protected async connectDatabase(): Promise<any> {
    return { beginTransaction: async () => ({ rollback: async () => {} }), close: async () => {} };
  }

  protected async createTestUser(name: string): Promise<any> {
    return { id: 1, name };
  }
}

// 구체적인 테스트 케이스
class UserRegistrationTest extends IntegrationTest {
  protected getTestName(): string {
    return "사용자 등록 테스트";
  }

  // Hook 활용: 테스트 데이터 준비
  protected async beforeTest(): Promise<void> {
    console.log("  → 테스트 데이터 생성");
    await this.createTestUser("기존사용자");
  }

  // 핵심 테스트 로직만 구현
  protected async executeTest(): Promise<void> {
    const newUser = await this.createTestUser("신규사용자");

    // 검증
    if (newUser.id !== 1) {
      throw new Error("사용자 생성 실패");
    }

    console.log("  ✓ 사용자 등록 성공");
  }
}

class UserDeletionTest extends IntegrationTest {
  protected getTestName(): string {
    return "사용자 삭제 테스트";
  }

  protected async executeTest(): Promise<void> {
    const user = await this.createTestUser("삭제될사용자");
    console.log("  ✓ 사용자 삭제 성공");
  }
}

// 테스트 실행
async function runTests() {
  const tests = [
    new UserRegistrationTest(),
    new UserDeletionTest()
  ];

  for (const test of tests) {
    await test.run();
  }
}

설명

이것이 하는 일: 테스트 프레임워크의 공통 구조를 Template Method로 표준화하여 모든 테스트가 일관된 방식으로 실행되도록 보장합니다. 개발자는 테스트의 핵심인 "검증 로직"에만 집중할 수 있습니다.

첫 번째로, run() 템플릿 메서드가 모든 테스트의 실행 흐름을 제어합니다. 이 메서드는 setup → beforeTest → executeTest → afterTest → teardown 순서를 강제하며, try-catch-finally로 에러 핸들링과 리소스 정리를 보장합니다.

또한 실행 시간을 자동으로 측정하고 로깅하므로, 개발자는 이런 부가 기능을 신경 쓸 필요가 없습니다. 그 다음으로, setup()teardown() 메서드가 모든 테스트의 공통 환경을 관리합니다.

데이터베이스 연결, 트랜잭션 시작, 롤백, 연결 종료 같은 복잡한 작업이 자동으로 처리되므로, 각 테스트는 깨끗한 환경에서 실행됩니다. 특히 finally 블록에서 teardown이 호출되므로 테스트가 실패해도 리소스가 확실히 정리됩니다.

세 번째로, executeTest() 추상 메서드가 각 테스트의 고유한 로직을 담당합니다. UserRegistrationTest는 사용자 등록을 테스트하고, UserDeletionTest는 사용자 삭제를 테스트하는 식으로 각 테스트의 목적이 명확하게 드러납니다.

이 메서드만 보면 "이 테스트가 무엇을 검증하는가?"를 즉시 파악할 수 있습니다. 마지막으로, beforeTest()afterTest() Hook 메서드가 선택적 확장 포인트를 제공합니다.

대부분의 테스트는 이 메서드를 오버라이드하지 않지만, UserRegistrationTest처럼 특별한 테스트 데이터가 필요한 경우에만 beforeTest()를 오버라이드하여 데이터를 준비합니다. 이렇게 필요한 경우에만 확장할 수 있어 코드가 간결해집니다.

여러분이 이런 테스트 베이스 클래스를 만들어두면 새로운 테스트를 추가할 때 5-10줄 정도의 코드만 작성하면 됩니다. 데이터베이스 설정, 트랜잭션 관리, 실행 시간 측정, 로깅 같은 복잡한 부분은 이미 구현되어 있으므로 생산성이 크게 향상됩니다.

또한 나중에 "모든 테스트에 성능 프로파일링을 추가하자"는 요구사항이 생겨도 베이스 클래스의 run() 메서드만 수정하면 되므로 유지보수가 매우 쉽습니다.

실전 팁

💡 테스트 베이스 클래스에 유틸리티 메서드(createTestUser, generateTestData 등)를 추가하면 모든 테스트에서 재사용할 수 있습니다. 테스트 데이터 생성 로직을 중앙화하면 일관성이 높아집니다.

💡 트랜잭션을 사용한 롤백은 테스트 속도를 크게 향상시킵니다. 실제 데이터 삭제 대신 트랜잭션 롤백을 사용하면 테스트 간 독립성도 보장되고 실행 시간도 단축됩니다.

💡 테스트 이름을 자동으로 추출하도록 만들 수 있습니다. TypeScript의 경우 this.constructor.name을 사용하면 클래스 이름을 가져올 수 있어 getTestName()을 구현하지 않아도 됩니다.

💡 환경별로 다른 베이스 클래스를 만들 수 있습니다. UnitTest, IntegrationTest, E2ETest 같은 계층을 만들면 각 테스트 레벨에 적합한 환경 설정을 제공할 수 있습니다.

💡 테스트 실패 시 스크린샷이나 로그를 자동으로 저장하도록 teardown()에 로직을 추가하세요. 이렇게 하면 CI/CD 환경에서 테스트가 실패했을 때 디버깅이 훨씬 쉬워집니다.


7. Hook 메서드 활용 전략

시작하며

여러분이 Template Method Pattern을 사용하다 보면 "이 기능은 모든 서브클래스에 필요할까, 아니면 일부만 필요할까?"라는 질문에 자주 직면합니다. 예를 들어 데이터 처리 파이프라인에서 "성공 시 알림 전송"은 중요한 파이프라인에만 필요하고, "캐시 무효화"는 특정 데이터 타입에만 필요합니다.

모든 기능을 추상 메서드로 만들면 대부분의 서브클래스가 빈 구현을 제공해야 하는 번거로움이 있습니다. 반대로 모든 기능을 부모 클래스에 구현하면 불필요한 동작이 실행되거나 확장성이 떨어집니다.

바로 이럴 때 Hook 메서드의 전략적 활용이 중요합니다. Hook 메서드를 어떻게 설계하느냐에 따라 패턴의 유연성과 사용 편의성이 크게 달라집니다.

잘 설계된 Hook 메서드는 "기본적으로는 아무것도 하지 않지만, 필요할 때 강력한 확장을 제공"하는 역할을 합니다. 이렇게 하면 단순한 서브클래스는 매우 간결하게 유지되고, 복잡한 요구사항이 있는 서브클래스만 Hook을 오버라이드하여 기능을 추가할 수 있습니다.

개요

간단히 말해서, Hook 메서드 활용 전략은 선택적 기능을 어떻게 설계하고 배치할 것인가에 대한 의사결정으로, 패턴의 유연성과 단순성 사이의 균형을 결정합니다. 실무에서 Hook 메서드 전략이 중요한 이유는 시스템이 진화하면서 요구사항이 다양해지기 때문입니다.

처음에는 단순했던 알고리즘이 시간이 지나면서 "로깅", "모니터링", "캐싱", "알림", "재시도" 같은 다양한 부가 기능을 요구하게 됩니다. 이런 기능들을 모두 필수로 만들면 단순한 케이스가 복잡해지고, 아예 빼버리면 고급 케이스를 처리할 수 없습니다.

기존에는 "필요한 기능은 서브클래스가 알아서 추가하세요"라는 식으로 방치했다면, Hook 메서드를 전략적으로 배치하면 "자주 필요한 확장 포인트는 Hook으로 제공하되, 기본 구현은 아무것도 하지 않음"으로 최적의 균형을 맞출 수 있습니다. Hook 메서드 전략의 핵심 원칙은 네 가지입니다.

첫째, Hook은 기본적으로 빈 구현이거나 안전한 기본 동작을 제공합니다. 둘째, Hook의 이름은 명확한 타이밍과 목적을 나타냅니다(before/after/on).

셋째, Hook은 알고리즘의 흐름을 방해하지 않습니다. 넷째, Hook은 문서화되어 개발자가 확장 가능성을 쉽게 파악할 수 있습니다.

이러한 원칙들이 유지보수 가능하고 확장 가능한 설계를 만듭니다.

코드 예제

abstract class DataProcessor {
  public async process(data: any[]): Promise<void> {
    await this.onStart(data); // Hook: 시작 시점

    await this.validate(data);
    await this.onValidated(data); // Hook: 검증 후

    const transformed = await this.transform(data);
    await this.onTransformed(transformed); // Hook: 변환 후

    await this.save(transformed);
    await this.onSaved(transformed); // Hook: 저장 후

    await this.onComplete(transformed); // Hook: 완료 시점
  }

  // 추상 메서드: 필수 구현
  protected abstract transform(data: any[]): Promise<any[]>;

  // 구체 메서드: 공통 로직
  protected async validate(data: any[]): Promise<void> {
    if (data.length === 0) throw new Error("데이터 없음");
  }

  protected async save(data: any[]): Promise<void> {
    console.log(`${data.length}건 저장`);
  }

  // Hook 메서드: 전략적으로 배치
  protected async onStart(data: any[]): Promise<void> {
    // 기본: 아무것도 하지 않음
  }

  protected async onValidated(data: any[]): Promise<void> {
    // 기본: 아무것도 하지 않음
  }

  protected async onTransformed(data: any[]): Promise<void> {
    // 기본: 아무것도 하지 않음
  }

  protected async onSaved(data: any[]): Promise<void> {
    // 기본: 아무것도 하지 않음
  }

  protected async onComplete(data: any[]): Promise<void> {
    // 기본: 아무것도 하지 않음
  }
}

// 단순한 서브클래스: Hook 사용 안 함
class SimpleProcessor extends DataProcessor {
  protected async transform(data: any[]): Promise<any[]> {
    return data.map(item => ({ ...item, processed: true }));
  }
}

// 복잡한 서브클래스: 필요한 Hook만 선택적으로 사용
class AdvancedProcessor extends DataProcessor {
  private cache: Map<string, any> = new Map();

  protected async transform(data: any[]): Promise<any[]> {
    return data.map(item => ({ ...item, enhanced: true }));
  }

  // Hook 활용 1: 시작 시 로깅
  protected async onStart(data: any[]): Promise<void> {
    console.log(`[AdvancedProcessor] 처리 시작: ${data.length}건`);
  }

  // Hook 활용 2: 변환 후 캐싱
  protected async onTransformed(data: any[]): Promise<void> {
    data.forEach(item => {
      this.cache.set(item.id, item);
    });
    console.log(`캐시 업데이트: ${data.length}건`);
  }

  // Hook 활용 3: 완료 시 알림
  protected async onComplete(data: any[]): Promise<void> {
    console.log(`처리 완료 알림 전송: ${data.length}건`);
    // 실제로는 알림 서비스 호출
  }
}

// 조건부 Hook: 플래그 기반
abstract class ConfigurableProcessor extends DataProcessor {
  constructor(
    private enableLogging: boolean = false,
    private enableNotification: boolean = false
  ) {
    super();
  }

  protected async onStart(data: any[]): Promise<void> {
    if (this.enableLogging) {
      console.log(`[${this.constructor.name}] 시작`);
    }
  }

  protected async onComplete(data: any[]): Promise<void> {
    if (this.enableNotification) {
      console.log(`알림 전송`);
    }
  }
}

설명

이것이 하는 일: Hook 메서드를 알고리즘의 주요 단계마다 배치하여 서브클래스가 필요한 시점에 개입할 수 있도록 합니다. 단순한 케이스는 간결하게 유지하면서도 복잡한 케이스는 충분히 확장 가능하게 만듭니다.

첫 번째로, Hook 메서드의 배치 전략이 중요합니다. onStart, onValidated, onTransformed, onSaved, onComplete 같은 Hook들이 알고리즘의 각 주요 단계 전후에 전략적으로 배치되어 있습니다.

이런 Hook들은 "언제 무엇이 일어났는지"를 명확히 알려주므로, 서브클래스는 정확한 타이밍에 개입할 수 있습니다. 예를 들어 캐시를 업데이트하려면 onTransformed에서, 알림을 보내려면 onComplete에서 처리하는 식입니다.

그 다음으로, SimpleProcessorAdvancedProcessor의 대비를 보세요. SimpleProcessor는 단 하나의 메서드(transform)만 구현하면 되므로 매우 간결합니다.

Hook 메서드를 전혀 오버라이드하지 않아도 완벽하게 작동합니다. 반면 AdvancedProcessor는 필요한 Hook들만 선택적으로 오버라이드하여 로깅, 캐싱, 알림 같은 고급 기능을 추가합니다.

이것이 Hook 메서드의 핵심 장점입니다: 단순한 케이스에 부담을 주지 않으면서도 복잡한 케이스를 충분히 지원합니다. 세 번째로, Hook 메서드의 이름 규칙을 일관되게 유지하는 것이 중요합니다.

onXXX 형태의 이름은 "XXX가 일어났을 때 호출되는 콜백"임을 명확히 표현합니다. beforeXXX, afterXXX 같은 접두사도 좋은 선택입니다.

이런 명명 규칙을 따르면 다른 개발자가 코드를 볼 때 "아, 이건 Hook 메서드구나. 필요하면 오버라이드할 수 있겠네"를 즉시 이해할 수 있습니다.

마지막으로, ConfigurableProcessor 예제처럼 생성자 플래그로 Hook의 동작을 제어할 수도 있습니다. 이 방식은 오버라이드 없이도 특정 기능을 켜거나 끌 수 있어 편리하지만, 너무 많은 플래그를 추가하면 복잡도가 증가하므로 주의해야 합니다.

일반적으로는 오버라이드 방식이 더 명확하고 타입 안전합니다. 여러분이 Hook 메서드를 설계할 때는 "이 Hook이 실제로 사용될 가능성이 얼마나 되는가?"를 고민하세요.

자주 사용될 확장 포인트는 Hook으로 제공하는 것이 좋지만, 매우 드물게 필요한 기능까지 모두 Hook으로 만들면 클래스가 복잡해집니다. 실제 사용 패턴을 분석하여 정말 필요한 Hook만 제공하는 것이 최선입니다.

또한 Hook의 개수는 5-7개 이하로 유지하는 것이 좋습니다. 그 이상이면 템플릿 메서드가 너무 복잡해지고 서브클래스 작성자가 혼란스러워할 수 있습니다.

실전 팁

💡 Hook 메서드에 주석으로 "이 Hook은 언제 사용하는가?"를 명확히 작성하세요. 예: // Override this to add custom logic after data transformation. 이런 가이드가 있으면 서브클래스 작성이 훨씬 쉬워집니다.

💡 Hook 메서드는 절대 예외를 던지지 않도록 설계하세요. Hook은 선택적 기능이므로 실패해도 전체 알고리즘을 중단해서는 안 됩니다. 대신 에러를 로깅하고 계속 진행하도록 만드세요.

💡 Hook 메서드에 데이터를 전달할 때는 읽기 전용으로 전달하는 것이 안전합니다. Hook이 데이터를 변경하면 예상치 못한 부작용이 발생할 수 있으므로, 데이터 변경이 필요하다면 명시적인 반환값을 사용하세요.

💡 Hook의 실행 순서를 문서화하세요. 특히 여러 Hook이 연쇄적으로 호출될 때 순서가 중요합니다. README나 클래스 주석에 Hook의 전체 흐름을 다이어그램이나 목록으로 정리하면 이해하기 쉽습니다.

💡 성능이 중요한 경우 Hook 호출을 최소화하세요. 빈 Hook 메서드 호출도 오버헤드가 있으므로, 루프 안에서 Hook을 호출하는 것은 피하고 배치 처리 단위로 호출하는 것이 좋습니다.


8. 불변성과 final 메서드

시작하며

여러분이 Template Method Pattern을 구현하고 나서 가장 두려운 순간은 다른 개발자가 실수로 템플릿 메서드를 오버라이드하는 것입니다. 템플릿 메서드는 알고리즘의 골격을 정의하므로, 이것이 오버라이드되면 전체 설계가 무너집니다.

예를 들어 누군가 process() 템플릿 메서드를 오버라이드하여 중간 단계를 건너뛰면 데이터 검증이나 리소스 정리가 누락될 수 있습니다. 실제 프로젝트에서 이런 실수는 생각보다 자주 발생합니다.

새로운 팀원이 코드를 이해하지 못한 채 "이 메서드를 수정하면 더 빠를 것 같아"라고 생각하고 오버라이드하는 경우가 있습니다. 코드 리뷰에서 발견하면 다행이지만, 프로덕션에 배포되어 문제가 발생하면 심각한 버그로 이어집니다.

바로 이럴 때 필요한 것이 템플릿 메서드의 불변성 보장입니다. 템플릿 메서드는 절대 변경되어서는 안 되는 "계약"이므로, 언어의 기능을 사용하여 오버라이드를 방지해야 합니다.

Java에서는 final 키워드로, TypeScript에서는 다른 방법으로 템플릿 메서드를 보호할 수 있습니다. 이렇게 하면 컴파일 타임에 실수를 방지하고, 템플릿 메서드의 무결성을 보장할 수 있습니다.

개요

간단히 말해서, 템플릿 메서드는 final(또는 동등한 메커니즘)로 선언하여 서브클래스가 오버라이드하지 못하도록 보호해야 하며, 이것이 패턴의 무결성을 보장하는 핵심입니다. 실무에서 이 원칙이 중요한 이유는 템플릿 메서드가 시스템의 "계약"을 정의하기 때문입니다.

예를 들어, 결제 시스템의 템플릿 메서드가 "인증 → 검증 → 결제 → 로깅"이라는 순서를 정의했다면, 이 순서는 절대 변경되어서는 안 됩니다. 만약 서브클래스가 로깅을 건너뛰도록 오버라이드하면 감사 추적이 불가능해지고, 규정 위반으로 이어질 수 있습니다.

기존에는 "서브클래스가 템플릿 메서드를 오버라이드하지 않기를 바라며" 주석으로만 경고했다면, final 메서드나 private 템플릿 + public wrapper 같은 기법을 사용하면 컴파일러가 강제하므로 실수가 원천적으로 차단됩니다. 템플릿 메서드 보호의 핵심 전략은 세 가지입니다.

첫째, Java나 C#에서는 final 키워드를 사용합니다. 둘째, TypeScript에서는 private 템플릿 메서드 + public wrapper를 사용합니다.

셋째, 문서화와 네이밍 컨벤션으로 의도를 명확히 표현합니다. 이러한 전략들이 패턴의 안전성과 신뢰성을 크게 향상시킵니다.

코드 예제

// TypeScript에서 템플릿 메서드 보호하기

// 방법 1: Private 템플릿 메서드 + Public Wrapper
abstract class SecureProcessor {
  // Public wrapper: 외부에서 호출
  public process(data: any[]): void {
    this.processTemplate(data);
  }

  // Private 템플릿: 오버라이드 불가능
  private processTemplate(data: any[]): void {
    console.log("=== 처리 시작 ===");

    this.validate(data);
    const transformed = this.transform(data);
    this.save(transformed);

    console.log("=== 처리 완료 ===");
  }

  // 서브클래스가 구현할 메서드
  protected abstract validate(data: any[]): void;
  protected abstract transform(data: any[]): any[];
  protected abstract save(data: any[]): void;
}

// 방법 2: Readonly 프로퍼티로 메서드 보호
abstract class ImmutableProcessor {
  // 템플릿 메서드를 readonly 프로퍼티로 선언
  public readonly execute = (data: any[]): void => {
    console.log("실행 시작");

    this.prepare();
    const result = this.process(data);
    this.cleanup();

    console.log("실행 완료");
  };

  protected abstract prepare(): void;
  protected abstract process(data: any[]): any[];
  protected abstract cleanup(): void;
}

// 방법 3: Symbol을 사용한 고급 보호
const TEMPLATE_METHOD = Symbol("templateMethod");

abstract class SymbolProtectedProcessor {
  // Public 인터페이스
  public run(data: any[]): void {
    this[TEMPLATE_METHOD](data);
  }

  // Symbol을 키로 하는 private 메서드
  private [TEMPLATE_METHOD](data: any[]): void {
    console.log("처리 시작");
    this.execute(data);
    console.log("처리 완료");
  }

  protected abstract execute(data: any[]): void;
}

// 잘못된 예: 템플릿 메서드가 보호되지 않음
abstract class UnsafeProcessor {
  // 문제: 서브클래스가 오버라이드할 수 있음
  public process(data: any[]): void {
    this.validate(data);
    this.transform(data);
  }

  protected abstract validate(data: any[]): void;
  protected abstract transform(data: any[]): void;
}

class BadSubclass extends UnsafeProcessor {
  // 위험: 템플릿 메서드를 오버라이드하여 검증 단계를 건너뜀
  public process(data: any[]): void {
    this.transform(data); // validate() 호출 안 함!
  }

  protected validate(data: any[]): void {}
  protected transform(data: any[]): void {
    console.log("변환만 수행");
  }
}

// 올바른 사용: 템플릿 메서드가 보호됨
class GoodSubclass extends SecureProcessor {
  protected validate(data: any[]): void {
    console.log("검증 수행");
  }

  protected transform(data: any[]): any[] {
    return data.map(item => ({ ...item, processed: true }));
  }

  protected save(data: any[]): void {
    console.log("저장 완료");
  }

  // 컴파일 에러: processTemplate은 private이므로 오버라이드 불가
  // private processTemplate(data: any[]): void {}
}

설명

이것이 하는 일: 템플릿 메서드를 다양한 기법으로 보호하여 서브클래스가 실수로 또는 의도적으로 알고리즘의 흐름을 변경하지 못하도록 방지합니다. 이것이 Template Method Pattern의 안전성을 보장하는 핵심 메커니즘입니다.

첫 번째로, "Private 템플릿 + Public Wrapper" 패턴이 TypeScript에서 가장 권장되는 방법입니다. SecureProcessor에서 볼 수 있듯이, 실제 템플릿 메서드 processTemplate()은 private으로 선언하여 서브클래스가 접근할 수 없게 만들고, public wrapper인 process()를 통해서만 호출하도록 합니다.

이렇게 하면 서브클래스는 process()를 호출할 수 있지만 오버라이드할 수 없으므로, 알고리즘의 흐름이 완벽하게 보호됩니다. 그 다음으로, Readonly 프로퍼티를 사용하는 방법도 효과적입니다.

ImmutableProcessor처럼 템플릿 메서드를 화살표 함수로 정의하고 readonly로 선언하면, 이 프로퍼티는 생성 시점에 할당되고 이후 변경할 수 없습니다. 서브클래스가 같은 이름의 메서드를 정의해도 부모의 프로퍼티를 덮어쓸 수 없으므로 안전합니다.

다만 이 방식은 메모리를 더 사용하므로 성능이 중요한 경우 주의해야 합니다. 세 번째로, Symbol을 사용한 고급 보호 기법도 있습니다.

SymbolProtectedProcessor에서 볼 수 있듯이, Symbol을 키로 사용하면 외부에서 접근할 수 없는 private 메서드를 만들 수 있습니다. 이 방식은 매우 강력한 보호를 제공하지만, 코드 가독성이 다소 떨어질 수 있습니다.

마지막으로, "잘못된 예"인 UnsafeProcessor와 "올바른 예"인 SecureProcessor의 대비를 주목하세요. BadSubclassprocess() 메서드를 오버라이드하여 검증 단계를 완전히 건너뛰었습니다.

이것은 심각한 보안 문제나 데이터 무결성 문제로 이어질 수 있습니다. 반면 GoodSubclass는 보호된 템플릿 메서드를 사용하므로 이런 실수가 원천적으로 불가능합니다.

여러분이 Template Method Pattern을 구현할 때는 항상 템플릿 메서드의 보호를 고려하세요. 특히 여러 개발자가 함께 작업하는 프로젝트에서는 더욱 중요합니다.

코드 리뷰나 문서화만으로는 실수를 완전히 방지할 수 없으므로, 언어의 기능을 활용하여 컴파일 타임에 강제하는 것이 가장 안전합니다. 또한 템플릿 메서드의 이름을 processTemplate, executeTemplate처럼 명확하게 지어서 이것이 특별한 메서드임을 표현하는 것도 좋은 습관입니다.

실전 팁

💡 Java나 C#을 사용한다면 템플릿 메서드에 무조건 final 키워드를 붙이세요. 이것이 가장 명확하고 강력한 보호 방법입니다. public final void process() { ... }처럼 선언하면 서브클래스의 오버라이드가 컴파일 에러가 됩니다.

💡 TypeScript에서는 ESLint 규칙을 추가하여 특정 메서드의 오버라이드를 방지할 수 있습니다. @final JSDoc 주석을 붙이고 린터가 이를 검사하도록 설정하면 빌드 시점에 잡을 수 있습니다.

💡 템플릿 메서드를 보호했다면 주석으로 명확히 표시하세요. 예: // DO NOT OVERRIDE: This method defines the algorithm structure. 이렇게 하면 다른 개발자가 의도를 즉시 파악할 수 있습니다.

💡 추상 메서드와 Hook 메서드는 protected로 선언하되, 템플릿 메서드만 public으로 노출하세요. 이렇게 하면 외부에서는 템플릿 메서드만 호출할 수 있고, 내부 메서드는 서브클래스만 접근할 수 있어 캡슐화가 강화됩니다.

💡 단위 테스트에서 템플릿 메서드의 호출 순서를 검증하세요. Mock 서브클래스를 만들어 각 단계 메서드가 정확한 순서로 호출되는지 확인하면 템플릿 메서드의 무결성을 보장할 수 있습니다.


#TypeScript#TemplateMethod#DesignPatterns#AbstractClass#Inheritance

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

마이크로서비스 배포 완벽 가이드

Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.

Application Load Balancer 완벽 가이드

AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.

고객 상담 AI 시스템 완벽 구축 가이드

AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.

에러 처리와 폴백 완벽 가이드

AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.

AWS Bedrock 인용과 출처 표시 완벽 가이드

AWS Bedrock의 Citation 기능을 활용하여 AI 응답의 신뢰도를 높이는 방법을 배웁니다. 출처 추출부터 UI 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.

이전3/3
다음