본문 바로가기

Language/Java

[EFFECTIVE JAVA 3/E] #2 생성자에 매개변수가 많다면 빌더를 고려하라

목차


    중점적으로 봐야 하는 내용

    • 선택적 매개변수가 많은 클래스는 생성자나 정적 팩터리 메서드만으로 표현하기 어렵다.
    • 점층적 생성자 패턴이나 자바빈즈 패턴은 유지보수성·안정성 측면에서 한계가 있다.
    • 이런 경우 빌더 패턴을 사용하면 가독성과 객체 일관성 면에서 유리하다.

    주요 개념 요약

    빌더 패턴은 복잡한 객체를 구성할 때 필수 매개변수와 선택 매개변수를 분리하고, 단계별 설정을 가능하게 하는 설계 기법이다.
    보통 클래스 내부에 정적 멤버 클래스로 Builder를 두고, 메서드 체인 방식으로 속성들을 설정한 뒤 build() 메서드로 객체를 생성한다.

    장점

    • 매개변수가 많은 경우 코드 가독성이 좋아진다.
    • 필수 매개변수와 선택 매개변수를 명확히 구분할 수 있다.
    • 메서드 체인(Fluent API)을 통해 설정이 직관적이다.
    • 계층적으로 설계된 클래스에서도 공변 반환(covariant return) 등을 통해 확장이 가능하다.

    단점

    • 객체 하나를 만들려면 Builder 객체부터 만들어야 하므로 코드가 좀 더 장황해질 수 있다.
    • 개발자가 선택 매개변수를 실수로 누락할 가능성이 있음.
    • 많은 경우, 간단한 객체라면 생성자 또는 정적 팩터리를 쓸 때보다 오히려 불편할 수 있다.

    실무 적용 예시

    선택적 필드가 많은 DTO/Domain/Entity 클래스를 다룰 때, 특히 필드의 유효성 체크가 중요한 경우에 직접 빌더 패턴을 구현 및 도입했다.

     

    public class UserProfile {
        private final Long id;              // 필수
        private final String username;      // 필수
        private final String email;         // 선택
        private final String phoneNumber;   // 선택
        private final String address;       // 선택
    
        private UserProfile(Builder builder) {
            this.id = builder.id;
            this.username = builder.username;
            this.email = builder.email;
            this.phoneNumber = builder.phoneNumber;
            this.address = builder.address;
        }
    
        public static class Builder {
            private final Long id;
            private final String username;
    
            private String email = "";
            private String phoneNumber = "";
            private String address = "";
    
            public Builder(Long id, String username) {
                this.id = id;
                this.username = username;
            }
    
            public Builder email(String email) { this.email = email; return this; }
            public Builder phoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; return this; }
            public Builder address(String address) { this.address = address; return this; }
    
            public UserProfile build() {
                // 필수 필드 누락 체크 가능
                if (id == null || username == null) {
                    throw new IllegalStateException("필수 매개변수가 누락되었습니다");
                }
                return new UserProfile(this);
            }
        }
    }
    UserProfile profile = new UserProfile.Builder(1L, "hyedddi")
        .email("hyedddi@example.com")
        .phoneNumber("010-1234-5678")
        .build();

     

    그 외의 클래스에는 롬복(Lombok)의 @Builder를 활용하여 반복되는 빌더 코드를 줄였다.

    나의 인사이트

    • 빌더 패턴을 도입하면서 복잡한 생성자 시그니처를 피할 수 있었고, 가독성과 유지보수성이 크게 좋아졌다.
    • 다만 실제로 협업 시, 선택적 매개변수를 누락하여 논리적 오류가 나는 경우가 있어서, 빌더내에서 필수 필드 검증 혹은 테스트 케이스를 꼭 넣어야 한다고 느꼈다.
    • 간단한 객체나 필드가 적은 경우에는 여전히 정적 팩터리나 생성자를 사용하는 것이 더 간편하다고 판단했다.