Design Pattern

[디자인 패턴] Builder 패턴

devJK93 2025. 3. 23.

빌더 패턴이 생성자보다 좋은 이유

객체를 만들 때 빌더 패턴이 왜 기존 생성자 방식보다 나은지 궁금하다.

1. 기존 생성자 방식, 뭐가 문제일까

일단 생성자 방식은 객체를 만들 때 필요한 값을 전부 한 번에 넘겨야 한다. 예를 들어, 유저 정보를 담는 클래스를 만들어보자:

// 기존 생성자 방식
public class User {
    private String name;
    private int age;
    private String email;
    private String phone;
    private String address;
    private boolean isActive;

    public User(String name, int age, String email, String phone, String address, boolean isActive) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.phone = phone;
        this.address = address;
        this.isActive = isActive;
    }
}

// 이렇게 사용함
User user = new User("홍길동", 30, "hong@example.com", "010-1234-5678", "서울시 강남구", true);
    

간단할 땐 괜찮아 보인다. 하지만 좀 더 생각해보면 문제가 있는데:

  • 매개변수가 너무 많음: 필드가 6개만 돼도 생성자가 엄청 길어지고 뭐가 뭔지 헷갈린다.
  • 선택적 필드가 골치 아프다: 주소나 전화번호가 없어도 만들고 싶으면 null이나 빈 문자열을 넣어야 한다.
  • 생성자 오버로드 지옥: 필드 조합마다 생성자를 따로 만들다 보면 코드가 엉망이 된다. 예를 들어:
// 오버로드 예시
public User(String name, int age) { ... }
public User(String name, int age, String email) { ... }
public User(String name, int age, String email, String phone) { ... }
// ... 계속 늘어남
    

이렇게 하면 코드가 길어지고 유지보수도 힘들다. 실수로 잘못된 생성자를 호출할 수도 있다.

2. 빌더 패턴이란?

빌더 패턴은 이런 문제를 깔끔하게 해결해주는데
객체를 단계별로 만들고, 필요한 값만 넣을 수 있게 해줌. 같은 User 클래스를 빌더로 바꿔보면:

// 빌더 패턴
public class User {
    private final String name;  // 필수
    private final int age;      // 필수
    private final String email; // 선택
    private final String phone; // 선택
    private final String address; // 선택
    private final boolean isActive; // 선택

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
        this.address = builder.address;
        this.isActive = builder.isActive;
    }

    public static class Builder {
        private final String name;
        private final int age;
        private String email = "";
        private String phone = "";
        private String address = "";
        private boolean isActive = false;

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

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder isActive(boolean isActive) {
            this.isActive = isActive;
            return this;
        }

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

// 사용 예시
User user = new User.Builder("홍길동", 30)
                .email("hong@example.com")
                .phone("010-1234-5678")
                .address("서울시 강남구")
                .isActive(true)
                .build();

// 최소한으로 만들고 싶을 때
User minimalUser = new User.Builder("김영희", 25).build();
    

이렇게 필수 필드(name, age)는 생성자로 강제하고, 나머지는 선택적으로 넣을 수 있다.

3. 빌더 패턴의 장점

다음과 같은 장점때문에 빌더패턴을 사용한다:

  • 가독성 good: 메서드 이름으로 어떤 값이 들어가는지 바로 알 수 있음. .email("hong@example.com") 보면 딱 email인 걸 알 수 있다. (생성자로 생성했던 경우 몇 변째 변수에 어떤 항목이 들어가는지 알고 있어야 함.)
  • 유연함: 선택적 필드를 자유롭게 넣거나 뺄 수 있음. 최소한의 정보만으로도 객체를 만들 수 있다. (위의 예시처럼)
  • 불변성 보장: final 키워드를 써서 객체가 만들어진 후에 값이 바뀌지 않게 할 수 있다. 실수로 데이터가 꼬일 일이 없음.
  • 생성자 하나로 끝: 오버로드 여러 개 만들 필요 없이 빌더 하나로 모든 경우를 커버함.
  • 유효성 검사 쉬움: build() 메서드에서 값이 제대로 들어갔는지 체크할 수 있음.

팁: 예를 들어, 이메일 형식이 잘못됐으면 build()에서 예외를 던지게 만들면 더 안전해진다.

4. 실전에서 어떻게 쓰이는지?

실제 프로젝트에서 빌더 패턴은 복잡한 객체를 만들 때 빛을 발한다. 예를 들어, 설정 객체를 만들 때:

// 서버 설정 객체
ServerConfig config = new ServerConfig.Builder("myServer", 8080)
                        .timeout(5000)
                        .maxConnections(100)
                        .sslEnabled(true)
                        .build();
    

이런 식으로 설정값이 많아도 깔끔하게 정리할 수 있음.
라이브러리에서도 자주 보는데, OkHttpClientRetrofit 같은 데서 빌더 패턴을 써서 설정을 유연하게 처리한다.

5. @Builder 등장 (Lombok 사용)

Lombok의 @Builder를 쓰면 위 코드를 엄청 간단하게 줄일 수 있다.

자바 프로젝트에 Lombok 의존성 추가하고 이렇게 쓰면 끝:


import lombok.Builder;

@Builder
public class User {
    private String name;
    private int age;
    private String email;
}

User user = User.builder()
                .name("홍길동")
                .age(30)
                .email("hong@example.com")
                .build();
    

`@Builder` 하나로 빌더 클래스랑 메서드 체이닝 다 만들어준다.

(Lombok이 컴파일 때 필요한 코드를 자동 생성해준다.)

5. @Builder 에서 필수 필드 설정

`@Builder`도 기본값이나 필수 필드를 설정할 수 있다. 예를 들어:


import lombok.Builder;
import lombok.NonNull;

@Builder
public class User {
    @NonNull
    private String name; // 필수 필드
    private int age;
    @Builder.Default
    private String email = "없음"; // 기본값 설정
}

User user = User.builder()
                .name("홍길동") // 필수라 안 넣으면 컴파일 에러
                .age(30)
                .build(); // email은 "없음"으로 설정됨
    

`@NonNull`으로 필수 필드를 강제하고, `@Builder.Default`로 기본값 줄 수 있다. (매우 편리)

6. 뭐가 제일 좋을까?

- 필드가 2~3개면 그냥 생성자 쓰자.
- 복잡한 객체인데 Lombok 못 쓰면 수작업 빌더로 가고.
- Lombok 쓸 수 있으면 `@Builder`로 편하게 가자

 

댓글