프로젝트/아카이뷰

[Spring boot] NoArgsConstructor에 Protected를 쓰는 이유

cks._.hong 2024. 1. 18. 16:26

NoArgsConstructor에 Protected를 쓰는 이유가 왜 궁금했을까

ArchiVIEW 프로젝트에서 JPA Entity를 설정하기 위해 공부를 하다보니 대부분의 블로그에서 NoArgsConstructor에 Protected 옵션을 주고 사용하고 있었다. 단순히 코드를 따라치기보다는 이해하고 사용하기 위해 학습해보려고 한다.

 

NoArgsConstructor(AccessLevel.PROTECTED)를 왜 사용하는 걸까?

일반적으로 객체를 생성하는 방법에는 3가지 방법이 존재한다.
  • 기본 생성자를 통해 객체를 생성하고 Setter를 통해 값을 주입하는 방법
  • 각 매개변수를 가지는 생성자를 통해 객체의 생성과 초기화를 동시에 하는 방법
  • 정적 팩토리 메소드 / 빌더 패턴을 통해 객체 생성과 초기화를 동시에 하는 방법

첫 번째의 경우 Setter를 통해서 언제 어디서든 객체의 값을 변경할 수 있기 때문에 객체의 일관성이 무너지고 개발자의 의도를 파악하기 힘들어서 지양하는 것이 좋다.

 

두 번째의 경우 여러 개의 생성자를 만들어서 관리해야 하며 객체를 생성할 때 어떤 필드에 값을 넣는지 협업이나 개발자 측면에서 봤을 때 이해하기 힘들 것이다.

 

따라서, 세 번째 방법을 사용하는 것을 지향하곤 한다. 이러한 객체 생성이랑 JPA Entity와 무슨 연관이 있는 것일까?

JPA는 지연 로딩을 할 때, 프록시 객체를 사용하는데 해당 객체의 경우 실제 객체의 참조 변수를 가지고 있어야 하기 때문에 기본 생성자의 접근 권한을 private으로 설정하면 super를 호출할 수 없어 public 이나 protected로 설정해야 한다.

 

하지만, public으로 생성할 경우 무분별한 객체 생성과 Setter를 통해 값 초기화가 이뤄지기 때문에 객체의 일관성이 무너질 수 있다는 문제점이 존재한다.

 

이러한 이유에서 private와 public이 아닌 protected를 사용하는 것이다. 그리고 객체를 생성하기 위해서는 생성자를 직접 만들어줘야 하므로 세 번째 방법 중 하나인 빌더 패턴을 사용해서 구현해보았다. (추후 정적 팩토리 메소드와 빌더 패턴의 차이점을 비교할 예정)

@Builder 활용

  • JPA Entity class 위에 @Builder를 설정하면 에러가 발생하게 된다.
  • @Builder는 생성자가 없는 경우에는 모든 멤버 변수를 파라미터로 받는 기본 생성자를 생성하지만 생성자가 존재하는 경우에는 따로 생성자를 생성하지 않는다.
  • 이후, 아래 코드와 같이 모든 멤버 변수를 설정할 수 있는 Builder Class를 생성하게 된다.
  • 이 과정에서 @NoArgsConstructor(access = AccessLevel.PROTECTED)에 의해 생성된 생성자와 @Builder가 만들어낸 build() 메소드의 생성자와 일치하지 않아 에러가 발생하게 된다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class User implements Persistable<String> {
    @Id
    @Column(name = "id", length = 16)
    private String id;
    
    ...
    
    // @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
    protected User() {}
    
    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }
    
    public static class UserBuilder {
        private String id;

        UserBuilder() {
        }

        public User.UserBuilder name(String id) {
            this.name = id;
            return this;
        }

        public User build() {
            /// 일치하는 생성자 X
            return new User(this.id, this.name, this.email, ... ); 
        }
    }
}

 

위 문제를 해결하기 위한 방법은 두 가지 존재한다.

1. @AllArgsConstructor

  • 단순히 모든 멤버 변수를 받는 생성자를 만들어주면 된다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
public class User implements Persistable<String> {
    @Id
    @Column(name = "id", length = 16)
    private String id;
    
    ...
}

2. 생성자에 @Builder 설정

  • 필요한 생성자를 만들고 해당 생성자 위에 @Builder를 설정하면 의미있는 객체만 생성할 수 있게 된다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class User implements Persistable<String> {
    @Id
    @Column(name = "id", length = 16)
    private String id;
    
    ...
    
    @Builder
    public User(String id, ... ) {
        this.id = id;
        
        ...
    }
}