NDM

3. FetchType = Lazy를 명시해도 Lazy Loading이 적용되지 않는다?? 본문

[프로젝트] Slow Delivery

3. FetchType = Lazy를 명시해도 Lazy Loading이 적용되지 않는다??

ndm.jr 2022. 8. 18. 14:48

 

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Shop extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "shop_id")
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "seller_id")
    private Seller seller;

    @OneToMany(mappedBy = "shop")
    private List<Product> products = new ArrayList<>();

    private String phone;
    private String shopName;
    private Integer minimumPrice;

    @Enumerated(EnumType.STRING)
    private openStatus openStatus;
}

@Entity
@Getter
@PrimaryKeyJoinColumn(name = "seller_id")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue(value = "SELLER")
public class Seller extends User {

    @OneToOne(mappedBy = "seller")
    private List<Shop> shop = new ArrayList<>();

    @Builder
    public Seller(String name, String nickname, Role role, String email, String password) {
        super(name, nickname, email, role, password);
    }
}

 

진행중인 프로젝트의 가게 - 판매자 관계를 발췌한 부분이다.

다른 것은 이번 포스팅에서 신경쓸 필요가 없고, 두 엔티티의 관계만 보자.

프로젝트의 정책 상 판매를 하고싶다면

"판매자로 회원가입 후 가게를 개설" 하는 과정을 거쳐야 했기에 가게 - 판매자 엔티티는 1:1로 묶어주었다.

여기서  "fetch = FetchType.Lazy" 로 설정했음에도 불구하고, 판매자를 조회했을 때 가게까지 한번에  Eager로 조회되는 상황이 발생했다.

 

상황을 정리하면

  • Seller - Shop은 1대 1 양방향 매핑이다.
    더보기
    1대1 단방향은 외래키를 어디 두냐에 따라 이슈도 있고,
    둘이 빈번히 서로의 데이터를 조회한다고 판단했기에 양방향을 선택했다.
  • 연관관계의 주인은 Shop이다.
  • 연관관계의 주인인 Shop을 조회할 때는 Lazy로 동작하지만,
    Seller를 조회할 떄는 Shop까지 Eager로 조회하게 된다.

 


 

 프록시를 알아보자

 

JPA의 Lazy Loading은 Proxy 기반으로 동작한다. 프록시에 대해 간단하게 짚고 넘어가자

 

출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편(인프런) : 김영한

 

프록시의 동작 방식은 다음과 같다.

  • 실제 인스턴스를 상속받은 프록시 객체를 생성. 프록시 객체에는 어떤 객체를 가리키는지에 대한 참조값만 실제
  • 클라이언트는 실제로는 프록시 객체에 요청하게 되지만, 이를 알 수는 없음
  • 내부적으로는 프록시 객체가 요청을 받고, 객체의 인스턴스 필드에 값에 대한 요청이라면 영속성 컨텍스트에 초기화
  • 이 때, 참조되는 객체가 영속성 컨텍스트에 존재한다면 그대로 반환, 아니라면 DB 조회(쿼리 날아감)
  • 영속성 컨텍스트의 값을 기반으로 실제 엔티티 생성
  • 값이 다 들어있는 실제 엔티티에 실제 값을 조회함

 

복잡해보이지만, 핵심은 아직 사용하지 않는 객체에 대해서는 어떤 객체를 가리키는지에 대한 참조값만 있는 껍데기(프록시)가 존재하고, 실제 인스턴스 필드에 접근할 때만 실제 객체에 대한 조회가 일어난다는 것이다.

 

Lazy Loading도 이와 같이 동작한다.

일단 필요한 객체만 조회하고, 아직 조회하지 않을 객체(Lazy)는 Proxy로 생성해 놓고 기다리는 것이다.

 

 

 


왜 그랬을까?

 

Thorben Janssen의 위 문제에 대한 답변이다. 해석해보면

 

Hibernate는 대상이 Null 혹은 Proxy인지 반드시 알고있어야 하며

Lazy의 대상이 되는 객체를 참조하는 엔티티를 찾으려면 참조 요청을 보낸 객체를 쿼리해봐야지만 알 수 있다는 내용이다.

 

어떤 뜻인지 자세히 알아보자. DB테이블을 알 필요가 있다.

 

 

현재의 테이블은 위와 같이 생겼다.
Shop(연관관계의 주인)을 조회한다면 아무 문제가 없는 것을 확인할 수 있다. 반대로 Seller는 어떨까?

 

Seller 테이블에는 외래키가 없다. 우리는 Seller를 조회했고, Shop은 Lazy로 설정되어있다.

Lazy는 Proxy 기반으로 동작해야 한다고 했고,

Thorben Janssen은 Hibernate가 대상이 Null 인지 Proxy인지 반드시 알고있어야 한다고 했다.

그럼 여기서 Seller는 Shop이 뭔지 어떻게 알 수 있을까? 외래키가 없는데 존재 자체는 알 수 있을까?

 

방법이 없다. 무조건 한번은 조회해봐야 한다.

 

 

떄문에 Lazy로 설정했음에도 불구하고 Eager로 동작하는 것이다.


해결 방안

 

완벽한 해결책은 없다고 한다.

 

  • 구조를 ManyToOne / OneToMany로 변경하거나
  • 조회할 때 FetchJoin을 사용해 한번에 끌어오는 것
  • 양방향 매핑을 끊는 것

정도가 불완전하긴 하지만 해결방법이라고 할 수 있다.

 

내 프로젝트의 경우, Seller - Shop은 서로 데이터를 조회할 일이 많아 양방향은 유지하기로 했고,

구조를 ManyToOne / OneToMany로 바꾸는 것을 택했다.

 

"한 판매자가 하나의 가게만을 개설할 수 있다" 는 정책으로 시작했지만

생각해보니 같은 사람이 여러 가게를 등록하기 위해 여러 아이디를 생성하는 것이 더 비효율적이라고 판단했기 떄문이다.

 


느낀점

 

김영한 님의 강의를 들을 때,

모든 관계는 Lazy로 설정해라. Eager로 설정했다가는 상상하지 못한 쿼리가 나간다.  는 말을 들은 적이 있다.

 

이 때는 당연히 그렇겠구나.. 했는데 실제 프로젝트 중에 마주치니 정말 당황스러웠다.

 

정말 왜 이 쿼리가 어디서 나왔는지 상상 자체를 못했고,

어디서 Eager로 조회된거지?? 라는 생각은 할 수 없었다.

만약 이같은 상황을 마주쳤을 떄 Eager로 동작했다는 것을 알았다면 그나마 양반이다.

 

나는 혹시 로직을 잘못짰거나, 무언가를 실수한 줄 알고 breakpoint를 찍으면서 하나하나 디버깅했었다.

그리고 나서야 Eager로 동작하고 있구나 를 알았지, 처음부터 Eager 로딩인 줄은 상상도 하지 못했다.

 

그래도 문제를 해결하려는 노력을 바탕으로 작은 이슈였지만 해결한 것에 뿌듯함을 느끼며

다시 한번 Proxy와 Lazy Loading에 대해 복습할 수 있는 좋은 기회가 되었다고 생각한다.


참조

https://1-7171771.tistory.com/143

 

[JPA] @OneToOne에서 Fetch 전략을 Lazy로 설정했을때 발생하는 이슈

Lazy Loading JPA의 유일한 단점은 사용하기 쉬운만큼 성능적인 측면에서 발생할 수 있는 이슈를 간과하기 쉽다는 것인데, 성능이 안나올때 가장 먼저 고려해봐야할 부분이 즉시로딩(EAGER LOADING)으로

1-7171771.tistory.com

http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788960777330 

 

자바 ORM 표준 JPA 프로그래밍 - 교보문고

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임 | ★ 이 책에서 다루는 내용 ★■ JPA 기초 이론과 핵심 원리■ JPA로 도메인 모델을 설계하는 과정을 예제 중심으로

www.kyobobook.co.kr

https://woodcock.tistory.com/23

 

One-to-One 관계에서 Lazy 로딩은 언제 동작되는 것일까?

서론 JPA를 사용하면서 일대일 연관관계를 맺었는데 예상치 못한 쿼리를 만나게 되었고 원인을 분석하면서 알게된 내용을 공유해보려고 한다. One-to-One 관계에서는 Lazy로딩은 특정 조건에서만 동

woodcock.tistory.com

https://thorben-janssen.com/hibernate-tip-lazy-loading-one-to-one/

 

Hibernate Tip: How to lazily load one-to-one associations

Lazy loading of one-to-one associations only works under certain conditions. Here is how it works and how you can optimize your mapping.

thorben-janssen.com