본문 바로가기
Dev/디자인패턴

[타입스크립트로 살펴보는 디자인패턴 1] 전략패턴(Strategy Pattern)

by 싯벨트 2024. 12. 4.
728x90

🙋‍♂️ 디자인패턴 구현코드 깃헙

전략 패턴(Strategy Pattern)이란?

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 준다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

 

개발에서 절대로 바뀌지 않을 진리는 바로 ‘변화’이다. 그래서 코드를 고칠 때 기존 코드에 미치는 영향을 최소화 하는 것이 중요하다. 상속을 활용하는 것은 코드의 재사용성을 높이는 좋은 방법이지만, 이런 변동성에 대응하기 어렵다는 단점을 가지고 있다. 오리 클래스를 구성하는 예제를 통해 아래 디자인 원칙 3가지가 구현된 전략 패턴을 어떻게 적용할 수 있는지 살펴보자.

디자인 원칙

  1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
  2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
  3. 상속보다는 구성을 활용한다.

처음 코드

상속을 통해 2가지 종류의 오리를 출력했다.

abstract class Duck {
  quack(): void { 
      console.log("꽥꽥")
  }
  swim(): void {
      console.log("물에 떠요")
  }
  abstract display(): void
}

class MallardDuck extends Duck {
  display(): void {
    console.log("청동오리 모양 출력")
  }
}

class RedHeadDuck extends Duck {
  display(): void {
    console.log("붉은머리오리 모양 출력")
  }
}

여기에 날 수 있는 기능에 대한 요구사항이 추가되었고, 슈퍼클래스에 fly 메서드를 추가했다.

abstract class Duck {
  quack(): void { 
      console.log("꽥꽥")
  }
  swim(): void {
      console.log("물에 떠요")
  }
  abstract display(): void
  fly(): void { 
      console.log("날 수 있어요")
  }
}

 

그런데 이렇게 되면 고무 오리와 같은 날 수 없는 서브 클래스까지 fly 메서드를 상속받는 오류가 발생하는 것을 알게 됐다.

그래서 고무 오리 서브클래스의 fly 메서드는 날지 못하도록 오버라이드 하여 문제를 해결했다.

class RubberDuck extends Duck {
  quack(): void {
    console.log("삑삑")
  }
  display(): void {
    console.log("고무오리 모양 출력")
  }
  fly(): void {
    console.log("날지 못해요")
  }
}

 

그런데 슈퍼클래스의 메서드를 사용하지 않는 모형오리와 같은 다른 클래스에 대한 요구사항이 들어왔고, 이번에도 오버라이드를 하여 모형오리 클래스를 구현했다.

class DecoyDuck extends Duck {
  quack(): void {
    console.log("소리내지 못해요")
  }
  display(): void {
    console.log("모형오리 모양 출력")
  }
  fly(): void {
    console.log("날지 못해요")
  }
}

 

지금까지의 과정만 봐도 슈퍼클래스에 기능을 추가하고 상속을 받는 방식을 사용할 경우, 앞으로 기능이 추가되거나 다른 유형의 오리들이 추가될 때, 오리 유형마다 특징에 맞게 오버라이드를 해야 한다는 번거로움이 발생하는 것을 알 수 있다. 단점들을 요약하면 다음과 같다.

  • 서브클래스에서 코드가 중복된다 (특정 메서드에 대한 오버라이드가 불가피하다)
  • 모든 오리의 행동을 알기 힘들다(각 서브클래스에 어떻게 오버라이드 되었는지 살펴봐야 한다)
  • 실행 시 특징을 바꾸기 힘들다
  • 코드를 변경했을 때 다른 오리들에게 원치 않는 영향을 미칠 수 있다.

요구사항에 변동이 계속 발생할 것이기에 변동성이 있는 기능들을 따로 인터페이스로 구성하는 방법도 생각해볼 수 있다.

abstract class Duck {
  swim(): void {
    console.log("물에 떠요")
  }
  abstract display(): void
}

interface Flyable {
  fly(): void
}

interface Quackable {
  quack(): void
}

class MallardDuck extends Duck implements Flyable, Quackable {
  display(): void {
    console.log("청동오리 모양 출력")
  }
  fly(): void {
    console.log("날 수 있어요")
  }
  quack(): void {
    console.log("꽥꽥")
  }
}

class RedHeadDuck extends Duck implements Flyable, Quackable {
  display(): void {
    console.log("붉은머리오리 모양 출력")
  }
  fly(): void {
    console.log("날 수 있어요")
  }
  quack(): void {
    console.log("꽥꽥")
  }
}

class RubberDuck extends Duck implements Quackable {
  display(): void {
    console.log("고무오리 모양 출력")
  }
  quack(): void {
    console.log("삑삑")
  }
}

class DecoyDuck extends Duck {
  display(): void {
    console.log("모형오리 모양 출력")
  }
}

 

그치만 코드의 재사용성이 현저히 떨어져서 한 가지 행동이 바뀔 때마다 그 행동이 정의되어 있는 서브클래스를 모두 찾아서 일일이 고쳐야 하는 문제가 발생할 것이 자명하다.

 

이제 앞서 살펴봤던 디자인 원칙 3가지를 다시 살펴보며 어떻게 적용해야 할지 알아가보자.

디자인 원칙
1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
3. 상속보다는 구성을 활용한다.

바뀌는 부분과 그렇지 않은 부분 분리하기

달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 “캡슐화”한다. 그러면 나중에 바뀌지 않는 부분에 영향을 미치지 않고 캡슐화된 부분만 고치거나 확장할 수 있다. Duck 클래스에서 바뀌는 부분은 fly, quack 메서드다. 각 행동을 나타낼 클래스 집합을 새로 만들고, 다양한 메서드를 구현할 것이다.

구현보다는 인터페이스에 맞춰서 프로그래밍한다

이때 인터페이스란 추상 상위 형식으로 인터페이스 또는 추상 클래스를 의미한다

 

행동에 대한 인터페이스를 만들고, 이를 이용해 행동을 구현한다. 더불어, 행동을 동적으로 바꿀 수 있도록 Duck 클래스에 행동과 관련된 setter 메서드를 마련한다. 이렇게 되면 오리의 행동은 특정 행동 인터페이스를 구현한 별도의 클래스 안에 들어있기 때문에 Duck 클래스는 그 행동을 구체적으로 구현할 필요가 없게 된다.

상속보다는 구성을 활용한다.

위와 같이 프로그래밍을 각 행동을 위임받은 인터페이스를 구현한 클래스를 합치게 된다. 이렇게 두 클래스를 합치는 것을 “구성(composition)을 이용한다고 한다.

전략 패턴 적용 코드

따로 테스트 파일을 두지 않고, duck.ts 에서 실행할 수 있도록 구성했다. 타입스크립트 코드 실행은 루트파일에서 tsc를 입력하여 js파일로 변환하고, 이후 node <파일명> 을 입력하면 콘솔에 입력한 값이 출력되는 것을 볼 수 있다.

1. duck.ts

import { FlyBehavior, FlyNoWay, FlyRocketPowered, FlywWithWings } from "./flyBehavior"
import { MuteQuack, Quack, QuackBehavior, Squeak } from "./quackBehavior"

abstract class Duck {
  constructor(
    private flyBehavioir: FlyBehavior,
    private quackBehavior: QuackBehavior) { }

  swim(): void {
    console.log("물에 떠요")
  }

  abstract display(): void

  performFly(): void {
    this.flyBehavioir.fly()
  }

  performQuack(): void {
    this.quackBehavior.quack()
  }

  setFlyBehavior(fb: FlyBehavior) {
    this.flyBehavioir = fb
  }

  setQuackBehavior(qb: QuackBehavior) {
    this.quackBehavior = qb
  }
}

class MallardDuck extends Duck {
  constructor() {
    super(new FlywWithWings(), new Quack())
  }

  display(): void {
    console.log("청동오리 모양 출력")
  }
}

class RedHeadDuck extends Duck {
  constructor() {
    super(new FlywWithWings(), new Quack())
  }

  display(): void {
    console.log("붉은머리오리 모양 출력")
  }
}

class RubberDuck extends Duck {
  constructor() {
    super(new FlyNoWay(), new Squeak())
  }

  display(): void {
    console.log("고무오리 모양 출력")
  }
}

class DecoyDuck extends Duck {
  constructor() {
    super(new FlyNoWay(), new MuteQuack())
  }

  display(): void {
    console.log("모형오리 모양 출력")
  }
}

class ModelDuck extends Duck {
  constructor() {
    super(new FlyNoWay(), new MuteQuack())
  }

  display(): void {
    console.log("모형오리 모양 출력")
  }
}

// TEST
const modelDuck = new ModelDuck()
modelDuck.setFlyBehavior(new FlyRocketPowered())

console.log(modelDuck.performFly())
console.log(modelDuck.performQuack())

2. flyBehavior.ts

export interface FlyBehavior {
  fly(): void
}

export class FlywWithWings implements FlyBehavior {
  fly(): void {
    console.log("날 수 있어요")
  }
}

export class FlyNoWay implements FlyBehavior {
  fly(): void {
    console.log("날 수 없어요")
  }
}

export class FlyRocketPowered implements FlyBehavior {
  fly(): void {
    console.log("로켓추친으로 날아요")
  }
}

3. quackBehavior.ts

export interface QuackBehavior {
  quack(): void
}

export class Quack implements QuackBehavior {
  quack(): void {
    console.log("꽥꽥")
  }
}

export class Squeak implements QuackBehavior {
  quack(): void {
    console.log("삑삑")
  }
}

export class MuteQuack implements QuackBehavior {
  quack(): void {
    console.log("소리를 못내요")
  }
}

타입스크립트 파라미터 프로퍼티

생성자 내부에 접근 제한자(public, private, protected)로 선언한 필드는 자동으로 클래스로 정의되고 초기화된다.

 

참고문서

헤드퍼스트 디자인패턴, 한빛미디어