Java

[Effective Java] 2-2. 객체 생성과 파괴: 생성자에 매개변수가 많다면 빌더를 고려하라

noahkim_ 2024. 12. 26. 23:57

조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.

 

1. 점층적 생성자 패턴

  • 매개변수 개수에 따라 여러 생성자를 오버로딩하는 방식
  • 선택적 매개변수가 많을 때 적절한 대응을 위해 사용됨
항목 내용
장점 - 컴파일 타임 타입 체크 가능
- 불변 객체 만들기 쉬움
단점
- 매개변수가 많아지면 코드 가독성 나빠짐
- 파라미터 순서 헷갈리면 런타임에만 오류 인지됨

 

2.자바빈즈 패턴

  • 기본 생성자 + setter를 이용해 단계적으로 값 설정
항목 내용
장점 - 코드 가독성 좋음
- 매개변수 많아도 명확히 설정 가능
단점
- 객체가 불변이 아님
- 생성 도중 객체가 일관성 없는 상태에 놓일 수 있음
- 버그 발생 시 디버깅 어려움

 

3. 빌더 패턴

  • 동일한 생성 절차를 거쳐 다양한 구성의 객체를 만드는 패턴
  • 특히 매개변수가 많거나 계층적으로 확장 가능한 객체 생성에 적합
구분 내용
장점
- 가독성 향상: 매개변수가 많아도 어떤 값이 무엇을 의미하는지 명확히 알 수 있음
- 유연한 객체 생성: 필수값 + 선택값을 조합해서 객체 생성 가능
- 불변 객체 생성 가능: final 필드와 함께 사용하면 안전한 불변 객체 구성 가능
- 객체 일관성 유지: 객체가 완전히 세팅된 후에만 생성됨 (build() 호출 시점)
- 계층 구조에서도 확장 용이: 재귀적 타입 한정 + 공변 반환 타입을 활용하면 상속 구조에서도 자연스럽게 빌더 체이닝 가능
단점
- 코드가 복잡해짐: 빌더 클래스 + 본 클래스 구조로 코드가 길어짐
- 간단한 객체에 오버엔지니어링 우려: 필드가 몇 개 안 되는 경우엔 오히려 불필요하게 무거움
- 객체 생성 전 build() 호출 누락 가능성: 실수로 builder.build()를 빼먹으면 객체가 안 만들어짐

 

예시) 매개변수를 여러개 사용할 수 있음

더보기
public class NutritionFacts {
    private final int servingSize, servings, calories, fat;

    public NutritionFacts(Builder builder) {
        this.servingSize    = builder.servingSize;
        this.servings       = builder.servings;
        this.calories       = builder.calories;
        this.fat            = builder.fat;
    }

    public static class Builder {
        private final int servingSize, servings;
        private int calories, fat;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            this.calories = val;
            return this;
        }

        public Builder fat(int val) {
            this.fat = val;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}
  • 가변인수 문법 사용
  • 각 프로세스를 독립적으로 분리할 수 있음

 

예시) 계층적

더보기
public abstract class Pizza {
    public enum Topping { HAM, HUSROOMS, 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 self();
        }

        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        this.toppings = builder.toppings.clone();
    }
}
public 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 = size;
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

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

 

항목 재귀적 타입 한정 (Recursive Type Bound) 공변 반환 타입 (Covariant Return Typing)
형태 T extends Builder<T> @Override public NyPizza.Builder self()
사용 위치 상속 가능한 추상 빌더 클래스 선언부 하위 빌더 클래스의 메서드 재정의
정의 제네릭 타입 T가 자기 자신의 하위 타입임을 명시 상위 클래스 메서드를 하위 타입 반환으로 재정의
목적 빌더 체이닝 시 타입 안정성 확보 build()와 self()에서 형변환 없이 사용 가능
장점 - 하위 타입을 명확히 제한
- 타입 오류를 컴파일 타임에 방지
- NyPizza.Builder → .addTopping() 호출 시 형변환 필요 없음