[JS] '빌더 패턴' 대신 '옵션 객체 패턴'으로

이번 글에서는 빌더 패턴의 의의를 고찰하면서, 이의 대안인 옵션 객체 패턴 이용을 고려해볼 것이다.

우선 빌더 패턴이란 무엇일까?

 

빌더 패턴 (리팩토링 구루, TTUMZZI님)

복잡한 객체를 생성할 때, 생성자 함수에 여러 인자를 넣을 시의 난해함, 유지보수의 어려움을 보완한 생성 패턴.

객체의 속성(property)을 선택적으로 설정하는 Builder 클래스가 필요하게 된다.

 

빌더 패턴의 특징을 코드 예시와 함께 확인해보자.

const house = new House("My House", 4, 3, false, null, null);

 

위의 코드를 확인해보면, 정확히 무엇을 의미하는지도 불확실한 여러 인자들이 생성자에 있음을 확인할 수 있다.

또 인자들 중에 선택적인 인자는 null로 넣고 있는 모습이다.

위와 같은 코드는 한눈에 보기에도 가독성이 나쁘고, 심지어 인자 순서에 따라 실수를 할 여지도 매우 크다.

 

따라서 이를 빌더 패턴으로 표현해본다면 아래와 같다.

const house = new House.Builder()
  .name("My House")
  .floors(4)
  .windows(3)
  .hasGarage(false)
  .build()

 

House 객체 생성자의 각 인자들이 명확하게 표시되어서 각각의 의미가 확실하게 나타나는 모습이다.

또 선택적으로 넣는 인자들의 경우에는 굳이 표현하거나 순서를 지키지 않고, 그냥 기입하지 않으면 된다.

 

장점1. 긴 클래스 생성자를 피할 수 있다

(1, 2, 3, 4, ...)

따위로 불명확하게 인자를 넣는 것을 피하고, 각 속성에 명시적으로 값을 넣을 수 있다.

 

장점2. 속성이 바뀔 경우의 유지보수가 쉬워진다

위에서 설명한 House 클래스에 새로운 선택적인 인자인 'address' 가 추가된다고 해보자.

각각의 상황에서의 코드 수정의 범위가 확연히 차이나게 된다.

// Builder 패턴은 이전에 작성했던 코드에 변경사항이 없다
const house1 = new House.Builder()
  // ...
  .build()

const house1 = new House.Builder()
  // ...
  .address("aaa")
  .build()


// 생성자는 모든 인자의 끝에 해당 속성에 알맞는 값을 *모두* 추가해줘야 한다
const house1 = new House(... , null)

const house2 = new House(... , "aaa")

 

단점1. 코드량이 증가한다

class House {
  // ...
  
  static Builder = class {
    #name
    #windows
    // ...
    
    name(name) {
      this.#name = name;
      return this;
    }
    
    windows(windows) {
      this.#windows = windows;
      return this;
    }
    
    // 각 속성을 설정하는 많은 매소드들...
  }
}

const house = new House.Builder()
  .name()
  .windows()
  // ...

 

빌더를 구현하고자 하는 클래스의 모든 속성에 대해서 설정 매소드를 마련해둬야 한다.

또한 인스턴스를 생성할 때면 모든 속성을 매소드로 설정해야 하기 때문에 이 또한 복잡하다 할 수 있다.

 

옵션 객체 패턴 (Benjamin Chadwick님, Travis Horn님)

위의 빌더 패턴의 기능은 옵션 객체 패턴으로 아래와 같이 표현할 수 있다.

// builder
const house = new House.Builder()
  .name("My House")
  .floors(4)
  .windows(3)
  .hasGarage(false)
  .build()
  
// option object
const house = new House({
  name: "My House",
  floors: 4,
  windows: 3,
  hasGarage: false
})

 

장점1. 빌더 패턴의 장점을 대부분 흡수한다

옵션 객체 패턴은 JS의 유연한 '객체 리터럴(기록일기님)' 표기를 활용했다고 볼 수가 있다.

 

Java와 같은 강타입 언어에서는 정해진 키-값 형태를 표현하기 위해서 '미리 정의된 클래스'를 이용하는 것이 유력했다.

그러나 굳이 클래스를 정의하지 않고 리터럴 표기를 통해 명확하게 인자를 전달할 수 있는 것이다.

따라서 옵션 객체 패턴만으로도 빌더 패턴의 장점을 대부분 흡수하는 것이 가능하다.

 

"옵션 객체의 타입이 명확하지 않아서 속성을 확인하는 것이 어렵지 않을까?"

'JSDoc'으로 옵션 객체의 타입을 정의해둘 수 있다.

/**
 * @typedef HouseOptions
 * @prop {number} floors
 * @prop {number} windows
*/

class House {
  /**
   * @param {HouseOptions} opts
  */
  constructor(opts) {
    this.#floors = opts.floors;
    this.#windows = opts.windows;
  }
}

 

IDE 상에서도 속성을 명확하게 표시할 수 있는 모습이다

 

장점2. 옵션 사전 정의가 용이해진다

이에 더해서 JS 객체의 '스프레드 연산자(MDN)'를 응용한다면, 옵션에 사전에 정의된 설정을 적용하는 기능을 간단하게 구현할 수 있다.

class House {
  static #hotelOptions = {
    floors: 40,
    windows: 80
  }
  
  constructor(opts) {
    // ...
  }
  
  static newHotel(opts) {
    const finalOpts = { ...House.#hotelOptions, ...opts };
    
    return new House(finalOpts);
  }
}

 

빌더 패턴에서 사전 정의를 구현하려면, 매니저 클래스를 추가로 마련하는 등으로 빌더의 일부분을 정의해야 한다.

 

요약

하자면 이렇게 된다.

  생성자 빌더 패턴 옵션 객체 패턴
속성 명시적임? X O O
옵션 사전 정의 용이성? X O
유지보수 용이? X △, 속성이 늘어날때 마다 변경됨
O
코드량 감소? X X O