인프런 커뮤니티 질문&답변

김정훈님의 프로필 이미지
김정훈

작성한 질문수

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

간단한 주문 조회 V1: 엔티티를 직접 노출

JPA 순환참조 질문 드립니다.

작성

·

515

·

수정됨

1

학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.

1. 강의 내용과 관련된 질문을 남겨주세요.
2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.
(자주 하는 질문 링크: https://bit.ly/3fX6ygx)
3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.
(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)

질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.
=========================================
[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예/아니오)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)

[질문 내용]
여기에 질문 내용을 남겨주세요.

 

EventGroupInformation.java

@NotFound(action = NotFoundAction.IGNORE)
@JsonBackReference
@ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
@JoinColumn(name="CATEGORY_ID", insertable = false, updatable = false)
private CategoryInformation categoryInformation;

@BatchSize(size = 1000)
@JsonManagedReference
@OneToMany(mappedBy = "eventGroupInformation", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
@Where(clause = "DBSTS = 'O' AND FEED_DISPLAY_YN = 'Y' AND START_DATE <= SYSDATE AND END_DATE >= SYSDATE ")
@Column(insertable = false, updatable = false)
private List<EventInformation> eventInformations;

 

CategoryInformation.java

@BatchSize(size = 1000)
@JsonManagedReference
@OneToMany(mappedBy = "categoryInformation", fetch = FetchType.LAZY)
@Where(clause = "DBSTS = 'O' AND START_DATE <= SYSDATE AND END_DATE >= SYSDATE ")
@Column(insertable = false, updatable = false)
//@Access(value = AccessType.PROPERTY)
public List<EventGroupInformation> eventGroupInformations;

 

여러개의 oneToMany가 걸려 있는 entitiy들을 강의 V5예제를 참고하여 개선중에 있습니다.

CategoryInformation(Order)

ㄴ EventGroupInformation(OrderItem)

ㄴ EventInformation(Item)

이런 식에 구조인데요, 각각 oneToMany 입니다.

 

EventGroupInformation과 EventInformation을 fetchJoin을 통해 쿼리를

@Query(value = "SELECT distinct eg FROM EventGroupInformation eg " +
"LEFT JOIN FETCH eg.eventInformations e " +
"WHERE eg.dbsts = :dbsts " +
"AND eg.categoryId IN :categoryIds" )

 

호출하면 해당 fetchjoin 쿼리 하나만 나갈꺼라고 생각을했는데,

갑자기 상위 entity인 CategoryInformation 테이블에 category_id(PK)로 category_id 개수만큼의

N+1 이슈가 발생 하였습니다.

 

JsonBackReference, JsonManagedReference 해당 옵션을 주면 역참조가 안되어야 하는게 맞지 않나요?? 추가로 ManyToOne 쪽에 @JsonIgnore 도 추가 해보았으나 마찬가지입니다 ㅠ

제가 놓치고 있는게 있을까요?

 

김영한 선생님 강의 에서는

OrderItem-Item 관계에서 IN쿼리를 호출했을때 IN절에 포함되는 orderId기준의 상위 엔티티인 Order 테이블에 역으로 쿼리가 나가는건 못봤는데 말이죠..

(물론, 양방향 관계를 끊고 단방향 설정시 N+1 쿼리는 나가지 않습니다.)

답변 2

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 김정훈님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

실제 동작하는 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx


주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

 

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

감사합니다.

1

안녕하세요, 인프런 AI 인턴입니다.

김정훈님의 JPA 순환참조에 관련된 질문에 답변 드리겠습니다.

먼저, @JsonBackReference@JsonManagedReference는 Jackson 라이브러리에서 JSON 직렬화 시 순환참조를 방지하는 어노테이션입니다. 순수 JPA나 Hibernate에서의 엔티티 로딩 동작과는 직접적인 관계가 없습니다.

JsonBackReference는 직렬화 시 해당 필드를 무시하도록 지시하고, JsonManagedReference는 정방향 참조를 가리켜 직렬화에 포함됩니다. 하지만 이 어노테이션들은 JSON으로 변환할 때만 적용되므로 JPA/Hibernate 수준에서의 쿼리 실행과는 무관합니다.

하이버네이트가 쿼리를 실행하며 연관된 엔티티를 로딩하는 과정에서 발생하는 N+1 문제는 fetch = FetchType.LAZY 로 설정되었거나, 명시적인 별도의 Fetch Join 지시가 없을 때 발생할 수 있습니다.

특정 엔티티를 조회할 때 관련된 여러 개의 하위 엔티티를 함께 가져와야 한다면 Fetch Join을 사용하여 N+1 문제를 해결할 수 있습니다. 하지만 Fetch Join을 할 때는 JPQL의 distinct를 사용하여 중복 결과를 제거하는 것이 일반적입니다.

질문하신 상황에서 발생하는 N+1 문제는 EventGroupInformation에서 CategoryInformation으로의 역방향 참조 때문으로 보이며, 이 경우 역방향 참조에 대한 Fetch Join도 추가로 명시해주어야 합니다. 현재 제공한 JPQL 쿼리에서는 EventGroupInformation에 대해 EventInformation과의 Fetch Join만 선언되어 있으며, 상위 엔티티인 CategoryInformation에 대한 Fetch Join은 명시되어 있지 않습니다.

이런 경우 해결 방법으로는 다음과 같은 선택지가 있습니다:

  1. @ManyToOne 관계에서 fetch = FetchType.EAGER로 설정하여 항상 즉시 로딩되도록 하는 것이 하나의 방법입니다. 그러나 이는 전역적으로 적용되므로 성능 문제가 있을 수 있습니다.

  2. Fetch Join을 명시적으로 추가하여 필요한 연관 관계를 한 번의 쿼리로 로딩하도록 하는 것입니다. 이를 위해서는 CategoryInformation도 Fetch Join 문에 포함시켜야 할 수 있습니다.

  3. JPQL 쿼리를 통해 N+1 문제 해결을 하기 어려운 경우에는 Hibernate의 @BatchSize 어노테이션을 사용하여, 연관 객체들을 한 번에 여러 개씩 불러오는 방식으로 N+1 문제를 완화할 수 있습니다.

마지막으로 주어진 쿼리와 클래스의 매핑 관계를 기반으로 직접 쿼리를 최적화하는 것 외에, 실제 발생하는 쿼리 로그를 분석하여 어느 부분에서 문제가 발생하는지 파악하는 것이 중요합니다.

질문하신 내용을 바탕으로 예상한 해결 방안 모두 실험해보시고, 여전히 문제가 해결되지 않으면 추가적인 정보와 함께 인프런 질문 게시판에 구체적인 내용을 올려주시면 다른 학습자나 강의 지식공유자분들이 더 좋은 답변을 드릴 수 있을 것 같습니다.

도움이 필요하신 경우, 강의 중에서 관련 부분을 명시하고 질문 게시판에 질문을 남기시는 것이 좋겠습니다.

김정훈님의 프로필 이미지
김정훈
질문자

답변 정말 감사드립니다.

해당 어노테이션이 그런 의미인줄은 몰랐네요.. 무지했던것 같습니다.

 

전달주신 해결안 참고로 확인해보도록 하겠습니다.

감사합니다.

김정훈님의 프로필 이미지
김정훈
질문자

1, 3번의 경우 동일하게 N+1의 쿼리가 발생하여 해결책이 되지 못하였습니다.

(3번의 경우 batchSize를 주었는데도, IN 쿼리가 나가지 않고 그대로 N+1 이슈가 동일하게 발생 하였습니다.)

 

2번의 경우는 사용은 하지 않지만, fecth join에 CategoryInformation도 같이 포함을 시켜

원하는 결과를 얻긴 하였으나, 결과에 필요하지 않은 CategoryInformation 과의 inner join이

추가 되어 쿼리 성능의 저하를 우려하여, 아쉽지만 해당 방법도 완벽하게 해결되지 못하였습니다ㅠ

 

그래서 결론은 EventGroupInformation 엔티티에 manyToOne 관계로 있는

CategoryInformation 엔티티는 역으로 조회하는 케이스가 없기때문에

EventGroupInformation <-> CategoryInformation 간의 양방향 관계에서

단방향 관계로 변경 하였습니다.

 

이부분 역시 강의에서 알려주신 단방향 관계 보다는 최대한 양방향 관계로 맺어

사용 하라고 말씀해주신 부분에 대해 위배 되지만, 해당 방법이 가장 가까운

답안으로 생각 됩니다.

 

의견 감사 드립니다!

김정훈님의 프로필 이미지
김정훈

작성한 질문수

질문하기