본문 바로가기

Dev Book/Effective Java

[Effective Java] item2. 생성자에 매개변수가 많다면 빌더를 고려하라

제약

정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.

 

방법 1. 점층적 생성자 패턴 (telescoping constructor pattern)

이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.

 

관련 깃허브 코드 : https://github.com/2Smean/ReadingBook/blob/main/src/main/java/Chap2_GenerateObjectAndDestory/item2/telescopingconstructor/NutritionFacts.java

 

장점

  • 생성자가 호출되는 시점에 객체가 유효한 상태를 가질 수 있다.

 

단점

  • 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 확장하기 어렵다

 

방법 2. 자바 빈즈 패턴 (JavaBeans pattern)

매개변수가 없는 생성자로 객체를 만든 후, 세터 (setter) 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

 

관련 깃허브 코드

https://github.com/2Smean/ReadingBook/blob/main/src/main/java/Chap2_GenerateObjectAndDestory/item2/javabeans/NutritionFacts.java

 

장점

  • 가독성, 유연성, 확장 용이성

 

단점

  • 일관성 장치 상실
    • 객체가 완전히 초기화되기 전에 사용할 가능성이 있어 객체가 유효하지 않은 상태
  • 불변성 상실
    • 객체가 생성된 후 상태가 변화될 수 있어 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없다.
  • 스레드 안정성
    • 멀티스레드 환경에서 동기화 문제로 인해 객체의 일관성이 깨질 수 있다.

 

단점 보완 freezing

생성이 끝난 객체를 수동으로 얼리고 , 얼리기 전에는 사용할 수 없도록 하기 위한 장치 JavaScript에서는 아래와 같은 메서드를 제공한다.

javascript Object.freeze()

Java에서는 이러한 메서드를 제공하지 않고 직접 커스텀하여 freeze 메서드를 사용해야 한다.

하지만 이 방법을 사용한다 하더라도 객체 사용 전에 freeze 메서드를 확실히 호출해 줬는지를 컴파일러가 보증할 방법이 없어서

런타임 오류에 취약하다.

 

 

방법 3. 빌더 패턴 (Builder pattern)

점층적 생성자 패턴의 안전성 + 자바빈즈 패턴의 가독성 = 빌더 패턴

 

  • 클라이언트는 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
  • 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
  • 매개변수가 없는 build 메서드를 호출해 객체(보통은 불변)를 얻는다.

빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두는 게 보통이다.

 

관련 깃허브 코드 

:https://github.com/2Smean/ReadingBook/blob/main/src/main/java/Chap2_GenerateObjectAndDestory/item2/builder/NutritionFacts.java

 

빌더의 세터 메서드의 연쇄성

빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다 -> 플루언트 API (fluent API), 메서드 연쇄 (method chaining)

 

예외

  • 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식(invariant)을 검사

 

불변과 불변식

불변 (immutable)

  • 가변(mutable) 객체와 반대어. 어떠한 변경도 허용하지 않는다.
  • ex) String 객체

불변식 (invariant)

  • 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건.
  • 불변은 불변식의 극단적인 예시이다.

 

빌더 패턴의 사용

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

 

시뮬레이트한 셀프 타입 (simulated self-type) 관용구 : Pizz.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상메서드인 self를 더해 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.

 

재귀적 타입 한정의 예시

재귀적 타입 한정은 제네릭 타입 파라미터가 자기 자신을 한정하는 것을 의미한다. 즉, 타입 파라미터가 자기 자신의 하위 타입을 제한하는 구조를 가지게 된다. T extends Builder<T>는 T가 Builder 하위 타입이어야 한다는 의미이다. 이 패턴을 사용하면 메서드 체이닝을 타입 안전하게 구현할 수 있다.

 

관련 깃허브 코드

:https://github.com/2Smean/ReadingBook/blob/main/src/main/java/Chap2_GenerateObjectAndDestory/item2/hierarchicalbuilder/Pizza.java

 

공변 반환 타이핑 (convariant return typing) : 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능. 클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있다.

 

빌더 패턴은 상당히 유연하다.

  • 빌더 하나로 여러 객체를 순회하면서 만들 수 있다.
  • 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
  • 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

빌더 패턴의 단점

빌더부터 만들어야 한다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.

점층적 생성자 패턴보다 코드가 장황하여 매개변수가 4개 이상은 되어야 값어치를 한다.

API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명시하자.

Lombok 사용의 장단점

장점 : 코드를 간결하게 작성할 수 있다. 반복적인 코드 작성을 줄여 개발 속도를 향상해 주어 생산성을 높여준다.

단점 : Lombok을 사용하는 코드의 경우, 자동으로 생성된 메서드들이 실제 코드에서 보이지 않기 때문에 코드의 흐름을 이해하는 데 어려움을 겪을 수 있다. 이로 인해 코드의 가독성이 떨어질 수 있다. 또한 제한된 구현력으로 직접 코드를 작성하는 것보다 구체적으로 코드를 구현하지 못한다.

그렇기에 잘 알고 사용하는 것이 중요하겠다.