즉시 로딩과 지연 로딩
📌 단독 조회
Member와 Team이 있고, Member는 Team의 참조값을 가진 상태라고 할 때, 다음 명령문에서 어떤 조회 쿼리가 나가야 할까
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getName());
위 명령문은 조회한 회원의 이름을 출력하는 것이다. 그런데 해당 명령을 수행하기 위해 아래와 같은 쿼리가 실행되었다.
위 결과를 보면 회원 이름을 출력하기 위해 회원과 연관된 Team을 조인으로 가져오는 것을 볼 수 있다. 만약, 회원과 연관된 엔티티가 몇 개씩 있다면 조인문은 더욱 복잡해질 것이다. 이처럼 단독 조회를 최적화하기 위한 방법과 일괄 조회를 위한 방법으로 나온 기법이 지연 로딩과 즉시 로딩이다.
📌 지연 로딩
지연 로딩은 단독 조회를 최적화하기 위해 연관 엔티티의 초기화를 미루는 기법으로, 초기화 전까지 연관 엔티티는 원본 엔티티의 껍데기에 불과하다. 이 껍데기를 Proxy 객체라고 한다.
Proxy 객체는 실제 객체의 참조를 가진 가짜 또는 자식 객체이다. 해당 참조는 초기화 과정에서 영속성 컨텍스트가 DB로부터 실제 엔티티를 조회 후 생성하면
값이 채워진다. 이 채워진 참조를 통해 Proxy 객체는 실제 Entity에 대한 프로퍼티 함수를 수행하도록 도와준다. 즉, 클라이언트와 DB 사이에서 명령을 수행하는 중간 매개체가 되는 것이다. 이러한 초기화는 최초 단 1회만 발생한다.
지연 로딩은 여러 가지 이점을 갖고 있다. 불필요한 조인을 없애고, 가벼운 Proxy 객체로 인해 메모리 사용을 줄이는 효과를 볼 수 있다. 이 가운데 가장 큰 이점은 불필요한 쿼리가 나가지 않아 추적이 용이하다는 것이다.
em.flush와 em.clear는 영속성 컨텍스트를 비워 em.find가 DB로부터 조회 쿼리를 수행한다. (쿼리 확인 목적)
위 결과를 보면 조회 시, 조인문이 나가지 않고 Member만 단독 조회하는 것을 볼 수 있다.
그렇다면 정말 초기화 시점에 참조를 통해 프로퍼티 함수를 수행할 수 있을까
위 결과를 보면 Team 객체는 Proxy 객체라는 것을 알 수 있다. 또한, 초기화를 수행하자 DB에 실제 엔티티를 조회했다. 이렇게 DB에서 꺼낸 실제 Team 객체를 Proxy 객체가 참조한다.
📌 즉시 로딩
즉시 로딩 기법은 Proxy를 사용하지 않는다. 앞서 조인문이 나갔던 결과처럼 연관된 엔티티를 SQL 한번에 함께 조회하는 것이다.
위 결과를 보면 Member를 조회하면서 연관 엔티티도 함께 조회하는 것을 알 수 있다. 그리고 이미 조인을 통해 실제 엔티티를 영속성 컨텍스트에 올렸기 때문에 초기화 시 DB에 다시 조회할 필요가 없다.
📌 어떤 기법을 선택해야 하나
실무에서 즉시 로딩 사용 금지
즉시 로딩은 연관된 엔티티를 자주 함께 사용한다면 고려해볼 수 있는 방법이지만, 예상치 못한 SQL이 발생한다는 큰 단점이 있고, JPQL에서 N+1 문제를 일으킨다.
N+1 문제는 처음 1개의 쿼리 때문에 조회된 데이터의 개수 N개만큼 쿼리가 나가서 추가로 데이터를 읽어온다는 의미로 붙여진 이름이다. 결과적으로 1번의 쿼리 때문에 조회된 Member 마다 연관된 Team에 대해 계속 쿼리를 날리는 것이다. 아래의 경우를 살펴보자.
조회된 member1과 member2는 각각 다른 팀에 소속되어 있어 Member 조회뿐만 아니라 아래처럼 Team을 조회하기 위한 2번의 추가 쿼리가 나갔다.
🔎 N+1 문제 해결 방법
1. LAZY(지연) 로딩
2. fetch join
💡 다중성에 따른 기본 로딩
@ManyToOne, @OneToOne ➡ @xxxToOne 시리즈는 default EAGER
@OneToMany, @ManyToMany ➡ @xxxToMany 시리즈는 default LAZY
Reference:
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard