1. 글을 작성하게 된 계기
빌더 패턴(Builder Pattern)은 불완전해서 사용할 때 주의해야 한다는 이야기를 많이 합니다. 하지만 이를 보완하는 방법도 존재하는데요, 이를 소개하기 위해 글을 작성하게 되었습니다.
2. 빌더 패턴이 불완전한 이유
빌더 패턴은 객체가 생성되기 전, 필드를 자유롭게 추가 할 수 있으며, 사이드 이펙트 를 예측하기 힘들기 때문에 주의해야 합니다. 예를 들어, 다음과 같이 Shop 클래스의 인스턴스를 빌더 패턴으로 생성해 보겠습니다.
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
@Getter
public class Shop extends BaseEntity {
private final Long id;
private Long userId;
private final String uniqueNumber;
private final String name;
private Shop(
String uniqueNumber,
String name
) {
this(null, uniqueNumber, name);
}
@Builder
private Shop(
Long id,
String uniqueNumber,
String name
) {
this.id = id;
this.uniqueNumber = uniqueNumber;
this.name = name;
}
......
}
Shop 인스턴스가 생성되기 전, 필드를 자유롭게 추가할 수 있습니다. 마치 Setter가 있는 것처럼요. 생성자가 private이어도 @Builder를 사용하면 필드를 추가할 수 있습니다.
1
2
3
4
5
6
7
8
9
public class Main {
public static void main() {
Shop shop = Shop.builder()
.id(1L)
.name("Vips")
.uniqueNumber(UUID.randomUUID().toString())
.build();
}
}
아래와 같이 사용하지 않지만, 이렇게 잘못 사용할 수도 있죠. 즉, 빌더 패턴은 객체가 생성되기 전, 필드를 자유롭게 추가할 수 있으며, 또 언제 어디서 부작용이 발생할지 예측할 수 없기 때문에 사이드 이펙트를 예측하기 어려워집니다. 잘못 사용할 수도 있고요.
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(){
ShopBuilder shopBuilder = Shop.builder()
.id(1L)
.name("Vips");
Shop newShop = shopBuilder.uniqueNumber(UUID.randomUUID().toString())
.build();
}
}
3. 해결 방법
이는 정적 메서드(Static Method) 에 @Builder 를 사용해 문제를 해결할 수 있습니다. 생성자는 private 으로 두고요.
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
38
39
40
41
42
43
44
@Getter
public class Shop extends BaseEntity {
private final Long id;
private Long userId;
private final String uniqueNumber;
private final String name;
// private 생성자
private Shop(
String uniqueNumber,
String name
) {
this(null, uniqueNumber, name);
}
......
/**
* static 메서드에 @Builder 사용
* */
@Builder
public static Shop createShop(
String uniqueNumber,
String name
) {
validate(uniqueNumber, name);
return new Shop(uniqueNumber, name);
}
private static void validate(
String uniqueNumber,
String name
) {
if (uniqueNumber == null || uniqueNumber.isBlank()) {
throw new IllegalArgumentException("Unique number cannot be empty.");
}
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty.");
}
}
......
}
필수 인자 가 누락되면 안되기 때문에 validate 메서드를 사용해 이를 체크해야 합니다. 이때 도메인 규칙이 있다면 이도 함께 검증할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public class Shop extends BaseEntity {
......
@Builder
public static Shop createShop(
String uniqueNumber,
String name
) {
validate(uniqueNumber, name);
return new Shop(uniqueNumber, name);
}
......
}
다만 정적 메서드가 사용되기 때문에 생성자 내부에 들어가는 메서드는 반드시 static으로 사용해야 한다는 단점이 있습니다.
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
@Getter
public class Shop extends BaseEntity {
......
@Builder
public static Shop createShop(
String uniqueNumber,
String name
) {
validate(uniqueNumber, name);
return new Shop(uniqueNumber, name);
}
private static void validate(
String uniqueNumber,
String name
) {
if (uniqueNumber == null || uniqueNumber.isBlank()) {
throw new IllegalArgumentException("Unique number cannot be empty.");
}
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty.");
}
}
......
3. 정리
빌더 패턴은 불완전합니다. 극단적으로 빌더 패턴을 사용하지 말라는 분도 봤는데요, 여기에는 동의하지 못하겠습니다. 정적 메서드를 통해 이를 보완할 수 있으며, 상황에 맞게 사용하면 되니까요. 여튼 이런 방법이 있다는 것도 알고 빌더 패턴을 잘 사용할 수 있으면 합니다.