😊 성능 테스트 가이드 시리즈😊 / 클릭 시 이동
1. 성능 테스트_성능 목표 잡기
2. 성능 테스트_HikariCP의 연결 최대 풀 설정
3. 성능 테스트_Caffeine 캐시 설정 및 적용
4. 성능 테스트_인덱싱과 트랜잭션 관리 최적화
5. 성능 테스트_하드웨어 리소스 업그레이드
결과
Total Average response time
- 7575ms → 4569ms (39.68% 최적화)
이번 테스트에서는 이렇게 DB 성능이 나왔는데 이전의 테스트 당시 모니터링 화면과 양상이 유사하지 않은가?
왜냐면 테스트할 때 R관련 API가 실행될 때 데이터가 25만 개 이기 때문에
Tuples out이 계속 증가하다가 이후 CUD관련 API가 실행되면 이는 줄어들고 Tuples in이 되는 것을 볼 수 있다.
Top 5 slowest requests based on their average response times.
캐시를 제대로 설정하니 전체 응답 시간은 약 40%가 개선되었고
문제가 됐던 배너 매칭 목록 조회 API는 아예 느린 순위에서 빠졌다.
자세하게 설명하자면
배너 매칭 목록 조회 API의 평균 시간과 상위 90프로까지 시간이 각각 5081ms, 9220ms이 되었는데
이는 평균시간 기준과 상위 90프로 기준
- 32414ms -> 5081ms의 경우: 개선된 시간 = 32414 - 5081 = 27333ms 개선율 = (27333 / 32414) * 100 ≈ 84.32%
- 50631ms -> 9220ms의 경우: 개선된 시간 = 50631 - 9220 = 41411ms 개선율 = (41411 / 50631) * 100 ≈ 81.79%
따라서,
- 32414ms에서 5081ms로의 개선은 약 84.32%의 성능 향상
- 50631ms에서 9220ms로의 개선은 약 81.79%의 성능 향상
을 이룬 것이다.
이를 통해 이제껏 문제가 됐던 배너 매칭 목록은 worst top 5 목록에서 아예 사라졌고 다른 5개가 리스팅 됐다.
원인 분석
한 단계씩 적용해 가며 단계적으로 최적화를 진행할 생각이어서 이전에 찾은 원인을 이어서 분석하였다.
각 엔티티에서 pk나 유니크 제약 조건과 별개로 비즈니스 로직에 맞게 인덱싱을 적용
트랜잭션 관리 최적화를 통해서
- 간단하게는 R 기능만 사용하는 트랜잭션임을 밝혀 성능 최적화
- 추후에 상용 DB를 연결할 때는 Master와 Slave에 각각 다른 트랜잭션을 날려 로드 밸런싱까지 할 수 있게 설정했다.
성능 개선
worst top 5 api는 전부 매칭과 관련된 API로 매칭과 관련해서
- 각 비즈니스 로직에 맞게 필요한 인덱싱 추가
- service와 repository의 모든 메서드를 참고해서 R기능의 메서드를 트랜잭션 관리 최적화
- 읽기 전용 메서드에는 @Transactional(readOnly = true)를 적용하여 성능을 향상하고
데이터베이스의 불필요한 락을 방지. - 쓰기, 삭제, 업데이트 작업을 하는 메서드에는 기본 트랜잭션 (@Transactional)을 사용하여 데이터 일관성을 유지
- 읽기 전용 메서드에는 @Transactional(readOnly = true)를 적용하여 성능을 향상하고
를 적용했다.
엔티티 클래스에서 인덱스 지정
현재 내 서비스는 JPA를 사용하기 때문에 어노테이션 안에 @Index 어노테이션을 통해 인덱스를 정의할 수 있다.
@Entity
@Table(name = "matching", indexes = {
@Index(name = "idx_matching_status", columnList = "status"),
@Index(name = "idx_matching_created_at", columnList = "createdAt"),
@Index(name = "idx_sender_receiver_member_id", columnList = "sender_member_id, receiver_member_id", unique = true)
})
public class Matching {
// 엔티티 필드들
}
- @Index: 특정 칼럼에 대한 인덱스를 지정.
- name: 인덱스의 이름을 지정.
- columnList: 인덱스를 적용할 칼럼(들)을 지정.
- unique = true: 복합 인덱스를 유니크 인덱스로 설정할 수 있음.
여기서 sender_member_id와 receiver_member_id를 묶어서 유니크 인덱스로 지정한 것은
두 멤버의 매칭이 중복되지 않게 보장해 줌.
특히 @Index(name = "idx_sender_receiver_member_id", columnList = "sender_member_id, receiver_member_id", unique = true)와 같이 복합 인덱스를 설정하면 두 칼럼을 조합해서 조회하는 경우 성능이 크게 향상된다.
- 예를 들어 한 명의 회원이 보낸 매칭 또는 받은 매칭을 조회하는 쿼리들이 빠르게 처리된다.
- 또한 unique = true를 사용하면 두 멤버가 서로 매칭되는 관계가 중복으로 발생하지 않도록 데이터베이스 레벨에서 제약을 설정
- 즉, 동일한 두 멤버가 여러 번 매칭되는 것을 방지할 수 있어. 예를 들어, sender_member_id = 1과 receiver_member_id = 2로 매칭된 경우, 동일한 두 사람에 대해 다시 매칭을 생성하려고 하면 오류가 발생
트랜잭션 최적화
읽기 전용 트랜잭션 적용
읽기 전용으로 처리되는 메서드는 데이터베이스에서 데이터를 변경하지 않으므로@Transactional(readOnly = true)를 적용
@Transactional(readOnly = true)
@Override
public List<Matching> findByReceiverId(Long memberId) {
return em.createQuery("SELECT m FROM Matching m WHERE m.receiver.id = :memberId", Matching.class)
.setParameter("memberId", memberId)
.getResultList();
}
@Transactional(readOnly = true)
@Override
public List<BannerListResponse> getBannerList() {
List<Object[]> results = matchingRepository.findMemberForBanner(Status.ACCEPTED);
return matchingListToDto.convertToBannerListResponse(results);
}
이렇게 하면 읽기 작업 시 불필요한 락을 방지하고 성능을 개선된다.
또한 데이터베이스에서의 읽기 작업이 더 가볍게 처리되어, 시스템 전체 성능 향상된다
쓰기/수정 트랜잭션
쓰기 작업은 데이터베이스에 변경을 일으키므로, 기본 트랜잭션을 적용한다.
@Transactional
@Override
public void save(Matching matching) {
try {
em.persist(matching);
} catch (Exception e) {
log.error("매칭 저장 중에 실패: {}", matching, e);
}
}
@Transactional
@Override
public void update(Matching matching) {
try {
em.merge(matching);
} catch (Exception e) {
log.error("매칭 업데이트 중 실패: {}", matching, e);
}
}
→ 하지만 서비스 자체의 도메인이 크지 않기 때문에 Matching을 하는 김에
모든 엔티티에 인덱싱과 트랜잭션 관리 최적화를 진행했다.
결과는 다음에..