그 때 당시에는 Spring은 물론 Java에 대한 개념도 매우 약할 때라
지금도 약하지만레퍼런스의 코드를 가져다 사용하기에 급급했다.
당시의 나는 해당 개념에 대한 정확한 이해보다는 기능 구현이 우선이 되었기에, 내가 코드에 적용했던 다른 코드나 기술들에 대해 정확히 왜 사용하고, 어떤 기능인지에 대한 것들은 모두 미뤄뒀었다.
그래서 지금부터 내가 잘 모르고 작성했던 코드들에 대한 공부를 차근히 진행하고자 한다.
나는 아직도 너무 무지하고, 개념을 확실히 알고 넘어 가야 한다는 습관이 완벽히 자리 잡지 않았기에
이 시리즈는 한동안 쭉 작성하지 않을까 싶다.
이 시리즈의 두번째 글의 주제는 지연 로딩을 왜 사용했을까라는 주제이다.
나는 JPA를 사용하여서 양방향 관계를 맺을 때 무조건 @ManyToOne(fetch = FetchType.*LAZY*)
즉, 지연 로딩을 작성해야 한다고만 이해를 하고 있었다.
정확히 언제, 왜 지연 로딩을 사용해야 하는지에 대해서는 이해하지 못하고 있었다.
이제부터 언제, 왜 지연 로딩을 사용해야 하는지 알아보자.
설명은 내가 프로젝트 했던 실제 코드들을 바탕으로 해나갈 것이다.
먼저 Post(게시물)과 User(유저)는 N:1, @ManyToOne 관계를 가지고 있다.
Post의 코드이다.
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);
}
}
User의 코드이다.
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Entity
public class User{
@Id
@Column(name = "user_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String password;
private String email;
private String name;
private LocalDate birth;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Post> postList = new ArrayList<>();
}
객체는 참조를 통해서 연관된 객체들을 마음껏 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.
Hibernate와 같은 JPA 구현체들은 이 문제를 해결 하기 위해서, 프록시라는 기술을 사용한다.
프록시를 사용하면, 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스로부터 조회를 할 수 있다.
💡 내가 주관적으로 JPA를 공부하면서 느낀 점은 연관 관계 편의 메서드에서도 공부 했듯이 JPA 의 요점은 데이터베이스와 객체간 패러다임의 불일치를 어떻게 해결했는지인 것 같다엔티티를 조회할 때 연관 관계를 맺고 있는 엔티티들이 늘 사용되는 것은 아니다.
아래의 예시 코드를 보면서 더 알아보자.
Post post = em.find(Post.class, "postId");
User user = post.getUser();
System.out.println("게시글 제목 : " + post.getTitle());
System.out.println("게시물 작성자 : " + user.getName());
Post post = em.find(Post.class, "postId");
User user = post.getUser();
System.out.println("게시글 제목 : " + post.getTitle());
1번 메서드에서는 Post 엔티티와 Post 엔티티의 연관된 User 엔티티까지 사용하기에 둘 다 불러오는게 효율적이지만,
2번 메서드에서는 Post 엔티티만 사용하므로, 사용하지않는 User 엔티티까지 불러온다면 효율적이지 않을 것이다.
2번 메서드와 같은 문제를 해결하기 위해서 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연 로딩이라고 한다.
지연 로딩 기능을 사용하려면, 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라 한다.
아래는 프록시 객체를 사용한 지연 로딩을 적용한 코드이다.
Post post = em.find(Post.class, "postId"); // User 프록시 객체를 생성한다.
User user = post.getUser(); //
System.out.println("게시글 제목 : " + post.getTitle());
System.out.println("게시물 작성자 : " + user.getName()); // 실제 User 엔티티의 값을 요청할 때 데이터베이스 접근
Post post = em.find(Post.class, "postId"); // User 프록시 객체를 생성한다.
User user = post.getUser();
System.out.println("게시글 제목 : " + post.getTitle());
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로, 실제 클래스와 겉 모양이 같다.
실제 클래스
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);
}
public User getUser(){
return this.user;
}
}
프록시 클래스
public class ProxyPost extends Post{
Post targetPost = null;
public void setUser(User user){
}
public User getUser(){
}
}
프록시 클래스의 코드를 보면 실제 객체에 대한 참조가 null인 것을 볼 수 있다.
Post targetPost = null;
프록시 객체는 user.getName()
처럼 실제 엔티티가 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성해서 참조하는데 이 것을 프록시 객체의 초기화라고 한다.
Post targetPost = post;
가 된다.public class ProxyPost extends Post{
Post targetPost = null; // 실제 엔티티 참조
public void getUser(){ // 해당 메서드가 호출될 때 프록시 객체의 초기화
if(targetPost == null) { // 아직 초기화가 되지 않았을 때 프록시 객체 초기화
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.targetPost = ...;
}
// 5. 실제 엔티티의 메서드 호출
return targetPost.getUser();
}
}
user.getUser()
를 통해 호출을 한다.if(targetPost = null;)
), 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 프록시 객체 초기화라고 한다.getUser()
를 호출해서 결과를 반환한다.Post post = em.find(Post.Class, "post1"); // -> 즉시 로딩시 User 엔티티 조회
User user = post.getUser(); // 객체 그래프 탐색 -> 지연 로딩시 User 프록시 엔티티 반환
System.out.println(user.getPost()); // -> 지연 로딩시 User 엔티티 조회
Post post = em.find(Post.Class, "post1");
를 호출할 때 연관된 엔티티 User도 함께 조회한다.@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(nullable=False)
를 설정하면 된다.post.getUser().getPost();
처럼 연관된 엔티티 User의 메서드를 실제로 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다.@ManyToOne(fetch = FetchType.LAZY)
fetch = FetchType.EAGER
, 즉시 로딩을 사용하게 되면 예를 들어서 연관된 엔티티의 컬렉션에 데이터를 수만 건의 데이터를 등록했을 때, 연관된 엔티티를 로딩하는 순간 수만 건의 데이터 또한 함께 로딩될 것이다.
이 때문에 JPA는 기본 페치 전략으로 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다.
내용을 정리하면 다음과 같다.
답은 바로 지연 로딩이 아니라 즉시 로딩을 사용하게 되면, 내가 필요로 하지 않을 때와 필요로하지 않은 연관된 엔티티의 데이터를 모두 가져오기 때문이었다.
좋아요 누르고 들렀다 가요~