백엔드 개발자 블로그

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

Java

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

backend-dev 2024. 3. 19. 22:25

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

 

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


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

매개변수가 점층적으로 늘어나는 방식으로 생성자를 작성하는 패턴입니다. 

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);

 

문제점

 

가독성이 좋지 않아서 매개변수가 많아지게 되면 문제가 발생합니다. 생성자 코드가 작성된 곳을 보면서 사용할 생성자를 고르는 데 시간이 오래걸리고, 생성자를 선택한다고 해도 순서를 기억해야 합니다. 


자바빈즈 패턴 (JavaBeans pattern)

매개변수가 없는 생성자로 객체를 만든 후 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)

일관성과 가독성을 모두 갖춘 패턴입니다.

  • 일관성을 위해서 멤버변수는 private final로 선언하고, 생성자도 private으로 선언합니다. 그리고 클래스 내부에 static Builder class를 작성합니다. 
  • 가독성을 위해서 builder class 내에 setter와 비슷한 함수를 작성합니다. 다른점은 값을 입력한 뒤, 자기자신을 반환하여 연쇄적으로 값을 입력할 수 있도록 한다는 것입니다. 그리고 마지막으로 생성자를 통해 builder의 멤버 변수 값을 입력하도록 작성합니다.
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;
    }
}

 

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

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

 

장점

  • 계층적으로 설계된 클래스와 함께 쓰기 좋습니다.
  • 빌더를 이용하면 가변인수(varargs) 매개변수를 여러개 사용할 수 있습니다.
  • 유연합니다.
    • 빌더 하나로 여러 객체를 순회하며 만들 수 있습니다.
    • 매개변수에 따라 다른 객체를 만들 수도 있습니다.
    • 특정 필드를 빌더가 알아서 채우도록 할 수도 있습니다.

단점

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

핵심정리

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면(4개 이상) 빌더패턴을 선택하는 게 더 낫다. 빌더는 점층적 생성자보다 클라이언트 코드보다 가독성이 좋고, 자바빈즈보다 훨씬 안전하다.