Builder (빌더)

JS (TIL & Remind)·2022년 2월 15일
1

Builder 패턴 이란?

복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴.

Builder 패턴을 사용하는 이유

// Basic
class Student(id: number, name: string, major: string, age: number, address: string) {
		id     : number;
		name   : string;
		major  : string;
		age    : number;
		address: string;
		
		constructor() {
				this.id = id;
				this.name = name;
				this.major = major;
				this.age = age;
				this.address = address;
		}
}

const student = new Student(20220103, '조준석', '정보통신', null, '서울시 노원구...'); 
  • 인스턴스를 생성할 때, 생성자(Constructor)만을 이용해서 생성할 때의 어려움을 해결하기 위해 사용한다.
    • 예를 들어, 생성자로 너무 많은 인자를 넘기게 되면, 각 인자가 무엇을 의미하는지 알기가 힘들고, 불필요한 인자를 넘겨야 할 경우가 있다.
  • 점층적 생성자 패턴(telescoping constructor pattern)의 장점과 자바 빈 패턴(Java Bean pattern)의 장점을 결합하여 객체를 생성할 수 있다.

점층적 생성자 패턴(telescoping constructor pattern)

클래스 내에 오버로딩을 통해서 생성자를 여러 개 작성하는 것을 말한다.

  • 사용하지 않는 코드를 작성하는 경우가 발생하여 코드가 지저분해질수 있다.
public class Member {
	
    private String name;				// 필수
    private int age;				   	// 필수
    private String address;			// 선택
    private String phone;				// 선택
    private String email;  			// 선택
    
    // 필수 매개변수를 가지는 생성자
    public Member(String name, int age) {
    	this(name, age, null, null, null);
    }
    
    // 선택 매개변수 address가 추가된 생성자
    public Member(String name, int age, String address) {
    	this(name, age, address, null, null);
    }
    
    // 선택 매개변수 phone이 추가된 생성자
   	public Member(String name, int age, String address, String phone) {
    	this(name, age, address, phone, null);
    }
    
    // 모든 매개변수를 가지는 생성자
    public Member(String name, int age, String address, String phone, String email) {
    	this.name = name;
        this.age = age;
        this.address = address;
        this.phone = phone;
        this.email = email;
    }
}

Member member1 = new Member("홍길동", 56);
// { name: "홍길동", age: 56, address: null, phone: null, email: null }

Member member2 = new Member("조준석", 29, "서울시 노원구", "01012345678");
// { name: "조준석", age: 29, address: "서울시 노원구", phone: "01012345678", email: null }

자바 빈 패턴(Java Bean pattern)

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

  • 함수 호출 1회로 객체 생성을 끝낼 수 없으므로 객체 일관성이 일시적으로 깨질 수 있고, immutable(불변성) 객체를 생성할 수 없다.
public class Member {
		private final String name;				
    private final int age;				   	
    private final String address;			
    private final String phone;				
    private final String email;
		
		public Member() {}

		// Setter 메서드
		public void setName(String name) {
				this.name = name;
		}
		public void setAge(int age) {
				this.age = age;
		}
		public void setAddress (String address) {
				this.address = address;
		}
		public void setPhone(String phone) {
				this.phone = phone;
		}
		public void setEmail(String email) {
				this.email = email;
		}
}

Member member = new Member();
member.setName('조준석');
member.setAge(29);
// { name: "조준석", age: 29 }
// 객체 일관성이 깨질 수 있으며, 불변성 객체를 만들 수 없다.

Builder 패턴의 장점과 단점

장점

// 단순 생성자로 만들 때 예시
const student = new Student(null, '조준석', null, 29, null);

// Builder Pattern 호출 예시
const student = Student.builder().name('조준석').age(29);
  • 객체를 생성할 때 필요한 데이터만 설정할 수 있다.
    • 불필요한 인자를 넘길 필요가 없고, 동적으로 처리할 수 있다.
  • 가독성을 높일 수 있다.
    • 매개변수로 넘기는 값이 무엇을 의미하는지 좀 더 명확하고 직관적이다.
  • 불변성을 확보할 수 있다.
    • Setter(수정자)를 사용하면, 불필요한 확장에 대한 가능성을 열어두어 불변성을 위배할 수 있지만, builder는 메서드 체이닝 방식의 호출로 객체의 불변성을 유지할 수 있다.
  • 유연성을 확보할 수 있다.

단점

  • 추가적인 빌더 클래스를 구현해야 한다.
    • 객체를 생성하려면 빌더 객체를 생성해야 하는데, 극단적으로 본다면 오버헤드로 인해 성능 이슈가 있을 수 있다. ( 크게 문제될 것 없는 단점이다. )
  • 매개변수가 적으면 다른 패턴에 비해 코드가 길어질 수 있다.
    • 따라서 매개변수가 4개 이상인 클래스일 때 사용하는 것을 권장.

예제 코드

구현 예제는 커피를 구성하는 객체를 생성자만 이용해 만드는 방법(AS-IS)과, Builder 패턴(TO-BE)을 이용해 만드는 방법을 나타낸 예제 입니다. 예제를 보면 Builder 패턴의 장점을 별다른 설명 없이 한눈에 파악할 수 있습니다.

// 생성자를 이용해 만드는 방법 (AS-IS)
class Coffee {
  ice: boolean;
  whippingCream: boolean;
  milk: boolean;
  numOfShots: number;
  addRequest: string;

  constructor(
    ice: boolean,
    whippingCream: boolean,
    milk: boolean,
    numOfShots: number,
    addRequest: string
  ) {
    this.ice = ice;
    this.whippingCream = whippingCream;
    this.milk = milk;
    this.numOfShots = numOfShots;
    this.addRequest = addRequest;
  }
}

// 각 인자로 넘긴 값이 무엇을 의미하는지 알기 힘들다.
// 사용하지 않는 값도 인자로 무조건 넘겨줘야 한다.
const iceAmericano = new Coffee(true, false, false, 2, "얼음 적게 주세요");
const hotCafeLatte = new Coffee(false, true, true, 4, "");

// Builder 패턴을 이용해 만드는 방법 (TO-BE)
class Coffee {
  ice: boolean;
  whippingCream: boolean;
  milk: boolean;
  numOfShots: number;
  addRequest: string;

  constructor() {
    this.ice = false;
    this.whippingCream = false;
    this.milk = false;
    this.numOfShots = 0;
    this.addRequest = "";
  }
}

class CoffeeBuilder {
  coffee: Coffee;

  constructor() {
    this.coffee = new Coffee(); // 빌더 객체를 생성한다.
  }

  addIce(addIce: boolean) {
    this.coffee.ice = addIce;
    return this;
  }

  addWhippingCream(addWhippingCream: boolean) {
    this.coffee.whippingCream = addWhippingCream;
    return this;
  }

  addMilk(addMilk: boolean) {
    this.coffee.milk = addMilk;
    return this;
  }

  howManyShots(numOfShots: number) {
    this.coffee.numOfShots = numOfShots;
    return this;
  }

  addRequest(message: string) {
    this.coffee.addRequest = message;
    return this;
  }

  // builder 객체를 리턴한다.
  build() {
    return this.coffee;
  }
}

// 어떤 객체를 만들어낼지 보다 직관적으로 알 수 있고,
// 필요한 인자만 넘겨 객체를 만들어 낼 수 있다.
// (메서드 체이닝 방식으로 프로퍼티들을 set 해주고 build()를 호출하여 객체를 리턴 받는다.)
const iceAmericano = new CoffeeBuilder().addIce(true).howManyShots(2).build();
// 결과
// { ice: true, whippingCream: false, milk: false, numOfShots: 2, addRequest: '' }

const hotMilk = new CoffeeBuilder().addMilk(true).addRequest("머그컵에 주세요");
// 결과
// { ice: false, whippingCream: false, milk: true, numOfShots: 0, addRequest: '머그컵에 주세요' }
profile
노션에 더욱 깔끔하게 정리되어있습니다. (하단 좌측의 홈 모양 아이콘)

0개의 댓글