웹, 앱

[NestJS] Strategy Pattern (전략 패턴)

Hyun-danpung2 2024. 11. 13. 23:03
728x90
반응형

1. 서론

전략 패턴에 대해서는 간단하게 알고 있었지만 한동안은 if-else 문 혹은 switch 문과의 차이를 크게 못 느꼈었다. 그러다가 실무에서 어떤 환경이 주어졌을 때 전략 패턴이 떠올랐고 적용하면서 이점을 느끼게 되어 기록하게 되었다. 실무에서 NestJS를 사용 중이기에 제목과 예시 코드가 NestJS 이지만 디자인 패턴 중 하나인 만큼 어디에도 적용이 가능할 것이다.

 

2. 본론

  1. 정의
    1. Strategy Pattern (전략 패턴)은 객체의 행위를 정의하는 방법 중 하나
    2. 알고리즘을 정의하고 이를 캡슐화하여 클라이언트 코드에서 독립적으로 사용할 수 있도록 하는 디자인 패턴
    3. 여러 알고리즘을 정의하고, 그 알고리즘을 동적으로 선택하여 사용
  2. 주요 구성 요소
    1. Context: 전략을 사용하는 클라이언트 객체
    2. Strategy Interface: 다양한 알고리즘을 정의하는 인터페이스
    3. Concrete Strategies: Strategy 인터페이스를 구현하여 구체적인 알고리즘을 제공하는 클래스
  3. 사용 사례
    1. 다양한 알고리즘 필요
      1. 동일한 작업을 수행하지만 여러 알고리즘이 필요할 때 사용 됨
      2. 예를 들어, 정렬 알고리즘이 다르게 적용되거나 결제 처리 방식이 다르게 적용되는 경우 등
    2. 런타임에 알고리즘 변경
      1. 프로그램 실행 중에 사용자의 입력이나 환경에 따라 알고리즘을 변경해야할 때 유용하게 사용
      2. 예를 들어, 사용자에게 다양한 결제 수단을 제공하고 선택에 따라 다른 알고리즘이 적용되는 경우 등
    3. 복잡한 조건문 제거
      1. if-else 또는 switch-case가 여러번 사용되어 알고리즘을 구분하는 코드를 간소화할 때 사용
    4. 통일된 인터페이스 제공
      1. 서로 다른 알고리즘이 있지만 클라이언트가 알 필요 없는 상황에 하나의 인터페이스를 클라이언트에게 제공하고 내부적으로 구분할 때 사용
  4. 예시: 결제 시 할인 로직
// 1. Strategy Interface 정의

export interface DiscountStrategy {
  applyDiscount(price: number): number;
}

 

// 2. Concrete Strategies 정의

import { DiscountStrategy } from './discount-strategy.interface';

// 할인 없음
export class NoDiscountStrategy implements DiscountStrategy {
  applyDiscount(price: number): number {
    return price; 
  }
}

// 퍼센트 할인
export class PercentageDiscountStrategy implements DiscountStrategy {
  constructor(private readonly percentage: number) {}

  applyDiscount(price: number): number {
    return price - (price * this.percentage) / 100; // 퍼센트 할인
  }
}

// 고정 금액 할인
export class FixedAmountDiscountStrategy implements DiscountStrategy {
  constructor(private readonly amount: number) {}

  applyDiscount(price: number): number {
    return price - this.amount;
  }
}

 

// 3. service 로직

@Injectable()
export class PaymentService {
  private strategy: DiscountStrategy;

  constructor(
    private readonly noDiscountStrategy: NoDiscountStrategy,
    private readonly percentageDiscountStrategy: PercentageDiscountStrategy,
    private readonly fixedAmountDiscountStrategy: FixedAmountDiscountStrategy,
  ) {}

  async pay(dto: PaymentRequestDTO) {
	  const discountedAmount = this.applyDiscount(dto);
	  // TODO: 결제 로직
  }

  private applyDiscount(dto: PaymentRequestDTO) {
    this.setStrategy(dto.discountType);
    this.strategy.applyDiscount(dto);
  }
  
  private setStrategy(discountType: string) {
    switch (discountType) {
      case 'NO_DISCOUNT':
        this.strategy = this.noDiscountStrategy;
        break;
      case 'PERCENTAGE_DISCOUNT':
        this.strategy = this.percentageDiscountStrategy;
        break;
      case 'FIXED_AMOUNT_DISCOUNT':
        this.strategy = this.fixedAmountDiscountStrategy;
        break;
      default:
        throw new Error('Invalid product type');
    }
  }
}

 

// 4. controller 로직

@Controller('payment')
export class PaymentController {
  constructor(private readonly paymentService: PaymentService) {}

  @Post()
  async pay(@Body() dto: PaymentRequestDTO) {
    await this.paymentService.pay(dto);
    return { message: 'success' };
  }
}

 

3. 결론

전략 패턴을 사용했을 때의 장점은 책임 분리와 유연성 향상, 그리고 테스트 용이성이다. 유닛 테스트를 진행할 때 service 단에서는 각 전략 별로 별도의 테스트가 가능하고 controller 단에서는 service 내부의 로직과 관계없이 테스트를 작성할 수 있었다.

728x90
반응형