JPA

[JPA] JPA Entity 클래스에서의 @builder 위치 : Constructor vs Class

kidmillionaire1998 2024. 4. 5. 14:15

개요

해당 글에서는 다음과 같은 내용을 다룬다. 

 

- @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를 함께 사용하는 것이다. 

@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