우아한테크코스 레벨2 장바구니 미션을 진행하면서 Lombok의 @Builder
어노테이션을 사용해봤다. 페어가 제안해서 사용해보게 되었는데, 가독성 측면에서 생성자를 사용하는 것보다 좋은 것 같다는 생각이 들어 적용하게 되었다.
잘 모르는 상태로 적용한 것 같아서 @Builder
어노테이션이 어떤 원리로 동작하는지, 어떤 옵션이 있는지 궁금해서 정리해보려고 한다!
Lombok
에서 제공하는 어노테이션- 생성자 인자를 메서드 체인을 통해 명시적으로 대입하여 생성자를 호출할 수 있게 빌더 클래스를 생성해주는 역할을 한다.
- 생성자나 정적 팩토리 메서드를 사용하여 객체를 생성할 때, 오기입으로 인해 발생하는 문제 발생 확률을 낮출 수 있고 순서에 종속적이지 않기 때문에 편리하게 사용할 수 있다.
클래스가 아닌 멤버에 어노테이션을 사용하는 경우 생성자 또는 메서드에 사용한다.
클래스에 어노테이션을 사용하는 경우, 모든 필드를 인자로 사용하여 패키지 비공개 생성자가 생성된다(@AllArgsConstructor(access = AccessLevel.PACKAGE)
).
@NoArgsConstructor
, @RequiredArgsConstructor
어노테이션을 사용하고 @AllArgsConstructor
를 함께 사용하지 않으면, 컴파일러 오류가 발생할 수 있다. 그 이유는 모든 인수 생성자가 있다고 가정하고 이를 사용하는 코드를 생성하므로 이 생성자가 없으면 오류가 발생하는 것이다.
매개변수가 적은 경우에는 생성자를 사용하는 것이 좋을 수 있다 (하지만 개인적인 의견으로는 변경 가능성을 생각한다면?🤔).
해당 어노테이션을 사용하기 위해서는 아래와 같이 build.gradle
에 의존성을 추가해야 한다.
dependencies {
// ...
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
사용 방법을 알아보기 위해 장바구니 미션에서 @Builder
를 사용한 코드 중 Product
클래스를 가져와봤다.
아래와 같이 @Builder
만 붙여주면 빌더를 사용할 수 있다!
package cart.domain;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Product {
private final String name;
private final int price;
private final String imageUrl;
}
객체를 생성할 때는 아래와 같이 사용할 수 있다.
@Service
@Transactional
@RequiredArgsConstructor
public class ProductService {
private final ProductDao productDao;
public ProductDto createProduct(String name, int price, String imageUrl) {
// builder 사용
Product product = Product.builder()
.name(name)
.price(price)
.imageUrl(imageUrl)
.build();
// ... 기타 코드
}
빌드 파일을 살펴보면 package private
생성자가 생성된 것을 확인할 수 있고, ProductBuilder
를 반환하는 builder()
가 생성된 것을 볼 수 있다.
그리고 그 아래 보면 정적 중첩 클래스로 ProductBuilder
가 자동 생성된 것을 볼 수 있고, Product
의 필드를 동일하게 가지는 것도 확인할 수 있다. 그리고 각 필드에 대한 setter 메서드도 생성된 것을 확인할 수 있다.
package cart.domain;
public class Product {
private final java.lang.String name;
private final int price;
private final java.lang.String imageUrl;
Product(java.lang.String name, int price, java.lang.String imageUrl) { /* compiled code */ }
public static cart.domain.Product.ProductBuilder builder() { /* compiled code */ }
public java.lang.String getName() { /* compiled code */ }
public int getPrice() { /* compiled code */ }
public java.lang.String getImageUrl() { /* compiled code */ }
public static class ProductBuilder {
private java.lang.String name;
private int price;
private java.lang.String imageUrl;
ProductBuilder() { /* compiled code */ }
public cart.domain.Product.ProductBuilder name(java.lang.String name) { /* compiled code */ }
public cart.domain.Product.ProductBuilder price(int price) { /* compiled code */ }
public cart.domain.Product.ProductBuilder imageUrl(java.lang.String imageUrl) { /* compiled code */ }
public cart.domain.Product build() { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
}
}
위 코드에서는 생성된 메서드들에 대한 구현 내용 확인이 어렵지만, 정리하자면 빌더를 통해 객체를 생성하는 방법은 아래와 같다.
builder()
메서드로 빌더 클래스의 인스턴스 생성build()
메서드 실제 생성자를 호출하여 객체를 생성그럼 이제 적용할 수 있는 옵션을 확인해보자.
- builderMethodName을 사용하여
builder()
메서드명을 변경할 수 있다.- 기본 값은 "builder" 이다.
아래와 같이 builderMethodName
을 직접 지정하여 builder()
메서드명을 변경할 수 있다.
@Builder(builderMethodName = "newBuilder") // 추가한 코드
public class Product {
// ...
}
builder 메소드 이름을 변경하면, 사용할 때 역시 지정한 이름의 메소드를 사용해야 한다.
public ProductDto createProduct(String name, int price, String imageUrl) {
Product product = Product.newBuilder() // 지정한 이름으로 builder 메소드를 사용한다.
.name(name)
.price(price)
.imageUrl(imageUrl)
.build();
// ...
}
빌드 파일을 살펴보면 지정한 이름대로 적용된 것을 확인할 수 있다.
public class Product {
// ...
// 지정한 이름이 적용된 것을 확인할 수 있는 부분
public static cart.domain.Product.ProductBuilder newBuilder() { /* compiled code */ }
// ...
}
- buildMethodName 를 이용하여
build()
메서드 명을 변경할 수 있다.
기본 값은 "build" 이다.
아래와 같이 buildMethodName
을 직접 지정하여 build()
메서드 명을 변경할 수 있다.
@Builder(buildMethodName = "joyBuild") // 추가된 코드
public class Product {
// ...
}
build 메소드 이름을 변경하면, 사용할 때 역시 지정한 이름의 메소드를 사용해야 한다.
public ProductDto createProduct(String name, int price, String imageUrl) {
Product product = Product.builder()
.name(name)
.price(price)
.imageUrl(imageUrl)
.joyBuild(); // 지정한 이름으로 build 메소드를 사용한다.
// ...
}
빌드 파일을 살펴보면 지정한 이름대로 적용된 것을 확인할 수 있다.
public class Product {
// ...
public static class ProductBuilder {
// ...
public cart.domain.Product joyBuild() { /* compiled code */ } // 지정한 이름이 적용된 부분
// ...
}
}
builderClassName()
을 통해 빌더의 클래스 이름을 지정할 수 있다. > 기본 값은return type + 'Builder'
이다.
아래와 같이 builderClassName
을 통해 빌더의 클래스 이름을 지정할 수 있다.
@Builder(builderClassName = "JoyProductBuilder") // 변경된 코드
public class Product {
// ...
}
이번에도 빌드 파일을 살펴보면 변경한 대로 적용된 것을 확인할 수 있다.
public class Product {
// ...
public static cart.domain.Product.JoyProductBuilder builder() { /* compiled code */ }
// ...
}
해당 옵션을 사용하면 기존에 구성한 빌더를 기반으로 새로운 객체를 재 구성 할 수 있다.
아래와 같이 toBuilder
옵션을 boolean 값으로 지정할 수 있다.
@Builder(toBuilder = true)
public class Product {
// ...
}
빌드 파일을 살펴보면 toBuilder
를 true로 하면 toBuilder()
메서드를 생성한다는 것을 알 수 있다.
public class Product {
// ...
public cart.domain.Product.ProductBuilder toBuilder() { /* compiled code */ }
// ...
}
}
아래와 같이 사용할 수 있다. product1
의 구성에서 imageUrl
속성만 변경해서 새로운 객체를 생성할 수 있는 것이다.
public ProductDto createProduct(String name, int price, String imageUrl) {
// builder 사용
Product product1 = Product.builder()
.name(name)
.price(price)
.imageUrl(imageUrl)
.build();
Product product2 = product1.toBuilder()
.imageUrl(newImageUrl)
.build();
// ... 기타 코드
}
access
옵션으로 accessLevel을 지정할 수 있다.setterPrefix
옵션으로 setter의 Prefix 를 변경할 수 있다. 아래는 두 옵션을 사용한 예이다.
@Builder(access = AccessLevel.PRIVATE, setterPrefix = "set")
@Builder
에 대해서 간략하게 알아보고, 사용할 수 있는 옵션들에 대해 확인해보았다. 해당 옵션들에 대한 존재와 사용하는 방법은 알았으나, 어느 시점에 용이하게 사용할 수 있는지는 아직 잘 모르겠다...
어쨌든 정리해본다면 매개변수가 적고 변경 가능성이 적은 경우에는 생성자나 정적 팩토리 메서드를 사용하는 것이 좋을 것 같다. 하지만 매개 변수가 많거나 조금이라도 변경 가능성이 있다면 @Builder
어노테이션을 사용해서 장점을 취하는 것이 좋을 것 같다.
만약 @Builder
를 사용하고자 한다면, 몇 가지 주의점을 고려하여 작성해야할 것 같다.
@Builder
[Java] 생성자 패턴 - Builder() 심화 속성 이해하기 : Lombok Annotation
빌더 패턴(Builder pattern)을 써야하는 이유, @Builder
[JAVA] @Builder 동작 원리, @Builder.Default, @Singular
생성자에 매개변수가 많다면 빌더를 고려하라