😊 성능 테스트 가이드 시리즈😊 / 클릭 시 이동
1. 성능 테스트_성능 목표 잡기
2. 성능 테스트_HikariCP의 연결 최대 풀 설정
3. 성능 테스트_Caffeine 캐시 설정 및 적용
4. 성능 테스트_인덱싱과 트랜잭션 관리 최적화
5. 성능 테스트_하드웨어 리소스 업그레이드
결과
Total Average response time
- 7575ms
Top 5 slowest requests based on their average response times.
연결 풀을 설정한 결과로, 문제가 됐던 에러는 발생하지 않게 되었다!
하지만 평균시간과 기준치로 삼았던 90th TTFB를 보면 가장 높은 경우 무려 32414ms, 50631ms가 걸리는 기염을 토했다.
테스트 서버 리소스가 적은 탓(512MB…)이 가장 크겠지만
우리의 목표는 현재 상황에서 얼마나 개선하는가 가 목표이기에 이를 우선 배너 매칭 목록 조회부터 원인 파악을 하기로 했다.
원인 파악
우선 해당 API에 관련된 코드를 확인했다.
// controller
@GetMapping("/banner")
public ResponseEntity<ResponseForm> getMemberForBanner() {
List<BannerListResponse> bannerList = matchingService.getBannerList();
if (bannerList == null) {
bannerList = List.of();
}
return ResponseEntity.ok(ResponseForm.of(ResponseCode.BANNER_LIST_QUERY_SUCCESS, bannerList));
// service
@Cacheable("bannerListCache")// 메서드의 결과를 캐시하여 동일한 인자로 호출되면 캐시된 결과 반환
@Override
public List<BannerListResponse> getBannerList() {
// 최적화된 쿼리로 데이터를 가져옴
List<Object[]> results = matchingRepository.findMemberForBanner(Status.ACCEPTED);
// 데이터를 DTO로 변환하여 반환
return matchingListToDto.convertToBannerListResponse(results);
}
}
// repository
@Override
public List<Object[]> findMemberForBanner(Status status) {
try {
return em.createQuery("SELECT s.nickname, s.mbti, s.gender, r.nickname, r.mbti, s.gender " +
"FROM Matching m " +
"JOIN m.sender s " +
"JOIN m.receiver r " +
"WHERE m.status = :status " +
"ORDER BY m.createdAt DESC", Object[].class)
.setParameter("status", status)
.setMaxResults(10)
.getResultList();
} catch (Exception e) {
log.error("배너에 적힐 10명의 매칭을 찾는 도중에 에러가 발생했습니다: {}", status, e);
return List.of(); // 빈 리스트 반환
}
}
이렇게 로컬 캐시도 적용하고, 쿼리를 날릴 때 엔티티 자체를 가져오지 않고 필요한 칼럼만 조회해서 가져오는 등 나름 깔끔하게 코드를 작성했는데 왜 이렇게 오래 걸리는지 의문이였다.
그래서 캐시 관련 설정을 다시 한번 검토하기로 했다.
캐시 활성화 문제
우선 Spring에서 캐시 기능을 활성화하기 위해서는 @EnableCaching 어노테이션을 추가해야 한다.
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching // 캐시 기능 활성화
public class NowHereApplication {
public static void main(String[] args) {
SpringApplication.run(NowHereApplication.class, args);
}
}
이후 캐시 설정을 추가해야 하는데 크게
- 캐시 타입 지정
- 최대 캐시 크기 및 만료 시간 설정
두 가지가 있다.
캐시 타입에는
- Caffeine
- 자바 기반 로컬 캐시
- 단일 서버 환경에서 캐시가 필요한 경우
- 빠른 C/R 기능이 중요한 DB 서버에서 데이터를 캐싱하는 경우
- 장점
- 로컬이라 읽기 쓰기 성능이 뛰어남
- 단점
- 여러 서버 인스턴스에서 동일한 데이터를 캐싱해야 한다면 분산 캐시가 아니라 문제가 발생 가능
- Ehcache
- 자바 기반 로컬 캐시
- 장점
- 디스크 기반 캐시를 지원해 메모리 제한을 초과해도 디스크에 데이터 저장
- 클러스터링을 통해 분산 캐시처럼 사용 가능
- 단점
- 로컬 캐시인 경우 서버 인스턴스마다 다른 캐시 데이터를 가질 수 있는 위험
- Redis
- 인메모리 데이터베이스로서 분산 캐시 용도로 사용
- 장점
- 키-값 저장소로 동작하여 메모리에 모든 데이터를 저장하여 매우 빠른 성능 제공
- 여러 서버 인스턴스에서 동일한 캐시 일관성 보장
- 단점
- 외부 서버 의존
- 이를 위한 추가적인 리소스 비용 발생
- Hazelcast
- Memcached
등등이 있다.
이런 캐시에 대한 설정이 미흡하여 제대로 캐싱이 되지 않는 것이 문제였다.
성능 개선
위에서 설명한 캐시 중에서 기본적으로 개발 환경에서는
빠른 캐싱 성능을 제공하고 설정이 간단한 Caffeine 캐시를 사용하는 것을 추천하고는 한다.
하지만 우리 서비스와 같은 경우 DB를 Master - Slave 구조로 멀티 데이터베이스 소스 형태로 구축해기에
Redis나 Hazelcast 같은 분산 캐시를 추천한다.
그렇지만 나는 Redis, Hazelcast를 학습하고
이를 응용하기 위한 러닝 커브가 현재 우리 서비스의 규모를 봤을 때 오버 엔지니어링이라는 판단을 했다.
또한 Master - Slave를 구축할 때 CUD는 Master가, R는 Slave가 담당하도록 구축하게 했기 때문에
단일 서버에 Master에서 주기적으로 Slave와 동기화하고, Slave에서 Caffeine을 통해서 로컬 캐시를 사용해도
충분히 성능 개선이 이뤄질 것이라고 판단했다.
그래서 아래와 같이 캐시는 caffeine, 최대 500개의 캐시 항목 저장, 10분 후에 만료되도록 설정하였다.
결과는 다음 글에..
spring.cache.type=caffeine # 캐시 타입 지정
spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=10m