Spring | Lombok @Builder

yeonk·2023년 5월 8일
1

spring & spring boot

목록 보기
9/10
post-thumbnail

개요


우아한테크코스 레벨2 장바구니 미션을 진행하면서 Lombok의 @Builder 어노테이션을 사용해봤다. 페어가 제안해서 사용해보게 되었는데, 가독성 측면에서 생성자를 사용하는 것보다 좋은 것 같다는 생각이 들어 적용하게 되었다.

잘 모르는 상태로 적용한 것 같아서 @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 */ }
    }
}



위 코드에서는 생성된 메서드들에 대한 구현 내용 확인이 어렵지만, 정리하자면 빌더를 통해 객체를 생성하는 방법은 아래와 같다.

  1. builder() 메서드로 빌더 클래스의 인스턴스 생성
  2. 빌더 클래스에서는 build() 메서드 실제 생성자를 호출하여 객체를 생성





그럼 이제 적용할 수 있는 옵션을 확인해보자.




builderMethodName

  • 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

  • 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

builderClassName() 을 통해 빌더의 클래스 이름을 지정할 수 있다. > 기본 값은 return type + 'Builder' 이다.

아래와 같이 builderClassName 을 통해 빌더의 클래스 이름을 지정할 수 있다.

@Builder(builderClassName = "JoyProductBuilder") // 변경된 코드
public class Product {
	// ...
}

이번에도 빌드 파일을 살펴보면 변경한 대로 적용된 것을 확인할 수 있다.

public class Product {
	// ...

    public static cart.domain.Product.JoyProductBuilder builder() { /* compiled code */ }
	
    // ...
}





toBuilder

해당 옵션을 사용하면 기존에 구성한 빌더를 기반으로 새로운 객체를 재 구성 할 수 있다.

아래와 같이 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
생성자에 매개변수가 많다면 빌더를 고려하라

0개의 댓글