개요
해당 글에서는 다음과 같은 내용을 다룬다.
- @builder의 동작 원리
- JPA Entity에서의 lombok의 @builder 어노테이션의 위치 선택
@builder 동작 원리
사용하려는 예시 User Entity는 다음과 같다. 아직 @builder 패턴은 적용하지 않은 상태이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(unique = true)
private String email;
@NotNull
private String snsType;
}
[동작 원리]
* 자세한 코드는 아래에 나온다.
- User 클래스 내부에는 UserBuilder라는 builder 클래스가 static inner class로 생성된다.
- User는 outer instance class, UserBuilder은 static inner class가 된다.
- outer instance class의 static 메서드인 User.builder() 호출 시 기본 생성자 new UserBuilder() 호출 를 통해 새로운 UserBuilder 객체가 생성된다.
- 각 변수에 대한 값을 지정(빌드)할 수 있는 메소드 사용하여 UserBuilder에 값을 할당한다. (email(), snsType())
- UserBuilder의 build() 메소드를 통해 , outer instance class인 User의 생성자를 new연산자를 통해 호출한다.
: 이 부분이 가장 핵심이 되는 부분이다. 결국 해당 문제는 생성자를 어떻게 구성하느냐에 대한 문제인데 생성자에 대한 호출이 이 부분에서 이뤄진다.
public static class UserBuilder {
private Long id;
private String email;
private String snsType;
UserBuilder() {
}
//... 생략
public User build() {
return new User(this.id, this.email, this.snsType); //생성자가 호출된다!
}
}
@builder의 위치
@builder의 가능한 위치는 크게 2가지가 있다.
1. 클래스 위
- 클래스위에 @builder을 붙이게 되면 build() 메소드 호출 시 해당 클래스에 있는 멤버 변수 모두를 parameter로 받는 생성자를 호출해야한다.
- 어떤 생성자도 생성하지 않을 시 자동으로 lombok에서 생성자를 만들어준다.
https://projectlombok.org/api/lombok/Builder
[ If a class is annotated, then a private constructor is generated with all fields as arguments (as if @AllArgsConstructor(access = AccessLevel.PRIVATE) is present on the class), and it is as if this constructor has been annotated with @Builder instead.]
- 만약 별도의 생성자를 직접 만들어야 한다면 모든 변수를 parameter로 받는 생성자를 동시에 꼭 직접 만들어줘야한다.
- 해당 글의 예시에서는 JPA Entity이기 때문에 이를 위해서는 기본 생성자인 @NoArgsConstructor가 필수적이다.
-이런 경우에는 모든 변수를 parameter로 받는 생성자를 만들어줘야한다. 이는 위에서 언급한 build()에서 클래스에 있는 모든 변수를 가지는 생성자를 호출하기 때문이다.
@Builder + @AllArgsConstructor를 함께 사용하는 것이다.
그렇다면 클래스 위 @Builder + @AllArgsConstructor의 예시 코드를 알아보자
[User.java ]
@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(unique = true)
private String email;
@NotNull
private String snsType;
}
[User.class ]
@Entity
public class User{
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Column(
unique = true
)
private @NotNull String email;
private @NotNull String snsType;
//@AllArgsConstructor로 생성된 생성자
private User(Long id, String email, String snsType) {
this.id = id;
this.email = email;
this.snsType = snsType;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public Long getId() {
return this.id;
}
public String getEmail() {
return this.email;
}
public String getSnsType() {
return this.snsType;
}
protected User() {
} //@NoArgsConstructor로 생성된 기본 생성자
//inner builder 클래스
public static class UserBuilder {
private Long id;
private String email;
private String snsType;
UserBuilder() {
}
public UserBuilder id(final Long id) {
this.id = id;
return this;
}
public UserBuilder email(final String email) {
this.email = email;
return this;
}
public UserBuilder snsType(final String snsType) {
this.snsType = snsType;
return this;
}
public User build() { // 모든 멤버 변수를 인자로 받는 생성자 호출
return new User(this.id, this.email, this.snsType);
}
public String toString() {
return "User.UserBuilder(id=" + this.id + ", email=" + this.email + ", snsType=" + this.snsType + ")";
}
}
}
2. 생성자 위
- 생성자에 대한 parameter은 직접 지정 가능하다는 장점을 가진다. (모든 변수에 대한 값을 parameter로 받을 필요는 없다.)
- 특정 변수의 값 자체를 지정하고 싶은 경우에 해당 값을 parameter로 받지 않고 생성자에서 값을 지정하여 객체를 생성할 수 있다.
-다음의 예시에서는 특정 변수인 email을 기본 값으로 생성자에서 값을 지정하여, 생성자 위에 @builder을 적용해 보았다.
[User.java]
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(unique = true)
private String email;
@NotNull
private String snsType;
@Builder
public User(Long id, String snsType) {
this.id = id;
this.email = "default@gmail.com";
this.snsType = snsType;
}
}
[User.class]
@Entity
public class User {
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Column(
unique = true
)
private @NotNull String email;
private @NotNull String snsType;
public User(Long id, String snsType) { //UserBuilder에서 build호출 시 User객체를 생성, 직접 지정한 값이 User 객체 생성시 적용된다.
this.id = id;
this.email = "default@gmail.com";
this.snsType = snsType;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public Long getId() {
return this.id;
}
public String getEmail() {
return this.email;
}
public String getSnsType() {
return this.snsType;
}
protected User() {
}
public static class UserBuilder {
private Long id;
private String snsType;
UserBuilder() {
}
public UserBuilder id(final Long id) {
this.id = id;
return this;
}
public UserBuilder snsType(final String snsType) {
this.snsType = snsType;
return this;
}
public User build() { //email 변수의 값을 outer class에 parameter로 전달할 필요가 없다.
return new User(this.id, this.snsType);
}
public String toString() {
return "User.UserBuilder(id=" + this.id + ", snsType=" + this.snsType + ")";
}
}
}
결론 및 적용
- 만약 클래스의 모든 값이 builder 패턴을 통해 전달 및 생성이 되야한다면?
=> 클래스 위에 @builder + @AllArgsConstructor 조합
- 만약 @builder 패턴을 통해 모든 값을 전달을 하지 않고 일부 값만 전달해야한다면?
=> 직접 만든 생성자 + 생성자 위 @builder
- @AllArgsConstructor은 지양해야한다?
@AllArgsConstructor를 지양해야한다는 얘기가 있는데 이는 전달하려는 new 연산자를 사용해 parameter의 순서를 생성자에 맞지 않게 잘못 입력할 수 있는 위험성 때문이다. 이는 생성자의 접근제어자를 private하게 하여 클래스 내부의 builder 클래스에서만 생성자를 호출하여 객체를 생성할 수 있게 한다면 해결할 수 있다고 생각했다. 이러한 우려는 어디까지나 직접 클라이언트가 외부에서 생성자를 직접 호출했을 때 만들어질 수 있는 문제점이라 생각한다. (이를 해결하려고 적용한 것이 빌더 패턴이고)
이와 관련한 @Entity 생성자의 accesslevel은 다음 포스팅 https://minjun98.tistory.com/104 에서 다룬다.
[참고]
Effective Java [아이템 2] : 매개변수가 많다면 빌더 패턴을 고려하라
'JPA' 카테고리의 다른 글
[JPA] Fetch join과 pagination (0) | 2024.08.12 |
---|---|
[JPA] JPA Entity 생성자의 접근 제어자 with @Builder (0) | 2024.04.05 |