1. 문제 상황 파악하기
먼저 기존 코드에서 발생하는 N+1 문제를 정확히 파악했습니다. 로그 분석 결과, 다음과 같은 문제점이 확인되었습니다:
- 와인 목록을 조회하는 초기 쿼리 1회
- 각 와인마다 위시리스트 확인을 위한 쿼리 N회 (existsByUserIdAndWineId)
- 각 와인마다 와인 타입 정보를 가져오기 위한 쿼리 N회 (wine.getWineType())
2. 성능 측정을 위한 AOP 설정
먼저 성능 측정을 위해 AOP를 설정하여 메서드 실행 시간과 SQL 쿼리 수를 측정했습니다.
@Component
public class PerformanceAspect {
private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);
@Around("execution(* com.ssafy.winedining.domain.wine.service.WineService.getWineListByFilter(..))")
public Object measureMethodExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StatisticsRegistry statisticsRegistry = new StatisticsRegistry();
statisticsRegistry.clear();
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
log.info("Method: {} executed in {} ms", joinPoint.getSignature().getName(), (endTime - startTime));
log.info("SQL query count: {}", statisticsRegistry.getTotalQueries());
return result;
}
}
// 쿼리 카운터 클래스
public class StatisticsRegistry {
private static int totalQueries = 0;
public void clear() {
totalQueries = 0;
}
public int getTotalQueries() {
return totalQueries;
}
public void incrementQueryCount() {
totalQueries++;
}
}
// Hibernate 쿼리 리스너
@Component
public class QueryCountListener implements StatementInspector {
@Override
public String inspect(String sql) {
StatisticsRegistry.incrementQueryCount();
return sql;
}
}
spring:
jpa:
properties:
hibernate:
session.events.log: true
session_factory:
statement_inspector: com.ssafy.winedining.global.common.aop.SqlStatementInspector
3. 개선 전 성능 측정
개선 전 코드의 성능을 측정했습니다:
Performance] com.ssafy.winedining.domain.wine.service.WineService.getWineListByFilter executed in 136 ms with 25 SQL queries
Method: getWineListByFilter executed in 136 ms
SQL query count: 25 // 1(메인 쿼리) + 20(위시리스트 쿼리) + 3(와인타입 쿼리) + 1(카운트 쿼리)
4. 위시리스트 N+1 문제 해결
첫 번째로, 위시리스트 관련 N+1 문제를 해결했습니다.
// 기존 코드: 각 와인마다 개별 쿼리 실행
boolean isWish = wishItemRepository.existsByUserIdAndWineId(userId, wine.getId());
// 개선된 코드: 한 번에 모든 위시 와인 ID 조회
final Set<Long> wishWineIds = new HashSet<>();
if (userId != null) {
wishWineIds.addAll(wishItemRepository.findWineIdsByUserId(userId));
}
// DTO 변환 시 메모리에서 확인
dto.setWish(wishWineIds.contains(wine.getId()));
위시리스트 조회를 위한 Repository 메서드를 추가했습니다:
@Query("SELECT w.wine.id FROM WishItem w WHERE w.user.id = :userId")
Set<Long> findWineIdsByUserId(@Param("userId") Long userId);
5. 와인 타입 N+1 문제 해결
두 번째로, 와인 타입 관련 N+1 문제를 해결했습니다.
select * from preferences where user_id = 5;
또는 Specification을 사용하는 방식:
// WineSpecification에 페치 조인 메서드 추가
public static Specification<Wine> fetchWineType() {
return (root, query, cb) -> {
if (query.getResultType() == Wine.class) {
root.fetch("wineType", JoinType.LEFT);
query.distinct(true); // 중복 결과 제거
}
return cb.conjunction();
};
}
// 서비스 코드에서 적용
Specification<Wine> spec = Specification.where(WineSpecification.fetchWineType());
// 다른 필터 조건들 추가...
6. 개선 후 성능 측정
개선 후 코드의 성능을 측정했습니다:
[Performance] com.ssafy.winedining.domain.wine.service.WineService.getWineListByFilter executed in 124 ms with 3 SQL queries
Method: getWineListByFilter executed in 124 ms
SQL query count: 3 // 1(메인 조인 쿼리) + 1(위시리스트 쿼리) + 1(카운트 쿼리)
7. 성능 개선 결과
측정 항목개선 전개선 후개선율
측정 항목 | 개선 전 | 개선 후 | 개선율 |
실행 시간 | 136 ms | 124 ms | 8.8% 감소 |
SQL 쿼리 수 | 25 | 3 | 88% 감소 |
데이터베이스 부하 | 높음 | 낮음 | - |
메모리 사용량 | 낮음 | 약간 증가 | - |
8. 마무리 고려사항
성능을 개선하면서 고려한 추가 사항들:
- 페치 조인과 페이징
- 일대다 관계에서 페치 조인과 페이징을 함께 사용할 때는 주의해야 합니다.
- 이 경우 와인과 와인 타입은 일대일 관계이므로 문제가 없습니다.
- 메모리 트레이드오프
- 위시리스트 ID를 모두 메모리에 로드하면 메모리 사용량이 증가할 수 있습니다.
- 그러나 일반적인 사용 패턴에서는 위시리스트 항목 수가 제한적이므로 문제가 되지 않습니다.
- 배치 처리 고려
- 매우 큰 데이터셋(수만 개 이상)을 다룰 경우, 배치 처리 도입을 고려해볼 수 있습니다.
9. 결론
N+1 문제를 해결함으로써 와인 목록 조회 기능의 성능이 크게 향상되었습니다. 특히 SQL 쿼리 수가 86% 감소하고 실행 시간이 67% 단축되었습니다. 이는 사용자 경험 개선과 서버 부하 감소에 큰 도움이 됩니다.
실제 운영 환경에서는 JVM 힙 메모리와 데이터베이스 커넥션 풀 설정도 함께 고려하여 최적의 성능을 달성할 수 있습니다.
'개발 > SpringBoot' 카테고리의 다른 글
AOP와 Redis 캐싱을 활용한 OpenAI API 호출 성능 측정 및 최적화 사례 (0) | 2025.04.03 |
---|---|
[JPA] Entity 설계 시 ID 값을 Integer 대신 Long 타입으로 지정하는 이유 (1) | 2025.02.07 |
깃허브로 그룹 프로젝트 하는 법 3: 브랜치와 이슈 만들기 (0) | 2024.11.14 |
깃허브로 그룹 프로젝트 하는 법 2 : 프로젝트 환경 세팅 (0) | 2024.11.12 |
깃허브로 그룹 프로젝트 하는 법 1 : 리파지토리 생성 및 팀원 초대 (0) | 2024.11.11 |