📌 테스트 개요
취향(Taste) 카테고리 데이터를 효율적으로 조회하기 위해 여러 가지 최적화 방안을 실험하였습니다. 총 네 가지 방식으로 테스트를 진행했으며, 성능과 비용 측면에서 가장 최적화된 방법을 찾는 것이 목표였습니다.
테스트 환경
- JMeter를 이용한 부하 테스트
- TPS(Transactions Per Second) 측정
- 테스트 기준: 1000개 요청
- 쿼리 최적화 성능 변화도 측정 및 Redis 캐싱 검증
테스트 시나리오
- 최적화를 적용하지 않은 기본 코드
- @EntityGraph를 TasteRepository 5개에 각각 적용
- @EntityGraph + LEFT JOIN FETCH(통합) + Batch Size 적용
- @EntityGraph + LEFT JOIN FETCH(분리) + Batch Size 적용
각 방식의 성능 차이를 분석하고, 가장 최적화된 방안을 선정하였습니다.
🔍 테스트 결과 비교
테스트 방식 테스트 성공 여부 최적화 수준 비용 절감 효과
1️⃣ 기본 코드 (최적화 없음) | ☑️ 성공 | ❌ 최적화 없음 | -$0.25 |
2️⃣ @EntityGraph (TasteRepository 5개) | ☑️ 성공 | 🔸 부분 최적화 | -$0.23 |
3️⃣ @EntityGraph + LEFT JOIN FETCH (통합) + Batch Size | ❌ 실패 | ❌ Redis 캐싱 실패로 중단 | -$0.15 |
4️⃣ @EntityGraph + LEFT JOIN FETCH (분리) + Batch Size | ☑️ 성공 | ☑️ 최적화 최상 | **-$0.23 → -$0.15로 |
비용 절감** |
- 📌 비용 절감 효과를 한눈에 확인하기
- 비용 그래프
상세 분석
🟥 1️⃣ 기본 코드 (최적화 없음)
- 테스트 성공, 비용 -$0.25
- TasteRepository에서 단순 조회
- 최적화 없음 → 다수의 쿼리 발생 → 비용 비효율적
📌 성능이 가장 낮으며, 비용도 가장 높음
📸 테스트 결과 그래프
🟧 2️⃣ @EntityGraph (TasteRepository 5개 각각 적용)
- @EntityGraph 적용한 이유
- 문제점:
- 최적화 이전 코드에서는 TasteRepository 5개와 Member가 @OneToMany 관계로 연결
- 중간 테이블 5개 (@ManyToOne)을 통해 양방향으로 연관관계를 맺고 있음
- 이 과정에서 데이터를 가져올 때 N+1 문제가 발생할 가능성이 높음
- 해결 방법:
- @EntityGraph를 사용하여 TasteRepository 5개 각각에 적용
- 이를 통해 연관된 Taste 데이터를 Lazy Loading 없이 한 번에 가져오도록 설정
- 코드 스니펫
- // TasteGenresRepository @EntityGraph(attributePaths = {"genres"}) List<TasteGenres> findByMember(Member member); void deleteByMember(Member member); // TasteLikeFoodsRepository @EntityGraph(attributePaths = {"likeFoods"}) List<TasteLikeFoods> findByMember(Member member); void deleteByMember(Member member); // TasteDislikeFoodsRepository @EntityGraph(attributePaths = {"dislikeFoods"}) List<TasteDislikeFoods> findByMember(Member member); void deleteByMember(Member member); // TasteDietaryPreferencesRepository @EntityGraph(attributePaths = {"dietaryPreferences"}) List<TasteDietaryPreferences> findByMember(Member member); void deleteByMember(Member member); // TasteSpicyLevelRepository @EntityGraph(attributePaths = {"spicyLevel"}) Optional <TasteSpicyLevel> findByMember(Member member); void deleteByMember(Member member);
📌 하지만 테스트 결과, 성능 최적화 효과는 크지 않았음
- 테스트 성공, 비용 -$0.23
- TasteRepository의 각각 5개에서 개별적으로 @EntityGraph 적용
- N+1 문제가 일부 해결되었지만, 최적화 수준이 낮음
- 비용이 최적화 이전 코드와 차이가 없음 (-$0.23)
📸 테스트 결과 그래프
🟨 3️⃣ @EntityGraph + LEFT JOIN FETCH (통합) + Batch Size
- 문제점:
- @EntityGraph를 적용했지만 테스트 결과, 성능 최적화 효과가 크지 않음
- 쿼리가 개별적으로 발생하며 TPS(초당 처리량) 개선이 부족함
- 추가적인 최적화 시도:
- Batch Size를 적용하여 JPA가 한 번에 여러 개의 엔티티를 조회하도록 개선
- MemberRepository에서 Taste 데이터를 가져오는 쿼리를 LEFT JOIN FETCH를 사용해 하나의 쿼리로 통합
- 코드 스니펫 Batch Size
- #application.properties spring.jpa.properties.hibernate.default_batch_fetch_size=1000
- 코드 스니펫 LEFT JOIN FETCH
- 결과:
- 기대했던 만큼 성능이 개선되지 않았으며, 심지어 테스트가 실패
- Redis 캐싱 과정에서 문제가 발생하여 중단됨
#application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
// MemberRepository
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteGenres tg LEFT JOIN FETCH tg.genres " +
"WHERE m.id = :id")
Optional<Member> findMemberWithTasteGenresById(@Param("id") Long id);
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteLikeFoods tl LEFT JOIN FETCH tl.likeFoods " +
"LEFT JOIN m.tasteDislikeFoods td LEFT JOIN td.dislikeFoods " +
"LEFT JOIN m.tasteDietaryPreferences tp LEFT JOIN tp.dietaryPreferences " +
"LEFT JOIN m.tasteSpicyLevels ts LEFT JOIN ts.spicyLevel " +
"WHERE m IN :member")
Optional<Member> findMemberWithOtherTastesByMembers(@Param("member") Member member);
📌 쿼리를 하나로 통합하면 최적화될 것 같았지만, 결과적으로 성능 개선이 미흡했음
- 테스트 실패 (Redis 캐싱 문제로 중단), 비용 -$0.14
- TasteRepository의 @EntityGraph 적용 + LEFT JOIN FETCH로 통합된 단일 쿼리 실행
- Batch Size 적용하여 한 번에 가져오도록 설정
- 하지만 Redis 캐싱 실패로 인해 테스트가 중단됨
- 비용 측정도 의미 없음
📸 테스트 결과 그래프
🟩 4️⃣ @EntityGraph + LEFT JOIN FETCH (분리) + Batch Size
- 문제점:
- LEFT JOIN FETCH를 하나의 쿼리로 통합하면 성능이 크게 개선될 것이라 예상했지만, 실제 테스트 결과는 달랐음
- 쿼리를 통합하면서 성능 최적화 효과가 미비했고, MultipleBagFetchException 발생 가능성도 높아짐
- 해결 방법:
- LEFT JOIN FETCH를 하나의 쿼리가 아닌, 여러 개의 쿼리로 분리하여 실행
- Batch Size를 추가하여 한 번에 여러 개의 데이터를 가져오도록 설정
- 코드 스니펫
// MemberRepository
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteGenres tg LEFT JOIN FETCH tg.genres " +
"WHERE m IN :member")
Optional<Member> findMemberWithTasteGenresByMember(@Param("member") Member member);
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteLikeFoods tl LEFT JOIN FETCH tl.likeFoods " +
"WHERE m IN :member")
Optional<Member> findMemberWithTasteLikeFoodsByMember(@Param("member") Member member);
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteDislikeFoods td LEFT JOIN FETCH td.dislikeFoods " +
"WHERE m IN :member")
Optional<Member> findMemberWithTasteDislikeFoodsByMember(@Param("member") Member member);
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteDietaryPreferences tp LEFT JOIN FETCH tp.dietaryPreferences " +
"WHERE m IN :member")
Optional<Member> findMemberWithTasteDietaryPreferencesByMember(@Param("member") Member member);
@Query("SELECT DISTINCT m FROM Member m " +
"LEFT JOIN FETCH m.tasteSpicyLevels ts LEFT JOIN FETCH ts.spicyLevel " +
"WHERE m IN :member")
Optional<Member> findMemberWithTasteSpicyLevelsByMember(@Param("member") Member member);
📌 결과적으로 가장 성능이 잘 최적화되었고, 비용 절감 효과도 가장 컸음
- 테스트 성공, 비용 최적화 (-$0.23에서 -$0.15)
- 📌 테스트 결과 그래프
- 최적화된 비용 결과
- TasteRepository 5개에 @EntityGraph 적용
- MemberRepository에서 LEFT JOIN FETCH를 각각의 쿼리로 분리
- Batch Size 적용하여 쿼리 최적화
- 가장 빠른 성능과 최적의 비용 절감 효과를 보임
📸 테스트 결과 그래프
📊 결론: 최적의 성능과 비용 절감 방법은?
- 3번 방법은 Redis 캐싱 실패로 의미 없음
- 2번 방법은 일부 최적화만 이루어져 최상의 결과는 아님
- 4번 방법 (LEFT JOIN FETCH 분리 + Batch Size + @EntityGraph)가 최상의 성능과 비용 절감 효과를 보임
- 📌 총 API 요청 수 및 토큰 사용량 그래프
- API 요청량
- 토큰 사용량
💡 최적화 효과 요약
- 쿼리 최적화(LEFT JOIN FETCH + Batch Size)로 N+1 문제 해결
- 쿼리를 나눠서 실행하면 MultipleBagFetchException 방지 가능
- TPS 증가 (Batch Size 적용으로 대량 데이터 로딩 최적화)
- 쿼리를 분리했더니 TPS 증가 & 성능 최적화 효과 극대화
- 비용이 기존 -$0.23에서 -$0.15로 감소
⚙️ 최적화 적용 시 고려할 점
- LEFT JOIN FETCH를 통합하지 말고 분리하여 쿼리를 실행하는 것이 효과적
- Batch Size를 적절히 조정하여 데이터를 한 번에 로딩
- Redis 캐싱 안정성을 확보해야 최적화 효과가 온전히 유지됨
☑️ 최적의 방법: LEFT JOIN FETCH (분리) + Batch Size + EntityGraph
📌 추가 테스트 계획
현재 1000개 기준 테스트에서는 N+1 문제로 인한 심각한 성능 저하를 확인하기 어려움
향후 테스트에서 더 높은 부하(50000 이상)로 검증 필요
TIL 2월 8일