본문 바로가기
Java

[이펙티브 자바] 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라

by 성건희 2021. 5. 25.
반응형
이팩자바 2

정적 팩터리, 생성자는 선택적 매개변수가 많을 때 대응하기 어려운 문제가 있다.

이때, 점층적 생성자 패턴을 사용할 수 있다.

 

점층적 생성자 패턴

쉽게 말해서 매개변수 마다 생성자를 늘려가는 방식이다. (매개변수 1개 생성자, 매개변수 2개 생성자...)

하지만 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기가 어렵다는 단점이 있다.

  • 각 값의 의미 파악이 어려움
  • 매개변수가 몇 개인지 파악해야함
  • 타입이 같은 매개변수가 있으면 버그가 발생할 때 찾기 어려움
  • 실수로 매개변수 순서를 바꿔도 버그가 발생할 수 있음

 

자바빈즈 패턴 (JavaBeans pattern)

점층적 생성자 패턴의 대안으로, 매개변수가 많을 때 자바빈즈 패턴을 사용할 수 있다.

매개변수가 없는 생성자로 객체를 생성 후, setter 메서드를 통해 원하는 매개변수의 값을 지정하는 방법이다.

 

장점

  • 인스턴스를 만들기 쉽다.
  • 읽기 쉬운 코드가 된다.

 

단점

  • 객체 하나를 생성하기 위해 메서드를 여러 개 호출해야 함.
  • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 됨. -> 클래스를 불변으로 만들 수 없다. (immutable)
  • 일관성이 깨진 객체가 만들어지면 버그로 인한 디버깅이 어렵다.

 

위와 같은 단점을 완화하고자 생성이 끝난 객체를 수동으로 얼리고(freezing), 얼리기 전에는 사용할 수 없도록 하기도 함.

하지만 다루기에 어렵고, 객체 사용 전 프로그래머가 freeze 메서드를 호출했는지 컴파일러가 보증할 수 없어 런타임 오류에 취약해 실전에서는 거의 사용 x

 

빌더 패턴 (Builder pattern)

점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 패턴이다.

 

사용법

  1. 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 만든다.
  2. 빌더 객체가 제공하는 세터 메서드들로 원하는 선택 매개변수를 설정한다.
  3. Build 메서드를 호출해 불변 객체를 얻는다.

 

장점

  • 쓰기 쉽고, 읽기 쉽다.

  • 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

  • 가변인수 매개변수를 여러 개 사용할 수 있다.

  • 빌더 패턴은 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 매개변수에 따라 다른 객체를 만들 수 있다.

    일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

  • 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

 

단점

  • 객체를 만들려면, 빌더부터 만들어야 한다. (이 부분은 Lombok을 사용하면 해결되지 않을까..)
  • 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. (하지만 API는 시간이 흐름에 따라 많아지는 경향이 있다.)

 

생성자나 정적 팩터리 방식으로 시작했다가 나중에 매개변수가 많아지면 빌더 패턴으로 전환할 수 있지만,

이전에 만들어둔 생성자와 정적 팩터리를 처리하기에도 시간이 오래걸릴듯. 그래서 애초에 빌더로 시작하는 편이 나을 때가 많다.

 

플루언트 API (fluent API), 메서드 연쇄 (method chaining)

빌더의 setter 메서드는 빌더 자신을 반환하므로 연쇄적 호출이 가능하다.

이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API 또는 메서드 연쇄라고 함.

 

공변 반환 타이핑 (convariant return typing)

하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능

이 기능을 이용하면 클라이언트가 형변환에 신경 쓰지 않고 빌더를 사용할 수 있다.

public class NyPizza extends Pizza {
    ...
  
    @Override
    public NyPizza build() {
      return new NyPizza(this);
    }
}
NyPizza pizza = new NyPizza.Builder(SMALL)
			   .addTopping(SAUSAGE)
			   .addTopping(ONION)
			   .build();

 

회고

회사 코드에서도 생성자 매개변수가 정말 많은 (6개 이상...) 코드가 있었는데,

생성자를 사용하는 쪽에서 각각의 매개변수 값이 무엇을 뜻하는 것인지 파악하기가 어려웠다.

그리고 타입이 동일한 매개변수의 경우, 실수로 값의 위치를 달리하여 넣으면 버그를 찾기가 너무 어려웠다.

그래서 이 부분을 빌더 패턴을 적용하였고, setter 메서드를 통해서 명확하게 어떤 값인지 파악할 수 있게 되어서 실수를 줄일 수 있었고,

생성자 매개변수가 추가되어도 이전에는 생성자 매개변수의 위치에 주의하며 값을 추가했는데,

빌더 패턴 적용 후에는 setter 메서드 하나만 더 추가해주면 되어서 편리했다.

 

참고

  • Effective Java 3 / E - 조슈아 블로크

 

예제 코드

 

반응형

댓글