디자인 패턴 - 생성 패턴 (1) (feat. 싱글톤, 빌더, 프로토타입)

생성 패턴의 싱글톤 패턴, 빌더 패턴, 프로토타입 패턴 학습하기


디자인 패턴 중 생성 패턴은 특정 상황에 맞는 객체 생성 매커니즘을 제공하며 이 매커니즘은 독립적인 시스템으로 작용된다.

여기서는 생성 패턴 중 싱글톤 패턴, 빌더 패턴, 프로토타입 패턴에 대해 설명한다.

싱글톤 패턴

싱글톤 패턴은 클래스의 인스턴스를 단 하나만 만들어 글로벌하게 제공한다. 생성자에 private 접근 제어자를 붙여 생성자 인스턴스를 만들지 못하도록 하고 대신에 static 접근 제어자를 통해 클래스에 전역적으로 접근할 수 있도록 한다.

장점과 단점

싱글톤 패턴의 장점은 단 하나의 객체만 생성되므로 메모리를 효율적으로 사용할 수 있다.

단점으로는 싱글톤 패턴을 구현하기 위한 코드를 많이 작성해야 하며 멀티 쓰레드 환경을 고려해야한다. 또한, 하나의 클래스에만 의존하기 때문에 개방 폐쇄 원칙을 위반할 수 있다.

예제 코드

class Settings {
  private static instance: Settings | null = null;
  private theme: string;
 
  private constructor() {
    this.theme = "default"; // 초기 설정
  }
 
  // 인스턴스를 가져오는 정적 메서드
  static getInstance(): Settings {
    if (!Settings.instance) {
      Settings.instance = new Settings();
    }
    return Settings.instance;
  }
 
  getTheme(): string {
    return this.theme;
  }
 
  setTheme(theme: string): void {
    this.theme = theme;
  }
}
 
const setting1 = Settings.getInstance();
const setting2 = Settings.getInstance();
console.log(setting1 === setting2);     // true

위 예제는 싱글톤 패턴이 맞지만 멀티 스레드 환경에서 안전하지 않다. if(!Settings.instance) 블록에 여러 스레드가 동시에 접근할 경우 인스턴스가 여러 개 생기기 때문이다. 자바스크립트는 싱글 스레드 언어지만 브라우저 환경에서는 멀티 스레드로 동작하기 때문에 이 부분을 주의해야한다. 자바에서는 syncronized 키워드를 사용하여 지연 초기화를 사용하면 되지만 자바스크립트는 지연 초기화를 따로 지원하지 않는다. 대신 자바스크립트의 모듈 시스템 (CommonJs, ESModule)은 단 한 번 로드되기 때문에 싱글톤 패턴을 구현하려는 클래스를 따로 모듈화하면 멀티 스레드 환경에서도 단 하나의 인스턴스만 생성할 수 있다.

빌더 패턴 (Builder Pattern)

빌더 패턴은 객체의 생성 과정이 복잡하고 다양할 때 다양한 구성의 인스턴스를 만들어서 객체 생성 과정을 유연하게 만들어준다.

객체의 프로퍼티가 점점 많아질수록 불완전한 객체가 생성될 수 있다. 인스턴스마다 갖고 있는 프로퍼티가 다를 수도 있는데 이 경우엔 생성자를 여러 개 만들어 처리해야한다. 이러한 번거로움을 해결하기 위해 나타난 것이 빌더 패턴이며, 빌더를 통해 생성자를 사용자 입맛에 맞게 조리할 수 있다. 필요한 프로퍼티를 설정하고 build()라는 메소드를 호출하면 최종적으로 하나의 인스턴스를 얻게 되며 더불어 이 메소드 내부에서 생성자를 검증하는 과정을 추가할 수 있다.

장점과 단점

빌더 패턴의 장점은 만들기 복잡한 객체를 순차적으로 만들 수 있다는 것이다. 클라이언트 코드는 단순하고 깔끔하며 복잡한 세부 과정은 숨길 수 있다. 또한, 같은 프로퍼티를 갖는 여러 빌더 객체가 있을 때 중복되는 빌더를 하나로 합쳐서 사용할 수 있으며 이 경우 이름에 Director가 포함된 새로운 클래스를 만들어 중복 코드를 관리할 수도 있다.

빌더 패턴의 단점은 여러 타입의 객체를 만들어야 하며 구조가 복잡해지는 것이다.

예제 코드

interface TourOptions {
  title: string;
  date: Date;
  nights: number;
  days: number;
  accommodation: string;
  plans: string[];
}
 
class Tour {
  private title: string;
  private date: Date;
  private nights: number;
  private days: number;
  private accommodation: string;
  private plans: string[];
 
  constructor(options: TourOptions) {
    this.title = this.options.title;
    this.startDate = this.options.date;
    this.nights = this.options.nights;
    this.days = this.options.days;
    this.accommodation = this.options.accommodation;
    this.plans = this.options.plans;
  }
 
  addPlan(plan: string) {
    this.plans.push(plan);
  }
}
 
class TourBuilder {
  // 기본값을 정의
  private tourOptions: TourOptions = {
    title: ""
    date: new Date(),
    nights: 0,
    days: 0,
    accommodation: "",
    plans: []
  }
 
  setTitle(title: string): TourBuilder {
    this.tourOptions.title = title;
    return this;
  }
 
  setStartDate(date: Date): TourBuilder {
    this.tourOptions.startDate = date;
    return this;
  }
 
  setNightsAndDays(nights: number, days: number): TourBuilder {
    this.tourOptions.nights = nights;
    this.tourOptions.days = days;
    return this;
  }
 
  setAccommodation(accommodation: string): TourBuilder {
    this.tourOptions.accommodation = accommodation;
    return this;
  }
 
  addPlan(plan: string): TourBuilder {
    this.tourOptions.plans.push(plan);
    return this;
  }
 
  build(): Tour {
    // 검증 과정 필요한 경우 추가
    return new Tour(this.tourOptions);
  }
}
 
const jejuTour = new TourBuilder()
                      .setTitle("제주 여행")
                      .setStartDate(new Date("2023-01-01"))
                      .setNightsAndDays(3, 4)
                      .addPlan("공항 도착해서...")
                      .build();
 
const oneNightTwoDays = new TourBuilder()
                              .setTitle("1박 2일 여행 (자세한 계획 없음)")
                              .setNightsAndDays(1, 2)
                              .build();

프로토타입 패턴

프로토타입 패턴은 기존 객체를 복제하여 새로운 객체를 만든다. 클래스나 생성자 함수를 사용하지 않고 기존 객체가 복제 메서드를 포함한다. 시간이 오래 걸리는 작업 (db에서 데이터 읽어오기) 또는 네트워크 요청 후 인스턴스를 생성할 경우에 유용하다. 프로토타입 패턴은 미리 만들어놓고 일부 값만 변경해서 사용한다.

프로토타입 객체와 복제된 객체는 동일한 프로퍼티 값을 갖지만 참조 값은 다르다. (깊은 복사를 한다.)

장점과 단점

프로토타입 패턴을 사용함으로써 복잡한 객체를 만드는 과정을 숨길 수 있다. 또한, 새 인스턴스를 만드는 것보다 기존 객체를 복제하는 것이 비용(시간 또는 메모리)적인 면에서 효율적일 수 있다.

단점으로는 객체가 계층(hierarchy)구조를 가지고 프로퍼티가 참조값을 갖는 경우 깊은 복사를 위한 로직을 추가로 구현해야 한다.

예제 코드

// 프로토타입 객체
const productPrototype = {
  name: "Prototype Product",
  price: 0,
  description: "This is a prototype product.",
  clone() {
    return Object.assign({}, this);
  },
};
 
// 새로운 객체 생성
const originalProduct = Object.create(productPrototype);
 
// 객체 복제
const clonedProduct1 = originalProduct.clone();
const clonedProduct2 = originalProduct.clone();

자바스크립트에는 객체를 복사하는 여러 문법이 있는데 여기선 Object.create()로 객체를 복사했다.

참고

코딩으로 학습하는 GoF의 디자인 패턴 - 백기선