[이펙티브 자바] 아이템2

hyng·2022년 11월 22일
0

이펙티브 자바

목록 보기
2/13

생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩터리와 생성자가 가지는 제약

  1. 선택적 매개변수가 많을 때 대응하기 어렵다.
    만약 테스트 코드에서 특정 필드 값은 필요 없다면 아래와 같이 새로운 매개변수 조합을 가지는 생성자(점층적 생성자 패턴)를 생성하거나 임의의 값을 넘겨주어야 한다.
class Book {
  private final String author;
  private final int name;

  public Book(String author, int name) {
    this.author = author;
    this.name = name;
  }

  public Book(int name) {
    this.author = null;
    this.name = name;
  }
}
Book book = new Book("이펙티브 자바");


혹은

class Book {

  private final String author;
  private final int name;

  public Book(String author, int name) {
    this.author = author;
    this.name = name;
  }
}
Book book = new Book(null, "이펙티브 자바");
  1. 매개변수의 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
Book book = new Book("조슈아 블로크", "이펙티브 자바", 3, "9788966262281", 36000");

예시의 경우 매개변수가 5개밖에 되지 않는데도 각 값들이 어떤 값을 나타내는지 이해하기 어렵다.

만약 동일한 타입을 사용하는 매개변수가 연달아 있다고 할 때 클라이언트에서 잘못해서 값을 넘기게 되면 찾기 어려운 버그로 이어질 수도 있다.

자바빈즈 패턴

생성자와 정적 팩터리가 가지는 문제점을 해결하기 위해 자바 빈즈 패턴을 사용해 보자.
매개변수가 없는 기본 생성자를 생성하고 setter 메서드를 호출해 매개변수의 값을 설정하는 방식이다.

class Book {

  private String author;
  private int name;

  public Book() {
  }

  public void setAuthor(String author) {
    this.author = author;
  }

  public void setName(int name) {
    this.name = name;
  }
}

// 필요한 값만 setter 메서드를 호출해서 주입할 수 있다.
// 메서드 이름을 통해 어떤 값이 설정되는지 한눈에 볼 수 있다.
Book book = new Book();
book.setAuthor("조슈아 블로그");
book.setName("이펙티브 자바");

코드가 더 길어지긴 했지만 생성자와 정적 팩터리가 가지는 문제점들이 해결되는 것을 볼 수 있다.

하자만 자바 빈즈 패턴에는 치명적인 문제가 있다.

  1. 객체 하나를 만들기 위해서 메서드를 여러 개 호출해야 하고 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이게 된다. 메서드 호출이 강제되지 않기 때문에 일관성이 무너진 상태에 있는 클래스를 어디서든 사용할 수 있게 된다. 반면에 점층적 생성자 패턴에서는 생성자에서 매개변수들이 유효한지를 확인하기 때문에 생성 시점에 일관성을 유지할 수 있다.
  2. 멤버에 final 을 붙일 수 없게 되고 setter 메서드가 public으로 열려 있기 때문에 클래스를 불변으로 만들 수 없다. 이는 언제라도 클래스의 필드값들이 변경될 수 있다는 것이고 이러한 문제는 찾기 어려운 버그로 이어질 수 있다.

빌더 패턴

대안으로 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴을 사용해 보자.

public class User{
    private final String name; // 필수
    private final int age; // 필수
    private final String socialLoginId; // 선택

    private User(Builder builder){
        this.name = builder.name;
        this.age = builder.age;
        this.socialLoginId = builder.socialLoginId;
    }

    private User(){}

    public static class Builder{
        private final String name;
        private final int age;
        private String socialLoginId = "-1"; // 선택 매개변수는 기본 값으로 세팅

        public Builder(String name, int age){
            this.name = name;
            this.age = age;
        }

        public Builder setSocialLoginId(String socialLoginId){
            this.socialLoginId = socialLoginId;
            return this;
        }
        public User build(){
            return new User(this);
        }

    }
}

클라이언트는 필요한 객체를 직접 만드는 대신 필수 매개 변수만으로 생성자를 호출해 빌더 객체를 얻고 빌더 객체가 제공하는 setter 메 서드를 통해 선택 매개변수들을 설정한다. 그리고 build 메 서드를 호출해 필요한 객체를 얻는다.

User user = User.Builder("user1", 15)
                .setSocialLoginId("124345")
                .build();

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

abstract class Pizza {

  public enum Topping {
    HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
  }

  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 (T)this;
      return self();
    }

    abstract Pizza build();

	// 하위 클래스가 오버라이딩 하게끔 하여 형 변환하지 않고 사용할 수 있다.
    protected abstract T self();
  }

  Pizza(Builder<?> builder) {
    toppings = builder.toppings.clone();
  }
}

class NyPizza extends Pizza {

  public enum Size {
    SMALL, 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
    Pizza build() {
      return new NyPizza(this);
    }

    @Override
    protected Builder self() {
      return this;
    }
    
    public Builder onlyNyPizza() {
      System.out.println("Only NyPizza");
      return this;
    }
  }

  
  private NyPizza(Builder builder) {
    super(builder);
    size = builder.size;
  }
}

Pizza pizza = new NyPizza.Builder(Size.SMALL)
      .addTopping(Topping.SAUSAGE)
      .addTopping(Topping.ONION)
      .onlyNyPizza()
      .build();

매개변수의 개수가 많거나 매개변수 중 다수가 필수가 아니거나 같은 타입이면 빌더 패턴을 고려하자.

profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글