티스토리 뷰

2️⃣

생성자에 매개변수가 많다면 빌더를 고려하라

 

❗ 객체를 생성할 때 사용하는 (1) 생성자와 (2) 정적 팩터리 메서드는 매개변수가 많을 때 대응하기 어렵다.

 

(예시) 아래와 같이 필드를 가진 User 클래스가 있다고 해보자.

회원가입할 때 사용자는 필수값인 [닉네임 + 비밀번호 + 이메일]과 함께 여러 선택정보를 등록할 수 있다. 즉, 회원가입 요청에는 수많은 경우의 수가 올 수 있다! 그리고 그 우린 수많은 경우의 수에 대응해야 된다..

public class User {
    // NonNull Fields
    private String nickname;
    private String password;
    private String email;
    
    // Nullable
    private Image profileImage;
    private String statusMessage;
    private String name;
    private String phoneNumber;
    private Gender gender;
    private LocalDate birthday;
    
    // ...
}

 

💁🏻‍♀️ 이런 문제를 어떻게 해결할 수 있을까?

1️⃣ 점층적 생성자 패턴

public User(String nickname, String password, String email);
public User(String nickname, String password, String email, Image profileImage);
public User(String nickname, String password, String email, Image profileImage, String name);
// ...

➖ 사용자가 설정하기 원치 않는 매개변수까지 설정해야 되는 경우 발생

➖ 사용자가 매개변수를 입력할 때 실수할 가능성↑ (ex. 비밀번호는 몇번째 파라미터에 넣어야 되지?)

➡️ 결론, 버그가 생기기 너무너무 쉬운 환경!

 

2️⃣ 자바빈즈 패턴

: 기본 생성자를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식

➕ 사용자가 원하는 값만 설정이 가능하고, 실수 가능성↓

➖ setter는 사용을 지양해야 함 (간접적으로 필드를 노출시켜서 캡슐화가 깨짐!)

➖ 객체 하나를 생성하는 데 메서드를 여러 개 호출 해야함

객체가 완전히 생성되기 전까지 일관성이 무너진 상태

     → 불변 객체를 만들 수 없어 스레드 안전성을 위해 따로 작업이 필요

 

🤔 일관성이 무너진 상태가 뭐지?

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

: (예시) User 클래스의 생성자에서 멤버변수(name, age)를 초기화 하기 때문에 User의 인스턴스가 있을 때 항상 멤버 변수의 값이 올바르게 설정되어 있음을 보장 (일관성)

but 만약 자바 빈즈 패턴을 적용하게 되면,  setName()만 하고 setAge()를 하기 전에는 일시적으로 멤버 변수 name 에는 올바른 값이 있지만, age에는 올바른 값이 없는 상태가 됨 (일관성이 무너진 상태)

 

그래서 점층적 생성자 패턴의 안전성 + 자바 빈즈 패턴의 가독성을 합친..

3️⃣ 빌더 패턴

  • 클라이언트는 필수 매개변수만으로 빌더 객체(User.Builder)를 생성
  • 빌더 객체가 제공하는 일종의 setter 메서드들로 원하는 매개변수를 설정
  • 마지막으로 build()를 호출해 원하는 객체(User)를 획득
public class User {
    // 모든 필드가 final이므로 User는 불변객체
    private final String nickname;
    private final Image profileImage;
    private final String statusMessage;
    
    // 빌더는 보통 static inner class로!
    public static class Builder() {
        // 필수
        private final String nickname;
        
        // 필수X - 기본 값으로 초기화
        private final Image profileImage   = Image.getDefaultImage();
        private final String statusMessage = "";
        
        // 생성자
        public Builder(String nickname) { this.nickname = Objects.requireNonNull(nickname); }
        
        // setter 같은 메서드
        public Builder profileImage(Image profileImage) { 
            this.profileImage = profileImage; 
            return this;
        }
        
        public Builder statusMessage(String statusMessage) { 
            this.statusMessage = statusMessage; 
            return this;
        }
        
        // bulid()
        public User build() {
            return new User(this);
        }
    }
    
    private User(Builder builder) {
        // 파라미터 유효성 검사는 여기서!
        nickname = builder.nickname;
        profileImage = builder.profileImage;
        statusMessage = builder.statusMessage;
    }   
}

 

User user = new User.Builder(nickname)
                        .statusMessage(statusMessage)
                        .build(); // 메서드 연쇄 (= fluent API)

 

🖐🏻 불변 객체 VS 불변식

  • 불변 객체 : 한번 만들어지면 절대 값을 바꿀 수 없는 객체 (ex. String)
  • 불변식 : 정해진 기간 동안(ex. 프로그램 실행) 반드시 만족해야 하는 조건 (ex. 리스트의 길이는 0이상)

 


 

👩🏻‍🏫 그외의 빌더 패턴의 장점

1️⃣ 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋음 (계층적 빌더)

🤔 계층적으로 설계된 클래스?

: 추상(추상클래스, 추상인터페이스)-구체 관계를 형성한 클래스들

public abstract class User {
    private final String nickname;
    private final String statusMessage;
    
    abstract static class Builder<T extends Builder<T>> { // T는 구체 클래스 빌더! (재귀적 타입 한정)
        private final String nickname;
        private final String statusMessage = "";
        
        // 생성자
        public Builder(String nickname) { this.nickname = Objects.requireNonNull(nickname); }
        
        // setter 같은 메서드
        public T statusMessage(String statusMessage) { 
            this.statusMessage = statusMessage; 
            return self(); // this를 할 순 없어서, self()가 여기서 필요해
        }
        
        // bulid()
        abstract User build();
        
        // 하위 클래스에서 이 메서드를 Override해서 this를 반환하도록 해야함! (셀프 타입 관용구)
        protected abstract T self();
    }
    
    User(Builder<?> builder) {
        nickname = builder.nickname;
        statusMessage = builder.statusMessage;
    }   
}
public class KakaoUser extends User {
    private final String kakaoId;
    
    // 계층적 빌더
    public static class Builder extends User.Builder<Builder> { // Builder는 this.Builder
        private final String kakaoId;
        
        // 생성자
        public Builder(String kakaoId) {
            super();
            this.kakaoId = Objects.requireNonNull(kakaoId); 
        }
        
        @Override
        public KakaoUser build() {
            return new KakaoUser(this);
        }
        
        @Override
        protected Builder self() {
            return this;
        }
    }
    
    private KakaoUser(Builder builder) {
        super(builder);
        kakaoId = builder.kakaoId;
    }   
}

 

2️⃣ 가변인수 처리가 유용

  • 이름이 다른 매개변수를 여러 개 정의해 가변인수를 위해 사용할 수 있음
  • 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모으기
public class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    
    public static class Builder() {
        EnumSet<Topping> toppings = EnumSet.nonOf(Topping.class);
        
        // setter 같은 메서드
        public Builder addTopping(Topping topping) { 
            toppings.add(Objects.requireNonNull(topping)); 
            return this;
        }
        
        // bulid()
        public Pizza build() {
            return new Pizza(this);
        }
    }
    
    private Pizza(Builder builder) {
        this.toppings = builder.toppings.clone(); // clone() 해야해!
    }   
}

→ addTopping() 메서드 같은 방식으로 가변인자를 다룰 수 있음! (두가지 방법 중 두번째 방법)

 

3️⃣ 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수 있음 (유연성↑)

    → 빌더에서 필드의 dafult값을 넣어줄 수 있으니까!

 


 

💁🏻‍♀️ 빌더 패턴의 단점은 없어?

1️⃣ 빌더(보통 static inner class)를 생성하는데 비용이 발생

2️⃣ 매개변수가 4개 이상일 경우에만 값어치를 함 (즉, 값어치를 하는 경우가 많다😚)

 


 

결론

API는 시간이 지날수록 매개변수가 많아지는 경향이 있기 때문에, 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면(많아질 것 같다면) 빌더 패턴을 선택하자!

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함