Builder 패턴 필수 값 지정하기
코드 : https://github.com/sykimtropical/entity-builder
필수 값 제한의 필요성
Builder 패턴을 이용하여 객체 생성 시 필수 값을 지정하기 전에
왜 꼭 제한을 해야 하는가 생각 해봐야 한다.
실무에서 작업을 하면 프로젝트를 한 명만 작업하는 경우 또는 그 프로젝트의 시작과 끝을 한 명이 맺는 경우는 거의 없다.
여러 명이 작업을 하다 보면 설계자의 의도를 모두가 100% 파악하기란 불가능에 가깝다.
작업을 하면서 데이터 유효성 검사의 물리적 제한은 일을 시작하기 전보다 더 중요하게 생각하게 되었다.
테이블을 생성하면서 컬럼의 N-N, 자료형, 사이즈, default 값 등 또한 잘못된 데이터가 들어오는 것을 막기 위한 조치이다.
이미 테이블에도 제한을 걸어두었겠으나 애플리케이션 레이어에서 유효성 검증을 하지 않으면 런타임 오류로 이어진다.
받아오는 데이터는 @Validate
와 같은 어노테이션 또는 다른 로직을 통해 유효성을 검증 할 것이다.
그러나 비즈니스 로직을 통해 최종 데이터 객체를 생성 하는 과정에서 필수 값의 누락이 존재할 수 있다.
Builder패턴이 Entity에서만 사용되지는 않겠으나 간단하게 와닿는 예시로 Entity만 테스트 하도록 한다.
Getter/Setter 를 이용한 Entity
빌더패턴 없이 Getter와 Setter 만으로 구성한 엔티티는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity(name = "test_user_no_builder")
@EntityListeners(AuditingEntityListener.class)
@Getter @Setter
public class NoBuilderUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 필수 컬럼
@Column(nullable = false, length = 10)
private String name;
@Column(nullable = false, length = 50)
private String email;
@CreatedDate @Column(nullable = false, updatable = false)
private LocalDateTime regDate;
@Builder.Default @LastModifiedDate @Column(nullable = false)
private LocalDateTime upDate = LocalDateTime.now();
// 옵션 컬럼
@Column private int age;
@Column(length = 50)
private String etc;
}
테이블에는 필수 컬럼과 옵션 컬럼이 존재 할 것이다.
위와 같이 만들어 둔 엔티티는 작업자가 엔티티를 들어와서 직접 확인하지 않는 이상 필수 컬럼을 누락 시킬 위험이 있다.
1
2
3
4
5
6
7
8
9
10
11
@Test
@DisplayName("Getter/Setter만 이용한 Entity")
void createUserGetterSetter() {
// 기본 생성자를 통해 Entity를 생성 한다면
NoBuilderUser user = new NoBuilderUser();
// 필수값은 name과 email 이지만 name만 set 한 뒤 쿼리를 수행 할 수 있게 된다.
user.setName("test1");
assertAll(
() -> assertDoesNotThrow(() -> em.persist(user)) // 오류 발생
);
}
위와 같이 수행 하면 컴파일 오류는 발생하지 않기 때문에 코드가 실행될 때 런타임 오류가 발생하게 된다.(DB에는 N-N 설정이 되어있으므로)
Builder 를 이용한 Entity
이번엔 @Builder
어노테이션을 이용해 Entity를 구성해본다.
엔티티는 기본 생성자가 필수이므로 @NoArgsConstructor
가 존재한다. (다만 PROTECTED 레벨로 다른데서 기본생성자를 호출 할 수 없도록 제한하였다.)
@Builder
는 @NoArgsConstructor
와 함께 사용 될 경우 @AllArgsConstructor
가 필요하다.
@AllArgsConstructor
또한 다른데서 호출 할 수 없도록 하려면 PRIVATE 레벨로 지정하면 된다.
접근 제어 레벨에 따라 문제가 생길 수 있다. PRIVATE와 PROTECTED는 유의해서 사용 해야 한다. 여기서는 다루지 않음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity(name = "test_user_builder")
@EntityListeners(AuditingEntityListener.class)
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OnlyBuilderUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 필수 컬럼
@Column(nullable = false, length = 10)
private String name;
@Column(nullable = false, length = 50)
private String email;
@CreatedDate @Column(nullable = false, updatable = false)
private LocalDateTime regDate;
@Builder.Default @LastModifiedDate @Column(nullable = false)
private LocalDateTime upDate = LocalDateTime.now();
// 옵션 컬럼
@Column private int age;
@Column(length = 50)
private String etc;
}
위와 같이 Builder만 사용 할 경우에는 Setter 선언과 동일하게 필수 컬럼을 누락하고 객체를 생성 할 수 있다.
다만 Setter와 달리 컬럼 값을 재설정 할 수 없으니 원치 않는 데이터 변경은 피할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("Builder만 이용한 Entity")
void createUserOnlyBuilder() {
// 컴파일 오류
// OnlyBuilderUser user1 = new OnlyBuilderUser();
// OnlyBuilderUser user2 = new OnlyBuilderUser(1L, "test", "test@test.com", LocalDateTime.now(), LocalDateTime.now(), 10, "etc");
OnlyBuilderUser user = OnlyBuilderUser.builder().build();
assertDoesNotThrow(() -> em.persist(user)); // 필수 값 누락으로 오류 발생
}
Builder 패턴에 필수 값 지정하기
이제 객체 생성 시 필수 값을 지정해서 필수 값의 누락이 없도록 수정 해 보자.
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
32
33
34
35
36
37
@Entity(name = "test_user_valid")
@EntityListeners(AuditingEntityListener.class)
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ValidBuilderUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 필수 컬럼
@Column(nullable = false, length = 10)
private String name;
@Column(nullable = false, length = 50)
private String email;
@CreatedDate @Column(nullable = false, updatable = false)
private LocalDateTime regDate;
@Builder.Default @LastModifiedDate @Column(nullable = false)
private LocalDateTime upDate = LocalDateTime.now();
// 옵션 컬럼
@Column private int age;
@Column(length = 50)
private String etc;
// 기본 빌더를 재정의 하여 필수 값은 매개변수로 무조건 받도록 수정하였다.
public static ValidBuilderUserBuilder builder(
String name,
String email
) {
return new ValidBuilderUserBuilder()
.name(name)
.email(email);
}
}
기존 Builder와 어노테이션은 동일하게 가져가나 builder()
를 재정의 하였다.
필수 컬럼을 매개변수로 받도록 정의하여 더이상 빈 객체를 생성할 수 없도록 한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@DisplayName("필수값 제한 Builder Entity")
void createUserValidBuilder() {
// 컴파일 오류
// ValidBuilderUser user1 = new ValidBuilderUser();
// ValidBuilderUser user2 = new ValidBuilderUser(1L, "test", "test@test.com", LocalDateTime.now(), LocalDateTime.now(), 10, "etc");
// 이전과 달리 빈 객체를 생성할 수 없게 된다.
// ValidBuilderUser user3 = ValidBuilderUser.builder().build();
// 필수 컬럼을 모두 지정해주어야만 ValidBuilderUser 가 생성된다.
ValidBuilderUser user = ValidBuilderUser.builder("tester", "test@test.com").build();
assertAll( // 모든 테스트 성공
() -> assertDoesNotThrow(() -> {
em.persist(user);
}),
() -> assertTrue(() -> em.find(ValidBuilderUser.class, 1L).equals(user))
);
}
더이상 필수 값을 누락하고는 객체를 생성하지 못하도록 컴파일 오류가 발생하게 제한하였다.
이렇게 작업 할 경우 추후 휴먼 오류를 줄일 수 있게 된다.
더티 체킹 기능을 이용하거나 setter가 필요 할 경우 해당 컬럼만 setter 메서드를 생성하는 방법 또는 해당 객체의 builder 패턴 적용 여부를 검토하면 된다.