Builder 패턴 - 필요성과 사용법

Builder Pattern이란?

Builder is a creational design pattern that lets you construct complex objects step by step.
The pattern allows you to produce different types and representations of an object using the same construction code.

빌더는 복잡한 Object들을 단계별로 구축할 수 있는 생성 디자인 패턴입니다.
이 패턴을 사용하면, 동일한 구성코드를 사용하여 다양한 타입과 표현을 제공합니다.

결과적으로는 생성자를 가독성 좋게 만들어주는 도구라고 할 수 있을것 같네요.

생성자 대신에 Setter를 사용하면 가독성이 개선되지 않나요?

Setter 함수를 사용한다면 가독성은 개선되지만, 문제점들이 있습니다.
그래서 먼저 빌더 패턴의 필요성을 설명하기 앞서서, 왜 Setter를 지양하는지부터 설명하겠습니다.

Setter 사용을 지양하는 이유

1. 의도를 파악하기 힘들다.

객체의 값을 바꾸는 경우는 대게 어떤 목적(비지니스 로직)을 위해서 바꾸는 경우가 많은데,
Setter 만 사용했을 때는, 무슨 의도로 값을 변경 했는지를 알기가 힘들기 때문입니다.
그래서 setter를 사용하기보다는 그 의도를 확실히 알수 있도록 함수를 만드는것이 좋습니다.

💩 Bad Example

@Getter @Setter
class User {
  private Long id;
  private String email;
  private String password;
}

// 💩 비밀번호를 변경하려고 사용하는건지,
// 💩 값을 그냥 Setting하기위해 사용하는건지 알기가 어렵다!
public void 비밀번호_변경_V1(Long id) {
  User user = user.findById(id);
  user.setPassword(변경비밀번호)
}

✨ Good Example

@Getter // ✨ @Setter 제거
class User {
  private Long id;
  private String email;
  private String password;

  //== 비지니스 로직 ==//
  public void changePassword(String password) {
    this.password = password;
  }
}

// ✨ 비밀번호를 변경하기 위해 사용하는구나! 라고 명확히 알 수 있다!
public void 비밀번호_변경_V2(Long id, String 변경비밀번호) {
  User user = user.findById(id);
  user.changePassword(변경비밀번호)
}

⭐️ 2. 객체의 일관성을 유지하기 어렵다.

@Getter @Setter
class User {
  private Long id;
  private String email;
  private String password;
}

// 객체 생성
User user <= new User();
user.setId(1L)
user.setEmail("[email protected]");
user.setPassword("1234);

Setter를 쓰게 된다면, 객체 하나를 만들기 위해서 함수를 여러개 호출해야되고,
객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됩니다.

그리고 Java Bean 규약에 따르면 Setter는 public으로 어떤 곳에서 변경이 가능합니다.
문제는 public 이므로 어디서도 접근해서 값을 변경할 수 있기 때문에 객체의 일관성을 유지하기가 힘듭니다.

한번 값이 설정 되었을때, 변경을 막기 위해 final 키워드를 사용해서 제한을 할 수도 있겠지만,
애초에 final로 제한을 했다면 생성자에서 값을 설정하고, Setter 함수를 만들지 않는게 맞다고 생각합니다.

이렇게 Setter를 사용하지 않았을 때, 어디서 값을 바꾸는지 신경을 쓰지 않아도 된다는 점 자체가 업무 생산성을 높인다고 생각합니다.

Builder 패턴이 필요한 이유

위와 같은 이유로 객체를 만들고 동시에 값을 설정가능한 생성자를 많이 사용합니다.

하지만 생성자를 사용했을때의 문제점은 가독성입니다.

@Getter
class User {
  private Long id;
  private String email;
  private String password;
  private String name;
  private String address;
}

public User (String email, String password, String name, String address) {
  this(null, email, password, name, address);
}

public User (Long id, String email, String password, String name, String address) {
  this.id = id;
  this.email = email;
  this.password = password;
  this.name = name;
  this.address = address;
}

만약에 위의 클래스에서, 이름이나 주소입력이 필수가 아니라면 어떻게 될까요?

private String email = "[email protected]";
private String pwd = "1234"

@Test
public void 테스트() {
  String name = "길동이"

  User user1 = new User(email, pwd, name, null); // 주소 빼고
  User user2 = new User(email, pwd, null, null); // 이름, 주소 제외
  // ...
}

위와 같이 필수 아닌값도 null로 채워주거나, 주소를 뺀 생성자 함수를 다시 만들어야 될것 입니다.
그리고 가독성 자체도 좋지가 않은게 순서가 어디가 이메일이고, 비밀번호인지를 알기 위해 User Class를 다시 찾아 봐야됩니다.
명확하게 어떤값을 지정하는지를 알수 없기 때문에 가독성이 좋지 않습니다.
이것이 빌더 패턴이 필요한 이유이고 권장하고 있습니다.

⚡️ 이펙티브 자바 책에서도 아래와 같이 이야기하고 있습니다.

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다

Builder 패턴 사용 방법

public class User {

  private Long id;
  private String email;
  private String password;
  private String name;

  public User(Long id, String email, String password) {
    this.id = id;
    this.email = email;
    this.password = password;
  }

  ...getter 함수

  // ✨ 빌더 패턴을 위한 빌더 클래스!
  static public class Builder {
    private Long id;
    private String email;
    private String password;

    public Builder() {
    }

    public Builder(User user) {
      this.id = user.id;
      this.email = user.email;
      this.password = user.password;
    }

    public Builder id(Long id) {
      this.id = id;
      return this;
    }

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

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

    public User build() {
      return new User(id, email, password);
    }
  }
}

위와 같이 클래스 내부에서 Builder 클래스를 따로 정의하여 사용할 수 있습니다.
각 함수들을 보면 알 수 있듯이, 값을 설정하고 자기자신을 반환하기 때문에 함수를 연속적으로 체이닝하듯이 사용할 수 있습니다.

아래의 코드는 생성자와 빌더패턴을 사용한 사용법 및 비교 예제입니다.

class UserTest {

  @Test
  @DisplayName("빌더와 생성자 객체가 동일하다!")
  public void 빌더_테스트() {
    String email = "[email protected]";
    String password = "1234";
    String name = "dooly";

    // 🤔 생성자 사용
    User user = new User(null, email, password, name);

    // ✨ 빌더 패턴 이용
    // ✨ 어떤 값을 설정 하는지 명확하게 알 수 있고!
    // ✨ 필요한 값만 정의한다면, 나머지는 null로 자동 설정 가능합니다.
    User build = new User.Builder()
            .email(email)
            .password(password)
            .name(name)
            .build();

    assertThat(build.getEmail()).isEqualTo(user.getEmail());        // 결과값: OK! 👍
    assertThat(build.getPassword()).isEqualTo(user.getPassword());  // 결과값: OK! 👍
    assertThat(build.getName()).isEqualTo(user.getName());          // 결과값: OK! 👍
  }
}

위와 같이 빌더 패턴을 사용해서 필요한 값만 설정 가능하고,
어떤 값을 설정하는지 명확하게 알 수 있기 때문에 가독성을 높일 수 있습니다.

Builder 패턴 사용방법 - Lombok

기본적으로 @Builder 어노테이션을 사용하면 간단하게 빌더 패턴을 사용할 수 있습니다.

  • 클래스 전체 Builder 적용

    @Getter @Builder // ✨ 클래스 전체 필드를 빌더로 사용 가능!
    public class UserLombok {
    
      private Long id;
      private String email;
      private String password;
      private String name;
    }
    
    // 사용예제
    public User join(String email, String password, String name) {
      UserLombok build = UserLombok.builder()
                .email(email)
                .password(password)
                .name(name)
                .build();
      ...
    }
  • 특정 생성자에서만 Builder 적용 가능

    @Getter
    public class UserLombok {
    
      private Long id;
      private String email;
      private String password;
      private String name;
    
      @Builder // ✨ 빌더는 email, password만 사용 가능
      public UserLombok(String email, String password) {
        this.email = email;
        this.password = password;
      }
    
      public UserLombok(Long id, String email, String password, String name) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.name = name;
      }
    }
    
    // 사용예제 - email, password만 가능!
    public User join(String email, String password, String name) {
      UserLombok build = UserLombok.builder()
                .email(email)
                .password(password)
                .build();
      ...
    }

참고

Refactoring: clean your code
이펙티브 자바
[Java] Setter 사용을 지양하자!!
[JAVAEE] 자바빈(JavaBean) 이란? 자바빈 규약에 대해

좋은 웹페이지 즐겨찾기