정적 팩터리 메소드와 생성자는 선택적 매개변수가 많을 떄 적절하게 대응하기 어렵다는 단점이 있다.
만약 있을 수도 없을 수도 있는 필드가 많은 객체가 있다고 가정하자.
1. 옛날에는 이러한 클래스에서 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다.
이 패턴은 필수 매개변수를 받는 생성자와, 선택 매개변수를 하나씩 늘여가며 생성자를 만드는 패턴이다.
=> 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다
class Person {
private String name; // 필수
private int age; // 필수
private String job; // 선택
private String hobby; // 선택
public Person(String name, int age, String job, String hobby) {
this.name = name;
this.age = age;
this.job = job;
this.hobby = hobby;
}
// 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데,
// 어쩔 수 없이 hobby 매개변수에도 ""라는 값을 지정해줘야 한다.
public Person(String name, int age, String job) {
this(name, age, job, "");
}
public Person(String name, int age) {
this(name, age, "", "");
}
}
코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 세어 보아야 한다.
또한 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어 질 수 있다.
클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러 는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다(아이템 51).
2. 위 문제점에 대한 대안으로는 자바빈즈 패턴(JavaBeans pattern)이 있다.
매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
@Setter
class Person {
private String name = ""; // 필수
private int age = 0; // 필수
private String job = ""; // 선택
private String hobby = ""; // 선택
public Person() {
}
}
public class DemoApplication {
public static void main(String[] args) {
Person person=new Person();
person.setName("soyeon");
person.setAge(25);
person.setJob("student");
person.setHobby("exercise");
}
}
자바빈즈 패턴의 경우 장점은 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 되었다는 점이다.
하지만 단점은 객체 하나를 만들기 위해 많은 메서드를 호출해야 하며, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다. => 버그가 존재하는 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅이 어려워진다.
점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변(Item17)으로 만들 수 없으며 스레드 안전성이 없다는 문제도 있다.
3. 마지막 대안은 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattem)[Gamma95]이다.
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 또는 정적 팩터리 메서드를 호출해 빌더 객체를 얻는다.
클라이언트는 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
마지막으로 매개변수가 없는 build 메서드를 호출해 드디어 우리에게 필요한 객체를 얻는다.(이 객체는 일반적으로 불변객체이다.)
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두도록 한다.
package com.yapp.crew.domain.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookMark extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
@Setter(value = AccessLevel.PRIVATE)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
@Setter(value = AccessLevel.PRIVATE)
private Board board;
public static BookMarkBuilder getBuilder() {
return new BookMarkBuilder();
}
public static class BookMarkBuilder { // 정적 멤버 클래스
private User user;
private Board board;
public BookMarkBuilder withUser(User user) {
this.user = user;
return this;
}
public BookMarkBuilder withBoard(Board board) {
this.board = board;
return this;
}
public BookMark build() {
BookMark bookMark = new BookMark();
bookMark.setUser(user);
bookMark.setBoard(board);
return bookMark;
}
}
}
private void saveBookMark(Board board, User user) {
// 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다.(method chaining)
BookMarkBuilder bookMarkBuilder = BookMark.getBuilder();
BookMark bookMark = bookMarkBuilder
.withUser(user)
.withBoard(board)
.build();
user.addBookMark(bookMark);
board.addBookMark(bookMark);
bookMarkRepository.save(bookMark);
}
이때, 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식 (invariant)을 검사하도록 하면 더 좋다. 공격에 대비해 이런 *불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다(Item50).
검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던지면 된다(Item75).
빌더 패턴은 계충적= 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하자.
추상 클래스는 추상 빌더를, 구체 클래스(concrete class)는 구체 빌더를 갖게 한다.
다음은 피자의 다양한 종류를 표현 하는 계층구조의 루트에 놓인 추상 클래스다.
용어
- 불변 vs 불변식
- 불변(immutable, immutability): 어떠한 변경도 허용하지 않는다는 뜻으로, 주로 변경을 허용하는 가변(mutable) 객체와 구분하는 용도로 쓰인다. ex) String 객체
- 불변식(invariant): 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 뜻한다. 즉, 변경을 허용할 수는 있으나 주어진 조건 내에서 만 허용한다는 뜻이다.(아이템 50)
- ex) 리스트의 크기는 반드시 0 이상 = 만약 한순간 이라도 음수 값이 된다면 불변식이 깨진 것이다.
- ex) 기간을 표현하는 Period 클래스에서 start 필드의 값은 반드시 end 필드의 값보다 앞서야 = 두 값이 역전되면 불변식이 깨진 것이다
따라서 가변 객체에도 불변식은 존재할 수 있으며, 넓게 보면 불변은 불변식의 극단적 인 예라 할 수 있다.
'책을 읽자 > Effective Java' 카테고리의 다른 글
Item6: 불필요한 객체 생성을 피하라 (0) | 2021.01.30 |
---|---|
Item5: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.01.26 |
Item4: 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2021.01.25 |
Item3: 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2021.01.23 |
Item1: 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.01.17 |