Programming/Java

java @Builder 기능 더 활용하기(toBuilder, @Singular 등)

Jan92 2023. 8. 5. 13:44

java @Builder 기능 더 활용하기 toBuilder(), @Singular, @Builder.Default

java @Builder

java에서는 클래스를 객체화하기 위해 '점층적 생성자 패턴(Telescoping Constructor Pattern)'의 안전성과 '자바 빈즈 패턴(Java Beans Pattern)'의 가독성을 더한 '빌더 패턴(Builder Pattern)'을 주로 사용하는데요.

 

@Builder  // 외 생략
public class Order {

    ...
}

위 코드와 같이 Lombok을 사용하면 직접 빌더 관련 코드를 구현할 필요 없이 @Builder 어노테이션만 적용하여 빌더 패턴을 사용할 수 있으며, 대부분 이러한 방식으로 빌더를 자주 사용하고 계실 거라고 생각됩니다.

 

해당 포스팅에서는 기본적인 builder의 기능을 조금 더 활용할 수 있는 toBuilder(), @Singular, @Builder.Default에 대해서 살펴보겠습니다.

 

 


1. toBuilder()

@Builder(toBuilder = ture)

@Builder 어노테이션에는 다음과 같이 'toBuilder' 속성을 지정할 수 있는데요. (default = false)

해당 속성을 지정하게 되면 아래와 같이 toBuilder() 메서드를 사용할 수 있으며, 해당 메서드는 기존 객체 인스턴스의 값으로 초기화되는 필더를 가져옵니다.

 

때문에 toBuilder() 메서드는 아래와 같이 기존의 인스턴스를 기반으로 builder를 사용하여 일부 값을 변경할 때 활용할 수 있습니다.

 

@Getter
@Builder(toBuilder = true)
@AllArgsConstructor
public class Order {

    private String orderNumber;
    private String productName;
    private Long totalPrice;
}

(예시를 위한 Order class)

 

//builder를 통한 객체 인스턴스 생성
Order order = Order.builder()
    .orderNumber("order123")
    .productName("something")
    .totalPrice(1000L)
    .build();

System.out.println("order: " + order);
//order: Order(orderNumber=order123, productName=something, totalPrice=1000)

//toBuilder() 메서드를 사용했을 때 OrderBuilder를 반환
Order.OrderBuilder orderBuilder = order.toBuilder();

//toBuilder()
Order updateOrder = order.toBuilder()
    .totalPrice(2000L)
    .build();

System.out.println("updateOrder: " + updateOrder);
//updateOrder: Order(orderNumber=order123, productName=something, totalPrice=2000)

(toBuilder 사용 예시)

 

 


2. @Singular

@Getter
public class Order {

    private String orderNumber;
    private String productName;
    private Long totalPrice;
    private List<String> stringList;
    private Map<String, String> stringMap;

    @Builder
    public Order(String orderNumber, String productName, Long totalPrice,
                 @Singular("stringListItem") List<String> stringList,
                 @Singular("stringMapItem") Map<String, String> stringMap) {
        this.orderNumber = orderNumber;
        this.productName = productName;
        this.totalPrice = totalPrice;
        this.stringList = stringList;
        this.stringMap = stringMap;
    }

(예시를 위한 Order class)

 

Order order = Order.builder()
    .orderNumber("order123")
    .productName("something")
    .totalPrice(1000L)

    //@Singular("stringListItem")
    .stringListItem("list1")
    .stringListItem("list2")
    .stringListItem("list3")

    //@Singular("stringMapItem")
    .stringMapItem("key1", "value1")
    .stringMapItem("key2", "value2")
    .stringMapItem("key3", "value3")
    .build();
}

(@Singular 사용 예시)

 

lombok v1.16.8부터 추가된 '@Singular' 어노테이션은 @Builder와 함께 사용되며, 위 예시와 같이 Collection에 대한 단일 요소를 추가하는 메서드를 작성하기 위해 적용됩니다.

 

 


3. @Builder.Default

@ToString
@Getter
@Builder
@AllArgsConstructor
public class Order {

    private String orderNumber;
    private String productName;
    private Long totalPrice;
    private int quantity;
}

(Order class)

 

Order order = Order.builder().build();
System.out.println("order: " + order);
//order: Order(orderNumber=null, productName=null, totalPrice=null, quantity=0)

builder는 값을 설정하지 않으면 기본적으로 null 또는 0을 채워주는데요.

이때 직접 기본값을 설정해주고 싶다면 아래 예시와 같이 '@Builder.Default'를 사용할 수 있습니다.

(Default는 lombok v1.16.16에서 추가된 기능입니다.)

 

@ToString
@Getter
@Builder
@AllArgsConstructor
public class Order {

    private String orderNumber;
    private String productName;
    @Builder.Default
    private Long totalPrice = 100L;
    @Builder.Default
    private int quantity = 1;
}

(@Builder.Default 사용 예시)

 

Order order = Order.builder().build();
System.out.println("order: " + order);
//order: Order(orderNumber=null, productName=null, totalPrice=100, quantity=1)

(기본 값이 설정되어 출력되는 것을 확인할 수 있습니다.)

 

 

***

하지만 현재 @Builder.Default 사용 시에 아래와 같은 문제점도 있는데요.

컴파일된 Order.class

위 예시의 Order 클래스와 같이 @Builder 어노테이션을 클래스 레벨에서 적용했을 때, 컴파일된 Order.class를 살펴보면 다음과 같이 Default 기능이 적용된 필드에 대해 set 여부를 확인하여 set이 되지 않은 경우 기본 값을 적용해 주는 것을 볼 수 있는데요.

 

 

@ToString
@Getter
public class Order {

    private String orderNumber;
    private String productName;
    @Builder.Default
    private Long totalPrice = 100L;
    @Builder.Default
    private int quantity = 1;

    @Builder
    public Order(String orderNumber, String productName, Long totalPrice, int quantity) {
        this.orderNumber = orderNumber;
        this.productName = productName;
        this.totalPrice = totalPrice;
        this.quantity = quantity;
    }
}

(Default 기능이 작동하지 않는 경우)

 

컴파일된 Order.class

order: Order(orderNumber=null, productName=null, totalPrice=null, quantity=0)

(값을 세팅하지 않고 builder를 사용한 인스턴스 생성 결과)

Order 클래스에 @Builder를 다음과 같이 사용했을 때는 위에서 컴파일된 Order.class와 다르게 set 여부 확인 및 기본 값을 넣어주는 부분이 없다는 것을 볼 수 있었는데요.

 

해당 문제에 대해서는 아래 lombok project의 github에서도 관련 내용이 계속해서 이슈가 되고 있다는 것을 찾아볼 수 있었습니다.

https://github.com/projectlombok/lombok/issues/1347

 

 

 

 

< 관련 자료 >

2021.08.22 - [Programming/Java] - Java 빌더 패턴 (Builder Pattern)이란?

2021.08.21 - [Programming/Java] - (Java) 점층적 생성자 패턴 & 자바 빈즈 패턴