🤖

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

⚠️

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

이미지 로딩 중...

Template Method Pattern 디자인 패턴 마스터 - 슬라이드 1/13
A

AI Generated

2025. 11. 1. · 15 Views

Template Method Pattern 완벽 가이드

알고리즘의 뼈대는 유지하면서 세부 구현을 서브클래스에 위임하는 Template Method Pattern을 실무 예제와 함께 깊이 있게 학습합니다. 코드 재사용성과 유지보수성을 극대화하는 핵심 디자인 패턴입니다.


목차

  1. Template Method Pattern 기본 개념
  2. Abstract Class와 Hook Method
  3. 실전 데이터 처리 파이프라인
  4. 게임 캐릭터 시스템 설계
  5. API 클라이언트 프레임워크
  6. 문서 생성기 구현
  7. 테스트 프레임워크 설계
  8. 결제 처리 시스템

1. Template Method Pattern 기본 개념

시작하며

여러분이 여러 종류의 음료를 만드는 카페 시스템을 개발한다고 상상해보세요. 커피든 차든 "물 끓이기 → 재료 우리기 → 컵에 따르기 → 토핑 추가"라는 동일한 순서를 따르지만, 각 단계의 세부 내용은 다릅니다.

이런 상황에서 모든 음료마다 전체 프로세스를 복사-붙여넣기 하면 어떻게 될까요? 나중에 "컵 예열하기" 단계를 추가하려면 모든 음료 클래스를 일일이 수정해야 합니다.

이는 유지보수의 악몽입니다. 바로 이럴 때 필요한 것이 Template Method Pattern입니다.

알고리즘의 뼈대는 부모 클래스에서 한 번만 정의하고, 각 단계의 구체적인 구현만 자식 클래스에서 커스터마이징할 수 있습니다. 이 패턴을 사용하면 중복 코드를 제거하고, 비즈니스 로직의 일관성을 보장하며, 새로운 변형을 추가할 때도 안전하게 확장할 수 있습니다.

개요

간단히 말해서, Template Method Pattern은 알고리즘의 구조를 부모 클래스에서 정의하고, 각 단계의 세부 구현을 자식 클래스에서 재정의하도록 하는 디자인 패턴입니다. 실무에서 이 패턴이 빛을 발하는 경우는 여러 곳에서 비슷하지만 조금씩 다른 처리 과정이 필요할 때입니다.

예를 들어, 여러 종류의 데이터 파일(CSV, JSON, XML)을 처리하는 시스템에서 "파일 열기 → 데이터 파싱 → 검증 → 저장"이라는 공통 흐름은 유지하면서, 파싱 방법만 다르게 구현하고 싶을 때 매우 유용합니다. 기존에는 각 파일 타입마다 별도의 처리 함수를 만들어 중복 코드가 많았다면, 이제는 공통 로직을 한 곳에 모으고 차이점만 구현할 수 있습니다.

이 패턴의 핵심 특징은 세 가지입니다. 첫째, 코드 재사용성을 극대화합니다.

둘째, Open-Closed Principle(확장에는 열려있고 수정에는 닫혀있음)을 실현합니다. 셋째, Hollywood Principle("Don't call us, we'll call you")을 따라 부모 클래스가 흐름을 제어합니다.

이러한 특징들이 대규모 시스템에서 일관성과 유지보수성을 보장하는 핵심 요소입니다.

코드 예제

// 음료 제조 템플릿 클래스
class Beverage {
  // 템플릿 메서드: 알고리즘의 뼈대 정의
  prepare() {
    this.boilWater();
    this.brew();           // 추상 메서드
    this.pourInCup();
    this.addCondiments();  // 추상 메서드
  }

  boilWater() {
    console.log('물을 끓입니다');
  }

  pourInCup() {
    console.log('컵에 따릅니다');
  }

  // 서브클래스에서 구현해야 할 메서드
  brew() {
    throw new Error('brew() must be implemented');
  }

  addCondiments() {
    throw new Error('addCondiments() must be implemented');
  }
}

// 커피 구현
class Coffee extends Beverage {
  brew() {
    console.log('커피를 드립합니다');
  }

  addCondiments() {
    console.log('설탕과 우유를 추가합니다');
  }
}

// 녹차 구현
class GreenTea extends Beverage {
  brew() {
    console.log('녹차를 우립니다');
  }

  addCondiments() {
    console.log('레몬을 추가합니다');
  }
}

// 사용 예제
const coffee = new Coffee();
coffee.prepare();
// 출력: 물을 끓입니다 → 커피를 드립합니다 → 컵에 따릅니다 → 설탕과 우유를 추가합니다

설명

이것이 하는 일: Template Method Pattern은 여러 단계로 이루어진 알고리즘의 구조를 고정하면서, 각 단계의 구체적인 동작만 변경할 수 있게 해줍니다. 첫 번째로, 부모 클래스인 Beverage에서 prepare() 메서드가 전체 알고리즘의 흐름을 정의합니다.

이 메서드가 바로 "템플릿 메서드"입니다. 이 메서드는 final로 선언되어야 하며(JavaScript에서는 명시적으로 표시할 수 없지만 재정의하지 않는 것이 관례), 자식 클래스가 전체 흐름을 변경하지 못하도록 보호합니다.

왜 이렇게 하는지? 비즈니스 로직의 일관성을 보장하기 위함입니다.

그 다음으로, brew()addCondiments() 같은 추상 메서드들이 실행되면서 자식 클래스에서 구현된 구체적인 동작을 호출합니다. 내부에서는 다형성(polymorphism)이 작동하여, 실행 시점에 실제 객체의 타입에 따라 올바른 메서드가 호출됩니다.

Coffee 객체라면 커피를 드립하고, GreenTea 객체라면 녹차를 우립니다. 마지막으로, boilWater()pourInCup() 같은 공통 메서드들이 모든 자식 클래스에서 재사용됩니다.

이들은 구체적인 구현을 가지고 있어서, 자식 클래스가 따로 구현할 필요가 없습니다. 이로써 코드 중복이 완전히 제거됩니다.

여러분이 이 패턴을 사용하면 다음과 같은 구체적인 이점을 얻을 수 있습니다. 첫째, 새로운 음료 종류를 추가할 때 brew()addCondiments()만 구현하면 되므로 개발 속도가 빨라집니다.

둘째, 전체 프로세스에 새로운 단계(예: 컵 예열)를 추가할 때 부모 클래스만 수정하면 모든 자식 클래스에 자동으로 반영됩니다. 셋째, 각 단계가 명확히 분리되어 있어 단위 테스트가 쉽고, 버그를 찾기도 수월합니다.

실전 팁

💡 템플릿 메서드는 절대 자식 클래스에서 재정의하지 마세요. TypeScript를 사용한다면 readonly 키워드나 #private 메서드와 조합하여 실수로 재정의하는 것을 방지할 수 있습니다.

💡 추상 메서드와 구체 메서드를 명확히 구분하세요. 주석이나 네이밍 규칙(예: _brew() 같은 언더스코어 접두사)을 사용하여 어떤 메서드를 재정의해야 하는지 명확히 표시하면, 팀원들이 코드를 이해하기 쉬워집니다.

💡 너무 많은 단계로 쪼개면 오히려 복잡도가 증가합니다. 일반적으로 3-7개 단계가 적절하며, 각 단계는 명확한 책임을 가져야 합니다. 단계가 10개를 넘어간다면 패턴을 재검토하세요.

💡 디버깅할 때는 부모 클래스의 템플릿 메서드에 로깅을 추가하면 전체 실행 흐름을 한눈에 파악할 수 있습니다. console.log('Step: brewing')처럼 각 단계 전에 로그를 찍으면 어느 단계에서 문제가 발생했는지 즉시 알 수 있습니다.

💡 Hook 메서드(선택적으로 재정의할 수 있는 빈 메서드)를 제공하면 유연성이 크게 향상됩니다. 예를 들어 customerWantsCondiments() 같은 메서드를 만들어 조건부로 토핑을 추가할 수 있게 하면, 모든 음료에 토핑이 필수가 아닐 때 유용합니다.


2. Abstract Class와 Hook Method

시작하며

여러분이 앞서 배운 Template Method Pattern을 실제 프로젝트에 적용하려다가 이런 문제를 만난 적 있나요? "이 단계는 필수인데, 저 단계는 선택사항이야.

어떻게 구현하지?" 예를 들어, 모든 음료에 토핑이 필요한 건 아닙니다. 블랙커피는 토핑이 없고, 라떼는 우유가 있죠.

만약 addCondiments()를 추상 메서드로 만들면 블랙커피 클래스에서도 억지로 구현해야 하고, 구체 메서드로 만들면 유연성이 떨어집니다. 바로 이럴 때 필요한 것이 Hook Method입니다.

기본 구현을 제공하되, 필요한 경우에만 재정의할 수 있는 확장 포인트를 만드는 것이죠. Abstract Class와 Hook Method를 제대로 이해하면, 강제성과 유연성의 균형을 완벽하게 맞출 수 있습니다.

개요

간단히 말해서, Abstract Class는 인스턴스화할 수 없고 상속받아 사용해야 하는 클래스이며, Hook Method는 부모 클래스에서 기본 구현을 제공하지만 자식 클래스가 선택적으로 재정의할 수 있는 메서드입니다. 실무에서 이 개념이 중요한 이유는 프레임워크 설계와 직결되기 때문입니다.

예를 들어, React의 라이프사이클 메서드 중 componentDidMount()는 Hook Method의 완벽한 예시입니다. 반드시 구현할 필요는 없지만, 필요하면 재정의하여 컴포넌트가 마운트된 후의 동작을 커스터마이징할 수 있습니다.

기존에는 "모두 필수"이거나 "모두 선택"이라는 극단적인 선택만 가능했다면, 이제는 핵심 로직은 강제하고 부가 기능은 선택적으로 만들 수 있습니다. Hook Method의 핵심 특징은 두 가지입니다.

첫째, 기본 동작을 제공하여 자식 클래스가 아무것도 안 해도 작동합니다. 둘째, 확장 포인트를 명확히 표시하여 어디를 커스터마이징할 수 있는지 알려줍니다.

JavaScript에는 공식적인 Abstract Class 문법이 없지만, 생성자에서 new.target을 체크하거나 추상 메서드에서 에러를 던지는 방식으로 구현할 수 있습니다. 이러한 패턴들이 라이브러리와 프레임워크의 확장성을 결정하는 핵심 요소입니다.

코드 예제

// Abstract Class 구현
class DataProcessor {
  constructor() {
    // Abstract Class 체크
    if (new.target === DataProcessor) {
      throw new Error('DataProcessor is abstract');
    }
  }

  // 템플릿 메서드
  process(data) {
    const validated = this.validate(data);  // 추상 메서드
    const processed = this.transform(validated);  // 추상 메서드

    // Hook Method: 선택적 전처리
    if (this.shouldCache()) {
      this.cache(processed);
    }

    return this.format(processed);  // 구체 메서드
  }

  // 추상 메서드 (필수 구현)
  validate(data) {
    throw new Error('validate() must be implemented');
  }

  transform(data) {
    throw new Error('transform() must be implemented');
  }

  // Hook Method (선택적 재정의)
  shouldCache() {
    return false;  // 기본값: 캐싱 안 함
  }

  cache(data) {
    console.log('Caching data...');
  }

  // 구체 메서드 (공통 로직)
  format(data) {
    return JSON.stringify(data, null, 2);
  }
}

// 구현 클래스
class UserDataProcessor extends DataProcessor {
  validate(data) {
    if (!data.email) throw new Error('Email required');
    return data;
  }

  transform(data) {
    return { ...data, email: data.email.toLowerCase() };
  }

  // Hook Method 재정의
  shouldCache() {
    return true;  // 사용자 데이터는 캐싱
  }
}

설명

이것이 하는 일: Abstract Class와 Hook Method는 프레임워크 수준의 설계를 가능하게 하며, "무엇은 반드시 해야 하고, 무엇은 선택"인지를 명확히 정의합니다. 첫 번째로, DataProcessor 생성자에서 new.target === DataProcessor 체크가 실행됩니다.

new.target은 실제로 생성된 클래스를 가리키므로, 누군가 new DataProcessor()를 직접 호출하면 에러를 던집니다. 이렇게 하는 이유는?

Abstract Class를 강제하기 위함입니다. Java나 TypeScript의 abstract 키워드와 동일한 효과를 JavaScript에서 구현하는 것이죠.

그 다음으로, 템플릿 메서드 process()에서 shouldCache() Hook Method를 조건문으로 감싸서 호출합니다. 내부에서는 먼저 shouldCache()가 실행되어 true/false를 반환하고, true일 때만 cache() 메서드가 호출됩니다.

UserDataProcessorshouldCache()를 재정의하여 true를 반환하지만, 다른 프로세서는 재정의하지 않아 기본값 false를 사용할 수 있습니다. 세 번째로, validate()transform()은 추상 메서드로 정의되어 있어, 자식 클래스가 반드시 구현해야 합니다.

구현하지 않으면 런타임에 에러가 발생합니다. 한편 format()은 구체 메서드로, 모든 자식 클래스가 동일한 JSON 포맷팅 로직을 공유합니다.

마지막으로, Hook Method의 네이밍 패턴에 주목하세요. should-, can-, will- 같은 접두사를 사용하면 "이건 조건을 판단하는 Hook이구나"라고 직관적으로 이해할 수 있습니다.

또한 Hook은 보통 boolean을 반환하거나 아무것도 안 하는 빈 메서드로 구현됩니다. 여러분이 이 패턴을 사용하면 다음과 같은 실무 이점이 있습니다.

첫째, API 사용자에게 명확한 계약(contract)을 제공하여 "이건 필수, 이건 선택"을 혼동 없이 전달할 수 있습니다. 둘째, 새로운 기능을 추가할 때 기존 코드를 깨뜨리지 않습니다(Hook에 기본 구현이 있으므로).

셋째, 코드 리뷰 시 "이 메서드는 왜 비어있나요?"라는 질문을 받지 않습니다. Hook Method임을 명확히 표시했기 때문이죠.

실전 팁

💡 Hook Method 이름은 의도를 명확히 드러내야 합니다. shouldCache(), beforeSave(), afterLoad() 같은 네이밍은 "언제, 왜" 호출되는지 즉시 알 수 있게 해줍니다. hook1(), customMethod() 같은 모호한 이름은 피하세요.

💡 TypeScript를 사용한다면 abstract 키워드로 추상 클래스와 메서드를 명시적으로 선언하세요. 컴파일 타임에 에러를 잡을 수 있어 런타임 에러보다 훨씬 안전합니다.

💡 Hook Method가 3개를 넘어가면 설계를 재검토하세요. 너무 많은 Hook은 오히려 복잡도를 증가시킵니다. 관련 있는 Hook들을 하나의 객체로 묶는 Strategy Pattern과의 조합을 고려하세요.

💡 Hook Method에 기본 구현을 제공할 때는 JSDoc으로 문서화하세요. @override 태그를 사용하여 "이 메서드는 재정의 가능합니다"라고 명시하면, IDE에서 자동완성과 힌트를 제공받을 수 있습니다.

💡 단위 테스트에서는 Hook Method를 모킹(mocking)하여 템플릿 메서드의 다양한 실행 경로를 테스트하세요. 예를 들어 shouldCache()가 true일 때와 false일 때의 동작을 각각 검증하면, 조건부 로직의 버그를 조기에 발견할 수 있습니다.


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

시작하며

여러분이 여러 형식의 데이터 파일(CSV, JSON, XML)을 처리하는 ETL(Extract, Transform, Load) 시스템을 개발한다고 상상해보세요. 각 파일 형식마다 "파일 읽기 → 파싱 → 검증 → 변환 → 저장"이라는 동일한 파이프라인을 거치지만, 파싱 방식만 다릅니다.

이런 경우 각 파일 타입마다 완전히 별도의 처리 클래스를 만들면 어떻게 될까요? 검증 로직을 수정할 때 CSV, JSON, XML 처리 클래스를 모두 찾아다니며 수정해야 합니다.

한 곳을 빠뜨리면 버그가 발생하죠. 바로 이럴 때 Template Method Pattern이 진가를 발휘합니다.

공통 파이프라인을 한 번만 정의하고, 파일 형식별 파싱 로직만 플러그인처럼 교체할 수 있습니다. 이 패턴을 사용하면 새로운 파일 형식을 추가할 때도 기존 코드를 전혀 수정하지 않고, 파싱 메서드만 구현하면 됩니다.

개요

간단히 말해서, 데이터 처리 파이프라인은 여러 단계로 구성된 데이터 변환 과정이며, Template Method Pattern을 적용하면 파이프라인 구조는 고정하고 각 단계의 구현만 교체할 수 있습니다. 실무에서 이 패턴이 특히 유용한 경우는 마이크로서비스 환경에서 여러 데이터 소스를 통합할 때입니다.

예를 들어, 고객 데이터가 레거시 시스템에서는 CSV로, 모바일 앱에서는 JSON으로, 파트너사에서는 XML로 들어온다면, 각각의 파서를 독립적으로 개발하되 공통 검증과 변환 로직은 재사용할 수 있습니다. 기존에는 각 데이터 소스마다 중복 코드가 수백 줄씩 발생했다면, 이제는 파싱 로직 10-20줄만 작성하면 전체 파이프라인을 활용할 수 있습니다.

이 접근법의 핵심 장점은 세 가지입니다. 첫째, 단일 책임 원칙(Single Responsibility Principle)을 준수하여 각 클래스가 하나의 파일 형식만 담당합니다.

둘째, 에러 처리와 로깅을 중앙화하여 모든 파서가 동일한 수준의 관찰 가능성(observability)을 제공합니다. 셋째, 파이프라인 단계를 추가하거나 재배치할 때 한 곳만 수정하면 모든 파서에 자동으로 반영됩니다.

이러한 특징들이 대규모 데이터 시스템의 확장성과 안정성을 보장합니다.

코드 예제

// 데이터 파이프라인 추상 클래스
class DataPipeline {
  async execute(filePath) {
    console.log(`Processing: ${filePath}`);

    // 템플릿 메서드: 파이프라인 정의
    const rawData = await this.readFile(filePath);
    const parsed = this.parse(rawData);  // 추상 메서드
    const validated = this.validate(parsed);
    const transformed = this.transform(validated);
    await this.save(transformed);

    console.log('Pipeline completed');
    return transformed;
  }

  async readFile(filePath) {
    // 공통 파일 읽기 로직
    return require('fs').promises.readFile(filePath, 'utf-8');
  }

  parse(rawData) {
    throw new Error('parse() must be implemented');
  }

  validate(data) {
    // 공통 검증 로직
    if (!Array.isArray(data)) throw new Error('Data must be array');
    return data;
  }

  transform(data) {
    // 공통 변환: 타임스탬프 추가
    return data.map(item => ({ ...item, processedAt: Date.now() }));
  }

  async save(data) {
    console.log(`Saving ${data.length} records`);
    // DB 저장 로직...
  }
}

// CSV 파서
class CSVPipeline extends DataPipeline {
  parse(rawData) {
    return rawData.split('\n')
      .slice(1)  // 헤더 제거
      .map(line => {
        const [id, name, email] = line.split(',');
        return { id, name, email };
      });
  }
}

// JSON 파서
class JSONPipeline extends DataPipeline {
  parse(rawData) {
    return JSON.parse(rawData);
  }
}

// 사용 예제
const csvPipeline = new CSVPipeline();
await csvPipeline.execute('users.csv');

설명

이것이 하는 일: 데이터 처리 파이프라인 Template Method는 복잡한 ETL 과정을 표준화하고, 새로운 데이터 형식 지원을 10분 안에 추가할 수 있게 해줍니다. 첫 번째로, execute() 템플릿 메서드가 전체 파이프라인을 순차적으로 실행합니다.

이 메서드는 async로 선언되어 있어, 파일 I/O 같은 비동기 작업을 자연스럽게 처리합니다. 각 단계가 완료될 때까지 기다린 후 다음 단계로 진행하므로, 데이터 무결성이 보장됩니다.

왜 이렇게 하는지? 파싱이 완료되기 전에 검증을 시작하면 undefined 에러가 발생하기 때문이죠.

그 다음으로, parse() 추상 메서드가 호출되면 실제 객체 타입에 따라 CSVPipeline.parse() 또는 JSONPipeline.parse()가 실행됩니다. CSV 파서는 줄 단위로 문자열을 분리하고, JSON 파서는 JSON.parse()를 사용합니다.

내부적으로 완전히 다른 알고리즘이지만, 외부에서는 동일한 인터페이스로 호출할 수 있습니다. 세 번째로, validate()transform() 같은 공통 메서드들이 모든 파일 형식에 대해 동일하게 작동합니다.

검증 로직에서 배열인지 체크하고, 변환 로직에서 processedAt 타임스탬프를 추가합니다. 만약 새로운 검증 규칙(예: 빈 레코드 필터링)을 추가하려면 validate() 메서드 한 곳만 수정하면 CSV, JSON 모두에 즉시 반영됩니다.

마지막으로, save() 메서드가 처리된 데이터를 영속화합니다. 실제 프로덕션에서는 여기에 데이터베이스 연결, 트랜잭션 처리, 에러 재시도 로직이 들어갑니다.

이 모든 복잡한 로직을 한 번만 작성하고, 모든 파서가 재사용하는 것이죠. 여러분이 이 파이프라인을 사용하면 다음과 같은 구체적인 이점을 얻습니다.

첫째, XML 파서를 추가하려면 XMLPipeline 클래스를 만들고 parse() 메서드만 구현하면 됩니다. 5분이면 충분합니다.

둘째, 모든 파이프라인에 캐싱 레이어를 추가하려면 execute() 메서드 시작 부분에 캐시 체크 로직만 넣으면 됩니다. 셋째, 단위 테스트에서 parse() 메서드만 모킹하면 파이프라인 전체를 테스트할 수 있어, 테스트 코드가 간결해집니다.

실전 팁

💡 각 파이프라인 단계에 로깅을 추가하세요. console.log(\Step: parsing ${rawData.length} bytes`)`처럼 입력 크기를 함께 기록하면, 성능 병목 지점을 빠르게 찾을 수 있습니다. 프로덕션에서는 Winston이나 Pino 같은 구조화된 로거를 사용하세요.

💡 에러 처리를 템플릿 메서드에 집중시키세요. execute() 전체를 try-catch로 감싸고, 에러 타입별로 다른 복구 전략을 적용할 수 있습니다. 파싱 에러는 스킵하고, 검증 에러는 재시도하는 식이죠.

💡 성능 최적화가 필요하면 스트리밍 방식으로 전환하세요. 대용량 파일의 경우 readFile() 대신 createReadStream()을 사용하고, 파싱과 변환을 파이프라인으로 연결하면 메모리 사용량을 크게 줄일 수 있습니다.

💡 파이프라인 단계별 성능 메트릭을 수집하세요. 각 단계의 시작과 끝에 performance.now()를 기록하면, 어떤 단계가 느린지 정량적으로 파악할 수 있습니다. 이 데이터를 Prometheus나 Datadog에 전송하면 실시간 모니터링도 가능합니다.

💡 병렬 처리가 필요하면 Promise.all()과 조합하세요. 여러 파일을 동시에 처리할 때 await Promise.all(files.map(f => pipeline.execute(f)))처럼 사용하면, 순차 처리보다 5-10배 빠를 수 있습니다. 단, 동시 실행 수를 제한하는 p-limit 라이브러리를 함께 사용하여 시스템 과부하를 방지하세요.


4. 게임 캐릭터 시스템 설계

시작하며

여러분이 RPG 게임을 개발하는데, 전사, 마법사, 궁수 등 여러 직업 캐릭터를 구현해야 한다고 생각해보세요. 모든 캐릭터는 "이동 → 공격 → 스킬 사용 → 데미지 처리"라는 동일한 전투 루프를 따르지만, 각 직업마다 공격 방식과 스킬이 완전히 다릅니다.

이런 상황에서 각 직업마다 전투 로직을 처음부터 작성하면 어떻게 될까요? 전사와 마법사의 코드에서 "이동 속도 계산" 로직이 미묘하게 달라지고, 나중에 버그를 찾기도 어려워집니다.

게임 밸런스 조정도 악몽이 되죠. 바로 이럴 때 Template Method Pattern이 필수입니다.

전투 흐름은 한 번만 정의하고, 각 직업의 특색 있는 공격과 스킬만 구현하면 됩니다. 이 패턴을 게임에 적용하면 새로운 직업을 추가하는 것이 기존 시스템을 깨뜨리지 않으면서도 엄청나게 빨라집니다.

개요

간단히 말해서, 게임 캐릭터 시스템에서 Template Method Pattern은 모든 캐릭터가 공유하는 게임 로직(체력 관리, 이동, 턴 순서 등)과 직업별 특화 로직(고유 스킬, 공격 방식)을 명확히 분리합니다. 실무 게임 개발에서 이 패턴이 중요한 이유는 밸런스 패치와 직결되기 때문입니다.

예를 들어, 모든 캐릭터의 공격력을 10% 증가시키는 이벤트를 진행한다면, 공통 데미지 계산 로직 한 곳만 수정하면 됩니다. 각 직업 클래스를 일일이 찾아다니며 수정할 필요가 없습니다.

기존에는 "전사 클래스 복사 → 마법사로 이름 변경 → 스킬만 교체"하는 식으로 개발했다면, 이제는 공통 로직은 상속받고 차이점만 구현할 수 있습니다. 이 접근법의 핵심 이점은 세 가지입니다.

첫째, 게임 메커니즘의 일관성을 보장합니다. 모든 캐릭터가 동일한 규칙을 따르므로 예상치 못한 버그가 줄어듭니다.

둘째, AI 적 캐릭터와 플레이어 캐릭터가 동일한 전투 로직을 사용하여 공정성이 보장됩니다. 셋째, 리플레이 시스템이나 전투 로그를 구현할 때 모든 캐릭터를 동일하게 처리할 수 있습니다.

이러한 특징들이 게임의 품질과 개발 속도를 동시에 향상시킵니다.

코드 예제

// 캐릭터 추상 클래스
class Character {
  constructor(name, hp, mp) {
    this.name = name;
    this.hp = hp;
    this.maxHp = hp;
    this.mp = mp;
    this.maxMp = mp;
  }

  // 템플릿 메서드: 턴 액션
  takeTurn(target) {
    console.log(`\n${this.name}'s turn`);

    this.beforeAction();  // Hook
    this.move();

    if (this.canUseSkill()) {
      this.useSkill(target);
    } else {
      this.attack(target);  // 추상 메서드
    }

    this.afterAction();  // Hook
  }

  move() {
    console.log(`${this.name} moves into position`);
  }

  attack(target) {
    throw new Error('attack() must be implemented');
  }

  useSkill(target) {
    throw new Error('useSkill() must be implemented');
  }

  canUseSkill() {
    return this.mp >= 20;
  }

  takeDamage(amount) {
    this.hp = Math.max(0, this.hp - amount);
    console.log(`${this.name} HP: ${this.hp}/${this.maxHp}`);
  }

  // Hook Methods
  beforeAction() {}
  afterAction() {}
}

// 전사 클래스
class Warrior extends Character {
  attack(target) {
    const damage = 30;
    console.log(`${this.name} slashes with sword! (${damage} damage)`);
    target.takeDamage(damage);
  }

  useSkill(target) {
    const damage = 60;
    this.mp -= 20;
    console.log(`${this.name} uses WHIRLWIND! (${damage} damage)`);
    target.takeDamage(damage);
  }

  beforeAction() {
    console.log(`${this.name} raises shield (+5 defense)`);
  }
}

// 마법사 클래스
class Mage extends Character {
  attack(target) {
    const damage = 25;
    console.log(`${this.name} casts fireball! (${damage} damage)`);
    target.takeDamage(damage);
  }

  useSkill(target) {
    const damage = 70;
    this.mp -= 20;
    console.log(`${this.name} casts METEOR! (${damage} damage)`);
    target.takeDamage(damage);
  }

  afterAction() {
    this.mp = Math.min(this.maxMp, this.mp + 5);
    console.log(`${this.name} regenerates 5 MP`);
  }
}

설명

이것이 하는 일: 게임 캐릭터 Template Method는 전투 시스템의 뼈대를 제공하면서도, 각 직업의 독특한 플레이 스타일을 보장합니다. 첫 번째로, takeTurn() 템플릿 메서드가 모든 캐릭터의 턴 진행을 표준화합니다.

이 메서드는 턴 기반 전투 시스템의 핵심입니다. Hook 메서드 beforeAction()을 먼저 호출하여 턴 시작 시 버프나 디버프를 처리하고, 그 다음 move()로 이동 애니메이션을 재생합니다.

왜 이 순서인지? 게임 UX 관점에서 플레이어가 자연스럽게 느끼는 전투 흐름이기 때문입니다.

그 다음으로, canUseSkill() 조건 체크가 MP를 확인하여 스킬 사용 가능 여부를 판단합니다. MP가 20 이상이면 useSkill()이 호출되고, 그렇지 않으면 기본 attack()이 호출됩니다.

내부적으로 이 로직은 모든 캐릭터에게 동일하게 적용되어, "MP가 부족하면 스킬을 못 쓴다"는 게임 규칙이 일관되게 작동합니다. 세 번째로, 실제 공격과 스킬은 각 직업 클래스에서 구현됩니다.

전사는 물리 공격으로 30 데미지를 주고, 스킬로 60 데미지를 줍니다. 마법사는 기본 공격이 25 데미지지만, 스킬은 70 데미지로 더 강력합니다.

이런 밸런스 수치들이 각 클래스에 캡슐화되어 있어, 직업별 밸런스 조정이 독립적으로 가능합니다. 마지막으로, Hook 메서드 beforeAction()afterAction()이 직업 특유의 패시브 능력을 구현합니다.

전사는 턴 시작 시 방어력을 올리고, 마법사는 턴 종료 후 MP를 회복합니다. 이 Hook들은 선택적이므로, 특별한 패시브가 없는 직업은 재정의하지 않아도 됩니다.

여러분이 이 시스템을 사용하면 다음과 같은 실무 이점이 있습니다. 첫째, 새로운 직업(예: 궁수)을 추가할 때 attack()useSkill() 메서드만 구현하면 전투 시스템이 자동으로 작동합니다.

둘째, 전투 로그나 리플레이 시스템을 구현할 때 모든 캐릭터를 Character 타입으로 통일하여 처리할 수 있습니다. 셋째, 멀티플레이어 동기화에서 "캐릭터 타입 + 턴 번호"만 전송하면 되므로 네트워크 트래픽이 줄어듭니다.

실전 팁

💡 데미지 계산을 별도 메서드로 분리하세요. calculateDamage(baseDamage, target)처럼 방어력, 크리티컬, 버프를 고려한 최종 데미지를 계산하는 공통 메서드를 만들면, 모든 캐릭터가 동일한 데미지 공식을 사용하여 밸런스가 깨지지 않습니다.

💡 상태 패턴과 조합하여 캐릭터 상태(정상, 스턴, 독, 무적 등)를 관리하세요. takeTurn() 시작 부분에서 if (this.state.isStunned()) return;처럼 체크하면, 상태 이상 효과를 일관되게 처리할 수 있습니다.

💡 커맨드 패턴으로 행동을 기록하면 되돌리기(undo) 기능을 쉽게 구현할 수 있습니다. takeTurn() 내에서 실행된 모든 액션을 커맨드 객체로 저장하면, 전투 리플레이나 디버그 모드에서 턴을 되감을 수 있습니다.

💡 타입 안정성을 위해 TypeScript의 제네릭을 활용하세요. Character<TSkill>처럼 스킬 타입을 제네릭으로 받으면, 전사는 물리 스킬만, 마법사는 마법 스킬만 사용하도록 컴파일 타임에 강제할 수 있습니다.

💡 성능 최적화가 필요하면 객체 풀링(object pooling)을 적용하세요. 게임에서 캐릭터 객체를 매번 생성/파괴하면 GC 압력이 높아집니다. 캐릭터 풀을 만들어 재사용하면 프레임 드롭을 방지할 수 있습니다.


5. API 클라이언트 프레임워크

시작하며

여러분이 여러 마이크로서비스와 통신하는 프론트엔드 애플리케이션을 개발한다고 생각해보세요. User API, Product API, Order API 등 각각 다른 엔드포인트를 호출하지만, "인증 헤더 추가 → 요청 전송 → 에러 처리 → 응답 파싱"이라는 동일한 패턴을 따릅니다.

이런 상황에서 각 API마다 axios 호출 코드를 복사-붙여넣기 하면 어떻게 될까요? 인증 토큰 형식이 바뀌면 수십 개 파일을 수정해야 하고, 에러 처리도 일관성이 없어집니다.

한 API는 재시도하고, 다른 API는 즉시 실패하는 식이죠. 바로 이럴 때 Template Method Pattern으로 API 클라이언트 프레임워크를 만들면 완벽합니다.

인증, 에러 처리, 로깅 같은 공통 관심사는 한 번만 구현하고, 각 API의 엔드포인트와 요청/응답 형식만 정의하면 됩니다. 이 패턴을 사용하면 새로운 API를 통합하는 시간이 30분에서 5분으로 줄어듭니다.

개요

간단히 말해서, API 클라이언트 Template Method는 HTTP 통신의 공통 로직(인증, 재시도, 에러 처리)을 추상화하고, 각 API별 특성(엔드포인트, 파라미터)만 구현하도록 합니다. 실무에서 이 패턴이 중요한 이유는 마이크로서비스 아키텍처의 복잡도 관리와 직결되기 때문입니다.

예를 들어, 회사의 인증 방식이 JWT에서 OAuth2로 변경되면, 기본 클라이언트의 authenticate() 메서드 한 곳만 수정하면 모든 API 클라이언트에 자동으로 반영됩니다. 기존에는 각 개발자가 자신만의 방식으로 API를 호출하여 코드 리뷰가 어려웠다면, 이제는 표준화된 인터페이스를 통해 일관된 코드 품질을 유지할 수 있습니다.

이 접근법의 핵심 장점은 네 가지입니다. 첫째, Cross-Cutting Concerns(로깅, 모니터링, 에러 추적)를 중앙화하여 관찰 가능성이 향상됩니다.

둘째, 모든 API 호출이 동일한 재시도 정책과 타임아웃 설정을 사용하여 안정성이 보장됩니다. 셋째, 테스트에서 기본 클라이언트를 모킹하면 모든 API를 쉽게 테스트할 수 있습니다.

넷째, API 버전 관리와 마이그레이션이 체계적으로 가능합니다. 이러한 특징들이 엔터프라이즈급 애플리케이션의 필수 요소입니다.

코드 예제

// API 클라이언트 추상 클래스
class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.timeout = 5000;
  }

  // 템플릿 메서드: 공통 요청 흐름
  async request(endpoint, options = {}) {
    try {
      // 1. 요청 전처리
      const headers = await this.getHeaders();
      const url = this.buildURL(endpoint);

      // 2. 요청 본문 변환 (추상 메서드)
      const body = this.transformRequest(options.body);

      // 3. HTTP 요청
      console.log(`[API] ${options.method || 'GET'} ${url}`);
      const response = await this.executeRequest(url, {
        ...options,
        headers,
        body,
        timeout: this.timeout
      });

      // 4. 응답 변환 (추상 메서드)
      const data = await this.transformResponse(response);

      // 5. 후처리 Hook
      this.onSuccess(data);

      return data;
    } catch (error) {
      return this.handleError(error);  // 공통 에러 처리
    }
  }

  async getHeaders() {
    // 공통 헤더 (인증, Content-Type 등)
    return {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.getToken()}`
    };
  }

  buildURL(endpoint) {
    return `${this.baseURL}${endpoint}`;
  }

  getToken() {
    return localStorage.getItem('auth_token') || '';
  }

  async executeRequest(url, options) {
    // 실제 HTTP 요청 (fetch, axios 등)
    return fetch(url, options);
  }

  // 추상 메서드들
  transformRequest(body) {
    return JSON.stringify(body);
  }

  async transformResponse(response) {
    return response.json();
  }

  // 에러 처리
  async handleError(error) {
    console.error('[API Error]', error);

    // 재시도 가능한 에러인지 체크
    if (this.shouldRetry(error)) {
      console.log('[API] Retrying...');
      await this.delay(1000);
      // 재시도 로직...
    }

    throw error;
  }

  shouldRetry(error) {
    return error.status === 503 || error.status === 429;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Hook Methods
  onSuccess(data) {}
}

// User API 클라이언트
class UserAPIClient extends APIClient {
  constructor() {
    super('https://api.example.com/users');
  }

  async getUser(userId) {
    return this.request(`/${userId}`, { method: 'GET' });
  }

  async createUser(userData) {
    return this.request('/', {
      method: 'POST',
      body: userData
    });
  }

  // 응답 변환 커스터마이징
  async transformResponse(response) {
    const data = await response.json();
    // 날짜 문자열을 Date 객체로 변환
    if (data.createdAt) {
      data.createdAt = new Date(data.createdAt);
    }
    return data;
  }

  onSuccess(data) {
    console.log('[Analytics] User API call succeeded');
  }
}

설명

이것이 하는 일: API 클라이언트 Template Method는 마이크로서비스 통신을 표준화하고, 인증부터 에러 처리까지 모든 API 호출의 품질을 보장합니다. 첫 번째로, request() 템플릿 메서드가 전체 HTTP 요청 라이프사이클을 관리합니다.

이 메서드는 async로 선언되어 있어, 모든 비동기 작업(인증, 네트워크 I/O, 응답 파싱)을 순차적으로 처리합니다. try-catch로 감싸져 있어, 어떤 단계에서 에러가 발생하든 handleError()로 중앙화된 에러 처리가 작동합니다.

왜 이렇게 하는지? 모든 API 호출에서 일관된 에러 로깅과 모니터링을 보장하기 위함입니다.

그 다음으로, getHeaders() 메서드가 인증 토큰을 자동으로 추가합니다. 내부적으로 getToken()을 호출하여 localStorage에서 토큰을 가져오고, Authorization 헤더를 구성합니다.

만약 회사의 인증 방식이 변경되면 이 메서드만 수정하면 됩니다. 예를 들어, OAuth2를 사용한다면 getToken()을 리프레시 토큰 로직으로 교체하면 모든 API 클라이언트에 즉시 반영됩니다.

세 번째로, transformRequest()transformResponse() 추상 메서드가 API별 데이터 형식 변환을 담당합니다. UserAPIClienttransformResponse()를 재정의하여 ISO 날짜 문자열을 JavaScript Date 객체로 변환합니다.

이런 변환 로직이 각 API 클라이언트에 캡슐화되어, 도메인 객체가 항상 올바른 타입을 가지도록 보장합니다. 네 번째로, handleError() 메서드가 에러를 분류하고 적절한 복구 전략을 실행합니다.

503(Service Unavailable)이나 429(Too Many Requests) 같은 일시적 에러는 shouldRetry()에서 true를 반환하여 1초 후 재시도합니다. 반면 401(Unauthorized)이나 404(Not Found) 같은 영구적 에러는 즉시 예외를 던집니다.

이런 재시도 로직이 모든 API에 동일하게 적용되어, 네트워크 불안정 상황에서도 높은 가용성을 유지합니다. 마지막으로, onSuccess() Hook 메서드가 API 호출 성공 시 분석 이벤트를 전송합니다.

UserAPIClient는 이를 재정의하여 사용자 관련 메트릭을 수집하고, 다른 API는 각자의 분석 로직을 구현할 수 있습니다. 여러분이 이 프레임워크를 사용하면 다음과 같은 실무 이점이 있습니다.

첫째, 새로운 Product API를 통합하려면 ProductAPIClient 클래스를 만들고 엔드포인트 메서드만 추가하면 됩니다. 인증, 에러 처리, 로깅은 자동으로 작동합니다.

둘째, 모든 API 호출에 요청 ID를 추가하여 분산 추적(distributed tracing)을 구현하려면 getHeaders()X-Request-ID 헤더만 추가하면 됩니다. 셋째, 단위 테스트에서 executeRequest()를 모킹하면 실제 네트워크 없이 모든 API 로직을 테스트할 수 있습니다.

실전 팁

💡 타임아웃을 각 API별로 조정 가능하게 만드세요. 생성자에서 this.timeout을 설정하고, UserAPIClient는 3초, FileUploadClient는 30초처럼 API 특성에 맞게 커스터마이징하면 사용자 경험이 향상됩니다.

💡 Interceptor 패턴과 조합하여 요청/응답 파이프라인을 확장하세요. this.interceptors.request.use(fn) 같은 API를 제공하면, 로깅, 암호화, 압축 같은 기능을 플러그인처럼 추가할 수 있습니다.

💡 Circuit Breaker 패턴을 handleError()에 통합하세요. 특정 API가 5번 연속 실패하면 30초 동안 호출을 차단하고 즉시 에러를 반환하면, 장애가 전파되는 것을 방지할 수 있습니다.

💡 응답 캐싱을 템플릿 메서드에 추가하세요. request() 시작 부분에서 캐시를 체크하고, 히트하면 네트워크 요청을 스킵하면 성능과 비용을 크게 절감할 수 있습니다. GET 요청만 캐싱하도록 주의하세요.

💡 TypeScript의 제네릭으로 타입 안정성을 보장하세요. request<T>(endpoint): Promise<T>처럼 응답 타입을 명시하면, 컴파일 타임에 API 응답 형식 불일치를 잡을 수 있습니다.


6. 문서 생성기 구현

시작하며

여러분이 고객에게 다양한 형식의 리포트를 제공하는 SaaS 플랫폼을 개발한다고 상상해보세요. 어떤 고객은 PDF를 원하고, 어떤 고객은 HTML 이메일을 원하며, 또 다른 고객은 Excel 파일을 원합니다.

모든 리포트는 "데이터 수집 → 분석 → 차트 생성 → 포맷팅 → 내보내기"라는 동일한 과정을 거칩니다. 이런 상황에서 각 형식마다 완전히 별도의 생성기를 만들면 어떻게 될까요?

분석 로직이 중복되고, 차트 생성 방식이 미묘하게 달라지며, 새로운 데이터 소스를 추가할 때마다 모든 생성기를 수정해야 합니다. 바로 이럴 때 Template Method Pattern이 최적의 솔루션입니다.

리포트 생성 파이프라인은 한 번만 정의하고, 각 형식의 렌더링 로직만 구현하면 됩니다. 이 패턴을 사용하면 새로운 출력 형식(예: Markdown)을 추가하는 것이 하루 작업에서 1시간으로 단축됩니다.

개요

간단히 말해서, 문서 생성기 Template Method는 리포트 생성의 공통 로직(데이터 수집, 분석, 차트 생성)과 형식별 특화 로직(PDF 레이아웃, HTML 스타일링)을 분리합니다. 실무에서 이 패턴이 빛을 발하는 경우는 화이트 라벨(white-label) 솔루션을 제공할 때입니다.

예를 들어, 각 고객사마다 브랜드 컬러와 로고가 다르지만, 리포트 내용과 분석은 동일하다면, 스타일링만 다르게 적용할 수 있습니다. 기존에는 고객사마다 별도의 리포트 생성 코드를 유지해야 했다면, 이제는 테마 클래스 하나만 추가하면 새로운 브랜드 리포트를 즉시 생성할 수 있습니다.

이 접근법의 핵심 이점은 네 가지입니다. 첫째, 비즈니스 로직(분석 알고리즘)과 프레젠테이션 로직(렌더링)이 완전히 분리되어, 각각 독립적으로 수정할 수 있습니다.

둘째, 새로운 차트 타입을 추가하면 모든 출력 형식에 자동으로 반영됩니다. 셋째, 단위 테스트에서 렌더링 없이 분석 로직만 검증할 수 있어 테스트가 빠릅니다.

넷째, 멀티 포맷 익스포트(PDF + HTML + Excel 동시 생성)를 쉽게 구현할 수 있습니다. 이러한 특징들이 엔터프라이즈 리포팅 시스템의 확장성을 보장합니다.

코드 예제

// 문서 생성기 추상 클래스
class DocumentGenerator {
  // 템플릿 메서드: 문서 생성 파이프라인
  async generate(dataSource) {
    console.log('Starting document generation...');

    // 1. 데이터 수집 (공통)
    const rawData = await this.fetchData(dataSource);

    // 2. 데이터 분석 (공통)
    const analyzed = this.analyzeData(rawData);

    // 3. 문서 시작 (형식별)
    this.startDocument();

    // 4. 헤더 렌더링 (형식별)
    this.renderHeader(analyzed.title);

    // 5. 요약 섹션 (공통 + 형식별)
    this.renderSummary(analyzed.summary);

    // 6. 차트 렌더링 (형식별)
    this.renderChart(analyzed.chartData);

    // 7. 테이블 렌더링 (형식별)
    this.renderTable(analyzed.tableData);

    // 8. 문서 마무리 (형식별)
    const document = this.finalizeDocument();

    console.log('Document generation completed');
    return document;
  }

  // 공통 메서드들
  async fetchData(dataSource) {
    console.log(`Fetching data from ${dataSource}`);
    // DB 쿼리, API 호출 등...
    return { sales: [100, 200, 150], months: ['Jan', 'Feb', 'Mar'] };
  }

  analyzeData(rawData) {
    const total = rawData.sales.reduce((a, b) => a + b, 0);
    const average = total / rawData.sales.length;

    return {
      title: 'Sales Report Q1 2025',
      summary: `Total: $${total}, Average: $${average.toFixed(2)}`,
      chartData: rawData,
      tableData: rawData.sales.map((s, i) => ({
        month: rawData.months[i],
        sales: s
      }))
    };
  }

  // 추상 메서드들 (형식별로 구현)
  startDocument() {
    throw new Error('startDocument() must be implemented');
  }

  renderHeader(title) {
    throw new Error('renderHeader() must be implemented');
  }

  renderSummary(summary) {
    throw new Error('renderSummary() must be implemented');
  }

  renderChart(chartData) {
    throw new Error('renderChart() must be implemented');
  }

  renderTable(tableData) {
    throw new Error('renderTable() must be implemented');
  }

  finalizeDocument() {
    throw new Error('finalizeDocument() must be implemented');
  }
}

// PDF 생성기
class PDFGenerator extends DocumentGenerator {
  constructor() {
    super();
    this.pages = [];
  }

  startDocument() {
    this.pages.push('<PDF>');
  }

  renderHeader(title) {
    this.pages.push(`<H1 fontSize="24">${title}</H1>`);
  }

  renderSummary(summary) {
    this.pages.push(`<P>${summary}</P>`);
  }

  renderChart(chartData) {
    this.pages.push(`<BarChart data="${JSON.stringify(chartData)}"/>`);
  }

  renderTable(tableData) {
    this.pages.push('<Table>');
    tableData.forEach(row => {
      this.pages.push(`<Row>${row.month}: $${row.sales}</Row>`);
    });
    this.pages.push('</Table>');
  }

  finalizeDocument() {
    this.pages.push('</PDF>');
    return this.pages.join('\n');
  }
}

// HTML 생성기
class HTMLGenerator extends DocumentGenerator {
  constructor() {
    super();
    this.html = '';
  }

  startDocument() {
    this.html += '<html><body>';
  }

  renderHeader(title) {
    this.html += `<h1 style="color: #333;">${title}</h1>`;
  }

  renderSummary(summary) {
    this.html += `<p><strong>${summary}</strong></p>`;
  }

  renderChart(chartData) {
    this.html += `<canvas id="chart" data-sales="${chartData.sales}"></canvas>`;
  }

  renderTable(tableData) {
    this.html += '<table border="1">';
    tableData.forEach(row => {
      this.html += `<tr><td>${row.month}</td><td>$${row.sales}</td></tr>`;
    });
    this.html += '</table>';
  }

  finalizeDocument() {
    this.html += '</body></html>';
    return this.html;
  }
}

설명

이것이 하는 일: 문서 생성기 Template Method는 복잡한 리포트 생성 로직을 체계화하고, 멀티 포맷 지원을 쉽게 만듭니다. 첫 번째로, generate() 템플릿 메서드가 문서 생성의 전체 흐름을 정의합니다.

이 메서드는 8개 단계로 구성되어 있으며, 각 단계가 명확한 책임을 가집니다. fetchData()analyzeData()는 모든 형식에서 동일하게 작동하는 공통 로직입니다.

왜 이렇게 분리하는지? 데이터 처리 로직을 한 번만 작성하고, 렌더링 방식만 바꾸고 싶기 때문입니다.

그 다음으로, analyzeData() 메서드가 원시 데이터를 처리하여 문서에 필요한 정보를 계산합니다. 내부적으로 총합, 평균 같은 통계를 계산하고, 차트용 데이터와 테이블용 데이터를 각각 구성합니다.

이 분석 로직은 출력 형식과 무관하므로, PDF든 HTML이든 동일한 분석 결과를 사용합니다. 세 번째로, 렌더링 메서드들(renderHeader(), renderChart() 등)이 각 형식 클래스에서 구현됩니다.

PDFGenerator는 PDF 라이브러리의 태그 형식으로 렌더링하고, HTMLGenerator는 HTML 태그를 생성합니다. 동일한 데이터를 받지만, 완전히 다른 형식으로 출력하는 것이죠.

예를 들어, 차트를 PDF는 벡터 그래픽으로, HTML은 Canvas로 렌더링합니다. 네 번째로, 각 생성기가 내부 상태(this.pages, this.html)를 유지하며 문서를 점진적으로 구성합니다.

startDocument()에서 초기화하고, 각 렌더링 메서드에서 내용을 추가하며, finalizeDocument()에서 최종 문서를 반환합니다. 이런 스테이트풀(stateful) 접근이 복잡한 문서 레이아웃을 쉽게 만들 수 있게 해줍니다.

마지막으로, 템플릿 메서드의 단계 순서가 의미를 가집니다. "헤더 → 요약 → 차트 → 테이블" 순서는 일반적인 리포트 구조를 반영하며, 이 순서를 바꾸면 모든 형식에 일괄 적용됩니다.

여러분이 이 시스템을 사용하면 다음과 같은 실무 이점이 있습니다. 첫째, Markdown 생성기를 추가하려면 렌더링 메서드 6개만 구현하면 됩니다.

데이터 수집과 분석은 자동으로 작동합니다. 둘째, 새로운 섹션(예: 푸터)을 추가하려면 generate() 메서드에 renderFooter() 호출 한 줄만 추가하면 모든 형식에 반영됩니다.

셋째, A/B 테스트에서 차트 위치를 바꾸려면 renderChart() 호출 순서만 조정하면 됩니다.

실전 팁

💡 Builder 패턴과 조합하여 문서 구성을 더 유연하게 만드세요. generator.withHeader().withChart().withTable().generate()처럼 플루언트 API를 제공하면, 필요한 섹션만 선택적으로 포함할 수 있습니다.

💡 템플릿 엔진(Handlebars, EJS 등)을 렌더링 메서드에 통합하세요. 하드코딩된 HTML 대신 템플릿 파일을 사용하면, 디자이너가 코드 수정 없이 레이아웃을 변경할 수 있습니다.

💡 대용량 리포트는 스트리밍 방식으로 전환하세요. generate() 메서드를 Generator Function으로 만들어 yield로 각 섹션을 반환하면, 메모리 사용량을 크게 줄이고 첫 페이지를 빠르게 표시할 수 있습니다.

💡 국제화(i18n)를 템플릿 메서드에 추가하세요. renderHeader()에서 this.t('report.title') 같은 번역 함수를 호출하면, 동일한 생성기로 다국어 리포트를 만들 수 있습니다.

💡 캐싱 전략을 레이어별로 적용하세요. analyzeData() 결과를 캐싱하면 동일한 데이터로 여러 형식을 생성할 때 분석 비용을 절감할 수 있습니다. Redis나 메모리 캐시를 활용하세요.


7. 테스트 프레임워크 설계

시작하며

여러분이 Jest나 Mocha 같은 테스트 프레임워크를 사용해본 적 있나요? 모든 테스트는 "Setup → 테스트 실행 → Teardown"이라는 동일한 라이프사이클을 따릅니다.

테스트 전에 DB를 초기화하고, 테스트를 실행하고, 테스트 후에 리소스를 정리하는 것이죠. 이런 구조를 각 테스트마다 수동으로 작성하면 어떻게 될까요?

Setup 코드가 중복되고, Teardown을 빠뜨려 테스트 간 간섭이 발생하며, 새로운 리소스(예: Redis 연결)를 추가할 때마다 모든 테스트를 수정해야 합니다. 바로 이럴 때 Template Method Pattern이 테스트 프레임워크의 핵심이 됩니다.

테스트 라이프사이클은 프레임워크가 관리하고, 개발자는 실제 테스트 로직만 작성하면 됩니다. 이 패턴을 이해하면 여러분만의 테스트 헬퍼나 커스텀 프레임워크를 만들 수 있습니다.

개요

간단히 말해서, 테스트 프레임워크의 Template Method는 테스트 실행 흐름(before → test → after)을 표준화하고, 개발자가 테스트 케이스만 구현하도록 합니다. 실무에서 이 패턴이 중요한 이유는 테스트 안정성과 직결되기 때문입니다.

예를 들어, 통합 테스트에서 DB 연결 풀을 관리해야 한다면, Setup에서 연결을 생성하고 Teardown에서 반드시 닫아야 합니다. 이 로직을 한 곳에만 작성하고, 모든 테스트가 자동으로 사용하게 만들면 리소스 누수를 방지할 수 있습니다.

기존에는 각 테스트 파일마다 beforeEach()afterEach()를 반복해서 작성했다면, 이제는 베이스 테스트 클래스를 상속받기만 하면 자동으로 Setup/Teardown이 작동합니다. 이 접근법의 핵심 장점은 네 가지입니다.

첫째, Fixture 관리가 중앙화되어 테스트 데이터가 일관됩니다. 둘째, Isolation이 보장되어 테스트 순서에 관계없이 결과가 동일합니다.

셋째, 에러 처리가 표준화되어 테스트 실패 시 항상 Teardown이 실행됩니다. 넷째, 테스트 실행 시간을 측정하거나 로깅을 추가하는 것이 한 곳에서 가능합니다.

이러한 특징들이 대규모 테스트 스위트의 유지보수성을 결정합니다.

코드 예제

// 테스트 베이스 클래스
class TestCase {
  constructor(testName) {
    this.testName = testName;
    this.passed = false;
  }

  // 템플릿 메서드: 테스트 실행 흐름
  async run() {
    console.log(`\n=== Running: ${this.testName} ===`);

    try {
      // 1. Setup (Hook)
      await this.beforeEach();

      // 2. 테스트 실행 (추상 메서드)
      const startTime = Date.now();
      await this.test();
      const duration = Date.now() - startTime;

      // 3. Teardown (Hook)
      await this.afterEach();

      this.passed = true;
      console.log(`✓ PASSED (${duration}ms)`);
    } catch (error) {
      console.error(`✗ FAILED: ${error.message}`);

      // 에러가 나도 Teardown은 실행
      try {
        await this.afterEach();
      } catch (teardownError) {
        console.error(`Teardown failed: ${teardownError.message}`);
      }
    }

    return this.passed;
  }

  // Hook Methods (선택적 재정의)
  async beforeEach() {
    // 기본 Setup (아무것도 안 함)
  }

  async afterEach() {
    // 기본 Teardown (아무것도 안 함)
  }

  // 추상 메서드 (반드시 구현)
  async test() {
    throw new Error('test() must be implemented');
  }

  // 어서션 헬퍼
  assertEqual(actual, expected) {
    if (actual !== expected) {
      throw new Error(`Expected ${expected}, but got ${actual}`);
    }
  }

  assertTrue(condition) {
    if (!condition) {
      throw new Error('Assertion failed: expected true');
    }
  }
}

// DB 테스트 베이스 클래스
class DatabaseTestCase extends TestCase {
  async beforeEach() {
    console.log('Setting up database connection...');
    this.db = { connected: true, data: {} };  // 실제로는 DB 연결
  }

  async afterEach() {
    console.log('Cleaning up database...');
    this.db.connected = false;  // 실제로는 연결 종료
  }
}

// 구체적인 테스트 케이스
class UserCreationTest extends DatabaseTestCase {
  async test() {
    // 테스트 데이터 준비
    const userData = { name: 'Alice', email: 'alice@example.com' };

    // 테스트 실행
    this.db.data.user = userData;

    // 검증
    this.assertEqual(this.db.data.user.name, 'Alice');
    this.assertTrue(this.db.connected);
  }
}

class UserDeletionTest extends DatabaseTestCase {
  async test() {
    // Setup
    this.db.data.user = { name: 'Bob' };

    // 테스트 실행
    delete this.db.data.user;

    // 검증
    this.assertEqual(this.db.data.user, undefined);
  }
}

// 테스트 실행
async function runTests() {
  const tests = [
    new UserCreationTest('User Creation'),
    new UserDeletionTest('User Deletion')
  ];

  const results = await Promise.all(tests.map(t => t.run()));
  const passedCount = results.filter(r => r).length;

  console.log(`\n=== Results: ${passedCount}/${tests.length} passed ===`);
}

runTests();

설명

이것이 하는 일: 테스트 프레임워크 Template Method는 테스트 격리와 리소스 관리를 자동화하여, 개발자가 비즈니스 로직 검증에만 집중할 수 있게 합니다. 첫 번째로, run() 템플릿 메서드가 테스트 전체 라이프사이클을 관리합니다.

try-catch로 전체를 감싸고 있어, 테스트 중 에러가 발생해도 afterEach()가 반드시 실행됩니다. 왜 이렇게 하는지?

테스트가 실패하더라도 DB 연결이나 파일 핸들 같은 리소스를 반드시 해제해야 하기 때문입니다. 이를 빠뜨리면 다음 테스트가 영향을 받아 "Flaky Test"(간헐적으로 실패하는 테스트)가 됩니다.

그 다음으로, beforeEach() Hook이 실행되면서 테스트 환경을 준비합니다. DatabaseTestCase는 이를 재정의하여 DB 연결을 생성하고, 모든 자식 테스트(UserCreationTest, UserDeletionTest)가 이 연결을 사용합니다.

내부적으로 this.db에 연결 객체를 저장하여, 테스트 메서드에서 접근할 수 있게 합니다. 세 번째로, 실제 테스트 로직이 test() 추상 메서드에서 실행됩니다.

UserCreationTest는 사용자 생성을 테스트하고, UserDeletionTest는 삭제를 테스트합니다. 각 테스트는 완전히 독립적이며, beforeEach()에서 매번 깨끗한 DB 상태를 받기 때문에 서로 간섭하지 않습니다.

네 번째로, 성능 측정 로직이 템플릿 메서드에 통합되어 있습니다. Date.now()로 테스트 시작과 끝 시간을 기록하여, 각 테스트의 실행 시간을 자동으로 로깅합니다.

이 데이터를 수집하면 느린 테스트를 찾아 최적화할 수 있습니다. 마지막으로, afterEach()가 catch 블록 안에서 다시 try-catch로 감싸져 있습니다.

Teardown 자체가 실패할 수도 있기 때문입니다. 예를 들어, DB 연결이 이미 끊어진 상태에서 닫으려고 하면 에러가 발생하는데, 이 에러가 원래 테스트 에러를 덮어쓰지 않도록 별도로 처리합니다.

여러분이 이 프레임워크를 사용하면 다음과 같은 실무 이점이 있습니다. 첫째, API 테스트 베이스 클래스를 만들어 HTTP 서버 시작/종료를 자동화할 수 있습니다.

둘째, Transaction을 beforeEach()에서 시작하고 afterEach()에서 롤백하면, 테스트 간 데이터가 섞이지 않습니다. 셋째, 모든 테스트에 타임아웃을 적용하려면 run() 메서드에 Promise.race()를 추가하면 됩니다.

실전 팁

💡 Test Fixture를 별도 클래스로 분리하세요. beforeEach()에서 this.fixture = new UserFixture()처럼 Fixture 객체를 생성하면, 테스트 데이터 관리가 훨씬 깔끔해집니다. Fixture는 Factory 패턴으로 구현하는 것이 좋습니다.

💡 BeforeAll/AfterAll Hook도 추가하세요. DB 마이그레이션이나 서버 시작처럼 테스트 스위트 전체에 한 번만 실행할 작업은 beforeAll() Hook에 넣으면, 테스트 실행 속도가 크게 향상됩니다.

💡 Snapshot 테스팅을 템플릿 메서드에 통합하세요. afterEach()에서 자동으로 스냅샷을 비교하고, 차이가 있으면 실패시키면, UI 회귀 테스트를 쉽게 만들 수 있습니다.

💡 병렬 테스트 실행 시 격리를 보장하세요. 각 테스트가 별도의 DB 스키마나 Redis 네임스페이스를 사용하도록 beforeEach()에서 고유 ID를 생성하면, 여러 테스트가 동시에 실행되어도 안전합니다.

💡 테스트 재시도 로직을 추가하세요. run() 메서드를 수정하여 실패한 테스트를 2-3회 재시도하면, 네트워크 타임아웃 같은 일시적 오류를 걸러낼 수 있습니다. 단, 재시도 횟수를 로깅하여 근본 원인을 파악하세요.


8. 결제 처리 시스템

시작하며

여러분이 여러 결제 수단(신용카드, PayPal, 암호화폐)을 지원하는 이커머스 플랫폼을 개발한다고 상상해보세요. 모든 결제는 "사용자 인증 → 금액 검증 → 결제 처리 → 영수증 발송 → 로그 기록"이라는 동일한 흐름을 따르지만, 실제 결제 방식은 완전히 다릅니다.

이런 상황에서 각 결제 수단마다 별도의 처리 함수를 만들면 어떻게 될까요? 금액 검증 로직이 중복되고, 한 결제 수단에서는 영수증을 보내는데 다른 수단에서는 빠뜨리고, 감사 로그가 일관성 없게 기록됩니다.

바로 이럴 때 Template Method Pattern이 생명줄이 됩니다. 결제 프로세스는 표준화하고, 각 결제 수단의 특성만 구현하면 됩니다.

이 패턴을 사용하면 PCI DSS 같은 컴플라이언스 요구사항을 한 곳에서 관리하고, 모든 결제 수단에 일관되게 적용할 수 있습니다.

개요

간단히 말해서, 결제 처리 Template Method는 결제 프로세스의 공통 로직(검증, 로깅, 알림)과 결제 수단별 특화 로직(결제 게이트웨이 API 호출)을 분리합니다. 실무에서 이 패턴이 중요한 이유는 금융 규제 준수와 보안 때문입니다.

예를 들어, 모든 결제는 반드시 금액 한도를 체크하고, 사기 탐지 시스템을 거치고, 감사 로그를 남겨야 합니다. 이 로직을 한 번만 올바르게 작성하고, 모든 결제 수단이 재사용하게 만들면 보안 취약점을 크게 줄일 수 있습니다.

기존에는 신용카드 결제와 PayPal 결제가 각각 다른 검증 로직을 사용하여 취약점이 발생했다면, 이제는 공통 검증 레이어를 통과한 후에만 실제 결제가 진행됩니다. 이 접근법의 핵심 장점은 다섯 가지입니다.

첫째, 결제 프로세스의 각 단계가 명확히 정의되어 코드 리뷰와 감사가 쉽습니다. 둘째, 새로운 결제 수단을 추가할 때 규제 준수가 자동으로 보장됩니다.

셋째, A/B 테스트나 점진적 롤아웃이 쉬워집니다(특정 사용자에게만 새 결제 수단 노출). 넷째, 에러 처리와 재시도 로직이 표준화되어 사용자 경험이 일관됩니다.

다섯째, 결제 분석과 모니터링을 중앙에서 수집할 수 있습니다. 이러한 특징들이 미션 크리티컬한 결제 시스템의 안정성을 보장합니다.

코드 예제

// 결제 처리 추상 클래스
class PaymentProcessor {
  // 템플릿 메서드: 결제 프로세스
  async processPayment(order, paymentDetails) {
    console.log(`\n=== Processing ${order.id} ===`);

    try {
      // 1. 사용자 인증 (공통)
      await this.authenticateUser(order.userId);

      // 2. 금액 검증 (공통)
      this.validateAmount(order.amount);

      // 3. 사기 탐지 (공통)
      const fraudScore = await this.checkFraud(order);
      if (fraudScore > 0.8) {
        throw new Error('High fraud risk detected');
      }

      // 4. 실제 결제 처리 (결제 수단별)
      const transactionId = await this.executePayment(
        order.amount,
        paymentDetails
      );

      // 5. 결제 확인 (Hook - 선택적)
      if (this.requiresConfirmation()) {
        await this.confirmPayment(transactionId);
      }

      // 6. 영수증 발송 (공통)
      await this.sendReceipt(order, transactionId);

      // 7. 감사 로그 (공통)
      await this.logTransaction({
        orderId: order.id,
        transactionId,
        amount: order.amount,
        method: this.getPaymentMethod(),
        timestamp: Date.now()
      });

      console.log(`✓ Payment successful: ${transactionId}`);
      return { success: true, transactionId };

    } catch (error) {
      console.error(`✗ Payment failed: ${error.message}`);
      await this.handlePaymentFailure(order, error);
      throw error;
    }
  }

  // 공통 메서드들
  async authenticateUser(userId) {
    console.log(`Authenticating user ${userId}...`);
    // JWT 검증, 세션 체크 등
  }

  validateAmount(amount) {
    if (amount <= 0) {
      throw new Error('Invalid amount');
    }
    if (amount > 10000) {
      throw new Error('Amount exceeds limit');
    }
  }

  async checkFraud(order) {
    console.log('Running fraud detection...');
    // ML 모델로 사기 점수 계산
    return Math.random() * 0.5;  // 0-0.5 범위
  }

  async sendReceipt(order, transactionId) {
    console.log(`Sending receipt to ${order.email}`);
    // 이메일 발송
  }

  async logTransaction(data) {
    console.log(`Logging transaction: ${JSON.stringify(data)}`);
    // DB에 감사 로그 저장
  }

  async handlePaymentFailure(order, error) {
    console.log('Handling payment failure...');
    // 실패 알림, 재시도 큐에 추가 등
  }

  // 추상 메서드 (결제 수단별 구현)
  async executePayment(amount, details) {
    throw new Error('executePayment() must be implemented');
  }

  getPaymentMethod() {
    throw new Error('getPaymentMethod() must be implemented');
  }

  // Hook Method (선택적)
  requiresConfirmation() {
    return false;
  }

  async confirmPayment(transactionId) {
    console.log(`Confirming payment ${transactionId}`);
  }
}

// 신용카드 결제
class CreditCardProcessor extends PaymentProcessor {
  async executePayment(amount, details) {
    console.log(`Charging card **** ${details.cardNumber.slice(-4)}`);
    // Stripe, Braintree API 호출
    return `CC-${Date.now()}`;
  }

  getPaymentMethod() {
    return 'CREDIT_CARD';
  }
}

// PayPal 결제
class PayPalProcessor extends PaymentProcessor {
  async executePayment(amount, details) {
    console.log(`Processing PayPal payment for ${details.email}`);
    // PayPal SDK 호출
    return `PP-${Date.now()}`;
  }

  getPaymentMethod() {
    return 'PAYPAL';
  }

  // 3D Secure 확인 필요
  requiresConfirmation() {
    return true;
  }

  async confirmPayment(transactionId) {
    console.log(`Waiting for PayPal 3D Secure confirmation...`);
    // 사용자의 확인 대기
  }
}

// 암호화폐 결제
class CryptoProcessor extends PaymentProcessor {
  async executePayment(amount, details) {
    console.log(`Sending ${amount} to wallet ${details.walletAddress}`);
    // Blockchain API 호출
    return `BTC-${Date.now()}`;
  }

  getPaymentMethod() {
    return 'CRYPTOCURRENCY';
  }

  requiresConfirmation() {
    return true;
  }

  async confirmPayment(transactionId) {
    console.log(`Waiting for blockchain confirmations (3/6)...`);
    // 블록 확인 대기
  }
}

설명

이것이 하는 일: 결제 처리 Template Method는 금융 거래의 안정성과 컴플라이언스를 보장하면서, 다양한 결제 수단을 유연하게 지원합니다. 첫 번째로, processPayment() 템플릿 메서드가 PCI DSS와 같은 결제 보안 표준을 구현합니다.

이 메서드는 7단계의 엄격한 프로세스를 정의하며, 각 단계가 실패하면 전체 결제가 취소됩니다. try-catch로 전체를 감싸고 있어, 어떤 단계에서 에러가 발생하든 handlePaymentFailure()로 일관된 에러 처리가 작동합니다.

왜 이렇게 하는지? 결제 중 에러 발생 시 사용자에게 명확한 메시지를 보여주고, 중복 결제를 방지하며, 감사 로그를 남기기 위함입니다.

그 다음으로, 공통 검증 로직들(validateAmount(), checkFraud())이 모든 결제 수단에 대해 동일하게 작동합니다. 금액 한도 체크는 비즈니스 규칙이므로 중앙에서 관리되며, 신용카드든 암호화폐든 동일한 한도가 적용됩니다.

내부적으로 사기 탐지는 ML 모델을 호출하여 주문 패턴을 분석하고, 위험 점수가 0.8을 넘으면 즉시 거부합니다. 세 번째로, executePayment() 추상 메서드가 실제 결제 게이트웨이와 통신합니다.

CreditCardProcessor는 Stripe API를 호출하고, PayPalProcessor는 PayPal SDK를 사용하며, CryptoProcessor는 블록체인 네트워크에 트랜잭션을 브로드캐스트합니다. 각 게이트웨이의 API가 완전히 다르지만, 외부에서는 동일한 인터페이스로 호출할 수 있습니다.

네 번째로, requiresConfirmation() Hook 메서드가 결제 수단별 특성을 반영합니다. 신용카드는 즉시 승인되지만, PayPal은 3D Secure 인증이 필요하고, 암호화폐는 블록 확인을 기다려야 합니다.

Hook이 false를 반환하면 확인 단계를 스킵하고, true를 반환하면 confirmPayment()가 실행됩니다. 다섯 번째로, 감사 로그가 모든 결제에 대해 자동으로 기록됩니다.

logTransaction()은 주문 ID, 거래 ID, 금액, 결제 수단, 타임스탬프를 구조화된 형식으로 저장합니다. 이 로그는 금융 감사, 분쟁 해결, 비즈니스 분석에 필수적이며, 한 곳에서 관리되므로 일관성이 보장됩니다.

마지막으로, handlePaymentFailure()가 실패한 결제를 처리합니다. 사용자에게 에러 메시지를 보여주고, 재시도 가능한 에러(네트워크 타임아웃)는 큐에 추가하며, 영구적 에러(카드 거부)는 즉시 알립니다.

여러분이 이 시스템을 사용하면 다음과 같은 실무 이점이 있습니다. 첫째, Apple Pay를 추가하려면 ApplePayProcessor 클래스를 만들고 executePayment()만 구현하면 됩니다.

검증, 사기 탐지, 로깅은 자동으로 작동합니다. 둘째, 모든 결제에 2단계 인증을 추가하려면 processPayment() 메서드에 인증 단계만 삽입하면 됩니다.

셋째, 결제 성공률을 모니터링하려면 logTransaction()에 메트릭 전송 코드를 추가하면, 모든 결제 수단의 통계를 실시간으로 수집할 수 있습니다.

실전 팁

💡 멱등성(Idempotency)을 보장하세요. 주문 ID를 키로 사용하여 중복 결제 요청을 탐지하고, 이미 처리된 요청은 캐시된 결과를 반환하면, 네트워크 재시도로 인한 이중 결제를 방지할 수 있습니다.

💡 Circuit Breaker를 결제 게이트웨이 호출에 적용하세요. executePayment() 내부에서 게이트웨이가 5번 연속 실패하면 30초 동안 호출을 차단하고 즉시 에러를 반환하면, 장애 전파를 막을 수 있습니다.

💡 결제 상태를 상태 머신으로 관리하세요. PENDING → PROCESSING → CONFIRMED → COMPLETED 같은 상태 전이를 명확히 정의하고, 템플릿 메서드의 각 단계에서 상태를 업데이트하면, 결제 진행 상황을 정확히 추적할 수 있습니다.

💡 Webhook 처리를 템플릿 메서드에 통합하세요. PayPal이나 Stripe는 비동기로 결제 결과를 Webhook으로 보내는데, confirmPayment()에서 Webhook을 대기하고 서명을 검증하면, 보안을 유지하면서 비동기 결제를 처리할 수 있습니다.

💡 PCI DSS 준수를 위해 카드 정보를 절대 로깅하지 마세요. logTransaction()에서 민감 정보를 자동으로 마스킹하는 로직을 추가하고, details.cardNumber는 마지막 4자리만 기록하세요. 감사 시 규제 위반으로 거액의 벌금을 피할 수 있습니다.


#TypeScript#DesignPatterns#TemplateMethod#OOP#AbstractClass

댓글 (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 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.

이전9/9
다음