ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring boot] NoArgsConstructor에 Protected를 쓰는 이유
    프로젝트/아카이뷰 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;
            
            ...
        }
    }
Designed by Tistory.