ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • FK 없는 OneToOne 즉시 로딩 개선하기
    Spring Framework 2022. 6. 7. 00:08
    반응형

    1) 서론

    이번 글에서는 업무 중 발견한 레거시 코드의 문제점을 개선한 것을 공유하고자 합니다.

     

    다양한 로직에서 빈번히 조회되고 있던 엔티티가 있었는데요. 특별히 문제가 발생하지 않았기 때문에 실제로 발생하는 쿼리를 주의 깊게 보지 않았었습니다. 하지만 우연히 지나치게 많은 쿼리가 발생하고 있는 것을 발견했습니다. 

     

    최초의 설계 의도는 지연 로딩을 하고자 했던 것 같지만, 실제로는 즉시 로딩이 되고 있었습니다. N+1 문제가 발생하고 있었는데요. 적은 스펙의 IDC 환경에서 돌아가는 애플리케이션을 운영하는 입장에서 불필요한 쿼리 개선 하나하나가 소중한 상황입니다.

     

    이러한 레거시 코드를 개선하고, 변경한 내용을 공유합니다.

     

    지연 로딩아~


    2) DB schema

    모든 테이블 구조 및 로직은 재현을 위해 임의로 만들었습니다. 실제 업무와 전혀 관계없습니다.

    출판사(publisher), 작가(author), 책(book) 3개의 테이블이 존재합니다.

     

    테이블 간의 연관 관계는 아래와 같습니다.

    • 출판사(1) : 책(N)
    • 작가(1) : 책(1)
      • 한명의 작가는 한 권의 책만 집필할 수 있다는 억지스러운(?) 설정입니다.
    • 책(book) 테이블이 출판사 및 작가의 정보를 FK로 참조하고 있습니다.

    3) Entity 구조

    Author entity
    Publisher entity
    Book entity

    테스트를 위해서 최소한의 설정으로 엔티티 설계했습니다. JPA OneToOne 양방향 맵핑에서 지연 로딩의 문제점을 재현하는데 목적을 두고 작성했는데요.

     

    어쨌든, Book 테이블Publisher와 Author의 FK를 가지고 있습니다. 그래서 Book 엔티티를 연관 관계의 주인으로 설정하고,

    @JoinColumn을 통해서 대상 테이블을 참조합니다.


    4) XToOne 단방향 지연 로딩 테스트

    출판사(publisher), 작가(author)의 FK를 가지고 있는 책(Book)을 조회합니다.

    • QueryDSL로 조회 쿼리를 명시적으로 사용합니다.
    • 책 이름으로 조회하는 단순한 쿼리입니다.

    • @BeforeEach를 활용해서 Author, Publisher, Book의 내용을 저장합니다.

    • Author, Publisher 엔티티는 XToOne 관계임에도 불구하고 지연 로딩이 잘 됐습니다.

     

    데이터베이스의 테이블 연관관계를 바탕으로 엔티티를 설계했을 때는 사실상 단방향일 것입니다. 정확히는 '무방향'이라는 표현이 더 맞을 것 같은데요. 왜냐하면 테이블간의 연관 관계에서는 '방향'이 존재하지 않습니다. 먼저 만들어진 부모 테이블을 참조하는 자식 테이블은 FK를 가집니다. 하지만 이러한 관계가 방향을 나타내지는 않습니다. Join 쿼리를 이용해서 부모-자식 관계없이 조회 가능합니다. 

     

    하지만 데이터베이스 테이블 간의 연관관계와 다르게, ORM은 관계형 데이터베이스를 객체와 맵핑시켜줍니다. 이때의 핵심은 관계형 테이블 간의 연관 관계를 객체지향을 통해서 만든다는 것인데요.

     

    제가 생각하는 객체지향은 객체 간의 대화를 통해서 프로그래밍을 하는 것이라고 생각합니다. Book 클래스는 Author, Publisher 클래스들을 참조하고 있습니다. 하지만 Author, Publisher 클래스 입장에서는 Book 클래스와 대화할 수 있는 방법이 없습니다. 객체 간의 참조가 없기 때문입니다.

     

    만약 연관 관계가 없는 다른 클래스에서 Book 클래스를 참조하면 어떻게 될까요?


    5)  OneToOne 양방향 지연 로딩 테스트

    업무 중 만난 코드에서는 아래와 같이 OneToOne 양방향 맵핑이 되어있었고, Lazy fetch가 설정되어 있었습니다.

    • Author 클래스에서 Book 클래스를 참조하고 있습니다.
    • OneToOne이지만 즉시 로딩을 피하기 위해 Lazy fetch 설정합니다.

    • 위에서 Author 엔티티가 Book을 참조하게 설계되었습니다.
    • 하지만 실제 비즈니스 로직에서는 Book을 조회하지 않으려고 합니다.

    • Book 엔티티를 조회하지 않았지만 select 쿼리 발생합니다.
    • Lazy fetch임에도 불구하고, 즉시 로딩으로 인한 N+1 문제가 발생했습니다.

    5. 1) FK가 없는 엔티티에서의 지연 로딩 불가 문제

    XToOne 관계에서 FK키가 없는 엔티티에서는 지연 로딩이 불가능합니다.

     

    하이버네이트에서 지연 로딩의 대상이 되는 엔티티는 프록시 객체로 조회가 되는데요. 이때 중요한 것은 null은 프록시 객체가 될 수 없습니다. 

     

    객체 간의 연관관계만 봤을 때는 해당 인스턴스의 null 여부를 명확히 알 수 있습니다. 하지만 결국 ORM은 객체의 연관관계를 통해서 데이터베이스의 데이터를 조회해야 합니다. 

     

    데이터베이스에서 Author 테이블은 Book 테이블에 대한 정보가 전혀 없습니다. null 혹은 프록시 객체 여부를 확인하려면 조회 쿼리를 통해 확인할 수밖에 없습니다. 그렇기 때문에 FK를 가지고 있지 않을 때 XToOne 맵핑은 N+1의 문제가 발생합니다.

     

    그렇다면 XToMany에서는 FK와 관계없이 프록시 객체를 가지고 올 수 있을까요?

     

    1:N의 관계에서 N은 List 타입입니다. 즉 조회 대상의 데이터가 없더라도 empty일 뿐이지, null은 아니기 때문입니다.

     

    1:N의 관계를 테스트해보겠습니다.

    FK키를 가진 Book 엔티티에서 Author, Publisher를 조회해봅니다.

    • 임의로 FK에 해당하는 author, publisher 컬럼을 null로 변경합니다.

    • null로 조회됩니다.

    • author, publisher 컬럼에 데이터가 존재합니다.

    • proxy 객체를 활용해서 지연 로딩합니다.

     

    개인적으로는 OneToOne 양방향, FK가 없는 XToOne 관계를 좋아하지 않습니다.

     

    특정 비즈니스 로직에서는 반드시 같이 조회가 되어야 하지만, 아닌 경우도 존재합니다. 꼭 같이 조회가 되지 않아도 되는 경우에는 불필요한 N+1 쿼리를 계속해서 발생시킬 것입니다. 

     

    그래서 저는 OneToOne 연관관계는 반드시 단방향으로 설계하고, 이후에 정말 필요하다면 양방향으로 변경하는 것이 좋다고 생각합니다.

    5. 2) 개선

    FK가 없는 엔티티에서 반대 엔티티를 조회하는 방법은 다양할 것 같습니다.

     

    개인적으로 OneToOne의 양방향 맵핑이 반드시 필요한 경우에는 가운데에 'EntityOtherMapping'와 같은 맵핑 테이블을 만드는 것이 더 좋다고 생각합니다.

     

    물론 지나친 정규화로 인해 테이블의 수가 너무 늘어날 수도 있고, 데이터베이스의 구조를 변경하는 것은 실제 서비스 운영 중에 쉬운 일이 아닐 것입니다.

     

    개선을 위해 선택한 방법은 두 가지입니다.

    • FK 없는 XToOne 연관 관계 모두 제거
    • 기존의 Query 메서드의 select → QueryDSL 변경 후 명시적으로 inner join 사용

    • return 타입으로 AuthorBookDto을 사용합니다.
    • inner join을 명시적으로 사용해서 book 엔티티를 조회합니다.

    • 추가 select 쿼리가 아닌, inner join 쿼리 발생

     

    개인적으로 생각하는 위의 장점은 각각의 필요에 따라서 조회를 다르게 사용할 수 있는 것이라고 생각합니다. 강제로 양방향 맵핑으로 변경해서 불필요한 N+1 쿼리를 발생시키는 것보다는 상황에 맞게 적절히 join 하는 것이 효율적이라고 생각합니다.


    6) 결론

    개인적으로 반드시 필요한 상황이 아니라면, FK가 없는 엔티티에서 XToOne 참조는 하지 않는 것이 좋다고 생각합니다. 위의 테스트 결과처럼 불필요한 N+1 쿼리를 발생시키기 때문입니다.

     

    엔티티 설계를 잘못했다고 가정합니다. 하나의 엔티티가 여러 엔티티를 참조하고, 해당 엔티티들이 또 다른 엔티티들을 참조한다면, 얼마나 많은 N+1 쿼리가 발생할지 상상하기 어렵습니다.

     

    저는 위와 같이 기존의 레거시 코드를 개선했습니다. 하지만 더 좋은 방법을 알고 계시거나, 틀린 내용이 있다면 망설이지 말고 댓글 부탁드리겠습니다! 감사히 듣고, 공유하겠습니다!


    7) 참고 문헌

    https://stackoverflow.com/questions/1444227/how-can-i-make-a-jpa-onetoone-relation-lazy

    https://stackoverflow.com/questions/13044567/hibernate-bidirectional-onetoone

    반응형

    댓글

Designed by Tistory.