빌더 패턴에서 기본값 설정 문제와 @Builder.Default 해결 방법

LeeYulhee·2024년 7월 14일

👉 빌더 패턴에서 기본값 설정 문제 발생


  • 아래와 같이 Entity에서 isDeleted의 초기 값 설정
    @Getter
    @Entity
    @Table(name = "users")
    @SuperBuilder
    @AllArgsConstructor
    @NoArgsConstructor(access = PROTECTED)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
    public class User extends SoftDeleteBaseEntity {
    
        @Id
        @EqualsAndHashCode.Include
        @Column(name = "user_id")
        private UUID uuid;
        
        @Column(name = "email")
        private String email;
        
        @Column(name = "password")
        private String password;
        
        @Enumerated(EnumType.STRING)
        @Column(name = "role")
        private Role role;
    }
    @Getter
    @MappedSuperclass
    @SuperBuilder
    @AllArgsConstructor(access = PRIVATE)
    @NoArgsConstructor(access = PROTECTED)
    public class SoftDeleteBaseEntity extends BaseEntity{
    
        @LastModifiedDate
        @Column(name = "deleted_at")
        private LocalDateTime deletedAt;
    
        @Column(name = "is_deleted")
        private Boolean isDeleted = false;
    }
  • 하지만 Builder로 User를 생성하니, 필수 값(is_deleted 컬럼은 Not Null)이 비었다고 뜸
    public static User toEntity(UserDto dto) {
        return User.builder()
                .uuid(UUID.randomUUID())
                .email(dto.getEmail())
                .password(dto.getPassword())
                .role(Role.USER)
                .build();
    }


👉 기본 생성자와 빌더 패턴의 차이로 인한 문제


  • Lombok의 @Builder는 객체 생성 시 기본 값을 설정하지 않음
  • ‘기본 생성자가 초기화를 담당하지만 빌더는 해당 값을 설정하지 않는다’는 차이에서 발생
  • 빌더 패턴을 사용하여 객체를 생성할 때, 빌더가 각 필드를 명시적으로 설정해야 기본값이 유지
    • 빌더는 각 필드를 명시적으로 설정하는 구조이기 때문에, 초기화 과정에서 이 정보를 인식하지 못함



👉 해결 방법 : @Builder.Default


  • 기본값을 설정할 필드에 @Builder.Default 어노테이션 사용
    @Getter
    @MappedSuperclass
    @SuperBuilder
    @AllArgsConstructor(access = PRIVATE)
    @NoArgsConstructor(access = PROTECTED)
    public class SoftDeleteBaseEntity extends BaseEntity{
    
        @LastModifiedDate
        @Column(name = "deleted_at")
        private LocalDateTime deletedAt;
    
    		@Builder.Default
        @Column(name = "is_deleted")
        private Boolean isDeleted = false;
    }


👉 기본 생성자와 빌더의 필드 초기화 차이


  • 기본 생성자와 필드 초기화
    public class SoftDeleteBaseEntity {
        private Boolean isDeleted = false;
    
        public SoftDeleteBaseEntity() {
            // 기본 생성자는 isDeleted 필드를 false로 초기화
        }
    
        public Boolean getIsDeleted() {
            return isDeleted;
        }
    
        public static void main(String[] args) {
            SoftDeleteBaseEntity entity = new SoftDeleteBaseEntity();
            System.out.println(entity.getIsDeleted()); // 출력: false
        }
    }
    
    • 기본 생성자를 사용하면, 클래스의 필드가 선언된 대로 초기화됨
    • SoftDeleteBaseEntity 클래스에서 isDeleted 필드를 false로 초기화했다면, 기본 생성자는 이 초기화를 그대로 유지

  • 빌더 패턴과 필드 초기화
    import lombok.Builder;
    import lombok.Getter;
    
    @Getter
    @Builder
    public class SoftDeleteBaseEntity {
        private Boolean isDeleted = false;
    
        // 빌더 패턴을 사용할 때는 기본값을 명시적으로 지정해야 함
        
        public static void main(String[] args) {
            SoftDeleteBaseEntity entity = SoftDeleteBaseEntity.builder().build();
            System.out.println(entity.getIsDeleted()); // 출력: null
        }
    }
    • 빌더 패턴을 사용할 때는 기본 생성자와 달리, 객체를 생성할 때 필드에 대한 명시적 초기화가 이루어지지 않음
    • 빌더는 객체 생성 시 각 필드에 대해 값을 설정하도록 디자인되어 있음
    • 그렇기 때문에, 필드에 기본값이 있다 하더라도, 빌더는 이를 인식하지 못함
    • @Builder.Default를 설정하면 기본 값 설정 가능
      import lombok.Builder;
      import lombok.Getter;
      
      @Getter
      @Builder
      public class SoftDeleteBaseEntity {
          @Builder.Default
          private Boolean isDeleted = false;
      
          public static void main(String[] args) {
              SoftDeleteBaseEntity entity = SoftDeleteBaseEntity.builder().build();
              System.out.println(entity.getIsDeleted()); // 출력: false
          }
      }
      

👉 Builder 패턴 정리


  • 빌더 패턴은 객체 생성 시 생성자의 매개변수가 많아질 때 가독성과 유지보수성을 높이기 위해 사용
  • 각 필드는 빌더 클래스의 메서드를 통해 명시적으로 설정
  • 예제
    public class User {
        private final String name;
        private final int age;
        private final boolean isDeleted;
    
        private User(Builder builder) {
            this.name = builder.name;
            this.age = builder.age;
            this.isDeleted = builder.isDeleted;
        }
    
        public static class Builder {
            private String name;
            private int age;
            private boolean isDeleted;
    
            public Builder name(String name) {
                this.name = name;
                return this;
            }
    
            public Builder age(int age) {
                this.age = age;
                return this;
            }
    
            public Builder isDeleted(boolean isDeleted) {
                this.isDeleted = isDeleted;
                return this;
            }
    
            public User build() {
                return new User(this);
            }
        }
    
        @Override
        public String toString() {
            return "User{name='" + name + "', age=" + age + ", isDeleted=" + isDeleted + '}';
        }
    
        public static void main(String[] args) {
            User user = new User.Builder()
                .name("John")
                .age(30)
                .isDeleted(false)
                .build();
            System.out.println(user); // 출력: User{name='John', age=30, isDeleted=false}
        }
    }
    • User 클래스 안에 Builder라는 정적 클래스를 정의
    • 이 빌더 클래스는 User 객체의 필드들을 가지고 있으며, 각 필드에 대응하는 메서드를 제공
    • 빌더 클래스의 각 메서드는 필드를 설정하고, 빌더 자신(this)을 반환하여 메서드 체이닝을 가능하게 함
    • build() 메서드는 최종적으로 User 객체를 생성
profile
끝없이 성장하고자 하는 백엔드 개발자입니다.

0개의 댓글