포스트

2024-08-08-TIL

2024-08-08-TIL

Today I Learned

Spring Boot Caching with Redis

Spring의 캐시 추상화는 @EnableCaching을 켜고 메서드에 @Cacheable, @CachePut, @CacheEvict를 붙이면, 실제 저장소와 상관없이 같은 방식으로 캐시를 적용하게 해 준다. 저장소는 CacheManager 구현으로 바꾼다. Redis를 쓰면 Spring Data Redis의 RedisCacheManager가 그 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10))
        .disableCachingNullValues()
        .serializeValuesWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    return RedisCacheManager.builder(connectionFactory)
        .cacheDefaults(config)
        .build();
}

@Cacheable(cacheNames = "product", key = "#id")
public ProductResponse getProduct(Long id) { ... }

공식 문서에서 확인한 설정 포인트는 다음과 같다.

  • 기본 직렬화: 키는 StringRedisSerializer, 값은 JdkSerializationRedisSerializer다. JDK 직렬화는 클래스 정보에 묶이고 사람이 읽기 어려워서, JSON 직렬화로 바꾸는 경우가 많다.
  • TTL: entryTtl로 캐시 항목의 만료 시간을 정한다. 캐시 이름마다 다른 설정을 줄 수도 있다.
  • disableCachingNullValues(): null 결과를 캐시하지 않는다.
  • 캐시 writer: 기본은 락을 쓰지 않는 writer로 처리량이 좋고, 락을 쓰는 writer는 putIfAbsent 같은 명령이 겹치지 않게 막는다.

@Cacheable도 프록시로 동작하므로 같은 클래스 안에서 호출하면 캐시가 적용되지 않는다는 점은 @Transactional과 같다.


Web Caching

웹 캐시는 브라우저, CDN, 중간 프록시처럼 여러 층에 있다. 각 층이 응답을 얼마나 오래 재사용할지는 HTTP 응답의 Cache-Control 헤더가 정한다. 토스 기술 블로그의 글이 이 동작을 잘 정리한다.

  • max-age: 캐시가 유효한 시간(초). 이 시간 동안은 서버에 묻지 않고 캐시를 쓴다.
  • s-maxage: CDN 같은 공유 캐시에만 적용되는 유효 시간.
  • no-cache: 캐시는 하되 쓸 때마다 서버에 재검증한다. max-age=0과 비슷하다.
  • no-store: 아예 저장하지 않는다.

유효 시간이 지나도 캐시를 바로 버리지 않는다. 브라우저는 If-None-Match(ETag)나 If-Modified-Since를 붙여 조건부 요청을 보내고, 내용이 그대로면 서버는 본문 없이 304 Not Modified만 돌려준다.

토스는 HTML에는 max-age=0, s-maxage=31536000을 줘서 브라우저는 매번 재검증하고 CDN은 오래 캐시하게 하고, 빌드마다 URL이 바뀌는 JS와 CSS에는 긴 max-age를 준다. 배포하면 CDN 캐시만 무효화하면 된다. CDN 무효화로는 사용자 브라우저에 이미 저장된 캐시를 지울 수 없으므로, 처음부터 max-age를 신중하게 정해야 한다는 것이 글의 요점이다.

AWS의 캐싱 문서는 같은 원리를 애플리케이션 계층으로 넓혀 데이터베이스 캐싱, 세션 캐싱, 캐시 전략(Lazy Loading, Write-Through)과 TTL 설정을 다룬다.


@ControllerAdvice Internal

@ControllerAdvice는 여러 컨트롤러에 공통으로 적용할 @ExceptionHandler, @InitBinder, @ModelAttribute 메서드를 한곳에 모으는 컴포넌트다. @RestControllerAdvice는 여기에 @ResponseBody가 더해진 것이다.

동작은 대략 다음과 같다. 애플리케이션이 시작될 때 Spring MVC는 @ControllerAdvice 빈들을 찾아 그 안의 @ExceptionHandler 메서드를 예외 타입별로 정리해 둔다. 컨트롤러에서 예외가 나면 HandlerExceptionResolver 중 하나인 ExceptionHandlerExceptionResolver가 처리할 메서드를 찾는데, 먼저 예외가 난 컨트롤러 자신의 @ExceptionHandler를 보고, 없으면 advice 빈들을 순서대로 확인한다. 여러 advice가 있으면 @Order로 우선순위를 정하고, basePackages나 assignableTypes로 적용 대상 컨트롤러를 좁힐 수 있다.

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handle(IllegalArgumentException e) {
        return new ErrorResponse("BAD_REQUEST", e.getMessage());
    }
}

SSL/TLS Version Check

서버가 어떤 TLS 버전을 지원하는지는 클라이언트 쪽에서 특정 버전만으로 연결을 시도해 보면 확인할 수 있다. openssl s_client는 버전을 지정하는 옵션을 제공하고, 핸드셰이크가 실패하면 그 버전을 지원하지 않는다는 뜻이다. nmap의 ssl-enum-ciphers 스크립트는 지원하는 버전과 cipher suite를 한 번에 나열한다.

1
2
3
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
nmap --script ssl-enum-ciphers -p 443 example.com

TLS 1.0과 1.1은 RFC 8996으로 공식 폐기되었으므로, 서버에서는 TLS 1.2 이상만 허용하도록 설정하는 것이 일반적이다.

참고한 자료외부 출처 20

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다