toBuilder를 사용하기 전까지는 따로 method를 사용해서 기존 내용을 수정하고 Dirth Checking을 통해 DB내용을 수정하는 방식을 사용하였다.
하지만 이 방식은 Setter메서드를 사용하는 방식이기 때문에 누군가 이 setter를 사용해서 값을 변경할 수도 있고, 안전하다고 느껴지지 않았다. 그래서 다른 방식을 찾아보고 @Builder에서 toBuilder를 속성으로 설정하면 해당 엔티티의 값을 편하게 수정 할 수 있다는 것을 찾아냈다!
우선 Builder 어노테이션에 대한 개념과 특징을 살펴보고 toBuilder 사용 방법을 알아보자.
Builder란?
우선 우리가 자주 사용하는 보일러 플레이트 메서드(직접 코드를 작성하지 않아도 대신 작성해주는 메서드)인 Getter,Setter,Constructor 등이 있다. Setter 같은 메서드는 값을 변경시키는 목적을 알 수 있다는 조언에 따라 사용을 지양하며, @Getter, @RequiredArgsConstructor, @AllArgsContsructor, @NoArgsContructor 정도를 사용하고 있는 것 같다.
그리고 요즘 가장 많이 쓰는 @Builder 어노테이션은 클래스 단위에 사용하거나 생성자에 붙여 사용하면 파라미터를 사용하여 빌더 패턴을 자동으로 생성한다. 매우 편하지만 원리를 알고 써야겠다 생각하여 이렇게 글을 작성한다.
우선 lombok의 공식문서를 살펴보자
If a member is annotated, it must be either a constructor or a method. If a class is annotated, then a package-private constructor is generated with all fields as arguments (as if
@AllArgsConstructor(access = AccessLevel.PACKAGE)
is present on the class), and it is as if this constructor has been annotated with
@Builder
instead. Note that this constructor is only generated if you haven't written any constructors and also haven't added any explicit
번역하자면 @Builder 어노테이션을 붙이면 모든 필드를 매개변수로 받는 package-private 생성자가 자동으로 생성되며 이 생성자에 @Builder 어노테이션을 붙인 것과 동일하게 동작한다. 즉 클래스 레벨도 중간 변환을 통해 생성자 레벨로 변환되어 동작한다는 것이었다.
예시를 하나 들어보자.
@Builder
public class BuildMe {
private String username;
private int age;
}
이렇게 하나의 클래스에 필드를 생성하고 @Builder 어노테이션을 적용해보자. 해당 파일이 컴파일되면 아래와 같은 클래스로 변환되는 것을 알 수 있다.
public class BuildMe {
private String username;
private int age;
BuildMe(String username, int age) {
this.username = username;
this.age = age;
}
public static BuildMe.BuildMeBuilder builder() {
return new BuildMe.BuildMeBuilder();
}
public static class BuildMeBuilder {
private String username;
private int age;
BuildMeBuilder() {
}
public BuildMe.BuildMeBuilder username(String username) {
this.username = username;
return this;
}
public BuildMe.BuildMeBuilder age(int age) {
this.age = age;
return this;
}
public BuildMe build() {
return new BuildMe(this.username, this.age);
}
public String toString() {
return "BuildMe.BuildMeBuilder(username=" + this.username + ", age=" + this.age + ")";
}
}
}
자바에서 BuildMe 클래스에는 public이 아닌 package-private 생성자가 생성되었다. Java에서는 접근제어자가 적혀있지 않을경우 default로 package 접근제한자 레벨로 동작한다.
finla 키워드를 갖는 변수가 있다면?
만약 필드에 final인 변수가 있다면 해당 필드는 Builder의 동작 특성과 맞지 않기때문에 setter가 생기지 않는다. 다시 말해 private, non-final, non-static 속성을 갖기 떄문이다. 한 번 설정한 속성을 여러번 호출하여 다시 설정해야 하기 때문에 final 키워드는 적합하지 않다.
그래서 final인 keyword 역시 빌더에서는 일반 전역 변수로 존재하게 되고 생성자로 실제 클래스의 기본값인 null로 전달되기 때문에 초기화에는 문제가 없다.
만약 초기화에 문제가 있다면 lombok의 @Builder.Default 속성을 사용하거나 선언 시점에 또는 생성자에서 초기화 하는 것이 좋다.
클래스의 필드와 동일한 필드를 가지고 필드 이름의 setter 메서드를 제공하는 빌더 클래스(BuildMe.BuildMeBuilder)가 자동으로 생성된 것을 볼 수 있다.
이미 생성한 생성자가 있다면?
Note that this constructor is only generated if you haven't written any constructors and also haven't added any explicit @XArgsConstructor annotations. In those cases, lombok will assume an all-args constructor is present and generate code that uses it; this means you'd get a compiler error if this constructor is not present.
해당 생성자가 클래스 레벨에서 요구하는 모든 필드를 주입하는 생성자가 아니더라도 lombok은 모든 필드를 생성하는 생성자라 가정하고 빌더 코드를 작성하기 때문에 컴파일 오류가 발생할 수 있다.
생성자에 Builder를 붙이면 클래스 단위와 어떻게 다를까?
public class BuildMe {
private String username;
private int age;
@Builder
public BuildMe(String username, int age) {
this.username = "Mr/Mrs. " + username;
this.age = age + 1;
}
}
이번에는 예외를 발생시키지 않고 정상적으로 생성된다. 생성자 로직에서는 어떤 일을 하든지 간에 빌더 클래스는 단순히 값을 갖고 있다가 생성자에 주입시키는 방식으로 작동된다.
The TBuilder class contains 1 method for each parameter of the annotated constructor / method (each field, when annotating a class), which returns the builder itself.
클래스 단위와 차이가 있다면 클래스에서는 무조건 모든 필드를 매개변수로 빌더 메서드를 생성한다면 생성자 레벨에서는 생성자 파라미터의 필드에 대해서만 빌더 메서드를 작성한다.
Singular 어노테이션?
@Singular 어노테이션을 사용하면 리스트나 셋 같은 컬렉션 객체를 Builder 패턴으로 다룰 때 리스트 객체 자체를 넘기는게 아니라 해당 리스트에 요소를 추가하는 방식으로 생성할 수 있다.
모든 자료구조를 지원하는 것은 아니지만 Java의 List,Set 등 자주 사용하는 자료구조에 대해서는 적용할 수 있는 것 같다.
@Builder
@Getter
public class BuildMe {
private String username;
private int age;
@Singular("alias") private List<String> alias;
}
아래처럼 List에 요소를 하나씩 추가할 수 있다.
BuildMe userA = BuildMe.builder()
.age(25)
.username("USER_A")
.alias("ALIAS1")
.alias("ALIAS2")
.alias("ALIAS3").build();
userA.getAlias().forEach(System.out::println);
toBuilder 어노테이션?
해당 속성을 지정하게되면 해당 메서드는 기존 객체 인스턴스의 값으로 초기화 되는 필더를 가져온다.
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor
public class Order {
private String orderNumber;
private String productName;
private Long totalPrice;
}
위 엔티티를 변경하는 로직을 실행시켜 본다.
//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를 사용하면서 주의할 점은 id값을 초기화 하면 새로운 엔티티가 생길수 있다는 점이다. toBuilder는 값을 초기화 시켜버리기 때문에 toBuilder로 작성한 값을 repository에 save메서드를 사용하면 새로운 객체가 생성된다.
참고자료
'Spring' 카테고리의 다른 글
[Spring/ 환경변수 설정] 로컬에서 IntelliJ 환경변수 설정하기 (0) | 2024.08.12 |
---|---|
[Spring- LogBack] LogBack과 Log4j2 중 어떤걸 사용할까? (0) | 2024.08.03 |
[Spring] AOP의 개념, AsertJ와 차이점, 실습해보기 (1) | 2024.06.10 |
[Spring] Spring Batch - 일정한 시간에 회원 유효성 검사하기 (0) | 2024.06.10 |
Spring Framework - init-param의 뜻? (0) | 2023.04.18 |