SpringBoot에서 Redis 글로벌 캐시 사용하기 / @Transactional과 @Cacheable / 역직렬화 문제 / 모니터링

양성준·2025년 4월 27일

스프링

목록 보기
36/49

일단, 로컬환경에서 Redis를 띄워서 사용할 것이기 때문에, Docker Desktop을 이용해 로컬 서버에 Redis를 띄워줘야 한다.

docker-compose.yml

services:
  redis:
    image: redis
    restart: always
    container_name: redis7
    ports:
      - "6379:6379"
    command: redis-server --port 6379
    volumes:
      - ./db/redis/data:/data
      - ./db/redis/conf:/usr/local/etc/redis/redis.conf
  • docker compose up -d로 docker 컨테이너 생성해서 docker-desktop 연결
  • docker ps로 동작중인 컨테이너 확인 가능
  • docker exec -it 컨테이너ID redis-cli 로 해당 redis 컨테이너 접속 가능
  • 현재는 로컬환경이기 때문에 docker-desktop에 띄우지만, 운영환경에서는 별도의 redis docker image를 클라우드에 띄워줘야함

build.gradle

	implementation 'org.springframework.boot:spring-boot-starter-cache'
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  • redis와 chache를 위한 의존성 추가

application.yml

spring:
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379
      password:
  • 추후 배포 환경에 맞게 host, port, password 설정

RedisCacheConfig

@Configuration
@EnableCaching
public class RedisCacheConfig {

  //캐싱 처리를 위한 빈 등록
  @Bean
  public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    //ObjectMapper에 JavaTimeModule 등록
    ObjectMapper objectMapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    // 직렬화기 생성
    GenericJackson2JsonRedisSerializer serializer =
        new GenericJackson2JsonRedisSerializer(objectMapper);

    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .serializeKeysWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
        )
        .serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(serializer)
        )
        .entryTtl(Duration.ofSeconds(120))
        .disableCachingNullValues();


    return RedisCacheManager.builder(redisConnectionFactory)
        .cacheDefaults(config)
        .build();
  }
}
  • ObjectMapper를 통해 여러 모듈을 등록하고 사용할 수 있다.
    • JavaTimeModule을 통해 Date 타입 필드를 캐싱할 수 있도록 함
  • serializeKeysWith : 키를 직렬화할 때 어떤 Serializer를 사용할지
    • Key를 String으로 직렬화하기 위해 StringRedisSerializer 사용
  • serializeValuesWtih: 값을 직렬화할 때 어떤 Serializer를 사용할지
    • 객체를 JSON으로 직렬화하기 위해 GenericJackson2JsonRedisSerializer 사용
  • entryTtl로 TTL (캐시 초기화 주기) 설정 가능
    • Redis의 기본 만료 정책은 write-based TTL(쓰기/생성 시점 기준으로 TTL이 만료)

직렬화기를 꼭 등록해야하는가?

기본적으로 Redis는 Key와 Value가 모두 바이트 배열로 저장되므로,
자바 객체 → 바이트 배열로 바꾸어 Redis에 저장해야 하고, 꺼낼 때는 바이트 배열 → 자바 객체로 다시 바꿔야하는데, 이걸 담당하는게 직렬화기이다.

  • Spring Date Redis는 기본적으로 JdkSerializationRedisSerializer를 사용
    • Java의 기본 직렬화를 사용하므로, 객체가 반드시 Serializable을 구현해야함
    • 직렬화된 데이터가 바이너리 형태로 저장됨 -> 가독성 저하
      • Java 환경에 종속적이어서, 다른 언어나 플랫폼에서는 데이터를 해석할 수 없다.
    • ObjectMapper 없이도 LocalDateTime을 직렬화/역직렬화 가능
      (LocalDateTime 클래스가 Serializable을 구현하고 있기 때문)
  • GenericJackson2JsonRedisSerializer는 Java 기반 직렬화가 아닌 Jackson 라이브러리 사용
    • Jackson의 ObjectMapper를 이용해 객체를 JSON으로 변환하고, 다시 JSON에서 객체로 역직렬화
      • Class Type에 상관 없이 모든 객체를 직렬화해준다는 장점이 있지만, Object의 클래스 및 패키지까지 전부 함께 저장하게 되어, 다른 프로젝트에서 redis에 저장되어 있는 값을 이용하려면 패키지까지 일치시켜줘야함
        • MSA 구조의 프로젝트에서는 문제가 발생
          • MSA 구조에서는 패키지 메타 정보를 함께 저장하지 않는 Jackson2JsonRedisSerializer를 사용해 클래스 정보를 함께 명시
          • 여러개의 클래스를 역직렬화 해야하는 경우에는 각 클래스 별로 Jackson2JsonRedisSerializer을 만들거나, StringRedisSerializer로 직렬화 한 뒤, ObjectMapper 변환 수동 코드를 짜줘야함
    • Jackson이 지원하는 모든 POJO에 대해 Serializable를 구현하지 않아도 문제 없이 Redis에 저장 및 조회가 가능
      (Java 16부터 정식으로 Jackson에서 record를 지원)
    • JSON 문자열을 바이트 배열로 변환하여 저장하기 때문에, 가독성이 좋다.
      • 운영 중 데이터 확인 및 분석 / 디버깅이 훨씬 용이함
      • 다른 서비스(예: Python, Node.js 등)와의 데이터 연동이나 MSA 환경에서의 데이터 공유가 용이 (JSON)
    • Java8의 LocalDateTime 등의 날짜/시간 타입이 처리되지 않을 위험
      • ObjectMapper에 JavaTimeModule 등록 필요
  • Key값 직렬화기에 StringRedisSerializer를 사용한 이유
    • (정정) RedisCacheConfiguration의 Key 직렬화기 기본값은 StringRedisSerializer기 때문에 별도로 등록해줄 필요가 없음.
      • RedisTemplate의 경우 기본값이 JDK기 때문에 그때만 등록해주자!
    • StringRedisSerializer는 Redis에 저장되는 데이터를 UTF-8 문자열(String)로 변환하는 직렬화 도구
    • 기본값인 JdkSerializationRedisSerializer는 Java 객체를 바이너리 형태로 직렬화하기 때문에,
      Key조차 사람이 읽을 수 없는 형태(\xac\xed\x00\x05t\x00...)로 Redis에 저장
    • Key값을 통해 Value를 조회해야하는데, Key값이 바이너리 형태라면 cli에서 값을 조회하기가 힘들다.
      • 디버깅 및 모니터링이 매우 어려워짐!

GenericJackson2JsonRedisSerializer 역직렬화 에러

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class 
com.sprint.springcache.entity.User (java.util.LinkedHashMap is in module java.base of loader 
'bootstrap'; com.sprint.springcache.entity.User is in unnamed module of loader 'app')
  • GenericJackson2JsonRedisSerializer가 기본적으로 직렬화 시 class type(@class 속성)을 함께 저장
  • 하지만, Custom한 ObjectMapper를 사용할 때는 문제가 발생
    • ObjectMapper는 기본적으로 직렬화/역직렬화 시 class type 정보를 포함하지 않기 때문에, 직렬화된 데이터에는 type 정보가 존재하지 않는다.
  • 역직렬화 시에 ObjectMapper가 type 정보를 모르고 역직렬화 진행 -> 기본 type인 LinkedHashMap으로 역직렬화 시도
    클래스 정보가 포함되어있지 않음
 BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("com.sprint.springcache") // 해당 패키지 하위만 허용
        .build();
        
    ObjectMapper objectMapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .activateDefaultTyping( // Redis 직렬화 시 타입 추론이 가능하도록 추가 설정
            ptv,
            ObjectMapper.DefaultTyping.NON_FINAL // 또는 EVERYTHING, OBJECT_AND_NON_CONCRETE 등 필요에 따라 선택
        )
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 
        

------
    redisObjectMapper.activateDefaultTyping(
        LaissezFaireSubTypeValidator.instance, //(아무거나 허용)
        DefaultTyping.EVERYTHING, //(모든 객체의 타입 정보 기록)
        As.PROPERTY //(기본값, 타입 정보를 property로 기록 -> 역직렬화 가능)
    );
  • 이렇게 어떤 클래스 타입을 허용할지 명시적으로 작성해주고, DefaultTyping을 적용해준다면, 직렬화할 때 클래스 메타정보를 함께 저장한다.
  • 해당 정보를 바탕으로 역직렬화가 이뤄지기 때문에 더이상 타입 에러가 발생하지 않는다!
  • 하지만, MSA 구조의 프로젝트에서는 문제가 발생할 수 있다. (패키지까지 맞춰줘야하므로 유연성 저하)
    • MSA 구조에서는 패키지 메타 정보를 함께 저장하지 않는 Jackson2JsonRedisSerializer를 사용해 클래스 정보를 함께 명시
    • 여러개의 클래스를 역직렬화 해야하는 경우에는 각 클래스 별로 따로 Jackson2JsonRedisSerializer을 만들거나, StringRedisSerializer로 직렬화 한 뒤, ObjectMapper 변환 수동 코드를 짜줘야함
// Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Product> productSerializer = new Jackson2JsonRedisSerializer<>(Product.class);
Jackson2JsonRedisSerializer<User> userSerializer = new Jackson2JsonRedisSerializer<>(User.class);


// StringRedisSerializer
String userJsonFromRedis = redisTemplate.opsForValue().get("user:1");
User user = objectMapper.readValue(userJsonFromRedis, User.class);

참고: https://velog.io/@bagt/Redis-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%82%BD%EC%A7%88%EA%B8%B0-feat.-RedisSerializer

.toList() 역직렬화 에러

  • .toList()로 결과값을 반환하고, 해당 정보를 캐싱하면 직렬화될 때 컬렉션에 대한 정보가 누락됨 -> 역직렬화 실패(SerializationException)
    • Umodifiable List 또는 Set을 직렬화하려고 할 때 발생하는 고질적인 문제
    • toList()는 ArrayList처럼 표준화된 포맷이 아닌, ImmutableCollection$ListN 같은 불변 리스트의 내부 구현 클래스를 사용
      • 직렬화/역직렬화 시에 구체적인 구현 정보가 보이지 않거나, 표준 ArrayList로 읽을 수 없다.
      • 정확한 원인은 모르겠으나, 예상하기엔 toList()가 자바16 이후에 나온 버전이라 Redis에서 지원하지 않는 컬렉션 타입인 것 같아서 발생하는 문제 같다.
      • ImmutableCollections.ListN는 Serializable을 구현하지 않기떄문에, 기본 직렬화기를 써도 에러 발생함
    • collect(Collectors.toList())는 명시적으로 ArrayList로 만듦
"[[\"com.sprint.mission.discodeit.dto.controller.notification.NotificationDto\",{\"id\":\"e31b7fe9-21ae-4651-a8d2-44d12bf61a33\",\"createdAt\":\"2025-07-17T17:04:03.787496Z\",\"receiverId\":\"f2421bd9-a4d9-4b9d-be6a-f1b2c555be55\",\"title\":\"admin (# \xec\xb1\x84\xeb\x84\x90)\",\"content\":\"\xec\xb9\xb4\xed\x94\x84\xec\xb9\xb4\xeb\xa1\x9c \xea\xb3\xbc\xec\x97\xb0\",\"type\":\"NEW_MESSAGE\",\"targetId\":\"6100760d-cb71-4e43-a03c-95910f2ba183\"}],[\"com.sprint.mission.discodeit.dto.controller.notification.NotificationDto\",{\"id\":\"89b691d8-d9ad-4fad-ac63-2e9e99bc23ec\",\"createdAt\":\"2025-07-17T17:04:08.531495Z\",\"receiverId\":\"f2421bd9-a4d9-4b9d-be6a-f1b2c555be55\",\"title\":\"\xea\xb6\x8c\xed\x95\x9c\xec\x9d\xb4 \xeb\xb3\x80\xea\xb2\xbd\xeb\x90\x98\xec\x97\x88\xec\x8a\xb5\xeb\x8b\x88\xeb\x8b\xa4.\",\"content\":\"[USER] -> [CHANNEL_MANAGER]\",\"type\":\"ROLE_CHANGED\",\"targetId\":\"f2421bd9-a4d9-4b9d-be6a-f1b2c555be55\"}],[\"com.sprint.mission.discodeit.dto.controller.notification.NotificationDto\",{\"id\":\"b6a2698e-4a3e-4548-b766-10e12d6756c9\",\"createdAt\":\"2025-07-17T17:04:04.920246Z\",\"receiverId\":\"f2421bd9-a4d9-4b9d-be6a-f1b2c555be55\",\"title\":\"admin (# \xec\xb1\x84\xeb\x84\x90)\",\"content\":\"\xec\x9e\x98\xea\xb0\x80\xeb\x82\x98\xec\x9a\x94?\",\"type\":\"NEW_MESSAGE\",\"targetId\":\"6100760d-cb71-4e43-a03c-95910f2ba183\"}]]"

  • 이를 해결하기 위해선, List 결과값을 직렬화 해야하는 경우, .toList()가 아닌 .collect(Collectors.toList())를 사용해야한다.
    • 직렬화될 때 Collection 정보도 함께 들어가기 때문에 역직렬화 가능!
"[\"java.util.ArrayList\",[[\"com.sprint.mission.discodeit.dto.controller.notification.NotificationDto\",{\"id\":\"42bb7941-4cef-4bc5-b651-325ce87c9a2f\",\"createdAt\":\"2025-07-17T17:25:32.602222Z\",\"receiverId\":\"51361ae6-30cc-4fa3-9f70-2f4a9a4bb663\",\"title\":\"yyjjmm2003 (# \xec\xb1\x84\xeb\x84\x90)\",\"content\":\"\xec\xba\x90\xec\x8b\x9c \xed\x99\x95\xec\x9d\xb8\",\"type\":\"NEW_MESSAGE\",\"targetId\":\"6100760d-cb71-4e43-a03c-95910f2ba183\"}]]]"

  • 또한, BasicPolymorphicTypeValidator를 사용해서 SubType을 제한해줬다면 역직렬화를 할 때, java.util 경로를 허용해줘야 List 타입 역직렬화가 가능해짐!
    (Generic2~를 사용하고, 커스텀 ObjectMapper를 사용해서 BasicPolymorphicTypeValidator가 필요한 경우에만)
  • Jdk 기본 직렬화기를 쓸 때 or Generic2~ 직렬화기만 쓸 때 는, 자동으로 @Class 정보가 들어가므로 Collect(Collectors(toList())만 사용해주면 될 듯하다.
    BasicPolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("com.sprint.mission.discodeit") // 해당 패키지 하위만 허용
        .allowIfSubType("java.util")
        .build();
흐름

JdkSerializationRedisSerializer를 사용할 경우, 바이너리 코드로 저장되기 떄문에 모니터링이 어려움 
-> GenericJackson2JsonRedisSerializer를 사용 
-> LocalDateTime 등의 날짜/시간 타입이 처리되지 않을 위험이 있다 
-> ObjectMapper에 JavaTimeModule 등록 필요 
-> ObjectMapper를 커스텀해서 사용할 경우, 클래스 정보가 함께 직렬화되지 않음 
-> 어떤 클래스 타입을 허용할지 명시적으로 작성해주기 위해 DefaultTyping을 적용해서 클래스 타입 함께 저장 
-> List.of의 경우 컬렉션 정보가 직렬화할 때 저장되지 않아 역직렬화 문제 발생 
-> collect(Collectors(toList)) 사용


기본 JdkSerializationRedisSerializer를 사용할 경우, 
클래스 메타 정보가 포함되어 직렬화되기 때문에 
Serializable 구현 + 
collect(Collectors(toList()) 정도만 사용해주면 된다.



Redis에서 저장된 값을 역직렬화 할 때, 클래스에 대한 메타 정보가 필요합니다. 어떤 클래스인지 알아야 JSON 문자열이나 바이너리 데이터를 클래스로 역직렬화 할 수 있기 떄문입니다. 

.toList()는 기본적으로 toList()는 ArrayList처럼 표준화된 포맷이 아닌 ImmutableCollection$ListN 같은 불변 리스트의 내부 구현을 사용합니다. 
때문에 .toList() 결과값을 Redis에 캐싱할 경우, 직렬화 시에 컬렉션에 대한 메타데이터가 포함이 안되고, 역직렬화할 때 실패하는 문제가 발생합니다.
(정확한 원인은 모르겠으나, 예상하기엔 toList()가 자바16 이후에 나온 버전이라 Redis에서 지원하지 않는 컬렉션 타입인 것 같아서 발생하는 문제 같습니다.)

.toList() 대신 .collect(Collectors(toList())를 사용하여 결과값을 만든다면, ArrayList 형태로 저장되고, Redis에서도 해당 컬렉션은 지원하기 때문에 직렬화할 때 컬렉션 메타 데이터를 함께 저장할 수 있게 됩니다. 
-> 메타 데이터가 있기 떄문에 역직렬화도 가능

이 문제가 아님에도 직렬화 예외가 터진다면 기본 직렬화기(JdkSerializationRedisSerializer)를 사용함에도  Serializable을 구현하지 않았거나 기타 등등의 문제가 너무 많아서.. 별도로 찾아보시면 될 것 같습니다.

요약: 역직렬화가 안된다면 toList()가 아니라 collect(Collectors(toList()) 사용

참고: https://stackoverflow.com/questions/51688838/unexpected-token-start-object-expected-value-string-need-json-string-that-co
https://github.com/FasterXML/jackson-databind/issues/3892

RedisCacheConfiguration vs RedisTemplate

RedisCacheConfiguration

  • Srping의 Cache 추상화(@Cacheable 등)를 사용할 때, Redis 캐시의 동작 방식을 설정하는 클래스
  • 직접 데이터를 Redis에 저장하거나 조회하는 기능은 없고, CacheManager를 통해 캐시 동작을 제어
  • 직렬화기 기본값
    • Key: StringRedisSerializer
    • Value: JdkSerializationRedisSerializer

RedisTemplate

  • Redis의 다양한 데이터 구조에 직접 접근하고 조작할 수 있는 핵심 클래스
    • DI를 통해 주입된다.
  • CRUD, 트랜잭션, 파이프라인, Lua 스크립트 등 Redis의 모든 명령을 코드로 직접 실행할 수 있음.
  • 직렬화기 기본값
    • Key: JdkSerializationRedisSerializer
    • Value: JdkSerializationRedisSerializer

실제 사용

  @CacheEvict(
      value = "departmentDistribution",
      allEntries = true,
      cacheManager = "redisCacheManager"
  )
  public void createTodayStats() {
  }

  @Cacheable(
      value = "departmentDistribution",
      key = "#p0 + #p1.toString()",
      cacheManager = "redisCacheManager"
  )
  public List<EmployeeDistributionDto> getDepartmentDistribution(EmployeeState status, LocalDate statDate) {
  }
  • 캐싱하려는 곳에서 어노테이션을 이용하여 자유롭게 사용!
  • 로컬 캐시와 혼용하는 경우, cacheManger 빈이 여러개이므로 redisCacheManger 명시 필요

  • cacher가 Redis에서 잘 등록되고 관리되는 것을 볼 수 있다!
  • keys *: 모든 키 조회
  • flushall : 모든 키 삭제
  • get key : 키값 조회

Redis 캐시 히트/미스 모니터링

  • Micrometer는 JVM 안의 캐시 (Caffeine) 만 모니터링 가능. Redis는 JVM 외부 캐시이므로 별도의 Exporter를 붙이거나, 직접 구현 필요 / Micrometer/Spring Actuator만으로는 메트릭이 자동 노출되지 않는다.
    • Redis Exporter는 Redis 서버의 내부 통계(예: keyspace_hits, keyspace_misses 등)를 Prometheus가 이해할 수 있는 Prometheus 메트릭 포맷으로 변환해 /metrics 엔드포인트로 노출
    • Redis Exporter는 Redis 서버의 전체적인 캐시 히트/미스 등 메트릭만 수집할 수 있고, "어떤 애플리케이션 인스턴스(서버)에서 캐시 히트가 발생했는지"는 알 수 없다.
      • 인스턴스별 캐시 히트/미스 모니터링이 필요하다면, 각 인스턴스에서 AOP나 이벤트 리스너를 통해 직접 카운팅한 뒤, 메트릭 노출을 구현해야함
      • 분산 환경에서는 Redis Exporter + Prometheus + Grafana 조합으로 전체 캐시 효율을 모니터링하고, 세부적인 인스턴스별 분석이 필요하다면 커스텀 메트릭 수집을 추가하는 것이 일반적

=> Prometheus에 Redis Exporter를 붙인다면, Redis 전체 캐시 히트/미스 메트릭 수집 가능!
(방법은 추후 정리 예정)

rate(redis_keyspace_hits_total[5m]) / (rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]))
  • Prometheus 쿼리 (캐시 적중률)

@Transactional과 @Cacheable을 함께 사용할 때

1) @Transactional과 @Cacheable

  • @Cacheable은 항상 메서드 실행 전에 캐시를 먼저 조회
  • 둘 다 기본적으로 Ordered.LOWEST_PRECEDENCE(낮은 우선순위, Integer.MAX_VALUE)로 등록되며, AOP 프록시의 어드바이저 순서를 명시적으로 바꾸지 않는 한 캐싱 로직은 언제나 트랜잭션 경계보다 앞에서 정상적으로 수행
  • 명시적으로 순서를 지정해서 @Transactional이 먼저 실행되더라도, 트랜잭션 생성 후 @Cacheable이 동작해서 캐시 조회 -> 히트 시 캐시 조회 후 메서드 종료, 미스 시 트랜잭션 경계 안에서 메서드 실행

=> 조회(읽기) 작업에서 @Transactional과 @Cacheable의 순서 지정은 실제 시스템 동작에 영향을 주지 않음

@Transactional이 먼저 작동할 때 동작 흐름

1. 트랜잭션 생성
@Transactional이 바깥쪽 프록시로 동작하므로, 먼저 트랜잭션 경계를 시작합니다.

2. 캐시 조회 (@Cacheable)
트랜잭션 범위 진입 후, @Cacheable 어드바이스(CacheInterceptor)가 적용되어
캐시 키에 해당하는 값이 있는지 먼저 확인합니다.

3. 캐시 히트(일치하는 값이 있을 때):
비즈니스 메서드를 실행하지 않고, 캐시 값을 즉시 반환하며, 트랜잭션 내부 로직과 DB 접근도 수행되지 않습니다.

4. 캐시 미스(캐시에 값이 없을 때):
실제 비즈니스 메서드를 실행(이때 트랜잭션 경계 안에서 실행),
DB에서 데이터를 조회한 후, 그 결과를 캐시에 저장한 뒤 반환합니다.

2) @Transactional과 @CacheEvict

  • Spring의 @CacheEvict는 기본적으로 afterInvocation(메서드 정상 반환 후)에 동작
    (beforeInvocation = false가 기본값)
    • 메서드 실행이 성공했을 때만 캐시를 삭제
    • 트랜잭션 안에서 예외가 발생해 롤백된 경우, @CacheEvict 자체도 실행되지 않는다.
  • 따라서, @Transactional이 먼저 실행되는 구조에서도 "예외 → 롤백"인 상황에서는 캐시가 삭제되지 않고 그대로 유지.
  • @CacheEvict가 먼저 실행되는 구조에서도 메서드 정상 반환 후 동작하므로, 트랜잭션이 커밋된다면 정상적으로 캐시가 갱신되고, 롤백된다면 실행 X
  • beforeInvocation = true라면 캐시 삭제가 메서드 실행 전에 발생 -> 메서드에서 예외가 터져서 트랜잭션이 롤백돼도 캐시 삭제가 롤백되지 않음
  • 크게 문제가 되진 않지만, 최대한 안전하게 하고싶다면 @TransactionalEventListener(phase = AFTER_COMMIT)을 사용해서 트랜잭션이 커밋 후 Evict가 발생하게 할 수 있음

AOP가 어떻게 동작하는지 생각해보면 됨
@Transational - 트랜잭션 생성 -> 원본 메서드 호출 -> 커밋 or 롤백
@Cacheable - 캐시 조회 -> 미스 -> 원본 메서드 호출
@CacheEvict - 원본 메서드 호출 -> 캐시 삭제

profile
백엔드 개발자

0개의 댓글