Post

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 패턴 적용 여부를 검토하면 된다.

This post is licensed under CC BY 4.0 by the author.