문제
엘라스틱 서치로 API 응답 최대소요시간 그래프를 통해 /api/v1/user-challenges인 URL path에서 높은 duration을 발견했다.
원인 파악 과정
- SQL 쿼리 분석 : N+1 문제 발견
- 연관관계 분석 :
2-1. UserChallenge 와 Challenge 는 N:1 단방향 연관관계
2-2. Challenge와 ChallengeRecord는 1:1 양방향 연관관계
2-3. UserChallenge를 조회하면서 연관관계인 Challenge를 조회한다. 그리고 Challenge와 연관관계인 ChallengeRecord를 조회함
3. 일대일 단방향 관계에서 N+1 문제 원인 분석 (Challenge와 ChallengeRecord)
3-1. ChallengeRecord가 Challenge의 외래키를 가지고 있으므로 연관관계 주인이다.
3-1. Challenge를 조회할 때 객체에서는 ChallengeRecord(@OneToOne)가 존재하지만 DB 테이블에서는 ChallengeRecord의 값이 없으므로 ChallengeRecord를 조회한다.
3-2. 그러므로 연관관계 주인이 아닌 곳인 Challenge에서 호출하게 되면 ChallengeRecord도 조회하기 때문에 지연로딩(Lazy loading)이 적용되지 않음
해결 과정
-주인이 아닌 Challenge를 조회할때 ChallengeRecord의 기본키값인 seq를 모르기 때문에 ChallengeRecord를 조회해야함
-fetchJoin 메서드를 사용하여 해결
fetchJoin 메서드 적용 전 ) 유저 챌린지 전체 조회
@Override
public List<UserChallenge> findAllByUserSeq(
Long userSeq,
Integer pageSize,
Long prevLastUserChallengeSeq
){
return from(qUserChallenge)
.innerJoin(qUserChallenge.challenge, qChallenge)
.fetchJoin()
.orderBy(qUserChallenge.startDate.asc())
.where(predicates)
.limit(pageSize)
.fetch()
.stream()
.distinct()
.collect(Collectors.toList());
}
fetchJoin 메서드 적용 후 ) 유저 챌린지 전체 조회
@Override
public List<UserChallenge> findAllByUserSeq(
Long userSeq,
Integer pageSize,
Long prevLastUserChallengeSeq
){
return from(qUserChallenge)
.leftJoin(qUserChallenge.challenge, qChallenge).fetchJoin()
.leftJoin(qChallenge.challengeRecord, qChallengeRecord).fetchJoin()
.orderBy(qUserChallenge.startDate.asc())
.where(predicates)
.limit(pageSize)
.fetch()
.stream()
.distinct()
.collect(Collectors.toList());
}
-SQL 튜닝 전후의 성능을 비교하기 위해, 부하테스트(Jmeter)를 통해 Throughput을 측정해보니 1.7배 성능이 향상( 10TPS → 17TPS) (-> 이전 글 포스팅함 2023.03.25 - [Java] - Jmeter을 이용한 성능 테스트 )
평가 및 회고
-지연 로딩으로 설정이 되어있는 엔티티를 조회할 때는 프록시로 감싸서 동작하게 되는데, 연관관계 주인이 아닌 Entity는 연관관계의 주인인 Entity가 null인지 아닌지 알 수 없기 때문에 프록시 객체를 만들 수 없다. 프록시는 null을 감쌀 수 없기 때문에 이와 같은 문제점이 발생하게 된다. 즉, 프록시의 한계로 인해 발생하는 문제라는 것을 알 수 있었다.
-fetchJoin으로 해결할 수 있지만 앞으로는 설계할 때 주 테이블인 challenge에서 대상 테이블인 challenge_record을 조회하는 일이 빈번하므로 challenge 테이블에 challenge_record_seq 컬럼을 추가한다. 이렇게 함으로서 challenge_record를 조회하지 않도록 한다.
'Backend' 카테고리의 다른 글
낙관적 잠금과 비관적 잠금으로 알아보는 동시성 처리 (1) | 2024.05.02 |
---|---|
Jmeter을 이용한 성능 테스트 (0) | 2023.03.25 |
[Error] No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.0.1 was found. (0) | 2023.01.18 |
[개념]DAO, DTO, VO, CRUD (0) | 2020.04.20 |