생성자와 Builder패턴
06 Nov 2020 | Spring 스터디 Builder패턴 생성자 디자인패턴Programmers 웹백엔드 스터디 7기 진행시 했던 고민과 공부한 내용들을 정리한 글입니다.
첫번째 미션으로 유저 객체를 생성할 때 Builder 패턴을 이용해야 했다.
프로그래머스 스터디 뿐만 아니라 이전에 참고했던 여러 서적들(책 ‘처음 배우는 스프링 부트2’와 ‘스프링 부트와 AWS로 혼자 구현하는 웹서비스’)에서도 빌더 패턴을 사용해서 구현한다. 무엇보다도 찾아봤던 블로그, 책마다 빌더 패턴을 구현하는 모습이 서로 조금씩 달라서 빌더패턴을 코드로 이렇게 작성하는게 맞는건지 너무 헷갈렸다.
왜 빌더 패턴을 사용해야 하는 걸까? 빌더 패턴과 생성자의 차이는 무엇이고 어떤 상황에서 사용해야 하는 걸까? 코드로는 어떻게 구현할까? 이에 대해 한번 정리해 봐야겠다.
먼저 생성자란,
인스턴스가 생성될 때 호출되는 인스턴스 초기화 메소드이다. 연산자 new가 인스턴스를 생성하는 것이지 생성자가 인스턴스를 생성하는 것은 아니다.
User Sara = new User();
빌더패턴이란
생성자처럼 인스턴스를 초기화하고 new 대신 build() 메소드를 이용해 객체를 생성하는 방법이다. 기존 생성자의 문제점을 보완해서 객체 생성을 더 깔끔하고 유연하게 할 수 있다.
빌더 패턴은 왜 / 언제 사용해야할까?
일반 생성자 이용시, 인자 수가 늘어나면 코드 작성이 어려워지고 읽기 어려운 코드가 되고 만다
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 3, 35, 27);
호출 코드만 보고서는 저 많은 인자가 도대체 무슨 값인지 알 수가 없고 특히 자료형이 같은 인자들이 많을 경우 컴파일 단계에서 걸러지지 않기 때문에 런타임시 문제 발생하기가 딱 좋다 - 실수하기 좋은 구조.
대안책으로 setter메소드를 이용한 자바빈 패턴이 있다.
// cocaCola을 영양 성분 정보를 setter메소드를 통해 초기화하는 자바빈 패턴
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohdydrate(27);
가독성은 좋아졌지만, 1회의 함수 호출로 객체 생성을 끝낼 수 없고 setter메서드로 인해 Immutable한 클래스를 만들 수가 없다. Setter 메소드 사용은 지양해야하며 변경 가능성은 최소화해야한다.
따라서 가독성도 좋고 immutable 객체로도 만들 수 있는 Builder 패턴을 사용하게 된다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
각 인자가 어떤 값을 의미하는지 알기가 쉽고 setter메소드가 없으므로 변경 불가능한 객체도 만들 수 있다. 한번에 객체를 생성하므로 객체 일관성이 깨지지 않으며, build() 함수로 잘못된 값이 입력되었는지 검증하게 할 수도 있다.
Builder패턴은
- 생성자의 인자수가 많을 때 사용하며
- 불변성과 네임드 파라미터(Named Parameter, 매개변수 이름을 통해서 인자 값을 전달)를 가져갈 수 있어
- 안전하고 가독성이 좋다.
빌더패턴을 코드로 작성해보자
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters(필수 인자)
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values(선택적 인자는 기본값으로 초기화)
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder() {
}
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this; // 이렇게 하면 . 으로 체인을 이어갈 수 있다.
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
위와 같이 하면 다음처럼 객체를 생성해 사용하면 된다.
NutritionFacts cocaCola = new NutritionFacts
.Builder(240, 8) // 필수값 입력
.calories(100)
.sodium(35)
.carbohydrate(27)
.build(); // build() 가 객체를 생성해 돌려준다.
이렇게 정리해보니 궁금함이 생겼다. 생성자의 단점을 보완해서 나온게 빌더패턴이라면 생성자말고 빌더패턴만 사용하는게 좋지 않나? 그런데 왜 생성자와 빌더패턴을 섞어서 사용할까? => 스터디 리더분께 물어보니 명쾌하게 의견을 주셨다. 정답은 없다고. 생성자의 매개변수가 많아지면 네임드 파라미터의 빌더 패턴 사용을 고려해보는 것처럼 그 상황과 용도에 맞게 더 fit한 생성자를 이용하는게 더 좋다고. 결국 프로그래밍에 있어서 각 방법의 장단점을 확실하게 인지하고 상황에 맞는 방법을 유연하게 적용할 수 있는 능력이 정말 중요한 것 같다.
references
Effective Java 2/E
자바의 정석
https://johngrib.github.io/wiki/builder-pattern/
https://softwareengineering.stackexchange.com/questions/380397/why-do-we-need-a-builder-class-when-implementing-a-builder-pattern