[Typescript] builder pattern

Falcon·2023년 12월 9일
1

typescript

목록 보기
5/6

글의 목적

  • 5분 내로 빌더 패턴을 언제 써야하는 지 이해한다.
  • 실제 코드로 빌더 패턴의 용례를 따라해볼 수 있게한다.

빌더 패턴은 언제 써야 하는가?

1. 생성자 파라미터가 많을 때

생성자의 파라미터가 많아질 수록 각 파라미터가 의미하는 속성이 무엇인지 알기 어렵다.
IDE 내에서는 파라미터 이름과 타입을 표시해줘서 문제가 없다고 생각할 수 있다.

github, gitlab MR 코드 리뷰를 한다면? 또는 IDE 기능이 부실하다면?

const champion = new Champion('khazix', 450, 200, false) // 450, 200, false 는 뭐지?

덩그러니 위 와 같이 생성자만 표시된다.
450, 200, false 는 무엇을 의미하는지 파악하기 어렵다.
이럴 때 Champion "클래스 선언 부로 이동하여 확인해야한다." 는 불편함이 있다.

2. 생성 로직 또는 조건이 복잡해질 때

챔피언 이름은 최소 3글자 이상, hp, mp는 0 이상이어야한다는 조건이 있다고 가정해보자.

생성자 내에 각 property validation 코드가 들어간다.

class Champion {
  
  constructor(
    private readonly _name: string,
    private readonly _hp : number,
    private readonly _mp: number
  ) {
    if (_name.length < 3) throw Error('name must be at least 3 characters.')
    if (_hp < 0) throw Error('HP should be greater than or equal to 0')
    if (_mp < 0) throw Error('MP should be greater than or equal to 0')
    
    // .. 만약 조건식이 더 많아진다면?
  }
// ..
}

더 많은 프로퍼티 혹은 복잡한 검증 로직을 가질 수록 생성자가 거대해진다.
거대한 생성자는 클래스의 본래 역할 (행위)를 파악하는데 방해된다.

빌더 패턴 적용

ChampionBuilder.ts

// Champion class의 '생성'로직을 담당
class ChampionBuilder {
  private _name: string
  private _hp: number
  private _mp: number
  constructor() {
  }

  name(name: string): this {
    if (name.length < 3) throw Error('name must be at least 3 characters.')
    this._name = name
    return this
  }

  hp(hp: number): this {
    if (hp < 0) throw Error('HP should be greater than or equal to 0')
    this._hp = hp
    return this
  }

  mp(mp: number): this{
    if (mp < 0) throw Error('MP should be greater than or equal to 0')
    this._mp = mp
    return this
  }

  build(): Champion {
    return new Champion(
      this._name,
      this._hp,
    )
  }
}

Champion.ts

// 기존 도메인 클래스는 오로지 도메인 클래스의 '표현'과 '행위' 만 담당한다.
class Champion {
  constructor(
    private readonly _name: string,
    private readonly _hp : number,
    private readonly _mp: number = undefined
  ) {
  }

  static builder(): ChampionBuilder {
    return new ChampionBuilder()
  }

  // getter ..
}

client.ts

const champion = Champion.builder()
      .name('khazix')
      .hp(500)
      .mp(200)
      .build()

빌더 패턴으로 2가지 효과를 얻었다.

(1) 코드 가독성 증가

champion 생성 단계에서 각 property 가 무엇인지 한 눈에 파악된다.
거대한 생성자를 쓸 필요가 없다.

(2) 생성 - 표현 및 행위 분리

빌더는 '생성' 을 도메인 객체는 '표현'(property) 과 '행위'(method) 를 담당한다.
속성에 대한 검증 로직은 빌더가 담당하고 본 도메인 객체는 표현과 행위만을 갖고 있다.

한계

Typescript 는 inner class 를 지원하지 않기 때문에, 사용할 클래스 바깥에 빌더 클래스를 정의해한다.

class Champion {
  constructor(
    private readonly _name: string,
    private readonly _hp : number,
    private readonly _mp: number = undefined
  ) {
  }
  
  static builder(): ChampionBuilder {
    return new ChampionBuilder()
  }
  static class ChampionBuilder {  // ❌ 불가능.
  }
}

p.s. - builder-pattern 라이브러리 사용

class UserInfo {
  id: number;
  userName: string;
  email: string;
}

const userInfo = Builder(UserInfo)
                   .id(1)
                   .userName('foo')
                   .email('foo@bar.baz')
                   .build();

builder-pattern 같은 라이브러리에서 쉽게 클래스에 대한 빌더 패턴 구현을 지원하나, 도메인 객체의 setter 가 열려있어야 한다는 한계가 있다.
(java lombok 의 @builder 어노테이션이 해주는 것 처럼 불변성을 보장해주기 어렵다.)

본 예제에선 도메인 객체의 불변성을 보장하기 위해서 setter 사용 없이 직접 구현했다.

profile
I'm still hungry

0개의 댓글