자바 프로젝트를 진행하면서 Lombok의 @Builder
를 매우 애용하고 있는데, 퍼뜩 Builder 방식을 직접 구현해본다면 좀 더 이해하면서 사용할 수 있지 않을까? 라는 생각에 작성하게 되었다.
public class Company {
private final String name;
private final String address;
private final String description;
public Company(String name, String address, String description) {
this.name = name;
this.address = address;
this.description = description;
}
}
위 클래스에 대한 Builder를 직접 만들어 보자!
먼저 Lombok에서 @Builder
를 선언한 경우를 생각해보니
Company company = Company.builder()
.name("이름")
.address("주소")
.build();
위와 같이 .builder()
static method를 시작으로 .build()
로 마무리 된 것을 확인할 수 있었다.
위 코드를 기반으로 구현 방식을 생각해보니 아래 그림처럼 나타낼 수 있었다.
Company Class에서 .builder()
를 호출하면 CompanyBuilder를 반환하고 CompanyBuilder의 .name()
, .address()
, .description()
을 통해서 값을 지정한 후 .build()
를 통해 Company 객체를 생성 후 반환하는 방식으로 Builder를 구상했다.
먼저 CompanyBuilder 클래스를 생성하고 각 메서드를 생성했다.
public class CompanyBuilder {
private String name;
private String address;
private String description;
protected CompanyBuilder() {}
public void name(String name) {
this.name = name;
}
public void address(String address) {
this.address = address;
}
public void description(String description) {
this.description = description;
}
public Company build() {
return new Company(name, address, description);
}
}
CompanyBuilder class에 대한 직접 생성을 막기 위해 생성자 접근을 protected로 설정하였다.
하지만 위와 같이 코드를 작성하면 .name().address().build()
처럼 메서드가 이어지지 않는다. 그래서 값 지정 메서드에 대해서 객체 자신을 반환하도록 코드를 수정했다.
public class CompanyBuilder {
private String name;
private String address;
private String description;
protected CompanyBuilder() {}
public CompanyBuilder name(String name) {
this.name = name;
return this;
}
public CompanyBuilder address(String address) {
this.address = address;
return this;
}
public CompanyBuilder description(String description) {
this.description = description;
return this;
}
public Company build() {
return new Company(name, address, description);
}
}
위와 같이 코드를 작성하면 .name().address().build()
와 같이 메서드를 이어서 활용할 수 있다.
이제 Company로 돌아가서 코드를 마무리해보자
public class Company {
private final String name;
private final String address;
private final String description;
protected Company(String name, String address, String description) {
this.name = name;
this.address = address;
this.description = description;
}
public static CompanyBuilder builder(){
return new CompanyBuilder();
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public String getDescription() {
return description;
}
}
생성자 생성 방식대신 Builder를 사용할 것이기에 CompanyBuilder class와 마찬가지로 생성자 접근을 protected로 설정하였다.
이제 테스트를 통해서 Builder가 제대로 Company를 생성하는지 테스트 해보자
테스트는 아래와 같이 작성되었다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Test Company Builder")
class CompanyTest {
private static final String NAME = "이름";
private static final String ADDRESS = "주소";
private static final String DESCRIPTION = "설명";
@DisplayName("이름, 주소, 설명이 주어졌을때 Company 생성")
@Test
void testCreateCompanyWithNameAddressDescription() {
// Act
Company actualResult = Company.builder()
.name(NAME)
.address(ADDRESS)
.description(DESCRIPTION)
.build();
// Assert
assertEquals(NAME, actualResult.getName());
assertEquals(ADDRESS, actualResult.getAddress());
assertEquals(DESCRIPTION, actualResult.getDescription());
}
@DisplayName("이름, 주소가 주어졌을때 Company 생성")
@Test
void testCreateCompanyWithNameAddress(){
// Act
Company actualResult = Company.builder()
.name(NAME)
.address(ADDRESS)
.build();
// Assert
assertEquals(NAME, actualResult.getName());
assertEquals(ADDRESS, actualResult.getAddress());
assertNull(actualResult.getDescription());
}
}
결과
Builder를 직접 구상하고 구현해보니 왜 Lombok의 @Builder
를 class에서 사용할때 @AllArgsConstructor
를 요구하는지에 대해서 이해할 수 있었다.
포스트에서 구현하는 방식은 생성자의 모든 args를 필요로 했지만 만약 @Builder
를 생성자에 사용하는 것처럼 Builder에서 사용하고 싶은 args를 제한하고자 하는 경우에는 Builder Class의 값들을 제한하면 될 것이다.
어노테이션에 대해서는 공부를 더 진행해서 Lombok처럼 @Builder
어노테이션을 직접 만들어 보는것도 앞으로 이해하는데 더 도움이 될 것같다!