그 때 당시에는 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() {}
정확히는 엔티티의 연관 관계에서 지연 로딩의 경우에는 실제 엔티티가 아닌 프록시 객체를 통해서 조회를 한다.
프록시 객체를 사용하기 위해서 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의 접근 권한을 다르게 한 경우들로 테스트 코드를 돌려보자.
먼저 첫번째 예시인 Post와 User 모두 @NoArgsConstructor(access = AccessLevel.PROTECTED)
인 경우를 알아보자.
undefined
이 경우에는 정상 출력 된다.
정상 출력 되는 이유는 접근 권한이 protected이므로, JPA 구현체가 User 프록시 객체를 정상적으로 생성하고, 실제 User Entity의 값이 필요해질 때 User 프록시 객체가 초기화를 통해서 실제 Entity를 참조해 값을 가져오기 때문이다.
그 다음은 두번째 예시인 Post는 @NoArgsConstructor(access = AccessLevel.PROTECTED)
User는 @NoArgsConstructor(access = AccessLevel.*PRIVATE*)
인 경우를 알아보자.
undefined
이 경우에는 정상 출력되지 않고, 에러가 터진다.
에러가 터지는 이유는 User의 접근 권한이 private이기 때문에, 지연 로딩시 JPA 구현체가 User 프록시 객체를 생성할 때 접근할 수 없기 때문이다.
그 다음은 세번째 예시인 Post는 @NoArgsConstructor(access = AccessLevel.PRIVATE)
User는 @NoArgsConstructor(access = AccessLevel.PROTECTED)
인 경우를 알아보자.
undefined
이 경우에는 정상 출력 된다.
정상 출력 되는 이유는 Post 엔티티는 접근 권한과 지연 로딩과는 상관 없이 em.find를 통해서 실제 Entity 객체로 조회되기 때문이다. 그리고 User 엔티티는 접근 권한이 protected이기 때문에 정상적으로 Proxy 객체가 생성된다.
쉽게 설명하자면 접근 권한을 private가 아니라 protected로 적용함으로써 프록시 객체를 생성하게 해줄 수 있기 때문이다.
아니 그러면 접근 권한을 public으로 해줘도 프록시 객체를 생성하게 해주는건가 아닌가ㅡㅡ? 라는 궁금증이 생겼다.
우리가 일반적으로 객체를 생성하고, 객체에 값을 채워넣는 방법은 아래의 3가지 방법이 있다.
먼저 첫번째 기본 생성자로 객체를 생성 후 setter를 통해 필드값을 주입하는 방법은 setter를 통해서 언제 어디서든 객체의 값을 변경할 수 있기에 나중에는 어디서 객체의 값을 변경했는지 추적하기 어렵고, 객체지향적으로도 옳지 못하기에 권장하는 방법이 아니다.
JPA에서는 기본적으로 기본 생성자를 요구하는데, 이 때문에 @NoArgsConstructor
를 작성하게 된다. 그러나 위의 이유로 인해서 이러한 JPA의 Entity Class의 요구사항 이외에는 기본 생성자를 이용할 일이 없게 된다
내용을 정리하면 다음과 같다.
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
답은 접근 권한을 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);
}