생성자에 매개변수가 많다면?
생성자(또는 정적 팩토리 메서드)는 선택적 매개변수가 많을 때 문제가 있습니다.
public class User {
private final String name; //필수
private final String birth; //필수
private final Integer height; //선택
private final Integer weight; //선택
public User(String name, String birth) {
this(name, birth, null, null);
}
public User(String name, String birth, Integer height) {
this(name, birth, height, null);
}
/* 컴파일에러 발생 (User(String, String, Integer) is already defined)
public User(String name, String birth, Integer weight) {
this(name, birth, null, weight);
}
*/
public User(String name, String birth, Integer height, Integer weight) {
this.name = name;
this.birth = birth;
this.height = height;
this.weight = weight;
}
}
위와 같은 형식을 점층적 생성자 패턴
이라고 하는데 매개변수가 많아질 수록 코드를 작성하기 어렵습니다.
또한 코드를 읽을 떄 각 값의 의미가 무엇인지, 매개변수가 몇개짜리 생성자인지도 주의해서 읽어야 합니다.
이에 대한 대안으로 자바빈즈 패턴
을 활용할 수 있습니다. 흔히 말하는 setter를 통해서 값을 넣는 방식입니다.
User user1 = new User();
user1.setName("홍길동");
user1.setBIrth("19920817");
User user2 = new User();
user2.setName("홍길동")
user2.setHeight(182);
이 방법도 단점이 있습니다. 자바빈즈 패턴의 가장 큰 단점은 불변 객체를 만들 수 없다는 것 입니다.
빌더 패턴
앞서 알아본 점층적 생성자 패턴
과 자바빈즈 패턴
의 대안으로 빌더 패턴
이 있습니다.
필수 매개변수만으로 생성자(또는 정적 팩토리 메서드)를 통해 빌더 객체를 얻습니다. 이 빌더 객체가 제공하는 메서드를 통해 원하는 매개변수를 설정, build
메서드를 통해 원하는 객체를 얻는 방법입니다.
public class User {
private final String name; //필수
private final String birth; //필수
private final Integer height; //선택
private final Integer weight; //선택
private User(Builder builder) {
name = builder.name;
birth = builder.birth;
height = builder.height;
weight = builder.weight;
}
public static class Builder {
private final String name; //필수
private final String birth; //필수
private Integer height; //선택
private Integer weight; //선택
public Builder(String name, String birth) {
this.name = name;
this.birth = birth;
}
public Builder height(Integer height) {
this.height = height;
return this;
}
public Builder weight(Integer weight) {
this.weight = weight;
return this;
}
public User build() {
return new User(this);
}
}
}
User user = new User.Builder("홍길동", "19920817")
.height(160)
.build();
//빌더의 setter 메서드는 자신을 반환하기 때문에 연쇄적으로 호출할 수 있습니다.(플루언트 API 또는 메서드 연쇄라고 부릅니다.)
빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것입니다.
자바빈즈 패턴과 빌더 패턴
//자바빈즈 패턴
User userByBeans = new User();
user.setName("홍길동");
user.setBirth("19920817");
//빌더 패턴
User userByBuilder = new User.Builder("홍길동", "19920817")
.build();
userByBeans 객체는 이 후에 어느 곳에서나 setter메서드를 호출하여 키, 몸무게를 넣을 수 있습니다.
하지만 userByBuilder 객체는 한번 생성하면 땡입니다. 더이상 변할수 없습니다.
계층적으로 설계된 클래스와 빌더 패턴
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, OLIVE}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
public abstract Pizza build();
protected abstract T self();
}
Pizza(Builder<?> bUilder) {
this.toppings = bUilder.toppings.clone();
}
}
public class BulgogiPizza extends Pizza{
public enum Size {MEDIUM, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public BulgogiPizza build() { //Pizza가 아닌 BulgogiPizza를 반환
return new BulgogiPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private BulgogiPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
BulgogiPizza pizza = new BulgogiPizza.Builder(Size.LARGE)
.addTopping(Topping.OLIVE)
.addTopping(Topping.HAM)
.build();
하위 클래스의 빌더(BulgogiPizza.Builder)의 build 메서드는 해당하는 구체 하위 클래스를 반환합니다.(BulgogiPizza)
하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환타입(Pizza)이 아닌, 그 하위 타입(BulgogiPizza)를 반환하는 기능을 공변 반환 타이핑(convariant return typing)이라고 합니다. 이 기능을 이용하면 형변환에 신경쓰지 않고 빌더를 다룰 수 있습니다.
위의 예제에서 2가지의 토핑을 추가했는데(.addTopping(Topping.OLIVE).addTopping(Topping.HAM)
) 이는 생성자에서는 불가능한 빌더패턴의 이점입니다.
빌더패턴의 단점
객체를 만들기 전 빌더를 생성해야 해서 빌더 생성비용이 듭니다. 따라서 성능이 민감한 상황에서는 문제가 될 수 있습니다.
이 책에서는 매개 변수가 4개 이상은 되어야 값어치를 한다고 합니다.
번외) lombok의 @Builder 사용하면 어떻게 구현될까?
public class User {
private final String name;
private final String birth;
private Integer height;
private Integer weight;
User(final String name, final String birth, final Integer height, final Integer weight) {
this.name = name;
this.birth = birth;
this.height = height;
this.weight = weight;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
private String name;
private String birth;
private Integer height;
private Integer weight;
UserBuilder() {
}
public UserBuilder name(final String name) {
this.name = name;
return this;
}
public UserBuilder birth(final String birth) {
this.birth = birth;
return this;
}
public UserBuilder height(final Integer height) {
this.height = height;
return this;
}
public UserBuilder weight(final Integer weight) {
this.weight = weight;
return this;
}
public User build() {
return new User(this.name, this.birth, this.height, this.weight);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", birth=" + this.birth + ", height=" + this.height + ", weight=" + this.weight + ")";
}
}
}
'스터디 > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] Item 6. 불필요한 객체 생성을 피하라 (0) | 2022.05.15 |
---|---|
[Effective Java] Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용 (0) | 2022.05.14 |
[Effective Java] Item 4. 인스턴스화를 막으려면 private 생성자를 사용 (0) | 2022.05.14 |
[Effective Java] Item 3. private 생성자나 Enum타입으로 싱글턴임을 보증하라 (3) | 2022.05.13 |
[Effective Java] Item 1. 생성자 대신 정적 팩토리 메서드를 고려하라 (0) | 2022.05.11 |