[JPA] OSIV 만 믿었다가 Lazy 로딩에 발등찍힌 썰

2024. 1. 27. 00:12·Logging/Spring
728x90
반응형

 

작업을 하다가 처음 보는 문제점을 확인해서 로깅 기록을 남겨보게 되었다. 요즘 글 많이 쓰기 힘들어서 문제 설명만 하고 빨리빨리 진행하겠다. 이 로직만 정말 하루 종일 작업했다. 

 

 

1. 문제 상황

 

 

Group 과 GroupCategory, Category 가 있다. 카테고리에는 categoryValue 가 있고, Group 과 Category 는 N:N 관계이며 GroupCategory 테이블이 둘을 연결해준다. 

 

 

Group 을 검색하는 로직을 만들고 있을 때, JPQL, 1:N 의 fetchJoin 에서의 Paging 처리 문제 등등으로 인해서, GroupCategory 와 Category 를 fetch Join 하지 못한채 원하는 Group List 를 가져온 상황이다. (이럴 수밖에 없었음.. 해쉬태그 구현이랑 유사했었다)

 

 

OSIV 는 켜져 있다. Controller 영역에서 영속성 컨텍스트 안을 계속 사용하기 위해서이다. 또한 Group 에는 다음과 같이 @OneToMany 로 양방향 매핑이 되어 있다. 

 

 

@Entity
public class Group {
    ...
    @OneToMany(fetch = fetchType.LAZY, mappedBy="group")
    private List<GroupCategory> groupCategories = new ArrayList<>();
    
    ...
}

 

 

문제가 되는 Service 영역은 다음과 같이 진행되고 있다

 

 

...

    List<Group> pagedGroups = repository.findByDynamicSearchCondition(...);
    
    return pagedGroups;

}

 

 

하지만 나는 이 요청의 응답에 Category 정보를 실어서 보내줘야 했다. 따라서 GroupCategory 를 기준으로 Category 를 fetchJoin 하는 쿼리를 하나 더 발생시켜서 영속성 컨텍스트에 보관해줄 생각을 했다. (다시 한번 말하지만 저 SearchCondition 쿼리에서 fetch Join 을 할 수가 없는 상황이다) 

 

 

차피 OSIV 도 ON 이니까, Category 가 fetch join 된 GroupCategory 들만 따로 영속성 컨텍스트에 로딩해 놓고, Controller 에서 Group 의 GroupCategory 들을 사용할 때 알아서 영속성 컨텍스트에서 불러와서 사용하게 하자!

 

 

내 생각은 위와 같았다. 따라서 pagedGroup 의 Id 들만 가져와서 GroupCategory 를 가져오는 in 절 쿼리를 발생시켰다. 

 

 

    ...
    List<Group> pagedGroups = repository.~~;
    List<Long> moimIds = pagedGroups.stream().map(Group::getId).collect(Collectors.toList());
    
    // GroupCategory 와 연관된 Category 들을 모두 영속성 컨텍스트에 로딩한다
    List<GroupCategory> groupCategoires = categoryRepo.findWithCategoryByGroupIds(groupIds);

    return pagedGroups;
}
// Transactional 종료

 

 

하지만 Controller 영역에서 Group.getGroupCategories() 를 하는 시점에 다음과 같이 쿼리가 발생하는 모습들을 확인할 수 있었다. 심지어 Group 의 갯수만큼 쿼리가 발생해서, N+1 문제를 확인할 수 있었다. 

 

 

헬로월드는 나의 로깅 흔적.. 발생 시점을 찾는 것도 한참 걸렸다 (moim 이 group 이다)

 

 

Lazy 로딩에서 N+1 문제라니.. 이 문제일거라고 생각을 못해서 찾는 것도 오래걸렸다. 여기저기 찾아봤을 때 정확하게 사유를 찾은 것은 아니지만, 내가 내린 결론은 다음과 같다. 

 

 

  • Group 을 조회할 때 GroupCategory 를 @OneToMany 관계를 이용해서 Fetch Join 을 한 경우가 아니라면, Group.getGroupCategory 를 통해서 사용을 하려 한다면 쿼리가 발생한다

  • 어찌보면 당연한게, 영속성 컨텍스트를 생각하지 않고 Java 입장에서 봤을 때, 저 위에서 조회한 List<GroupCategory> 안에 있는 객체들과 Group.getGroupCategory 객체들은 다르다. fetch join 을 하지 않았기 때문에 Group 객체 안에 있는 GroupCategory List 들은 모두 프록시로 감싸져있는 가객체이다

  • 따라서 Group 입장에서 .getGroupCategory 가 쓰이면, 영속성 컨텍스트는 신경쓰지 않고 "내가 가지고 있는건 프록시 객체니까 조회해와야 겠다!" 하고 쿼리를 발생시키는 것이다. 

 

 

2. 해결 및 고찰

 

 

나의 해결방안은 비교적 간단하게 해결했는데, 따로 매핑을 해주는 것이였다. 차피 List<CategoryGroup> 을 모두 들고 있으니, 이를 Controller 영역에 같이 전달해서 Response Dto 를 매핑해줄 때 Group Id 를 통해서 매핑을 해서 넘겨주는 방법을 사용하였다. 이렇게 했더니 더 이상 쿼리는 발생하지 않았다. 

 

 

하지만 아직 의문인 점이 있다. 내 앱에서 이미 적용되어 있던 것처럼,  default_batch_size=100 으로 했으면 OneToMany 관계에 있는 List 를 사용하려고 했을 때 in 절을 사용해서 한번에 들고와야 하는 것이 아닌가? 그렇지 않고 하나씩 가져오는 이유를 모르겠다. 

 

 

어쨌든 이런 JPA 에 순수하게 의존하는 것은 정말 한계가 많다고 느끼고, JPA 를 100% 마스터 했다! 하기전까지 이해안되는 문제들이 너무 많은 것 같다. 따라서 가끔은 이번 방법처럼 NAIVE 하게 해결해보는 것도 좋은 것 같다. (비슷한 느낌으로 점점 QueryDSL 도 안쓰고 그냥 JPQL 이 편한 것 같다. String 오류 나봤자 해결하면 되고.. 동적 쿼리 작성하는데 String 으로 붙였다 떼는게 훨씬 편한 것 같다...)

 

 

오랜만에 밟아본 N+1 문제였다. OSIV랑 JPA 만 너무 믿지 말고 항상 콘솔에 쿼리 찍히는걸 보면서 개발하는게 너무 중요하다.

 

 

N+1 은 성능에 치명적이다

 

 

728x90
반응형

'Logging > Spring' 카테고리의 다른 글

[Spring MVC] org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 오류  (0) 2024.01.29
Spring Boot 개발하면서 요즘 드는 생각  (0) 2023.12.13
[문제기록] Lombok @Getter 사용시 boolean 값 처리에 대하여  (0) 2023.08.30
[문제기록] DataJpaTest 시 NoSuchBeanDefinitionException / UnsatisfiedDependencyException  (0) 2023.08.14
'Logging/Spring' 카테고리의 다른 글
  • [Spring MVC] org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 오류
  • Spring Boot 개발하면서 요즘 드는 생각
  • [문제기록] Lombok @Getter 사용시 boolean 값 처리에 대하여
  • [문제기록] DataJpaTest 시 NoSuchBeanDefinitionException / UnsatisfiedDependencyException
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    디자인 패턴
    단위테스트
    Configuration
    Composite
    OOP
    김영한
    JPA
    k8s
    객체지향
    composition
    BEAN
    analyzer
    decorator
    Setter
    spring container
    di
    Spring
    SRP
    GoF
    junit
    Design Pattern
    spring boot
    elasticsearch
    mockito
    Java
    n+1
    runtime exception
    lombok
    OOAD
    lazy loading
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[JPA] OSIV 만 믿었다가 Lazy 로딩에 발등찍힌 썰
상단으로

티스토리툴바