본문 바로가기
개발

GET 방식 API에서 Cache-Control을 중앙 통제로 관리하는 실무 전략

by 새싹 아빠 2026. 1. 20.

최근 서버를 설계하면서 조회 API의 캐시 전략에 대해 깊게 고민하게 됐다. 특히 트래픽이 늘어날수록 서버 부하를 어떻게 줄일 것인가, 그리고 보안 이슈 없이 캐시를 어떻게 적용할 것인가는 피할 수 없는 주제다.

이 글은 다음 질문에서 출발한다.

  • GET 조회 API에 Cache-Control은 왜 필요한가?
  • Redis 캐시와 HTTP 캐시는 무엇이 다른가?
  • Cache-Control을 컨트롤러마다 두는 게 맞을까?
  • 중앙에서 캐시 정책을 통제하는 방법은?

 

1. 캐시는 왜 쓰는가? (진짜 이유)

캐시를 쓰는 이유를 흔히 "응답을 빠르게 하기 위해서"라고 말하지만, 실무에서는 더 중요한 목적이 있다.

캐시는 트래픽이 커질수록 서버가 같은 일을 반복하지 않게 막기 위한 장치다.

예를 들어 일정 조회 API가 초당 수천 번 호출될 때, 매번 DB를 조회한다면 결국 병목은 DB에서 발생한다. 캐시는 이 반복을 제거해 서버와 DB를 보호한다.

 

2. HTTP 캐시 vs Redis 캐시

2-1. HTTP 캐시 (브라우저 / CDN / 프록시)

  • 서버 밖에서 동작
  • 같은 요청이면 서버까지 아예 오지 않음
  • Cache-Control 헤더로 제어
Cache-Control: max-age=30

위 설정이 있으면, 같은 요청은 30초 동안 서버를 거치지 않는다.

2-2. Redis 캐시 (애플리케이션 캐시)

  • 서버 내부에서 동작
  • GET/POST 구분 없음
  • DB 조회/계산 결과를 캐싱
val key = "schedule:${date}"
redis.get(key) ?: service.getSchedule(date).also {
    redis.set(key, it, 30)
}

중요한 차이점은 HTTP 캐시는 서버까지 요청을 막고, Redis 캐시는 서버 내부에서 DB 접근을 막는다는 점이다.

 

3. 왜 로그인/인증 API에는 HTTP 캐시를 쓰면 안 될까?

HTTP 캐시는 클라이언트 또는 중간자에 저장된다. 로그인 정보나 개인정보 응답이 캐시되면 심각한 보안 사고로 이어질 수 있다.

그래서 인증/개인정보 API의 철칙은 다음과 같다.

Cache-Control: no-store
  • 로그인
  • /me, /profile
  • 토큰 재발급

이런 API는 HTTP 캐시 금지, 대신 필요하면 Redis 캐시만 사용한다.

 

4. GET 조회 API에서 Cache-Control은 어디에 두는 게 좋을까?

처음에는 컨트롤러마다 이렇게 두게 된다.

return ResponseEntity.ok()
    .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS))
    .body(response)

하지만 API가 늘어나면 캐시 정책이 흩어지고 관리가 어려워진다.

이때 등장하는 게 중앙 통제 방식이다.

 

5. Cache-Control을 중앙에서 통제하는 Interceptor

Spring에서는 HandlerInterceptor를 사용해 Cache-Control을 중앙에서 관리할 수 있다.

5-1. 기본 예시

class CacheControlInterceptor : HandlerInterceptor {

    override fun postHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        modelAndView: ModelAndView?
    ) {
        if (request.requestURI.startsWith("/schedule")) {
            response.setHeader("Cache-Control", "max-age=30")
        }
    }
}

이렇게 하면 /schedule/**로 들어오는 모든 요청에 대해 캐시 정책을 한 곳에서 관리할 수 있다.

 

5-2. 실무에서 반드시 필요한 안전장치

그대로 쓰면 위험할 수 있기 때문에, 실무에서는 다음 조건을 꼭 추가한다.

  • GET 요청만 적용
  • 민감한 API는 캐시 금지
  • 컨트롤러에서 이미 설정한 헤더는 존중
class CacheControlInterceptor : HandlerInterceptor {

    override fun postHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        modelAndView: ModelAndView?
    ) {
        // 이미 설정된 Cache-Control이 있으면 덮어쓰지 않음
        if (response.getHeader("Cache-Control") != null) return

        // GET 요청만 대상
        if (request.method != "GET") return

        // 인증/개인정보 API는 캐시 금지
        if (request.requestURI.startsWith("/auth") ||
            request.requestURI.startsWith("/me")) {
            response.setHeader("Cache-Control", "no-store")
            return
        }

        // schedule 조회 캐시
        if (request.requestURI.startsWith("/schedule")) {
            response.setHeader("Cache-Control", "max-age=30")
        }
    }
}

 

 

6. 중앙 통제 방식의 장점

  • 컨트롤러 코드가 깔끔해진다
  • 캐시 정책 변경을 한 곳에서 관리
  • 팀 단위 규칙을 강제할 수 있다

컨트롤러는 비즈니스 로직에 집중하고, 캐시는 정책으로 관리하는 구조가 된다.

 

7. 정리

  • 캐시는 성능보다 안정성을 위한 도구다
  • HTTP 캐시와 Redis 캐시는 역할이 다르다
  • 민감한 데이터에는 HTTP 캐시를 절대 쓰지 않는다
  • GET 조회 API는 Cache-Control을 명시적으로 관리하자
  • Interceptor를 활용하면 중앙 통제가 가능하다

캐시는 옵션이 아니라 API 스펙의 일부다.

조회 API를 설계할 때부터 캐시 전략을 함께 고민하면, 트래픽이 커져도 훨씬 안정적인 서버를 만들 수 있다.

 

English Ver.

https://jaemoi8.tistory.com/57

 

Practical Strategies for Centralized Cache-Control in GET APIs

Recently, while designing a server, I found myself thinking deeply about cache strategies for read-only APIs. As traffic grows, questions like how to reduce server load and how to apply caching without introducing security risks become unavoidable.This art

jaemoi8.tistory.com