티스토리 뷰
저번 글에 이어서 Effective Java 스타일의 Builder 패턴과 @Builder
어노테이션에 대해 알아보자.
먼저 이번 글에서 계속해서 사용할 예제 클래스는 다음과 같다.
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Home {
private String basement;
private String structure; //required
private int door; //required
private int window; //required
private String roof; //required
private String interior;
}
Effective Java의 Builder Pattern
앞의 글과는 다르게 Home
클래스 객체를 생성하는 Builder
클래스를 Home
의 내부 클래스로 구현해 볼 것이다. 일반적으로 Builder
클래스의 이름은 생성하고자 하는 객체의 클래스 이름(여기서는 Home) + -Builder 형식으로, 이 경우에는 HomeBuilder
가 된다. HomeBuilder
클래스를 생성하는 방법은 다음과 같다.
1. Home
클래스의 모든 필드를 가지는 HomeBuilder
클래스를 Home
클래스 내부에 static으로 정의한다.
2. HomeBuilder
에 각 필드의 이름을 메소드 이름으로 가지는 setter를 정의한다. 이때 각 메소드에서는 chaining을 위해 자기 자신(HomeBuilder
)을 반환한다.
3. 마지막으로 생성된 Home
객체를 반환하는 build()
메소드를 정의한다.
@AllArgsConstructor
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
public static class HomeBuilder {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
public HomeBuilder basement(String basement) {
this.basement = basement;
return this;
}
public HomeBuilder structure(String structure) {
this.structure = structure;
return this;
}
public HomeBuilder door(int door) {
this.door = door;
return this;
}
public HomeBuilder window(int window) {
this.window = window;
return this;
}
public HomeBuilder roof(String roof) {
this.roof = roof;
return this;
}
public HomeBuilder interior(String interior) {
this.interior = interior;
return this;
}
public Home build() {
return new Home(basement, structure, door, window, roof, interior);
}
}
}
이후 다음과 같이 Home
객체 생성이 가능하다. @Builder
어노테이션을 한번이라도 써본 적이 있다면 익숙한 형태의 코드일 것이다.
public class Main {
public static void main(String[] args) {
Home home = new Home.HomeBuilder()
.basement("basement")
.structure("structure")
.door(1)
.window(4)
.roof("roof")
.interior("interior")
.build();
}
}
앞선 글에서 구현했던 빌더 패턴과 굳이 비교하자면, Builder
클래스를 내부 클래스로 정의하여 전체 코드의 복잡성을 그나마 줄였으며 각 setter 메소드에서 자기 자신을 반환함으로써 객체 생성 코드를 chaining하는 것이 가능하다.
여기서 structure
, door
, window
, roof
필드는 필수적이다. 하지만 위의 HomeBuilder
에서 해당 필드에 대한 setter를 호출하지 않으면 각 필드는 null이 된다. 다음과 같이 HomeBuilder
생성자를 정의하면 structure
, door
, window
, roof
필드에 대한 설정을 강제할 수 있다.
@AllArgsConstructor
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
public static class HomeBuilder {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
public HomeBuilder(String structure, int door, int window, String roof) {
this.structure = structure;
this.door = door;
this.window = window;
this.roof = roof;
}
public HomeBuilder basement(String basement) {
this.basement = basement;
return this;
}
public HomeBuilder interior(String interior) {
this.interior = interior;
return this;
}
public Home build() {
return new Home(basement, structure, door, window, roof, interior);
}
}
}
public class Main {
public static void main(String[] args) {
Home home = new Home.HomeBuilder("structure", 1, 4, "roof")
.basement("basement")
.interior("interior")
.build();
}
}
Lombok의 @Builder 어노테이션
Builder Pattern을 적용하고 싶은 모든 클래스마다 내부에 -Builder 클래스를 매번 정의해주기도 힘들고 해당 클래스의 복잡도 또한 높아진다. 어떻게 보면 -Builder 클래스를 정의하는 것은 반복적인 작업에 가까운데... 이 귀찮은 일을 대신해주는 아주 편리한 어노테이션이 있다. 바로 @Builder
어노테이션이다. 이제 Home
클래스에 @Builder
어노테이션을 추가해보자.
@Builder
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
}
이렇게 @Builder
어노테이션 하나만으로 위에서는 직접 정의한 HomeBuilder
를 대체할 수있다.
컴파일된 Home.class 파일은 다음과 같다. 어떠한가? 위에서 직접 정의한 HomeBuilder
와 거의 똑같다. 다른 부분은 HomeBuilder
생성자의 접근 제한자를 protected로 바꾸고 Home
클래스의 static 메소드인 builder()
를 통해 HomeBuilder
객체를 생성한다는 점이다.
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
Home(final String basement, final String structure, final int door, final int window, final String roof, final String interior) {
this.basement = basement;
this.structure = structure;
this.door = door;
this.window = window;
this.roof = roof;
this.interior = interior;
}
public static Home.HomeBuilder builder() {
return new Home.HomeBuilder();
}
public static class HomeBuilder {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
HomeBuilder() {
}
public Home.HomeBuilder basement(final String basement) {
this.basement = basement;
return this;
}
public Home.HomeBuilder structure(final String structure) {
this.structure = structure;
return this;
}
public Home.HomeBuilder door(final int door) {
this.door = door;
return this;
}
public Home.HomeBuilder window(final int window) {
this.window = window;
return this;
}
public Home.HomeBuilder roof(final String roof) {
this.roof = roof;
return this;
}
public Home.HomeBuilder interior(final String interior) {
this.interior = interior;
return this;
}
public Home build() {
return new Home(this.basement, this.structure, this.door, this.window, this.roof, this.interior);
}
}
}
그러나 문제가 있다. 필수적인 필드인 structure
, door
, window
, roof
에 대한 setter를 호출하지 않으면 이 또한 null 값이 설정된다. 우리가 직접 Builder를 정의했던 경우에는 HomeBuilder
생성자의 매개변수로 해당 필드들을 넘기도록 하였는데 @Builder
어노테이션을 사용할 때에는 어떻게 해야 할까?
@Builder에서 필수적인 필드 설정 강제하기
아주 간단하게 required한 필드에 @NonNull
어노테이션을 붙여줌으로써 builder()
메소드에서 해당 필드에 대한 setter를 호출하도록 유도할 수는 있다. 하지만 이 또한 컴파일 오류가 아닌 NPE(NullPointerException), 즉 실행 오류가 발생한다. 일반 생성자와 같이 필드 설정을, 즉 필드를 매개변수로 넘겨주는 것을 강제하고 싶다면 @Builder
어노테이션에 의해 생성되는 builder()
메소드를 오버로딩해주면 된다.
@Builder
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
public static HomeBuilder builder(String structure, int door, int window, String roof) {
return new HomeBuilder()
.structure(structure)
.door(door)
.window(window)
.roof(roof);
}
}
이렇게 builder()
메소드를 오버로딩해주면 컴파일된 Home.class 파일에는 다음의 builder()
메소드 하나만이 존재하게 되어 필수적인 필드들에 대한 설정을 강제할 수 있다.
public static Home.HomeBuilder builder(String structure, int door, int window, String roof) {
return (new Home.HomeBuilder()).structure(structure).door(door).window(window).roof(roof);
}
public static void main(String[] args) {
Home home = Home.builder("structure", 1, 4, "roof")
.basement("basement")
.interior("interior")
.build();
}
@Builder의 설정값
@Builder
어노테이션에 대해 좀 더 자세히 알아보는 것을 끝으로 글을 마무리하려고 한다. @Builder
어노테이션이 정의된 파일을 보면 다음과 같이 @Target
이 TYPE
, METHOD
, CONSTRUCTOR
로 설정되어 있는 것을 알 수 있다. 따라서 @Builder
어노테이션은 클래스, 메소드, 생성자 위에 위치시킬 수 있다.
다음과 같이 일부 필드만을 매개변수로 받는 생성자 위에 @Builder
를 위치시키면, 생성되는 HomeBuilder
를 통해서는 structure
, door
, window
, roof
필드값만을 설정할 수 있다.
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
@Builder
public Home(String structure, int door, int window, String roof) {
this.structure = structure;
this.door = door;
this.window = window;
this.roof = roof;
}
}
반면 메소드에 @Builder
가 붙으면 VoidBuilder
라는 이름의 빌더가 자동으로 생성된다.
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
private List<String> rooms; //추가
@Builder
public void addRooms(List<String> rooms) {
if (this.rooms == null) this.rooms = new ArrayList<>();
this.rooms.addAll(rooms);
}
}
public class Home {
private String basement;
private String structure;
private int door;
private int window;
private String roof;
private String interior;
private List<String> rooms;
public Home() {
}
public void addRooms(List<String> rooms) {
if (this.rooms == null) {
this.rooms = new ArrayList();
}
this.rooms.addAll(rooms);
}
public Home.VoidBuilder builder() {
return new Home.VoidBuilder();
}
public class VoidBuilder {
private List<String> rooms;
VoidBuilder() {
}
public Home.VoidBuilder rooms(final List<String> rooms) {
this.rooms = rooms;
return this;
}
public void build() {
Home.this.addRooms(this.rooms);
}
}
}
위와 같은 컴파일된 Home.class가 생성되는데 딱히 메소드에 @Builder
어노테이션을 사용할 일은 없을 것 같으므로 참고로만 알아두자.
정말 마지막으로 @Builder
어노테이션의 각 속성과 의미는 다음과 같다.
builderMethodName
Builder 클래스 객체를 반환하는 메소드 이름, 기본값 builder
buildMethodName
생성된 객체를 반환하는 Builder 클래스 내 메소드 이름, 기본값 build
builderClassName
Builder 클래스 이름, 기본값 생성하는 객체의 클래스 이름 + -Builder
toBuilder
toBuilder() 메소드 자동 생성 여부, 기본값 false
//Home 클래스의 메소드
public Home.HomeBuilder toBuilder() {
return (new Home.HomeBuilder()).basement(this.basement).structure(this.structure).door(this.door).window(this.window).roof(this.roof).interior(this.interior);
}
access
Builder 클래스, builder(), build(), setter 메소드 등의 접근 제한자 설정, 기본값 PUBLIC
setterPrefix
setter 메소드의 prefix 설정
끝.
참고
https://stackoverflow.com/questions/29885428/required-arguments-with-a-lombok-builder
'자바 > 개념' 카테고리의 다른 글
[디자인 패턴] Facade 패턴 (1) | 2023.12.17 |
---|---|
[디자인 패턴] Strategy Pattern과 Template Callback Pattern (0) | 2023.09.26 |
[디자인 패턴] Template Method Pattern (0) | 2023.09.19 |
[개념] FrontController 패턴 (with 자바) (0) | 2023.08.27 |
[개념] Builder Pattern과 자바의 @Builder 어노테이션1 (0) | 2023.02.13 |
- Total
- Today
- Yesterday
- 템플릿 콜백 패턴
- spring
- spring aop
- Gitflow
- Transaction
- 서블릿 컨테이너
- JPA
- 디자인 패턴
- 전략 패턴
- QueryDSL
- servlet filter
- Git
- Linux
- C++
- 모두의 리눅스
- Front Controller
- github
- Spring Security
- 단위 테스트
- Java
- facade 패턴
- spring boot
- junit5
- Assertions
- mockito
- SSE
- FrontController
- vscode
- rest api
- ParameterizedTest
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |