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

2022년 02월 23일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

이번 주제는 매개변수가 많은 클래스에서 적용할 수 있는 여러 패턴과 각각의 장단점, 추천하는 방법 등을 설명한 챕터입니다. 그럼 지금부터 하나씩 알아보겠습니다.

포스트에서 사용하는 예시는 환자 건강정보라는 새로운 예시를 만들어 하나씩 작성해봤습니다. 건강정보 객체에는 환자이름, 전화번호, 키, 몸무게, 왼쪽/오른쪽 시력, 체지방률을 변수로 갖고 있습니다.

점층적 생성자 패턴 (Telescoping constructor pattern)

변수가 많은 건강정보 객체와 비슷한 객체에는 많은 개발자들은 점증적 생성자 패턴을 자주 사용하였다. 해당 패턴은 아래의 코드와 같이 매개변수를 n개를 받는 생성자, n+1개를 받는 생성자, n+2개를 받는 생성자.. 등으로 점층적으로 받는 매개변수가 늘어나는 방식이라서 점층적 생성자 패턴이라는 이름이 붙게 되었습니다.

public class HealthInformaiton {
    private final String name; // 환자이름 (필수)
    private final String phoneNumber; // 전화번호 (필수)
    private final float height; // 키
    private final float weight; // 몸무게
    private final float leftVision; // 왼쪽 시력
    private final float rightVision; // 오른쪽 시력
    private final float bodyFatPercent; // 체지방률

    public HealthInformaiton(String name, String phoneNumber) {
        this(name, phoneNumber, 0, 0);
    }

    public HealthInformaiton(String name, String phoneNumber, float height, float weight) {
        this(name, phoneNumber, height, weight, 0, 0);
    }

    public HealthInformaiton(String name, String phoneNumber, float height, float weight, float leftVision, float rightVision) {
        this(name, phoneNumber, height, weight, leftVision, rightVision, 0);
    }

    public HealthInformaiton(String name, String phoneNumber, float height, float weight, float leftVision, float rightVision, float bodyFatPercent) {
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.height = height;
        this.weight = weight;
        this.leftVision = leftVision;
        this.rightVision = rightVision;
        this.bodyFatPercent = bodyFatPercent;
    }
}

점증적 생성자 패턴을 사용한 클래스의 인스턴스를 만들려면 원하는 매개변수를 포함한 생성자 중에서 가장 짧은 것을 골라서 호출해주면 됩니다.

예를 들어 이름, 전화번호, 키의 정보만을 저장하는 인스턴스를 생성하고 싶다면 아래와 같이 필요한 정보를 모두 저장할 수 있는 생성자 중 가장 짧은 생성자를 이용하면됩니다.

HealthInformation healthInformation = new HealthInformation(name, phoneNumber, height, 0);

점증적 생성자 패턴은 여기서 단점이 나오게 됩니다. 앞서 말한 예시는 4개의 매개변수 중 1개의 값만을 0으로 의미없는 초기화값을 넣어줬습니다. 예시 코드로 봤을떄는 별 문제가 없어보이지만 만약 건강정보 객체에 더 많은 인스턴스 변수가 생겨 생성자의 매개변수가 수없이 많아지게 된다면 클라이언트 코드를 작성하거나 읽기 어려워집니다. 이러한 문제는 찾기 어려운 버그로 이어질 수도 있고 컴파일러도 찾지 못하는 오류를 발생시켜 런타임에 이상한 동작을 찾게되는 상황이 발생할 수 있습니다.

단점 정리
점층적 생성자 패턴은 생성자에 많은 파라미터가 들어가서 파라미터 순서가 틀릴 수 있고, 각각 매개변수가 무엇을 의미하는지 생성자를 열어보기 전에는 알기 어렵다는 단점이 있습니다.

자바빈즈 패턴 (JavaBeans pattern)

자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후 Setter메서드를 호출해 원하는 매개변수의 값을 설정하는 방식입니다.

Setter매서드를 활용한다라...벌써부터 좋지 않은 방법이라는 것을 알 수 있겠군요 🤓

public class HealthInformaiton {
    // 매개변수들은 기본값으로 초기화한다.
    private String name; // 환자이름 (필수)
    private String phoneNumber; // 전화번호 (필수)
    private float height = 0; // 키
    private float weight = 0; // 몸무게
    private float leftVision = 0; // 왼쪽 시력
    private float rightVision = 0; // 오른쪽 시력
    private float bodyFatPercent = 0; // 체지방률

    public HealthInformaiton() {}

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

    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }

    public void setHeight(float height) { this.height = height; }

    public void setWeight(float weight) { this.weight = weight; }

    public void setLeftVision(float leftVision) { this.leftVision = leftVision; }

    public void setRightVision(float rightVision) { this.rightVision = rightVision; }

    public void setBodyFatPercent(float bodyFatPercent) { this.bodyFatPercent = bodyFatPercent; }
}

해당 클래스를 보면 앞서 봤던 점층적 생성자 패턴의 단점은 발생하지 않을것으로 보이며 더욱 읽기 쉬워졌습니다. 하지만 자바빈즈 패턴은 하나의 객체를 만들려변 여러개의 메서드를 호출해야하고 객체가 완전히 만들어지기 전까지 일관성이 무너진 상태에 놓이게 됩니다.

이러한 일관성이 무너진 객체가 만들어지면, 버그를 심은 코드와 해당 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅도 만만치 않습니다.

이러한 일관성이 문어지는 단점을 보완하고자 생성이 끝난 객체를 freeze메서드를 호출하여 수동으로 얼리고 녹이는 해결방안이 있으나 해당 방법은 다루기 어렵고 객체 사용전에 프로그래머가 freeze메서드를 확실히 호출하였는지 컴파일러가 보증할 방법이 없어서 런타임 오류에 취약합니다.

단점 정리
자바빈즈 패턴은 일관성이 무너지는 문제가 있어 클래스를 불변으로 만들 수가 없다.

빌더 패턴 (Builder pattern)

빌더패턴은 점증적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 패턴입니다. 빌더패턴은 클라이언트가 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(or 정적 팩터리)를 호출하여 빌더 객체를 얻고 빌더 객체가 제공하는 setter메서드를 통해 원하는 매개변수들의 값을 세팅하게 됩니다. 그리고 마지막으로 매개변수가 없는 build메서드를 호출해 최종적으로 필요한 객체를 얻습니다.

public class HealthInformaiton {
    private final String name; // 환자이름 (필수)
    private final String phoneNumber; // 전화번호 (필수)
    private final float height; // 키
    private final float weight; // 몸무게
    private final float leftVision; // 왼쪽 시력
    private final float rightVision; // 오른쪽 시력
    private final float bodyFatPercent; // 체지방률

    public static class Builder {
        // 필수 매개변수
        private final String name; // 환자이름
        private final String phoneNumber; // 전화번호

        // 선택 매개변수 - 기본값으로 초기화한다.
        private float height = 0; // 키
        private float weight = 0; // 몸무게
        private float leftVision = 0; // 왼쪽 시력
        private float rightVision = 0; // 오른쪽 시력
        private float bodyFatPercent = 0; // 체지방률

        // 필수 매개변수만을 담은 Builder생성자 생성
        public Builder(String name, String phoneNumber) {
            this.name = name;
            this.phoneNumber = phoneNumber;
        }

        public Builder height(float height) {
            this.height = height;
            return this;
        }
        public Builder weight(float weight) {
            this.weight = weight;
            return this;
        }
        public Builder leftVision(float leftVision) {
            this.leftVision = leftVision;
            return this;
        }
        public Builder rightVision(float rightVision) {
            this.rightVision = rightVision;
            return this;
        }
        public Builder bodyFatPercent(float bodyFatPercent) {
            this.bodyFatPercent = bodyFatPercent;
            return this;
        }

        public HealthInformaiton build() {
            return new HealthInformaiton(this);
        }
    }

    private HealthInformaiton(Builder builder) {
        this.name = builder.name;
        this.phoneNumber = builder.phoneNumber;
        this.height = builder.height;
        this.weight = builder.weight;
        this.leftVision = builder.leftVision;
        this.rightVision = builder.rightVision;
        this.bodyFatPercent = builder.bodyFatPercent;
    }
}

보통 빌더는 클래스 안에 정적 멤버 클래스로 만들어둡니다. 만약 유효성 검사 코드를 넣고 싶다면 빌더의 생성자와 메서드에서 매개변수 검사를 하고 build메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식 검사를 하면됩니다.

위의 코드를 보면 HealthInformation클래스는 불변이고 빌더의 setter메서드들은 빌터 자신을 반환하기 떄문에 연쇄적으로 호출할 수 있는 것이 해당 패턴의 특징입니다.

이렇게 메서드 호출이 흐르듯이 연결되는 것을 플루언트(fluent) API 또는 메서드 연쇄(method chaining) 이라고 합니다.

해당 클래스를 사용하는 예시는 다음과 같습니다.

HealthInformaiton healthInformaiton = new HealthInformaiton.Builder("Bat Man", "010-0000-0000")
  .height(183)
  .weight(80)
  .bodyFatPercent(8.6F)
  .build();

사용 예시를 보면 확실히 가독성이 좋은 것을 확인할 수 있습니다.

빌더 패턴의 장점

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

    • 자세한 내용은 이펙티브 자바 p.19~21참조
  • 빌더를 이용하면 가변인수(varargs) 매개변수를 여러개 사용할 수 있다.
  • 상당히 유연하다.

    • 빌더 하나로 여러 객체를 순회하며 만들 수 있다.
    • 매개변수에 따라 다른 객체를 만들 수도 있다.
    • 특정 필드를 빌더가 알아서 채우도록 할 수도 있다.

빌더 패턴의 단점

  • 객체를 만들기 위해서는 빌더부터 만들어야한다.
  • 점층적 생성자 패턴보다 코드가 장황해서 매개변수가 4개 이상은 되어야지 이점을 느낄 수 있다.

매개변수가 4개 이상 되어야 값어치를 하나 API들은 시간이 지날수록 매개변수가 많아지는 경향이 있어 빌더 패턴으로 시작을 하는 것도 나쁘지 않다.

핵심정리

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 피수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh