내가 @NoArgsConstructor (access = AccessLevel.PROTECTED)를 작성했던 이유

Kevin·2023년 5월 24일
33

반성합니다.

목록 보기
3/9
post-thumbnail

그 때 당시에는 Spring은 물론 Java에 대한 개념도 매우 약할 때라 지금도 약하지만

레퍼런스의 코드를 가져다 사용하기에 급급했다.

당시의 나는 해당 개념에 대한 정확한 이해보다는 기능 구현이 우선이 되었기에, 내가 코드에 적용했던 다른 코드나 기술들에 대해 정확히 왜 사용하고, 어떤 기능인지에 대한 것들은 모두 미뤄뒀었다.

그래서 지금부터 내가 잘 모르고 작성했던 코드들에 대한 공부를 차근히 진행하고자 한다.

나는 아직도 너무 무지하고, 개념을 확실히 알고 넘어 가야 한다는 습관이 완벽히 자리 잡지 않았기에

이 시리즈는 한동안 쭉 작성하지 않을까 싶다.

이 시리즈의 세번째 글의 주제는 @NoArgsConstructor (access = AccessLevel.PROTECTED)를 왜 사용했을까라는 주제이다.

나는 레퍼런스에서 @NoArgsConstructor 어노테이션의 access 인자를 AccessLevel.PROTECTED로 하여야 보안상 좋다고만 이해를 하고 있었다.

정확히 왜 access 인자를 AccessLevel.PROTECTED로 해야 하는지에 대해서는 이해하지 못하고 있었다.

이제부터 왜 access 인자를 private도 아닌 proteceted로 해야하는지 알아보자.


@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 값 변경 목적으로 접근하는 메시지들 차단
@Entity
public class Post {
    @Id
    @Column(name = "post_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private String hashTag;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    public void setUser(User user){
        // 기존 user와의 관계를 제거
        if (this.user != null) {
            this.user.getPostList().remove(this);
        }
        this.user = user;
        user.getPostList().add(this);
    }
}

위는 내가 프로젝트간 작성했던 코드이다.

해당 Post 엔티티에서는 @NoArgsConstructor(access = AccessLevel.PROTECTED) 어노테이션을 가지고 있다.

이 어노테이션의 뜻은 “아무런 매개변수가 없는 생성자를 생성하되 다른 패키지에 소속된 클래스는 접근을 불허한다”라는 뜻이다.

즉 아래와 같은 생성자 코드를 생성해 준다는 것이다.

protected Post() {}


그럼 여기서 궁금한 점은 왜 접근 권한을 Protected로 하였을까??!


💡 답은 Entity의 **Proxy 조회** 때문이다.

정확히는 엔티티의 연관 관계에서 지연 로딩의 경우에는 실제 엔티티가 아닌 프록시 객체를 통해서 조회를 한다.

프록시 객체를 사용하기 위해서 JPA 구현체는, 실제 엔티티의 기본 생성자를 통해 프록시 객체를 생성하는데, 이 때 접근 권한이 private이면 프록시 객체를 생성할 수 없는 것이다.

이 때 즉시로딩으로 구현하게 되면, 접근 권한과 상관없이 프록시 객체가 아닌 실제 엔티티를 생성하므로 문제가 생기지 않는다


Proxy와 지연, 즉시 로딩이 정확히 뭔지 모르겠다면 내 이전 글을 참고해주면 좋을 것 같다!!

내가 지연 로딩으로 작성했던 이유


위에 대한 설명은 내가 프로젝트 했던 실제 코드들을 바탕으로 직접 테스팅을 하면서 설명을 해보겠다.

먼저 Post(게시물)과 User(유저)는 N:1, @ManyToOne 관계를 가지고 있다.

Post의 코드이다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 값 변경 목적으로 접근하는 메시지들 차단
@Entity
public class Post {
    @Id
    @Column(name = "post_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

User의 코드이다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 값 변경 목적으로 접근하는 메시지들 차단
@Entity
public class User {
    @Id
    @Column(name = "user_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
		private String password;

}

테스트 코드는 아래와 같다.

@SpringBootTest
class DemoApplicationTests {

	@Autowired
	private EntityManager em;

	@BeforeEach
	void makeEntity() {
		UserKim user = UserKim.builder()
				.password("q1w2e3r4")
				.build();

		em.persist(user);

		Post post = Post.builder()
				.title("글 제목입니다.")
				.content("글 내용입니다.")
				.user(user)
				.build();
		em.persist(post);
	}

	@Test
	@Transactional
	void proxyTest() {

		Post post = em.find(Post.class, 1L);

		System.out.println("post의 ID 값은 : " + post.getId()); 
		System.out.println("user의 ID 값은 : " + post.getUser().getId());
	}
}

아래와 같이 NoArgsConstructor의 접근 권한을 다르게 한 경우들로 테스트 코드를 돌려보자.

  1. Post, User 모두 protected일 때
  2. Post는 protected, User는 private일 때
  3. Post는 private, User는 protected일 때


  1. 먼저 첫번째 예시인 Post와 User 모두 @NoArgsConstructor(access = AccessLevel.PROTECTED) 인 경우를 알아보자.

    이 경우에는 정상 출력 된다.

    정상 출력 되는 이유는 접근 권한이 protected이므로, JPA 구현체가 User 프록시 객체를 정상적으로 생성하고, 실제 User Entity의 값이 필요해질 때 User 프록시 객체가 초기화를 통해서 실제 Entity를 참조해 값을 가져오기 때문이다.


  1. 그 다음은 두번째 예시인 Post는 @NoArgsConstructor(access = AccessLevel.PROTECTED) User는 @NoArgsConstructor(access = AccessLevel.*PRIVATE*) 인 경우를 알아보자.

    이 경우에는 정상 출력되지 않고, 에러가 터진다.

    에러가 터지는 이유는 User의 접근 권한이 private이기 때문에, 지연 로딩시 JPA 구현체가 User 프록시 객체를 생성할 때 접근할 수 없기 때문이다.



  2. 그 다음은 세번째 예시인 Post는 @NoArgsConstructor(access = AccessLevel.PRIVATE) User는 @NoArgsConstructor(access = AccessLevel.PROTECTED) 인 경우를 알아보자.

    이 경우에는 정상 출력 된다.

    정상 출력 되는 이유는 Post 엔티티는 접근 권한과 지연 로딩과는 상관 없이 em.find를 통해서 실제 Entity 객체로 조회되기 때문이다. 그리고 User 엔티티는 접근 권한이 protected이기 때문에 정상적으로 Proxy 객체가 생성된다.



위의 예시들을 통해서 왜 `@NoArgsConstructor(access = AccessLevel.PROTECTED)` 를 적용해야하는지를 알아보았다.

쉽게 설명하자면 접근 권한을 private가 아니라 protected로 적용함으로써 프록시 객체를 생성하게 해줄 수 있기 때문이다.

아니 그러면 접근 권한을 public으로 해줘도 프록시 객체를 생성하게 해주는건가 아닌가ㅡㅡ? 라는 궁금증이 생겼다.



그러면 Public으로 해주면 안되는거야? Protected로 했을 때의 장점은 뭐야??

우리가 일반적으로 객체를 생성하고, 객체에 값을 채워넣는 방법은 아래의 3가지 방법이 있다.

  1. 기본 생성자를 통해 객체를 생성 후 setter를 통해 필드값을 주입하는 방법
  2. 매개변수를 가지는 생성자를 통해 객체 생성과 동시에 필드값을 초기화하는 방법
  3. 정적 팩토리 메서드 또는 빌더 패턴을 통해 객체 생성과 동시에 필드 값 초기화

먼저 첫번째 기본 생성자로 객체를 생성 후 setter를 통해 필드값을 주입하는 방법은 setter를 통해서 언제 어디서든 객체의 값을 변경할 수 있기에 나중에는 어디서 객체의 값을 변경했는지 추적하기 어렵고, 객체지향적으로도 옳지 못하기에 권장하는 방법이 아니다.


JPA에서는 기본적으로 기본 생성자를 요구하는데, 이 때문에 @NoArgsConstructor 를 작성하게 된다. 그러나 위의 이유로 인해서 이러한 JPA의 Entity Class의 요구사항 이외에는 기본 생성자를 이용할 일이 없게 된다


결국 2, 3번을 통해서 무분별한 객체 생성을 방지하며, 접근 권한을 public이 아닌 protected로 제한함으로 최대한 접근 범위를 작게 가져가는 것이다.

내용을 정리하면 다음과 같다.

  • @NoArgsConstructor(access = AccessLevel.PUBLIC)
    • 기본 생성자를 이용하여, 값을 주입하는 방식을 최대한 방지하기 위해서 사용을 권장하지 않는다.
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • 위, 아래와 같은 프록시 객체의 생성과 객체에 대한 접근 범위 문제를 해결하기 위해서 사용한다.
  • @NoArgsConstructor(access = AccessLevel.PRIVATE)
    - 프록시 객체 생성시 문제가 생기기 때문에 사용을 권장하지 않는다.



내가 @NoargsConstructor (access = AccessLevel.PROTECTED)를 작성했던 이유

답은 접근 권한을 Private로 하면 프록시 객체 생성에 문제가 생기고, 접근 권한을 Public으로 하면 무분별한 객체 생성 및 setter를 통한 값 주입을 할 수 있기에 접근 권한을 Protected로 작성 하는 것이다.



옥에 티

도중 분명 User 엔티티의 @NoArgsConstructor(access = AccessLevel.private) 로 설정했는데도 자꾸 둘의 값이 정상적으로 불러와 지는 것이다.. 그래서 이유가 뭔지 생각을 해보니 아래의 코드에서 문제의 이유를 찾을 수 있었다.

@BeforeEach
	void makeEntity() {
		UserKim user = UserKim.builder()
				.password("q1w2e3r4")
				.build();

		em.persist(user);

		Post post = Post.builder()
				.title("글 제목입니다.")
				.content("글 내용입니다.")
				.user(user)
				.build();
		em.persist(post);
	}
  • 위와 같은 코드인데, 문제가 생긴 이유는 바로 영속성 컨텍스트에 User와 Post가 남아 있기 때문이다. 해당 영속성 컨텍스트에 엔티티들이 남아있으면, JPA 구현체는 지연 로딩시 프록시 객체를 생성하지 않고, 영속성 컨텍스트에 남아있는 실제 엔티티를 이용하기 때문에 계속 Test가 성공 했던 것이다.
profile
Hello, World! \n

10개의 댓글

comment-user-thumbnail
2023년 5월 25일

잘 봤어요~~~

답글 달기
comment-user-thumbnail
2023년 5월 26일

끊임 없이 생각하고 발전하려는 모습이 멋지네요~

답글 달기
comment-user-thumbnail
2023년 5월 29일

이 시리즈 너무 기대됩니다!
잘 보고 가요~~

답글 달기
comment-user-thumbnail
2023년 9월 8일

깔끔하게 정리하신 글 정말 많은 도움이 되었습니다.. 다른 포스팅도 들여다봤는데 정성스럽게 작성해놓으셨더라구요. 그래서 포스팅보고 제가 작성하는데 참고해도 될까요?? 블로그 출처는 맨 하단에 남겨놓도록 할 예정이구요!

1개의 답글
comment-user-thumbnail
2024년 1월 2일

좋은 글 잘보고 갑니다! 저도 마침 마구잡이로 작성하던 코드에 대해 반성하던 중이었는데 다른 글도 많이 읽어서 같이 반성하겠습니다. :)

1개의 답글
comment-user-thumbnail
2024년 3월 9일

아하 PROTECTED를 사용한 이유가 프록시 객체를 상속받아서 그런거였군요 대부분 엔티티들은 N+1 문제때문에 지연 로딩을 사용하니까요 덕분에 궁금증이 해결됬습니다 :)

1개의 답글
comment-user-thumbnail
2024년 6월 10일

너무 좋은 포스팅입니다.
왜 AccessLevel.PROTECTED로 설정해야 좋은지를 궁금했는데 이유와 다른 레벨로 설정했을경우 어떤 일까지 벌어지는지 함께 파악할 수 있는 글이네요.
대충 가져다 쓰기만 했던 과거를 반성하게 됩니다

답글 달기