본문 바로가기
JPA Basic

지연 로딩 & 조회

by 완두완두콩 2022. 3. 23.

 

@JsonIgnore


스프링 부트 활용 강의 중, @JsonIgnore 에 관한 내용이 존재한다.

@JsonIgnore 는 무한 루프 조인을 막기 위해서 쓰는 어노테이션인데, 

예를 들면 다음과 같다.

(위의 예에서 @JsonIgnore 는 무시)

 

Order 를 조회하는 경우, XtoOne 관계로 이어진

Member 와 Delivery 가 문제가 된다.

Order 에서 Member 를 조회하면 위와 같은

Member 에서 조회를 하게 되는데 이 때, List<Order> orders 가

조회되게 된다.

그렇게 되면 또 다시 Order 테이블로 들어가서 조회를 하다가

다시 Member 를 조회하고 이게 무한 루프로 조회하며 장애를 일으킨다.

 

이렇듯 양방향 연관관계가 걸린 경우, 한쪽은 반드시 @JsonIgnore 처리를 해주어야 한다.

 

(fetch = LAZY) & Fetch Join


LAZY 조인은 영속성 컨텍스트에서 값을 찾고(프록시로 객체를 조회.),

없으면 DB 로 쿼리를 날린다.

 

하지만 그냥 사용하게 되면 1+N 문제 발생

(Order 에서 조회하면 , Member 와 Delivery 모두 조회)

만약 2명의 멤버가 존재하는 경우 1 + 2 + 2 의 결과가 발생

 

데이터의 양이 많아지는 경우 너무 많은 쿼리문과 JOIN 이 발생

이를 해결하기 위해 JPQL fetch 를 사용한다.

Order 를 조회하는데 이 때, fetch 조인으로 Member 와 Delivery 를

한번에 모두 조인한다(select 로 전부 같이 가져옴).

즉, Order 에 member, delivery 를 다 넣어놓고 select 쿼리로 한번에 모두 조회한다.

 

이 경우, LAZY (지연로딩)로 설정돼있더라도 Proxy 값이 아닌 실제 값으로

모두 조회하게 된다.

 

이렇게 하면 쿼리가 단 '한번' 만으로 모두 조회하게 된다.

1 + N 문제를 손 쉽게 해결 !!.. 

성능 최적화!!

(실제로 장애를 경험해 본 적이 없어서 감이 조금 잘 안오지만!..)

 

Fetch Join 을 적극적으로 활용하자

(XtoOne 의 조인하는 경우가 많지 않은 경우!)

 

지연 로딩 & 즉시 로딩


즉시 로딩, 지연 로딩, Proxy 에 대한 개념이 조금 덜 잡혀 있는 것 같아

따로 자료를 좀 더 찾아보았다.

 

지연 로딩 (LAZY) 이란? 

자신과 연관된 엔티티를 실제로 사용할 때, 연관된 엔티티를 조회(Select) 하는 것을 뜻한다.

 

즉시로딩(EAGER) 이란?

엔티티를 조회할 때 , 자신과 연관된 엔티티를 Join 을 통해 한번에 조회하는 것을 뜻한다.

 

즉시 로딩의 경우, 어떤 엔티티를 조회하는데 그 엔티티와 관련된 모든 엔티티들이

조회되게 되고 이는 성능상의 문제를 발생시킨다. (위의 1 + N 경우)

예를 들어, Order 값을 조회하려 하는데, 연관관계로 매핑된 모든 엔티티들(Member, Delivery)

등이 원치 않음에도 조회가 되는 것이다.

 

그래서 order.getMember().getName() 과 같이 강제로 Member 를 조회해야 해당 엔티티가

조회되는 지연로딩 방식이 필요하다.

 

지연 로딩의 경우, 페이스북의 게시글을 생각해보면 좋을 것 같다.

내가 게시글을 클릭했을 때, 댓글이 즉시 로딩으로 모두 조회되면 사람에 따라

원치 않는 데이터를 조회하는 것이기도 하고, 이로 인해 성능이 느려지는 단점이 생긴다.

이런 경우, 댓글 더보기를 클릭 했을 때 댓글을 불러오는 방식의 지연 로딩을 사용하면 좋을 것이다.

 

반면, 상품을 구매 했을 때, 장바구니에서 Order 에 있는 OrderItem 만 보여주는 것보다는

유저의 정보(주소) 등을 같이 보여주는 즉시 로딩 방식이 더 좋을 것이다.

 

이론으로는 그렇지만 거의 대부분의 상황에서는 지연 로딩을 사용하는 것이 좋다!...(라는 강의 내용)

 

그렇다면 지연로딩, 어떻게 사용해야하는가 ?!

지연로딩을 하기 위해서는 Proxy(프록시)가 필요하다.

 

프록시란, 실제 엔티티 객체 대신에 사용되는 객체로 실제 엔티티 클래스와 상속 및 위임 관계에 있다.

프록시 객체는 실제 엔티티 클래스를 상속받아 만들어지므로 실제 엔티티와 겉모습이 같다.

즉 Order 객체 == 지연로딩시 만들어지는 Proxy Order 객체

1
2
3
4
5
6
7
public static void proxy1(EntityManager em) {
    // Entity proxy 객체 반환
    // Book 엔티티를 상속받은 Book Proxy를 리턴
    Book book = em.getReference(Book.class1);
    
    // System.out.println(book.getTitle());
}
cs

프록시는 getReference 를 통해 얻을 수 있다.

하지만, 위의 코드에서는 Select 쿼리를 실행하지 않는다.

6번 라인의 주석을 풀고, book 객체를 실제로 사용하면 그제서야 Select 쿼리를 통해 조회한다.

즉, 프록시는 엔티티가 사용될 때 까지 데이터를 담아두었다가 필요로 할 때, 조회하는 방식이다.

(실제 엔티티가 아닌 상속받은 객체를 가지고 온다!)

 

그렇다면 지연 로딩방식은 언제 DB 에 접근해서 엔티티를 가져오는가?

이는 프록시 초기화와 관련 돼 있다.

 

프록시 초기화란, 프록시 객체가 참조하는 실제 엔티티가 영속 컨텍스트(Persistence Context)에

존재하지 않을 때, 영속 컨텍스트에 실제 엔티티 생성을 요청하고 -> 생성된 실제 엔티티를 ->

프록시 객체의 참조 변수에 할당하는 과정을 말한다.

 

즉, 아래 과정을 설명하면 .getTitle() 로 영속 컨텍스트를 조회했지만 없는 경우(영속 엔티티가 아닌 경우),

DB 에서 값을 조회해서(초기화 요청) 이를 영속 컨텍스트에 담고, 프록시는 영속 컨텍스트에 담긴 엔티티를

참조하게 된다.

(이전에 말했던 영속 컨텍스트, 영속 엔티티와 관련이 있으므로 이해가 필요한 경우, 전 챕터 확인!)

 

 

컬렉션 조회 최적화 (XtoMany)


지연로딩으로 설정돼 있기 때문에 기본 값들은 다

Proxy 에 저장되어 있는 것을 가져온다.

Proxy 가 아닌 실제 값을 가져오기 위해서 강제로

.get을 사용해서 DB 에 접근해서 값을 가져온다.

 

하지만 위의 경우는 좋지 않은 방법!..

return 에 List<Order> 로 Api 엔티티를 직접 노출하는 방법이기 때문이다.(Dto 사용하는 방법으로 바꾸자)

 

Dto 를 이용해서 주문 정보인 Order 와 Order 에서 주문된 Item 을 가져오고자 하는 쿼리를 작성한다.

findAllWithItem 에서 쿼리를 작성하면 , 다음과 같이 작성할 수 있는데

문제는 데이터의 중복이 생긴다는 것이다.

Order Id : 4

Order_Item : JPABook1

Order_Item : JPABook2

의 정보가 들어있다고 가정해보자.

우리가 생각했을 때는, OrderId 키 값으로 두개의 정보만 가져온다고 생각했겠지만 실제로 실행시키면

중복된 두개의 데이터가 조회된다.

이러한 불필요한 조회를 없애기 위해 distinct 키워드를 작성하면 Pk 값에 따라

하나의 데이터만 나오게 됨을 확인할 수 있다.

Distinct 의 기능

1. DB에 distinct 키워드를 날려준다.

2. 엔티티가 중복인(PK) 경우에, 중복을 제외해서 컬렉션에 담아준다.

 

여러번 쿼리가 날아가던 것을 대폭 줄일 수 있다!!

 

중요한 단점

페이징이 불가능하다.

1:N (xtoMany)인 경우 페이징이 불가능하다.

== 컬렉션 페치 조인을 사용하면 페이징이 불가능하다. (List<>를 조회하는 orderItems 의 경우!!)

 + 컬렉션 둘 이상에 페치 조인을 사용하면 안된다!! (1:N:N 이 되면 데이터 뻥튀기!..)

메모리에서 페이징 처리를 해버리게 됨 -> 데이터가 많아지면 OutOfMemory 에러 발생!!

.setFirstResult 와 .setMaxResult 로 원하는 값들을 가져오고자 했다.

내가 Postman 으로 뿌려준 값은 OrderId 4번 에 해당하는 하나만을 가져오고 싶은데

실제로 DB에서 조회되는 값은 4개이다.

-> PK 값은 같지만, 주문 품목이 다르기 때문이다.

이러한 매칭의 불일치 때문에 페이징 처리가 불가능.

(= Order 기준으로 조회하려 했는데 OrderItem 이 여러개 있어서 Order 가 여러개 늘어남

-> OrderItem 기준으로 조회가 되게 됨)

 

그렇다면 어떻게 해야 페이징 처리를 할 수 있을까? (컬렉션 페치 조인)

1:N 을 제외한 나머지 XtoOne 은 기존의 방법대로 페치 조인을 진행한다.

그리고, 위의 default_batch_fetch_size 조건을 입력하면

LAZY 로딩 + in 쿼리를 통해 한번에 데이터를 가져오게 된다.

처음에 fetch 조인으로 긁어오는 데이터 1

지연 로딩 + default_batch_fetch_size 로 인한 in 쿼리 OrderItem 1

지연 로딩 + default_batch_fetch_size 로 인한 in 쿼리 item 1

총 3번으로 줄일 수 있다!

1 N M -> 1 1 1 로 쿼리를 줄일 수 있다.

(데이터 중복 조회 X)

 

정리

XtoOne 관계는 페치 조인해도 페이징에 영향 주지 않기 때문에 fetch 조인!

xtoMany 의 컬렉션의 경우, 페치 조인을 사용하지 않고, hiebernate 의 default_batch_fetch_size 로 최적화한다!

->

페이징이 가능해진다!

(fetch_size 는 100 ~ 1000 사이로 권장)

 


사담

 

그동안 졸작 프로젝트를 준비하면서 조금 바쁘기도 했고,강의를 듣다보면 점점 뒤로 갈수록 무슨 말인지 잘 모르겠어서 (공감이 안된다고 할까..)

강의 듣는 걸 점점 멀리 했는데 간만에 다시 강의 들으니 뭔가 음 그렇군.. 하는 그런 느낌이조금 생겨서 기쁘달까?..

여전히 잘 모르거나 이해가 부족한 부분은 패스하고 있지만

래도 30% 정도는 흡수하고 있는 것 같아서좋다..

 

그동안 졸작 프로젝트에서 DTO 관련해서 그냥 막연하게  음 DTO 있어야지!

하고 썼는데 강의를 듣다보니 제대로 쓰고 있지 않는 것 같다는그런 생각이 마구마구 들고 있다.

DTO 에 대한 이해와, stream 으로 엔티티를 dto 로 바꾸는 부분을 좀 찾아서 수정해봐야 할 것 같다.

 

참고 자료


'JPA Basic' 카테고리의 다른 글

변경 감지(Dirty Checking) & 병합(Merge)  (1) 2022.03.21
JPA Basic - 6  (0) 2021.12.30
JPA Basic - 5  (0) 2021.12.29
JPA Basic - 4  (0) 2021.12.28
JPA Basic - 3  (0) 2021.11.18

댓글