문제 상황
오 딱 요즘 고민인 상황에 걸맞은 지식인듯하다.
정적 팩토리를 쓰던, 생성자를 사용하던 매개변수가 달라지면 메서드가 너무 많이 추가되는 어려움이 있었다.
예를 들면
public static Question newQuestion(long id, String title, String contents) {
return new Question(id, title, contents,null);
}
public static Question newQuestionWithDeleted(Long id, String title, String contents, boolean status) {
return new Question(id, title, contents, status);
}
private Question(Long id, String title, String contents, boolean status) {
super(id);
this.title = title;
this.contents = contents;
}
이렇게 status를 가지는 거, 안 가지는 거 만드는 것만 해도 매겨변수가 달라서 , 함수가 늘어났다.
그리고 선택적으로 매개변수를 넣기 때문에 nul이나 0과 같은 값을 넣어줘야 한다.
책에서는 , 점층적 생성자 패턴이라고 한다. 점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.(개수 맞게 썼나 읽어보는 등 귀찮아진다.)
빌더 패턴
그렇게 나온 게 빌더 패턴이다.
1. 필수 매개 변수만으로 생성자 혹은 정적 팩토리를 호출해 빌더 객체를 얻는다.
2. 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
3. 매개변수가 없는 build메서드를 호출해 필요한 객체를 얻는다.
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두는 게 보통이다
public class Person {
private final String name;
private final Integer age;
private final Integer birth;
private final boolean isStudent = true;
public static class Builder {
// 필수 매개변수
private final String name;
private final Integer age;
// 선택 매개변수
private Integer birth = 000000;
private boolean isStudent = true;
// 필수 매개변수만 받는 생성자 호출
public Builder(String name, Integer age) {
this.name = name;
this.age = age;
}
// setter
public Builder birth(int val){
birth = val;
return this;
}
public Builder isStudent(boolean val){
isStudent = val;
return this;
}
public Person build(){
return new Person(this);
}
}
private Person(Builder builder) {
name = builder.name;
age = builder.age;
birth = builder.birth;
isStudent = builder.isStudent;
}
}
빌더의 setter는 빌더 자기 자신을 호출하기 때문에 메서드를연속적으로 호출 가능하다
-> 메서드 연쇄, 플루언트 API
사용할 때에는 아래와 같이 사용한다.
Person me = new Person.Builder("name", 10) // 필수 인자
.isStudent(false).build(); // 선택 인자
유효성 검사 코드도 build메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하면 된다.
빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
- 단점
그렇지만 빌더 생성 비용이 든다.(코드의 양) 단점을 해결하기 위하여 스프링에서는 롬복 기능을 제공한다.
롬복 Lombok
다음과 같이 생성자 위에 Builder를 달아준다.
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;
@Builder
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
// ...
}
}
위의 빌더와 같이 사용하면 된다.
- 장점
- 인자가 많을 경우 쉽고 안전하게 객체를 생성할 수 있습니다.
- 인자의 순서와 상관없이 객체를 생성할 수 있습니다.
- 적절한 책임을 이름에 부여하여 가독성을 높일 수 있습니다.
생각해볼 사항들
- 클래스 선언부에 @Builder을 달아줘도 된다.
하지만 @Builder를 클래스에 달아주면 @AllArgsConstructor도 같이 달아주는 것과 같기 때문에 바람직하지 않다.
-
- 가급적 직접 만든 생성자에 달아주는 것이 낫다.
- AllArgsconstructor는 field의 선언 순서가 뒤 바뀌어도 객체가 생성되기 때문에 , 최대한 지양하는 롬복이다
- 빌더 패턴에서 필수 생성자를 검증 한느 유효성 로직의 필요성
Entity에서 nullable = false로 설정된 경우가 있다.(데이터베이스가 칼럼이 null= false인 경우)
이 변수가 만일 builder의 필수 생성자가 된다면, 이 변수의 유효성을 검사해야 한다.
다음과 같이 address, products의 유효성을 체크한다.
+ Test코드도 만들면 좋다.
@Builder
public Order(Address address, List<Product> products) {
Assert.notNull(address, "address must not be null");
Assert.notNull(products, "products must not be null");
Assert.notEmpty(products, "products must not be empty");
this.address = address;
this.products = products;
}
@Test(expected = IllegalArgumentException.class)
public void Account_bankName_비어있으면_exception() {
Account.builder()
.accountHolder("홍길동")
.accountNumber("110-22345-22345")
.bankName("")
.build();
}
- 빌더의 이름으로 역할과 책임을 명시하는 것도 좋은 방안이다.
- 빌더의 이름으로 책임을 명확하게 부여하고, 받아야 하는 인자도 명확해진다
@Builder(builderClassName = "ByAccountBuilder", builderMethodName = "ByAccountBuilder") // 계좌 번호 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확
public Refund(Account account, Order order) {
Assert.notNull(account, "account must not be null");
Assert.notNull(order, "order must not be null");
this.order = order;
this.account = account;
}
@Builder(builderClassName = "ByCreditBuilder", builderMethodName = "ByCreditBuilder") // 신용 카드 기반 환불, Builder 이름을 부여해서 그에 따른 책임 부여, 그에 따른 필수 인자값 명확
public Refund(CreditCard creditCard, Order order) {
Assert.notNull(creditCard, "creditCard must not be null");
Assert.notNull(order, "order must not be null");
this.order = order;
this.creditCard = creditCard;
}
}
자세한 내용은 출처 블로그를 참조해보자.
Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그
Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그
cheese10yun.github.io
빌더 패턴(Builder Pattern)
객체의 생성 방법과 표현 방법을 분리한다
johngrib.github.io
'도서' 카테고리의 다른 글
[객체 지향의 사실과 오해] 6. 객체지도 (0) | 2022.11.30 |
---|---|
[객체지향의 사실과 오해] 5장 책임과 메세지 (0) | 2022.11.17 |
[Effective Java] 1. 생성자 대신 팩터리 메서드를 고려하라 (0) | 2022.11.12 |
[객체 지향의 사실과 오해] 4. 역할 ,책임,협력 (0) | 2022.11.03 |
[객체 지향의 사실과 오해] 3. 타입과 추상화 (1) | 2022.11.02 |