Skip to content

Feat/#120 : media redis cache#124

Merged
Darren4641 merged 16 commits intostagingfrom
feat/#120
Feb 13, 2026
Merged

Feat/#120 : media redis cache#124
Darren4641 merged 16 commits intostagingfrom
feat/#120

Conversation

@koosco
Copy link
Member

@koosco koosco commented Feb 10, 2026

summary

  • media domain redis cache를 추가
  • binary redis template 추가

details

media domain redis cache를 추가

media 도메인에 redis cache adapter를 추가했습니다. 기존에 존재하던 FakeMediaBinaryCacheAdapter 대신 RedisMediaBinaryCacheAdapter를 사용합니다.

캐싱은 24시간동안 지속되며, 2시간이 남았을 때 다시 조회하면 TTL이 갱신되도록 설정하였습니다.

캐싱이 깨졌을 때 여러 건의 요청이 몰리게 되면 각 요청들이 모두 S3에 요청을 보내는 문제를 방지하고자 redis 기반 분산락을 추가하였습니다. redis 인스턴스 실행 후 캐싱된 값이 없을 때도 동일하게 적용됩니다.

binary redis template 추가

기존 Jackson Serializer이 Binary data에 대해 정상 동작하지 않는 이슈를 해결하기 위해 BinaryRedisTemplate을 추가하였습니다.
Jackson Serializer를 사용하면 ByteArray가 json으로 직렬화되고, 캐싱된 값을 꺼내오는 과정에서 Casting 오류가 발생하여 매번 S3 조회가 발생하는 문제가 발생하였습니다. Binary 전용 RedisTemplate을 추가하여 캐스팅 오류를 방지하고, byte array를 redis에서 정상적으로 꺼내오도록 수정을 반영하였습니다.

cache missing / hit log

2026-02-10 18:39:56.937 DEBUG [onPool-worker-2] c.y.m.i.l.r.RedisDistributedLockAdapter  : [NO_REQUEST_ID] [ANONYMOUS] [DistributedLock] Lock acquired for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:39:56.938 DEBUG [onPool-worker-2] c.y.m.i.c.r.RedisMediaBinaryCacheAdapter : [NO_REQUEST_ID] [ANONYMOUS] [MediaCache] Cache miss for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:39:56.938 DEBUG [onPool-worker-2] c.y.m.a.usecase.GetImageByKeyUseCase     : [NO_REQUEST_ID] [ANONYMOUS] [GetImage] Fetching from S3 for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:39:56.994 DEBUG [onPool-worker-2] c.y.m.i.c.r.RedisMediaBinaryCacheAdapter : [NO_REQUEST_ID] [ANONYMOUS] [MediaCache] Cache put successful for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png (TTL: PT24H)
2026-02-10 18:39:56.995 DEBUG [onPool-worker-2] c.y.m.i.l.r.RedisDistributedLockAdapter  : [NO_REQUEST_ID] [ANONYMOUS] [DistributedLock] Lock released for key: lock:media:fetch:photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:39:56.996 DEBUG [nio-8080-exec-9] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : [3a4395a67da74c67af198e03995be88e] [ANONYMOUS] Found 'Content-Type:image/png' in response
2026-02-10 18:39:57.062 DEBUG [nio-8080-exec-9] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : [3a4395a67da74c67af198e03995be88e] [ANONYMOUS] Writing [{-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 6, 88, 0, 0, 4, 2, 8, 6, 0, 0, (truncated)...]
2026-02-10 18:39:57.063 DEBUG [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet        : [3a4395a67da74c67af198e03995be88e] [ANONYMOUS] Completed 200 OK
2026-02-10 18:39:57.064 DEBUG [nio-8080-exec-9] o.s.s.w.a.AnonymousAuthenticationFilter  : [3a4395a67da74c67af198e03995be88e] [ANONYMOUS] Set SecurityContextHolder to anonymous SecurityContext
2026-02-10 18:39:57.755 DEBUG [io-8080-exec-10] o.s.security.web.FilterChainProxy        : [c7d9125b16274b5c947c1ebf2662aed7] [ANONYMOUS] Securing GET /favicon.ico
2026-02-10 18:39:57.756 DEBUG [io-8080-exec-10] o.s.s.w.a.AnonymousAuthenticationFilter  : [c7d9125b16274b5c947c1ebf2662aed7] [ANONYMOUS] Set SecurityContextHolder to anonymous SecurityContext
2026-02-10 18:40:00.281 DEBUG [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] Securing GET /file/image/photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:40:00.282 DEBUG [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] Secured GET /file/image/photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:40:00.282 DEBUG [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] GET "/file/image/photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png", parameters={}
2026-02-10 18:40:00.283 DEBUG [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] Mapped to com.yapp2app.media.api.controller.FileController#getImage(HttpServletRequest)
2026-02-10 18:40:00.341 DEBUG [nio-8080-exec-1] c.y.m.i.c.r.RedisMediaBinaryCacheAdapter : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] [MediaCache] Cache hit for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png
2026-02-10 18:40:00.347 DEBUG [nio-8080-exec-1] c.y.m.a.usecase.GetImageByKeyUseCase     : [a1a3e2d2a628423ea7bd97d503acc168] [ANONYMOUS] [GetImage] Cache hit for key: photo-booth/b0968299-61aa-4ca9-af22-e287d71886a8.png

@gemini-code-assist
Copy link

Summary of Changes

Hello @koosco, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 미디어 바이너리 데이터 조회 성능을 향상시키고 S3 스토리지 비용을 절감하기 위해 Redis 기반 캐싱 시스템을 도입합니다. 캐시 미스 시 발생하는 S3에 대한 동시 요청 폭주(cache stampede)를 방지하기 위해 분산 락 메커니즘을 구현하여 시스템 안정성을 강화했습니다. 또한, 바이너리 데이터의 효율적인 Redis 저장을 위한 전용 템플릿을 추가하여 캐싱 솔루션의 완성도를 높였습니다.

Highlights

  • Redis 캐시 도입: 미디어 도메인에 Redis 캐시를 추가하여 이미지 바이너리 데이터를 캐싱하고, 기존의 FakeMediaBinaryCacheAdapterRedisMediaBinaryCacheAdapter로 대체했습니다.
  • 캐시 스탬피드 방지: 캐시 미스 시 S3에 대한 동시 요청 폭주(cache stampede)를 방지하기 위해 Redis 기반 분산 락을 도입했습니다.
  • Lazy Refresh 구현: 캐시 만료 시간을 24시간으로 설정하고, 만료 2시간 전 조회 시 TTL을 갱신하는 lazy refresh 기능을 구현하여 인기 콘텐츠의 캐시 만료를 방지합니다.
  • 바이너리 데이터 처리 개선: Jackson Serializer의 바이너리 데이터 직렬화 문제를 해결하기 위해 BinaryRedisTemplate을 추가하여 byte array를 Redis에서 정상적으로 처리하도록 개선했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/main/kotlin/com/yapp2app/media/application/port/DistributedLockPort.kt
    • 분산 락 기능을 위한 인터페이스를 추가했습니다.
  • src/main/kotlin/com/yapp2app/media/application/usecase/GetImageByKeyUseCase.kt
    • 이미지 조회 유스케이스에 분산 락과 캐시 스탬피드 방지 로직을 적용하도록 수정했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/cache/FakeMediaBinaryCacheAdapter.kt
    • 기존의 FakeMediaBinaryCacheAdapter 파일을 삭제했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/cache/fake/FakeMediaBinaryCacheAdapter.kt
    • 테스트 환경 전용으로 TTL 추적 기능을 포함한 새로운 FakeMediaBinaryCacheAdapter를 추가했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/cache/redis/MediaRedisCacheKey.kt
    • Redis 캐시 키 명명 규칙을 관리하는 MediaRedisCacheKey 객체를 추가했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/cache/redis/RedisMediaBinaryCacheAdapter.kt
    • Redis를 활용한 미디어 바이너리 캐시 어댑터를 추가했으며, lazy refresh 및 장애 시 S3 폴백 기능을 포함합니다.
  • src/main/kotlin/com/yapp2app/media/infra/client/RestClientImageFetcher.kt
    • RestClientImageFetcher 파일을 삭제했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/lock/fake/FakeDistributedLockAdapter.kt
    • 테스트용 인메모리 분산 락 어댑터를 추가했습니다.
  • src/main/kotlin/com/yapp2app/media/infra/lock/redis/RedisDistributedLockAdapter.kt
    • Redis 기반 분산 락 어댑터를 추가했으며, single-flight, 지수 백오프 재시도, Lua 스크립트를 사용한 원자적 락 해제 기능을 포함합니다.
  • src/main/kotlin/com/yapp2app/media/infra/storage/fake/FakeMediaStorageConfig.kt
    • FakeMediaStorageAdapter에 S3 조회 횟수 추적 및 콜백 기능을 추가하여 테스트 용이성을 높였습니다.
  • src/test/kotlin/com/yapp2app/e2e/file/GetImageConcurrencyE2ETest.kt
    • 분산 락을 통한 동시성 제어를 검증하는 E2E 테스트를 추가했습니다.
  • src/test/kotlin/com/yapp2app/media/infra/cache/RedisMediaBinaryCacheAdapterTest.kt
    • RedisMediaBinaryCacheAdapter의 TTL 기반 lazy refresh 기능을 테스트하는 단위 테스트를 추가했습니다.
  • src/test/kotlin/com/yapp2app/media/infra/lock/RedisDistributedLockAdapterTest.kt
    • RedisDistributedLockAdapter의 동작을 검증하는 단위 테스트를 추가했습니다.
Activity
  • 이 PR은 아직 리뷰어 활동이나 추가적인 코멘트가 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 미디어 도메인에 Redis 캐시와 분산 락을 도입하여 성능을 개선하고 cache stampede 문제를 해결하는 중요한 변경사항을 담고 있습니다. 전반적으로 포트와 어댑터 패턴을 잘 활용했고, 분산 락 구현도 견고합니다. 테스트 코드 또한 동시성 시나리오를 포함하여 잘 작성되었습니다. 다만, 몇 가지 중요한 수정이 필요한 부분이 있습니다. 가장 시급한 문제는 바이너리 데이터 캐싱에 부적절한 Redis 직렬화 설정이 사용된 점입니다. 이로 인해 캐시가 의도대로 동작하지 않을 수 있습니다. 또한, 분산 락 획득 실패 시의 fallback 로직이 여러 인스턴스 환경에서 여전히 stampede를 유발할 수 있는 잠재적 위험을 가지고 있습니다. 마지막으로, 비동기 작업에 공용 스레드 풀을 사용하는 부분에 대한 개선 제안을 포함했습니다.

@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

@github-actions
Copy link

Test ❌ FAILED

Test Result: failure

⚠️ Tests failed!

Please check the test output and fix the failing tests.

./gradlew test

Pushed by: @koosco, Action: pull_request

@koosco koosco self-assigned this Feb 11, 2026
@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

@github-actions
Copy link

Test ✅ PASSED

Test Result: success

✨ All tests passed!


Pushed by: @koosco, Action: pull_request

@koosco koosco marked this pull request as ready for review February 11, 2026 14:44
@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

@github-actions
Copy link

Test ✅ PASSED

Test Result: success

✨ All tests passed!


Pushed by: @koosco, Action: pull_request

Copy link
Member

@Darren4641 Darren4641 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment 추가했습니다~!

private val log = LoggerFactory.getLogger(javaClass)

// In-memory single-flight map (same as production)
private val inFlightOperations = ConcurrentHashMap<String, CompletableFuture<Any?>>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 분산락을 구현해야해서 동일한 WAS끼리만 중복을 막아주는 건 의미 없을것 같습니다. 어차피 Redis를 이용하여 분산락을 쓰기 떄문에 해당 Map은 불필요해보입니다!

@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

@github-actions
Copy link

Test ✅ PASSED

Test Result: success

✨ All tests passed!


Pushed by: @koosco, Action: pull_request

private val log = LoggerFactory.getLogger(javaClass)

// In-memory single-flight map: 동일 JVM 내 동시 요청 중복 제거
private val inFlightOperations = ConcurrentHashMap<String, CompletableFuture<Any?>>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 분산락을 구현해야해서 동일한 WAS끼리만 중복을 막아주는 건 의미 없을것 같습니다. 어차피 Redis를 이용하여 분산락을 쓰기 떄문에 해당 Map은 불필요해보입니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 락과는 별개로 약간 성능 최적화 관점에서 이점이 있습니다.
Thread.sleep 사용 시 주기마다 Redis 락 점유 시도를 반복하는데, CompletableFuture 사용 시 Redis 호출 없이 첫 번째 스레드의 결과를 바로 공유받는 차이가 있습니다.
트래픽이 거의 없어 큰 차이는 없을 것 같은데, 없애는게 나을까요?

@github-actions
Copy link

Code Format Check ✅ PASSED

Spotless Check: success

✨ All code formatting checks passed!


Pushed by: @koosco, Action: pull_request

@github-actions
Copy link

Test ✅ PASSED

Test Result: success

✨ All tests passed!


Pushed by: @koosco, Action: pull_request

Copy link
Member

@Darren4641 Darren4641 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 LGTM~

@Darren4641 Darren4641 merged commit 2e63039 into staging Feb 13, 2026
2 checks passed
@Darren4641 Darren4641 deleted the feat/#120 branch February 13, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants