[Spring] Spring Cache란? @Cacheable로 DB 조회 성능 개선
by 우와한개발자
1. Spring Cache 란?
- 반복적인 DB 조회를 줄이기 위해 데이터를 메모리에 저장하고 재사용하여 성능을 최적화하는 기술
- 매 요청마다 DB에서 조회하면 불필요한 쿼리가 반복 실행되고 응답 속도가 느려지기 때문에, 한 번 조회한 결과를 메모리에 저장해두고 재사용함.
- Spring은 @Cacheable 같은 어노테이션만 붙이면 캐시 로직을 자동으로 처리해주는 추상화 계층을 제공
- 내부적으로는 AOP 프록시가 메서드 호출을 가로채서 동작
2. 전체 흐름
- 핸들러 메서드 안에는 캐시 관련 코드가 전혀 없어도, @Cacheable만 붙어 있으면 Spring이 알아서 처리함.
사용자가 API 요청
↓
Spring AOP 프록시가 캐시 확인
├── Hit(있음) → 메서드 실행 없이 캐시 값 바로 반환
└── Miss(없음) → 메서드 실행(DB 조회) → 결과를 캐시에 저장 → 반환
3. Spring Cache 설정
1) pom.xml
- spring-boot-starter-cache 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2) Entry Point 클래스 (main() 메서드가 포함된 클래스)
- 캐시 활성화 설정
@EnableCaching // 캐시 기능 활성화
@EnableScheduling
@SpringBootApplication
public class MyddareungiApplication {
public static void main(String[] args) {
SpringApplication.run(MyddareungiApplication.class, args);
}
}
4. Spring Cache 구현
1) 캐시 어노테이션 종류
| 어노테이션 | 동작 |
| @Cacheable | 캐시에 있으면 바로 반환, 없으면 DB 조회 후 캐시에 저장 |
| @CacheEvict | 해당 캐시 삭제 (데이터 변경 시 사용) |
| @CachePut | 항상 DB 조회 후 캐시 갱신 |
2) 캐시 이름과 키 개념
(1) 캐시 이름 (cache name)
- 캐시를 구분하는 큰 저장공간 이름
- 데이터 종류별로 저장 공간을 분리하는 개념
(2) 캐시 키(cache key)
- 같은 캐시 이름 안에서 어떤 값을 꺼낼지 식별하는 값
- 파라미터가 없으면 키가 항상 동일함
// 파라미터 없음 → 키 동일 → 값 하나만 저장
@Cacheable("kpi")
public BikeKpiSummaryResponseDto getKpi() { ... }
// 저장 예시
{
"kpi": {
"SimpleKey[]": {
"totalUsers": 1200,
"totalRides": 84500,
"totalStations": 620
}
}
}
- 파라미터가 있으면 파라미터 값이 키가 됨 (같은 "monthlySummary" 캐시 안에서도 파라미터 값마다 별도 저장)
// 파라미터 있음 → 파라미터가 키 → 값마다 별도 저장
// year=2024, year=2025 → 각각 따로 캐싱됨
@Cacheable("monthlySummary")
public BikeMonthlySummaryResponseDto getMonthlySummary(int year) { ... }
// 저장 예시
{
"monthlySummary": {
"2024": {
"months": ["01", "02", "03"],
"useCounts": [12000, 15000, 17000],
"stationCounts": [610, 615, 620]
},
"2025": {
"months": ["01", "02", "03"],
"useCounts": [18000, 21000, 23000],
"stationCounts": [625, 630, 635]
}
}
}
3) @Cacheable을 적용
- 핸들러 메서드에 @Cacheable을 적용해 캐시 이름을 분리했다. 캐시 이름을 분리한 이유는 데이터 종류마다 갱신 주기와 관리 범위가 다르기 때문이다.
@Service
@RequiredArgsConstructor
public class DashboardService implements IDashboardService {
private final DashboardRepository dashboardRepository;
@Cacheable("kpi")
@Override
public BikeKpiSummaryResponseDto getKpi() {
BikeKpi bikeKpi = dashboardRepository.selectLatestKpi();
// ... DTO 변환 후 반환
}
@Cacheable("monthlySummary")
@Override
public BikeMonthlySummaryResponseDto getMonthlySummary() {
List<BikeMonthlySummary> list = dashboardRepository.selectMonthlySummaryList();
// ... DTO 변환 후 반환
}
@Cacheable("weekdaySummary")
@Override
public BikeWeekdaySummaryResponseDto getWeekDaySummary() { ... }
@Cacheable("ageGroupSummary")
@Override
public BikeAgeGroupSummaryResponseDto getAgeGroupSummary() { ... }
@Cacheable("districtSummary")
@Override
public BikeDistrictSummaryResponseDto getDistrictSummary() { ... }
@Cacheable("rentTypeSummary")
@Override
public BikeRentTypeSummaryResponseDto getRentTypeSummary() { ... }
}
7. in-memory 캐시 vs Redis 캐시
| 구분 | in-memory | Redis |
| 저장 위치 | JVM 힙 메모리 | 외부 캐시 서버 |
| 서버 재시작 시 | 캐시 사라짐 | 캐시 유지 |
| 서버 여러 대일 때 | 각자 따로 캐시 보유 | 하나의 Redis 공유 |
| 설정 난이도 | 의존성 추가만으로 바로 동작 | Redis 서버 설치 + 별도 설정 필요 |
| 적합한 환경 | 단일 서버, 개발/테스트 환경 | 운영 환경, 다중 서버 환경 |
8. 성능 테스트
- 캐시 효과를 수치로 확인하기 위해 각 메서드의 1차 호출(DB 조회)과 2차 호출(캐시 반환) 시간을 비교하는 테스트를 작성.
@SpringBootTest
class DashboardCachePerformanceTest {
@Autowired
private DashboardService dashboardService;
@Autowired
private CacheManager cacheManager;
@BeforeEach
void clearCache() {
// 테스트 간 캐시 초기화
cacheManager.getCacheNames().forEach(name -> {
var cache = cacheManager.getCache(name);
if (cache != null) cache.clear();
});
}
@Test
@DisplayName("[캐시 성능 비교] KPI 요약")
void compareKpiPerformance() {
comparePerformance("KPI 요약", () -> dashboardService.getKpi());
}
// ... 나머지 메서드도 동일한 방식으로 테스트
private void comparePerformance(String label, Runnable callable) {
long s1 = System.nanoTime();
callable.run(); // 1차 호출 : DB 조회
long noCacheNs = System.nanoTime() - s1;
long s2 = System.nanoTime();
callable.run(); // 2차 호출 : 캐시 반환
long cachedNs = System.nanoTime() - s2;
double ratio = cachedNs > 0 ? (double) noCacheNs / cachedNs : 0;
System.out.printf(
"[%s] 1차(DB): %.3f ms | 2차(Cache): %.3f ms | 속도향상: %.2f배%n",
label,
noCacheNs / 1_000_000.0,
cachedNs / 1_000_000.0,
ratio
);
}
}
테스트 결과 예시
[KPI 요약] 1차(DB): 312.441 ms | 2차(Cache): 0.053 ms | 속도향상: 5895.11배
[월별 요약] 1차(DB): 124.873 ms | 2차(Cache): 0.021 ms | 속도향상: 5946.80배
[요일별 요약] 1차(DB): 98.214 ms | 2차(Cache): 0.018 ms | 속도향상: 5456.33배
캐시 적용 후 수천 배 수준의 응답 속도 향상을 확인할 수 있다.
'프레임워크 > Spring' 카테고리의 다른 글
| [Spring] Spring Boot 테스트 코드 작성하기 – JUnit5, Mockito, MockMvc 구현 (0) | 2026.04.12 |
|---|---|
| [ Spring ] Spring Boot 배포 방식 – JAR vs WAR, 외부 톰캣 배포 (0) | 2026.04.07 |
| [Spring] SSE(Server-Sent Events)란? EventSource, SseEmitter 구현 (1) | 2026.04.07 |
| [Spring] WebSocket이란? 스프링 프로젝트 웹소켓 구현 (0) | 2026.04.07 |
| [Spring] Spring HATEOAS란? – EntityModel, ModelAssembler 구현 (0) | 2026.04.07 |
블로그의 정보
우와한개발자 님의 블로그
우와한개발자