메랜샵 서비스 런칭 2주차, 특정 지역의 조회가 간헐적으로 느려진다는 사용자 피드백을 받았습니다.
특히 사용량이 급증하는 루더스 호수(루디브리엄) 지역에서 문제가 두드러졌는데요.
24시간 내 모든 거래 내역을 조회하는 비교적 단순한 API임에도 불구하고, 왜 체감 속도가 떨어졌을까요?
사용자 피드백을 쭉 정리해보니 패턴이 보였습니다:
이번 글에서는 인덱스 최적화만으로는 한계가 있었던 성능 저하의 원인을 분석하고,
캐시(Cache) 전략을 도입하여 응답 시간을 650ms에서 120ms까지 개선한 과정과 그 결과를 공유합니다.
⚠️ 주의: 아래에 등장하는 서비스명, 지역명, API 경로, 엔티티명, 데이터 값은 모두 임의의 예시이며 실제 서비스와 무관합니다.
느려짐의 원인은 데이터베이스(DB) 단에서 발생하고 있었습니다.
저희의 지역 조회 API는 다음과 같은 로직을 가집니다.
SELECT *
FROM tb_sample
WHERE col_region = ?
AND col_created >= ?
AND col_deleted = false
ORDER BY col_created DESC
단순해 보이지만, 이 구조에는 두 가지 성능 저하 포인트가 있었습니다.
정렬 비용(Filesort)
특정 조건으로 데이터를 찾은 후, 최신순으로 보여주기 위해 생성시간
으로 정렬하는 과정에서 filesort가 발생했습니다.
반복적인 동일 쿼리
인기 지역(Hot Region)은 짧은 시간 동안 수많은 동일 조회 요청이 집중되는 패턴을 보였습니다.
또한, 페이지네이션 없이 24시간 내 모든 결과를 한 번에 보여줘야 했기 때문에 결과 세트의 크기 자체가 부담이었습니다.
인덱스를 추가했지만 정렬 비용을 완전히 해소하기 어렵다고 판단하여, 캐시(Cache) 를 도입했습니다. 특히 인기 지역의 경우 같은 요청이 짧은 시간에 몰리는 패턴이었으니까요.
저희 상황을 고려해봤을 때, 로컬 메모리 캐시(Caffeine) 가 가장 적합해 보였습니다. 이유는:
구체적으로 이런 전략으로 설계했습니다:
일단 Caffeine 라이브러리를 사용해서 캐시를 설정했습니다:
@Configuration
@EnableCaching
public class CfConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCache c1 = buildCache("cache_a1", 20);
CaffeineCache c2 = buildCache("cache_a2", 20);
return new ConcurrentMapCacheManager("cache_a1", "cache_a2");
}
}
가장 문제가 됐던 지역 조회 API에 캐시를 적용했습니다:
@Cacheable(
value = "cache_a1",
key = "#kw == null ? null : normalizeKey(#kw)"
)
public List<ResDto> findByTag(String kw) {
// DB 조회 로직
}
키 정규화 함수도 만들었습니다. 대소문자나 공백 차이 때문에 같은 요청이 다른 캐시로 들어가는 걸 방지하려고요:
private String normalizeKey(String kw) {
return kw.trim().toLowerCase().replaceAll("\\s+", "_");
}
데이터가 변경될 때마다 캐시를 삭제하도록 했습니다:
@CacheEvict(value = {"cache_a1", "cache_a2"}, allEntries = true)
public void createNewItem(ReqCreate req) {
repo.save(newEntity);
}
@CacheEvict(value = {"cache_a1", "cache_a2"}, allEntries = true)
public void updateItem(Long id, ReqUpdate req) {
repo.save(updatedEntity);
}
관리자는 실시간 데이터를 봐야 하니까 캐시를 우회하도록 했습니다:
public List<ResDto> searchData(String kw, UserInfo principal) {
if (principal != null && principal.isAdmin()) {
return repo.findActiveByKeyword(kw);
}
return getCachedResult(kw);
}
백엔드 캐시 적용과 함께, 프론트엔드에서도 추가 최적화를 진행해주었습니다:
이런 프론트엔드 최적화까지 더해져서 실제 사용자가 체감하는 로딩 속도는 더욱 빨라졌습니다.
항목 | 변경 전 | 변경 후 | 개선 효과 |
---|---|---|---|
지역 조회 평균 응답 시간 | 240ms | 80ms | 66.7% 감소 |
지역 조회(루더스호수) 응답 시간 | 650ms | 120ms | 81.5% 감소 |
키워드 조회 평균(일반) | 270ms | 90ms | 66.6% 감소 |
DB 호출 수 (동일 요청) | ~20회 | 1회 | 획기적 감소 |
특히 피크 시간대에 성능이 많이 안정화됐습니다. 사용자 피드백도 "이제 빨라졌다"는 긍정적인 반응이 많이 들어왔고요.
처음에는 "그냥 서버 좀 더 좋은 걸로 바꾸면 되는 거 아닌가?" 하고 생각했는데, 막상 체계적으로 접근해보니 생각보다 고려할 게 많더라고요.
특히 사용자 피드백을 단순히 "느리다"로만 받아들이지 않고, 언제, 어디서, 어떤 상황에서 느린지 패턴을 찾아보는 게 중요했습니다. 그 과정에서 진짜 원인을 찾을 수 있었고, 효과적인 해결책도 도출할 수 있었던 것 같아요.
다음에 또 이런 성능 이슈가 생기면, 이번 경험을 바탕으로 더 빠르고 정확하게 해결할 수 있을 것 같습니다.