[고도화 #4] 전시 데이터 Redis 도입

2026. 5. 27. 12:27·Project/O+T

도입

이번 포스팅에서는 OTT의 메인 화면은 자주 보여지는 전시 데이터이고, 이를 Redis를 활용하여

응답 시간을 줄여보는 과정을 정리 하려고 한다.

 

해당 서비스에서 Trending API는 북마크가 많은 영상 Top 20개를 주기적으로 보여주는 기능이다.

모든 사용자에게 동일하게 노출되는 공통 데이터이고, 개별 사용자의 북마크 1건이 즉시 순위를

뒤집어야 하는 데이터도 아닌 시간 윈도우 기반 집계 데이터의 특성을 가진다고 판단했다.

현 로직은 매 요청 마다 집계를 하고 있지만.. 도메인 특성 상 배치나 스케쥴러를 통한 집계가 추후 필요해보인다.

 

 

전시 데이터 특성 상 응답 속도는 매우 빨라야 하는데 k6로 50명 접속, 1분간 부하 테스트 결과 다음과 같았다.

http_req_duration.....: avg=4.39s med=1.31s max=19.03s p(95)=18.11s
trending_duration....: avg=4398ms med=1313ms max=19037ms p(95)=18113ms

 

p(95)가 18초로 사용자 한 명이 trending을 누르면 거의 20초를 기다려야 한다는 의미였다.

이번 글에서는 Trending API에 Redis 캐시를 적용하여 마주친 결정 사항과 문제점, 그리고 해결 과정을 정리한다.

 

 

 


캐싱 전략 

1.1 Read 전략 : Cache-Aside

쓰기 전략은 여러 가지를 검토해봤다.

전략 설명 일관성 쓰기 지연
Cache-Aside DB에 쓰고 캐시는 삭제(또는 갱신) 약함 낮음
Write-Through 캐시와 DB에 동시에 쓰기 (동기) 강함 높음
Write-Back 캐시에만 쓰고 DB는 비동기 반영 약함 매우 낮음

 

Trending은 사용자의 직접 쓰기가 없는 읽기 전용 API라서 사실 위 전략은 큰 의미가 없었고,

결국 Cache-Aside를 선택하기로 했다. Spring이 제공하는 Cache의 추상화로 간단하게 구현이 가능하기 때문이다.

 

 

1.2 캐시를 채우는 주체

읽기 전략을 Cache-Aside로 정해도, 캐시를 채울 시점에 대해서 정해봐야 고민해봐야 한다.

 

선택지 1 : Cache-Aside + TTL

사용자 요청 → 캐시 조회 → Miss면 DB 계산 → 캐시 저장(TTL 5분) → 반환

 

가장 단순한 방식이지만 두 가지 문제가 있었다.

- TTL 만료 직후 첫 요청자가 무거운 계산을 하게 됨 → 응답 속도 불안정

- TTL 만료 순간 동시 요청 다수가 캐시 미스가 나면서 DB에 접속 → Cache Stampede 발생

 

 

 

선택지 2 : Scheduled Refresh

[백그라운드] @Scheduled로 주기적 계산 → Redis 덮어쓰기
[사용자 요청] 항상 Redis Hit → 반환

 

스케줄러가 미리 캐시를 채워두기 때문에 모든 요청에 대해서 캐시 Hit가 난다.

다만, 사용자의 요청이 없어도 계산이 도는 자원 누수가 발생하고 다중 인스턴스 환경에서는 중복 실행 방지가 필요하다

 

 

 

선택지 3 : Scheduled Refresh + TTL (최종 선택)

[Scheduler] 45분마다 trending 계산 → Redis JSON 덮어쓰기
[Redis]     TTL 60분 (스케줄러 죽어도 다음 요청이 재계산)
[API]       Redis 조회 → 반환

 

스케줄러를 TTL 보다 짧은 주기로 돌리면 모든 요청에 대해서 캐시 Hit가 난다.

이러한 패턴을 Refresh-Ahead라고 부르며, 또한 부팅 직후 캐시가 비어있는 Cold Start 문제도 

설정 값을 통해 해결이 가능했다.

 

 


 

자료구조 - String vs Zset

다음은 Redis에서 응답을 저장할 자료구조를 골라야 했다.

 

2.1 String - JSON 통채로 저장

key:   trending:playlists
value: [{"id":1,...}, {"id":2,...}, ...]   ← 응답 DTO 통째로

 

- 구현이 단순 하고 GET 한 번이면 된다

- 부분 갱신/제거 불가능 → 특정 영상 1개만 빼려고 해도 전체 캐싱을 무효화 해야 한다.

 

 

 

2.2 Sorted Set - ID와 점수만 저장

key:    trending:playlists
member: playlistId
score:  인기 점수

 

- ZREM으로 특저어 ID만 제거가 가능

- 순위 기반 페이지네이션이 자연스러움

- 응답을 만드려면 별도의 조회 과정을 거쳐야됨

 

 

 

2.3 결정 과정

처음에는 특정 영상을 즉시 내려야 할 수도 있다는 피드백 때문에 Sorted Set이 매력적으로 보였다. 

다만 우리 서비스는 3가지 특징을 가지고 있었다.

 

- Trending API는 순위 기반 페이지네이션을 사용 x

- 특정 영상의 점수만 갱신하는 기획 x

- 각 영상마다 키를 관리할 경우 비용 증가

 

이러한 문제들로 오버엔지니어링이라고 판단했고, Trending은 시간 기반 집계이고 강한 일관성 보장이 불필요한 데이터이므로,

단순 String 방식으로 충분하다고 판단 했다.

라이선스 영상 긴급 내림 같은 부분 제거 문제는 뒤에 다시 다룬다. 결과적으로 이 문제도 Sorted Set 없이 해결했다.

 

 

 

구현 - @Cacheable 적용과 역직렬화 문제

3.1 전략 패턴

처음에는 PlaylistStrategyService.getPlaylists()에 @Cacheable을 바로 붙이려 했으나 해당 메소드는

모든 플레이리스트 조회의 진입점이였다.

 

@Cacheable(condition = "#condition.contentSource.name() == 'TRENDING'")

이와 같은 조건식으로 분기 처리를 할 수 있었지만, 캐싱될 API가 많아 짐에 따라 복잡해질 것이라 생각하여

별도 메소드를 추가로 구현하고 해당 메소드에 @Cacheable을 붙이기로 했다.

@Cacheable(cacheNames = CACHE_NAME, key = CACHE_KEY)
@Transactional(readOnly = true)
public List<PlaylistResponse> getTrending() {
    return loadFromDb();
}

 

 

 

 

3.2 역직렬화 문제1 

캐시 저장까지는 잘 되었지만, 사용자의 두 번째 호출에서 캐시 Hit가 발생하고 에러가 터졌다.

Caused by: MismatchedInputException: Unexpected token (START_OBJECT),
    expected VALUE_STRING: need String/Number/Boolean ... for subtype of java.lang.Object
    at ... GenericJackson2JsonRedisSerializer.deserialize

 

 

 

원인은 내가 기존에 설정한 RedisConfig의 ObjectMapper 였다.

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(ptv, NON_FINAL, JsonTypeInfo.As.PROPERTY);

 

 

Jackson은 원래 타입 정보를 JSON 내부에 적지 않지만, Redis 캐시 처럼 꺼낼 때 호출자가 타입을 모르는 상황에서
JSON 내부에 타입 정보를 적어놔야 하는 데 이 설정이 activateDefaultTyping이고 2가지 방식이 있다.

 

 

As.PROPERTY — 객체 안에 @class 필드로 박힘

{
  "@class": "com.app.PlaylistResponse",
  "id": 1,
  "name": "playlist1"
}

 

As.WRAPPER_ARRAY — 배열 첫 칸이 타입명

[
  "java.util.ArrayList",
  [ {...}, {...}, {...} ]
]

 

 

As.PROPERTY로 설정해도, List 같은 컨테이너 타입은 객체가 아닌 배열이라 필드로 박을 수 없었다.

그래서 Jackson은 내부적으로 List에 대해서는 자동으로 As.WRAPPER_ARRAY 형태로 처리해 버린다.

 

 

그래서 실제로 Redis에 저장된건 ARRAY였고 역직렬화 하는 과정에서 AsArrayTypeDeserializer가 첫 토큰을 String으로

기대했는데 객체가 들어와서 에러가 난 것이였다.

 

 

해결 방법은 직접 설정한 ObjectMapper를 넘지지 않으면 된다. 이 경우 기본 생성자가 내부에서 자체 호환되는 설정으로

Mapper를 등록해준다.

 

 

 

 

3.3 역직렬화 문제 2

위에서 As.PROPERY 방식의 기본 mapper로 변경해서 각 원소에 @Class가 박혔지만 최상위에는

타입 정보가 없는 문제가 발생했다.

 

이유는 @Class는 객체의 필드로만 들어갈 수 있고 배열에는 들어가지 못하기 때문에 최상위 반환 타입이 List인 경우

타입을 넣을 자리가 없어서 공백으로 들어가는 문제였다.

 

 

해결 방식은 반환 타입을 기존 List에서 DTO로 한번 더 감싸면 된다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class TrendingCache {
    private List<PlaylistResponse> items;
}

 

 

 

이렇게 할 경우 Redis에 저장 형태는 다음과 같이 잘 저장된다

{
  "@class": "...TrendingCache",
  "items": [
    "java.util.ArrayList",
    [
      { "@class": "...PlaylistResponse", "mediaId": 7, ... },
      ...
    ]
  ]
}

 

 

 

정리하면, @Cacheable이 List를 직접 캐싱하지 못하는 건 Spring이나 Redis의 한계가 아니라 Jackson의 polymorphic 타입 정보 표현 방식의 한계다.

 

 

 

 

3.4 성능 측정

이 시점에서 다시 부하 테스트를 진행해봤다.

http_req_duration..............: avg=44.77ms  med=28.39ms  max=595.49ms  p(95)=104.86ms
trending_duration..............: avg=44.77ms  med=28.39ms  max=595.49ms  p(95)=104.86ms

p(95) 18,113ms → 104ms. 약 170배 개선.

 

 

tt-api-user  | 2026-05-26T18:18:00.017Z  INFO 1 --- [nio-8080-exec-4] p6spy                                    : 4 ms | statement | connection 7 | select c1_0.id,c1_0.actors,c1_0.created_date,c1_0.duration,c1_0.master_playlist_url,c1_0.media_id,c1_0.modified_date,c1_0.origin_url,c1_0.series_id,c1_0.status,c1_0.video_size from contents c1_0 where c1_0.media_id in (7,8,11,15,20,12,4,2,3,9,14,10,19,18,1,13,17,16,6,5,4853,4964,4854,4855,4856,4857,4858,4859,4860,4861,4862,4863,4864,4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875,4876,4877,4878,4879,4880,4881,4882,4883,4884,4885,4886,4887,4888,4889,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4950,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4957,4963,4962,4961,4960,4959,4958,4952,4953,4954)
ott-api-user  | 2026-05-26T18:18:00.123Z  INFO 1 --- [nio-8080-exec-4] p6spy                                    : 1 ms | commit | connection 7 | 
ott-api-user  | 2026-05-26T18:18:01.229Z  INFO 1 --- [nio-8080-exec-5] p6spy                                    : 1 ms | commit | connection 8 | 
ott-api-user  | 2026-05-26T18:18:02.285Z  INFO 1 --- [nio-8080-exec-6] p6spy                                    : 1 ms | commit | connection 9 | 
ott-api-user  | 2026-05-26T18:18:03.344Z  INFO 1 --- [nio-8080-exec-7] p6spy                                    : 1 ms | commit | connection 10 | 
ott-api-user  | 2026-05-26T18:18:04.381Z  INFO 1 --- [nio-8080-exec-8] p6spy                                    : 0 ms | commit | connection 11 | 
ott-api-user  | 2026-05-26T18:18:05.414Z  INFO 1 --- [nio-8080-exec-9] p6spy                                    : 0 ms | commit | connection 12 | 
ott-api-user  | 2026-05-26T18:18:06.445Z  INFO 1 --- [io-8080-exec-10] p6spy                                    : 2 ms | commit | connection 13 | 
ott-api-user  | 2026-05-26T18:18:07.485Z  INFO 1 --- [nio-8080-exec-3] p6spy                                    : 0 ms | commit | connection 16 | 
ott-api-user  | 2026-05-26T18:18:08.530Z  INFO 1 --- [nio-8080-exec-4] p6spy                                    : 1 ms | commit | connection 17 | 
ott-api-user  | 2026-05-26T18:18:09.586Z  INFO 1 --- [nio-8080-exec-5] p6spy                                    : 2 ms | commit | connection 18 | 
ott-api-user  | 2026-05-26T18:18:10.618Z  INFO 1 --- [nio-8080-exec-6] p6spy                                    : 1 ms | commit | connection 19 | 
ott-api-user  | 2026-05-26T18:18:11.677Z  INFO 1 --- [nio-8080-exec-7] p6spy                                    : 1 ms | commit | connection 20 | 
ott-api-user  | 2026-05-26T18:18:12.743Z  INFO 1 --- [nio-8080-exec-8] p6spy                                    : 2 ms | commit | connection 21 | 
ott-api-user  | 2026-05-26T18:18:13.785Z  INFO 1 --- [nio-8080-exec-9] p6spy                                    : 1 ms | commit | connection 22 | 
ott-api-user  | 2026-05-26T18:18:14.801Z  INFO 1 --- [io-8080-exec-10] p6spy                                    : 1 ms | commit | connection 23 | 
ott-api-user  | 2026-05-26T18:18:15.822Z  INFO 1 --- [nio-8080-exec-1] p6spy                                    : 0 ms | commit | connection 24 | 
ott-api-user  | 2026-05-26T18:18:16.880Z  INFO 1 --- [nio-8080-exec-4] p6spy                                    : 2 ms | commit | connection 27 | 
ott-api-user  | 2026-05-26T18:18:17.941Z  INFO 1 --- [nio-8080-exec-5] p6spy                                    : 2 ms | commit | connection 28 | 
ott-api-user  | 2026-05-26T18:18:18.988Z  INFO 1 --- [nio-8080-exec-6] p6spy                                    : 2 ms | commit | connection 29 | 
ott-api-user  | 2026-05-26T18:18:20.030Z  INFO 1 --- [nio-8080-exec-7] p6spy                                    : 1 ms | commit | connection 30 | 
ott-api-user  | 2026-05-26T18:18:21.049Z  INFO 1 --- [nio-8080-exec-8] p6spy                                    : 1 ms | commit | connection 31 | 
ott-api-user  | 2026-05-26T18:18:22.083Z  INFO 1 --- [nio-8080-exec-9] p6spy                                    : 0 ms | commit | connection 32 | 
ott-api-user  | 2026-05-26T18:18:23.104Z  INFO 1 --- [io-8080-exec-10] p6spy                                    : 1 ms | commit | connection 33 | 
ott-api-user  | 2026-05-26T18:18:24.131Z  INFO 1 --- [nio-8080-exec-1] p6spy                                    : 0 ms | commit | connection 34 | 
ott-api-user  | 2026-05-26T18:18:25.164Z  INFO 1 --- [nio-8080-exec-2] p6spy                                    : 1 ms | commit | connection 35 | 
ott-api-user  | 2026-05-26T18:18:26.352Z  INFO 1 --- [nio-8080-exec-3] p6spy                                    : 1 ms | commit | connection 36 | 
ott-api-user  | 2026-05-26T18:18:27.403Z  INFO 1 --- [nio-8080-exec-6] p6spy                                    : 1 ms | commit | connection 39 | 
ott-api-user  | 2026-05-26T18:18:28.469Z  INFO 1 --- [nio-8080-exec-7] p6spy                                    : 0 ms | commit | connection 40 |

 

p6spy 로그를 보면 첫 요청에만 trending 집계 쿼리가 한 번 나가고, 그 이후로는 쿼리가 나가지 않는 모습이다.

 

 


 

 

운영상 문제점과 해결

현재 기능은 동작하지만 운영 관점에서 3가지 문제점이 남아있었다.

 

 

4.1 문제점 1 : TTL 만료 시점의 갭

Cache-Aside만 사용하면 TTL이 만료되는 그 순간 캐시가 비는 짧은 갭이 생기고, 그 갭 사이에 다중 서버가 동시 요청할 경우

DB는 과부하에 걸리게 된다.

 

이를 해결하기 위해 TTL 만료 전에 스케쥴러가 미리 캐시를 새 값으로 덮어쓰는 패턴을 도입했다.

문제 무엇 해결 방식
Cold Start 부팅 직후 캐시 비어있어 첫 요청들이 DB로 initialDelay=0으로 부팅 즉시 1회 채움
Latency Spike TTL 만료 직후 한 요청이 DB로 떨어져 그 사용자만 느림 만료 전 미리 덮어쓰기 → 캐시가 비는 순간 없음
Cache Stampede 만료 직후 동시 요청 N개가 DB 폭주 빈 순간 자체가 없으므로 발생 조건이 사라짐
@CachePut(cacheNames = CACHE_NAME, key = CACHE_KEY)  // 반환값을 캐시에 덮어쓰기
@Scheduled(fixedRate = 45 * 60 * 1000, initialDelay = 0)  // 45분 주기, 부팅 즉시 1회
@Transactional(readOnly = true)
public TrendingCache refreshTrending() {
    return new TrendingCache(loadFromDb());
}

 

 

스케줄러 45분 / TTL 1시간

15분의 시간 차이를 두었다.

스케줄러가 정상 동작하면 모든 요청은 항상 Hit. 스케줄러가 한 번 실패해도 TTL 60분 안에 다음 스케쥴이 갱신해준다.

 

다만, 스케줄러를 도입하면서 새로운 문제가 발생했다.

다중 서버 환경에서 어느 서버가 스케줄러를 실행한 것인지!

 

 

 

4.2 문제점 2 - 다중 인스턴스 중복 실행

서버가 N대라면 각 서버의 스케줄러가 모두 동시에 trending 계산을 시도한다.

DB 부하가 N배, 캐시 락 경합, 일관성 문제가 동시에 발생한다.

 

Redis의 SETNX(SET if Not eXists)를 이용한 분산 락으로 해결할 수 있다.

Redis는 단일 스레드라서 키가 없을 때만 저장하는 동작이 원자적으로 보장된다.

직접 구현하는 대신 ShedLock 라이브러리를 사용했다.

 

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT40M")
public class ShedLockConfig {

    @Bean
    public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
        return new RedisLockProvider(connectionFactory);
    }
}

 

@CachePut(cacheNames = CACHE_NAME, key = CACHE_KEY)
@Scheduled(fixedRate = 45 * 60 * 1000, initialDelay = 0)
@SchedulerLock(name = "refreshTrending", lockAtMostFor = "PT40M", lockAtLeastFor = "PT1M")
@Transactional(readOnly = true)
public TrendingCache refreshTrending() {
    return new TrendingCache(loadFromDb());
}

 

 

해당 작업으로 인해 여러 인스턴스가 동시에 부팅되면 단 하나의 인스턴스만 trending 계산을 수행하고

나머지는 skip하게 된다.

 

 

 

4.3 문제점 3 - 영상 긴급 내림

마지막으로 가장 까다로웠던 문제. 백오피스에서 특정 영상을 비공개(publicStatus = PRIVATE)로 전환했을 때 어떻게 즉시 반영할 것인가.

시간차 문제

현재 시스템은 45분마다 자동으로 캐시를 갱신한다. 만약 12:00에 백오피스에서 콘텐츠 1번을 내려도:

  • 12:00 백오피스에서 콘텐츠 1번 비공개 처리
  • 12:01 사용자 trending 조회 → 여전히 캐시된 콘텐츠 1번이 노출됨
  • 12:45 다음 스케줄러 실행 → 그제야 사라짐

최대 45분간 차단된 콘텐츠가 그대로 노출된다는 의미. 라이선스 이슈 같은 법적 문제라면 절대 허용할 수 없다.

첫 번째 아이디어 — 차단 Set으로 필터링

처음에는 별도의 Redis Set에 차단된 ID를 저장하고 조회 시 필터링하는 방식을 검토했다.

key1: trending:playlists      → JSON 전체 응답 (TTL 60분)
key2: blocked:playlists       → 차단된 playlistId Set

조회: trending JSON 가져옴 → blocked Set의 ID 제외하고 반환
차단: SADD blocked:playlists {영상ID} → 즉시 반영

비용을 비교하면 차단 Set 방식이 더 효율적이다.

방법즉시 반영DB 부하추가 메모리

캐시 evict O 다음 요청에 무거운 집계 1회 0
차단 Set 필터링 O 없음 차단 ID 수 × 8바이트 (미미)

두 번째 결론 — 결국 evict로 회귀

그런데 실제 백오피스 API를 들여다보니 문제가 있었다. 영상 상태만 변경하는 전용 API가 없었다. 영상 비공개 처리는 PATCH /{contentsId}/upload라는 메타데이터 통합 수정 API를 통해서만 가능했다.

이 API는 publicStatus뿐 아니라 title, description, categoryId, tagIdList 등을 함께 받는다. 즉:

  • publicStatus가 안 바뀌어도 title만 바뀌면 trending 응답에도 새 title이 반영되어야 함
  • 결국 메타데이터 변경 자체가 캐시 무효화 사유가 됨

이렇게 되면 차단 Set으로 분기 처리하는 의미가 사라진다. 어차피 메타데이터 변경 시점에 캐시를 evict 해야 한다면, 차단도 그냥 같이 처리하는 게 단순하다.

결국 차단 Set은 영상 상태 변경 전용 API가 따로 있을 때 의미가 있는 선택지였다. 우리의 도메인 API 구조에서는 evict가 더 적합했다.

최종 구현 — AFTER_COMMIT 이벤트 기반 evict

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onTrendingCacheInvalidation(TrendingCacheInvalidationEvent event) {
    Cache cache = cacheManager.getCache(CACHE_NAME);
    if (cache != null) {
        cache.evict(CACHE_KEY);
        log.info("trending cache evicted (key={})", CACHE_KEY);
    }
}

핵심은 **TransactionPhase.AFTER_COMMIT**이다. DB 트랜잭션이 성공적으로 커밋된 다음에만 캐시를 비운다. 만약 트랜잭션 안에서 evict 했다가 DB가 롤백되면 캐시만 비고 DB는 그대로라는 일관성 문제가 발생한다.

흐름은 이렇게 된다:

  1. 백오피스 PATCH /{contentsId}/upload 호출
  2. @Transactional 안에서 DB update (publicStatus, 메타데이터)
  3. 트랜잭션 커밋 성공 시 TrendingCacheInvalidationEvent 발행
  4. AFTER_COMMIT 리스너가 Redis evict
  5. 다음 사용자 trending 요청 → Redis Miss → DB 재계산 (publicStatus = PUBLIC 자동 필터링) → Redis 저장

다음 요청 한 건만 약간 느려지지만, 그 이후로는 다시 모두 Hit. 45분 vs 1번의 재계산 트레이드오프에서 후자가 명백히 낫다.

 

 

 


5. 마무리

결정 요약

항목선택이유

Read 전략 Cache-Aside Spring Cache로 단순 구현, Trending은 읽기 중심
Write 전략 Scheduled Refresh + TTL 안전망 Cold Start, Latency Spike, Stampede 동시 해결
자료구조 String (JSON) 페이지네이션·부분 갱신 요구 없음, 단순함이 이김
다중 인스턴스 ShedLock Redis SETNX 기반, 어노테이션으로 간편
긴급 내림 AFTER_COMMIT evict 메타데이터 통합 수정 API 구조와 정합성

 

성능 개선 결과

지표적용 전적용 후개선

p(95) 18,113ms 104ms 약 174배
평균 응답 4,398ms 44ms 약 100배
처리량 9.2 req/s 47.0 req/s 약 5배

 

 

 


 

회고

이번 작업에서 가장 인상 깊었던 건, "이론적으로 더 정교한 선택지"가 항상 옳지는 않다는 점이었다. Sorted Set은 분명 부분 갱신에 강하지만 우리 도메인에는 오버스펙이었고, 차단 Set도 라이선스 차단 시나리오에선 효율적이지만 우리 API 구조에선 evict 한 줄이 더 깔끔했다.

캐시 설계는 자료구조와 패턴의 우아함이 아니라 실제 도메인의 요구사항과 코드 구조와의 정합성으로 결정해야 한다는 걸 다시 확인했다.

또한 Jackson 역직렬화 문제 두 번을 거치면서 @Cacheable이 List를 직접 캐싱하지 못하는 이유가 Spring/Redis가 아니라 Jackson의 polymorphic 타입 표현 한계라는 점도 알게 되었다. 다음에 비슷한 문제를 만나면 처음부터 DTO로 감싸는 선택을 하게 될 것 같다.

'Project > O+T' 카테고리의 다른 글

[고도화 #3] fetchJoin 적용  (0) 2026.05.05
[고도화 #2] JOIN OR 병목 해결  (1) 2026.04.29
[고도화 #1] 성능 측정  (2) 2026.04.22
'Project/O+T' 카테고리의 다른 글
  • [고도화 #3] fetchJoin 적용
  • [고도화 #2] JOIN OR 병목 해결
  • [고도화 #1] 성능 측정
김마루
김마루
개발자 취직 원해요
  • 김마루
    허거덩
    김마루
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 대외 활동
        • 오픈소스 개발자 대회
        • LG 유레카
      • Project
        • SlackJudge
        • TR1L
        • Blocker
        • O+T
      • CS
        • DB
        • OS
        • Java
        • Spring
        • Language
      • Algorithm
      • etc
        • Certification
        • Goals
        • TIL
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    KDT
    부트캠프후기
    백트래킹
    BufferedReader
    SQLD
    멂티캠퍼스IT부트캠프
    오픈소스 개발자대회
    부트탬프후기
    유레카
    BFS
    유레카3기
    멀티캠퍼스IT부트ㅐㅁ프
    멀티캠퍼스부트캠프
    MySQL
    유레카백엔드
    SQL
    LG유플러스유레카백엔드개발자
    유레카 백엔드
    멀티캠퍼스IT부트캠프
    부트캠프
    자바
    멀티캠퍼스 부트캠프
    유레카 부트캠프
    N-Queen
    오픈소스 개발자 대회
    유레카백엔드개발자
    백엔드
    알고리즘
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
김마루
[고도화 #4] 전시 데이터 Redis 도입
상단으로

티스토리툴바