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

Kevin·2024년 2월 22일
0
post-thumbnail

그 때 당시에는 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 의 요점은 데이터베이스와 객체간 패러다임의 불일치를 어떻게 해결했는지인 것 같다

Proxy를 알아보자.

엔티티를 조회할 때 연관 관계를 맺고 있는 엔티티들이 늘 사용되는 것은 아니다.

아래의 예시 코드를 보면서 더 알아보자.

  1. Post와 Post의 작성자인 User 정보를 출력하는 메서드
Post post = em.find(Post.class, "postId"); 
User user = post.getUser(); 
System.out.println("게시글 제목 : " + post.getTitle());
System.out.println("게시물 작성자 : " + user.getName());
  1. Post의 정보만 출력하는 메서드
Post post = em.find(Post.class, "postId");
User user = post.getUser();
System.out.println("게시글 제목 : " + post.getTitle());

1번 메서드에서는 Post 엔티티와 Post 엔티티의 연관된 User 엔티티까지 사용하기에 둘 다 불러오는게 효율적이지만,

2번 메서드에서는 Post 엔티티만 사용하므로, 사용하지않는 User 엔티티까지 불러온다면 효율적이지 않을 것이다.


2번 메서드와 같은 문제를 해결하기 위해서 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연 로딩이라고 한다.

지연 로딩 기능을 사용하려면, 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라 한다.

아래는 프록시 객체를 사용한 지연 로딩을 적용한 코드이다.

  • 좀 더 아래에서 더 정확한 프록시 객체에 대해서 공부해보고, 지금은 느낌만 살펴보자.

  1. Post와 Post의 작성자인 User 정보를 출력하는 메서드
Post post = em.find(Post.class, "postId"); // User 프록시 객체를 생성한다.
User user = post.getUser(); // 
System.out.println("게시글 제목 : " + post.getTitle());
System.out.println("게시물 작성자 : " + user.getName()); // 실제 User 엔티티의 값을 요청할 때 데이터베이스 접근
  1. Post의 정보만 출력하는 메서드
Post post = em.find(Post.class, "postId"); // User 프록시 객체를 생성한다.
User user = post.getUser();
System.out.println("게시글 제목 : " + post.getTitle());


Proxy에 대해서 더 깊게 공부해보자.

프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로, 실제 클래스와 겉 모양이 같다.

실제 클래스

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();	

    }
}
  1. 프록시 객체 ProxyPost에 user.getUser() 를 통해 호출을 한다.
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면(if(targetPost = null;)), 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 프록시 객체 초기화라고 한다.
  3. 영속성 컨텍스트는 데이터베이스를 조회해서, 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 targetPost 멤버변수에 보관한다.
  5. 프록시 객체는 실제 엔티티 객체의 getUser()를 호출해서 결과를 반환한다.

이러한 프록시 객체는 우리가 처음 이야기 했던 것처럼 주로 연관된 엔티티를 지연 로딩할 때 사용한다.

연관된 엔티티의 조회 시점은 보통 2가지가 있다.

  1. Post 엔티티를 조회할 때 연관된 User 엔티티도 함께 데이터베이스에서 조회
  2. Post 엔티티만 조회해두고, 연관된 User 엔티티는 실제 사용할 때 데이터베이스에서 조회
Post post = em.find(Post.Class, "post1"); // -> 즉시 로딩시 User 엔티티 조회
User user = post.getUser(); // 객체 그래프 탐색 -> 지연 로딩시 User 프록시 엔티티 반환
System.out.println(user.getPost()); // -> 지연 로딩시 User 엔티티 조회

JPA는 개발자가 연관된 위 엔티티의 조회 시점중 한 가지를 선택할 수 있도록 2가지 방법을 제공한다.

즉시 로딩

  • Post post = em.find(Post.Class, "post1"); 를 호출할 때 연관된 엔티티 User도 함께 조회한다.
  • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
  • 즉시 로딩의 경우 Post 엔티티와 User 엔티티를 동시에 가져오기에 각각 2번의 쿼리를 실행할 것 같지만, 즉시 로딩을 최적화하기 위해서 가능하면 조인 쿼리를 사용한다.
    • 단 Post 엔티티의 작성자가 없을 경우(그런 경우는 거의 없겠지만)를 대비해서 JPA는 외부 조인을 사용한다.
    • 만약 내부 조인을 사용하고 싶으면 @JoinColumn(nullable=False)를 설정하면 된다.

지연 로딩

  • post.getUser().getPost(); 처럼 연관된 엔티티 User의 메서드를 실제로 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다.
  • 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)
  • 지연 로딩은 쉽게 생각하면, 처음에는 연관된 엔티티의 프록시 엔티티를 생성했다가 실제로 해당 프록시 엔티티를 사용할 때, 프록시 엔티티의 멤버 변수로 참조된 실제 엔티티의 메서드를 호출해서 값을 리턴해준다고 생각하면 된다.
    • 물론 프록시 엔티티의 멤버변수가 null이면, 프록시 초기화 과정을 거쳐야 한다!!


근데 왜 지연 로딩 사용을 추천할까?

fetch = FetchType.EAGER , 즉시 로딩을 사용하게 되면 예를 들어서 연관된 엔티티의 컬렉션에 데이터를 수만 건의 데이터를 등록했을 때, 연관된 엔티티를 로딩하는 순간 수만 건의 데이터 또한 함께 로딩될 것이다.

이 때문에 JPA는 기본 페치 전략으로 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다.

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

  • 지연 로딩 : 연관된 엔티티를 프록시로 조회하고, 프록시를 실제 사용할 때 프록시를 초기화하면서 데이터베이스를 조회한다.
  • 즉시 로딩 : 연관된 엔티티를 즉시 조회한다, 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.



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

답은 바로 지연 로딩이 아니라 즉시 로딩을 사용하게 되면, 내가 필요로 하지 않을 때와 필요로하지 않은 연관된 엔티티의 데이터를 모두 가져오기 때문이었다.

profile
Hello, World! \n

0개의 댓글