[Feat] 멱등성 처리 인프라 및 API 문서 추가#64
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 1 minutes and 57 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthrough멱등성 처리 인프라가 추가되었습니다: Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller
participant IdempotencyAspect
participant IdempotencyStore
participant Cache as Caffeine\ Cache
Client->>Controller: 요청 (Idempotency-Key 포함)
Controller->>IdempotencyAspect: `@Idempotent` 메서드 호출
IdempotencyAspect->>IdempotencyStore: find(key)
IdempotencyStore->>Cache: getIfPresent(key)
Cache-->>IdempotencyStore: CachedResponse?
IdempotencyStore-->>IdempotencyAspect: CachedResponse?
alt COMPLETED
IdempotencyAspect-->>Client: 저장된 body 반환
else PROCESSING
IdempotencyAspect-->>Client: 409 DUPLICATE_REQUEST
else null or FAILED
IdempotencyAspect->>IdempotencyStore: tryLock(key)
IdempotencyStore->>Cache: cache.get(key) { PROCESSING }
Cache-->>IdempotencyStore: CachedResponse (참조 비교)
IdempotencyStore-->>IdempotencyAspect: 락 성공 여부
alt 락 성공
IdempotencyAspect->>Controller: 원본 메서드 실행
Controller-->>IdempotencyAspect: 결과
IdempotencyAspect->>IdempotencyStore: saveCompleted(key, 결과)
IdempotencyStore->>Cache: COMPLETED로 저장
IdempotencyAspect-->>Client: 결과 반환
else 락 실패
IdempotencyAspect-->>Client: 409 DUPLICATE_REQUEST
end
end
sequenceDiagram
participant Client
participant IdempotencyAspect
participant IdempotencyStore
participant Cache as Caffeine\ Cache
participant BusinessLogic
Client->>IdempotencyAspect: 요청 (Idempotency-Key)
IdempotencyAspect->>IdempotencyAspect: 키 생성 (method:uri:memberId:rawKey)
rect rgba(200, 150, 100, 0.5)
IdempotencyAspect->>IdempotencyStore: tryLock(key)
IdempotencyStore->>Cache: cache.get(key) { CachedResponse(PROCESSING) }
Cache-->>IdempotencyStore: CachedResponse
IdempotencyStore-->>IdempotencyAspect: 락 성공 여부
end
IdempotencyAspect->>BusinessLogic: 원본 실행
alt 성공
BusinessLogic-->>IdempotencyAspect: 결과
IdempotencyAspect->>IdempotencyStore: saveCompleted(key, body)
IdempotencyStore->>Cache: COMPLETED 저장
IdempotencyAspect-->>Client: 결과 반환
else 예외
BusinessLogic-->>IdempotencyAspect: 예외
IdempotencyAspect->>IdempotencyStore: saveFailed(key)
IdempotencyStore->>Cache: invalidate(key)
IdempotencyAspect-->>Client: 예외 전파
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt (1)
13-18: 멱등성 TTL/용량은 설정값으로 빼두는 편이 안전합니다.지금 값은 운영 정책인데 코드 상수로 고정돼 있어서 환경별 조정이 어렵습니다. 트래픽이나 엔드포인트 특성에 따라 보존 시간과 캐시 크기가 달라질 수 있으니 프로퍼티로 외부화해 두는 쪽이 유지보수에 유리합니다.
예시 diff
import com.linktrip.application.port.output.idempotency.CachedResponse import com.linktrip.application.port.output.idempotency.IdempotencyStatus import com.linktrip.application.port.output.idempotency.IdempotencyStore import org.springframework.stereotype.Component +import org.springframework.beans.factory.annotation.Value import java.time.Duration `@Component` -class CaffeineIdempotencyStore : IdempotencyStore { +class CaffeineIdempotencyStore( + `@Value`("\${idempotency.cache.ttl-minutes:10}") private val ttlMinutes: Long, + `@Value`("\${idempotency.cache.max-size:10000}") private val maxSize: Long, +) : IdempotencyStore { private val cache: Cache<String, CachedResponse> = Caffeine.newBuilder() - .expireAfterWrite(Duration.ofMinutes(TTL_MINUTES)) - .maximumSize(MAX_SIZE) + .expireAfterWrite(Duration.ofMinutes(ttlMinutes)) + .maximumSize(maxSize) .build() @@ - - companion object { - private const val TTL_MINUTES = 10L - private const val MAX_SIZE = 10_000L - } }Also applies to: 36-38
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt` around lines 13 - 18, The hardcoded TTL and capacity (TTL_MINUTES and MAX_SIZE) used when building the Caffeine cache in CaffeineIdempotencyStore should be externalized to configuration: add constructor parameters or inject a config/properties object into CaffeineIdempotencyStore to provide ttlMinutes and maxSize, replace Duration.ofMinutes(TTL_MINUTES) with Duration.ofMinutes(ttlMinutes) and maximumSize(MAX_SIZE) with maximumSize(maxSize), and do the same refactor for the other cache instance referenced at the same locations (lines 36-38) so both caches read values from properties/env rather than constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStore.kt`:
- Around line 17-21: IdempotencyStore 인터페이스의 KDoc이 깨져 읽히지 않으니 tryLock 함수의 주석을
복구하세요; IdempotencyStore 및 메서드 tryLock의 KDoc을 명확한 한국어 문장으로 교체(예: "특정 key에 대해
PROCESSING 상태로 락을 건다. 이미 다른 상태가 존재하면 false를 반환한다.")하여 구현체와 AOP가 참조하는 계약 문구가 올바르게
남도록 수정하세요.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/TripPlanController.kt`:
- Around line 70-71: The deleteTripPlan endpoint currently has the `@Idempotent`
annotation commented out so the Idempotency-Key behavior described in docs
(409/retry semantics) is not applied; restore the annotation above the
deleteTripPlan handler (uncomment or re-add `@Idempotent`) and ensure the relevant
idempotency interceptor/import is present so the Idempotency-Key header is
processed, or if you intentionally won't support idempotency, update the API
docs to remove/mark the 409/retry behavior as not applicable for deleteTripPlan.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt`:
- Around line 41-42: The analyzeVideo endpoint in VideoController has the
`@Idempotent` annotation commented out which breaks the documented Idempotency-Key
behavior; re-enable the annotation by removing the comment so `@Idempotent` is
applied to the method annotated with `@PostMapping`("/analyze") (ensure the class
VideoController still imports the Idempotent annotation and that analyzeVideo
will read/validate the Idempotency-Key header and return the documented
409/cache responses when duplicates are detected).
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/idempotency/IdempotencyAspect.kt`:
- Around line 25-39: The idempotency key is currently just the raw
Idempotency-Key header, causing cross-user/endpoint collisions; update
checkIdempotency to build a namespaced key by combining extractIdempotencyKey()
with request-specific metadata (e.g., authenticated user id or principal, HTTP
method, request path) and reject or ignore empty header values; if the endpoint
semantics depend on body content, include a stable request-body fingerprint
(e.g., SHA-256) in the composite key before using idempotencyStore.find(key) or
storing responses in execute; ensure all references to Idempotency-Key handling
(extractIdempotencyKey, checkIdempotency, execute, and idempotencyStore usage)
use the new composite key format.
- Around line 49-55: 현재 try/catch가 Exception만 잡아 일부 Throwable(예: Error) 발생 시
idempotencyStore.saveFailed(key)가 호출되지 않아 키가 PROCESSING 상태로 남습니다; 변경할 곳은
IdempotencyAspect의 joinPoint.proceed() 블록으로, Exception 대신 Throwable을 잡도록 catch를
확장하여 어떤 Throwable이든 발생하면 idempotencyStore.saveFailed(key)를 호출하고 발생한 Throwable을
그대로 다시 던지도록 수정하세요; 참고 심볼: joinPoint.proceed(), idempotencyStore.saveFailed(key),
idempotencyStore.saveCompleted(key).
---
Nitpick comments:
In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt`:
- Around line 13-18: The hardcoded TTL and capacity (TTL_MINUTES and MAX_SIZE)
used when building the Caffeine cache in CaffeineIdempotencyStore should be
externalized to configuration: add constructor parameters or inject a
config/properties object into CaffeineIdempotencyStore to provide ttlMinutes and
maxSize, replace Duration.ofMinutes(TTL_MINUTES) with
Duration.ofMinutes(ttlMinutes) and maximumSize(MAX_SIZE) with
maximumSize(maxSize), and do the same refactor for the other cache instance
referenced at the same locations (lines 36-38) so both caches read values from
properties/env rather than constants.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b233a576-1351-4361-b023-367dea0ace4e
📒 Files selected for processing (11)
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/CachedResponse.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStatus.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStore.ktlinktrip-common/src/main/kotlin/com/linktrip/common/annotation/Idempotent.ktlinktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/TripPlanController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/TripPlanDocs.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/idempotency/IdempotencyAspect.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: 자동 검증 (ktlint + test)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.
Applied to files:
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: Similarly, `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt` likely has the same deferred concurrent-write policy (single server, ShedLock planned for replication phase).
Applied to files:
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt
🔇 Additional comments (4)
linktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.kt (1)
26-27: DUPLICATE_REQUEST 코드 추가는 적절합니다.Line 26-27의 409 예외 코드 추가가 멱등성 중복 요청 처리 흐름과 잘 맞습니다.
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/CachedResponse.kt (1)
3-6:CachedResponse모델링은 현재 idempotency 흐름과 잘 맞습니다.상태(
IdempotencyStatus)와 응답 본문(body) 분리가 명확해 사용처와 일관됩니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStatus.kt (1)
3-12: 상태 enum 정의가 명확합니다.Line 3-12의 상태 분리가 멱등성 라이프사이클 표현에 충분합니다.
linktrip-common/src/main/kotlin/com/linktrip/common/annotation/Idempotent.kt (1)
14-16: 어노테이션 메타 설정이 AOP 적용 목적에 적합합니다.
FUNCTION타겟 +RUNTIME보존 설정이 의도와 정확히 맞습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobScheduler.kt (1)
39-42: 10초 주기 info 로그는 운영 로그 노이즈를 크게 늘릴 수 있습니다.Line 39-41은 고정 10초마다
info를 남겨서 로그 비용/가독성에 부담이 큽니다. 로그 레벨을debug로 낮추거나, queue size가 0보다 클 때만 기록하고 예외를 내부 처리해 반복 에러 로그 폭주를 막는 쪽을 권장합니다.제안 diff
- `@Scheduled`(fixedDelay = 10_000) + `@Scheduled`(fixedDelayString = "\${batch.video-analysis.queue-log-delay-ms:10000}") fun logQueueSize() { - logger.info { "영상 분석 큐 사이즈: ${videoAnalysisQueuePort.size()}" } + runCatching { videoAnalysisQueuePort.size() } + .onSuccess { size -> + if (size > 0) { + logger.debug { "영상 분석 큐 사이즈: $size" } + } + } + .onFailure { e -> + logger.warn(e) { "영상 분석 큐 사이즈 조회 실패" } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobScheduler.kt` around lines 39 - 42, The logQueueSize() scheduled task currently emits an info log every 10 seconds which creates noise; change it to either logger.debug or only log when videoAnalysisQueuePort.size() > 0, and wrap the body in a try/catch to swallow and log errors at debug/trace level (not repeatedly at error/info) to prevent log flooding. Locate the scheduled function logQueueSize() and update the logging call from logger.info to logger.debug or add a conditional check on videoAnalysisQueuePort.size(), and add exception handling around the size() call so any failure logs minimally and does not repeat.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/idempotency/IdempotencyAspect.kt`:
- Around line 33-42: The code calls idempotencyStore.find(key) twice in
IdempotencyAspect (checking status and then using !! to get body), which creates
a TOCTOU NPE risk if the entry disappears between calls; fix by calling val
entry = idempotencyStore.find(key) once, check entry?.status against
IdempotencyStatus.PROCESSING and IdempotencyStatus.COMPLETED, and when COMPLETED
return entry.body (safely handling null if needed) — update the branch logic in
the method where idempotencyStore.find(key) is used so all checks/read use the
single local variable instead of repeated lookups.
---
Nitpick comments:
In
`@linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobScheduler.kt`:
- Around line 39-42: The logQueueSize() scheduled task currently emits an info
log every 10 seconds which creates noise; change it to either logger.debug or
only log when videoAnalysisQueuePort.size() > 0, and wrap the body in a
try/catch to swallow and log errors at debug/trace level (not repeatedly at
error/info) to prevent log flooding. Locate the scheduled function
logQueueSize() and update the logging call from logger.info to logger.debug or
add a conditional check on videoAnalysisQueuePort.size(), and add exception
handling around the size() call so any failure logs minimally and does not
repeat.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e71b0d9f-7c98-4b16-8e0d-ce8d1f2df021
📒 Files selected for processing (8)
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStore.ktlinktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobScheduler.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/TripPlanController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/TripPlanDocs.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/idempotency/IdempotencyAspect.ktlinktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt
✅ Files skipped from review due to trivial changes (2)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/TripPlanDocs.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/TripPlanController.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/port/output/idempotency/IdempotencyStore.kt
- linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/CaffeineIdempotencyStore.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: 자동 검증 (ktlint + test)
🔇 Additional comments (5)
linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobScheduler.kt (1)
3-3: 의존성 주입 추가는 적절합니다.
VideoAnalysisQueuePort를 생성자 주입으로 연결한 변경은 스케줄러 책임과 잘 맞고 확장성 측면에서도 깔끔합니다.Also applies to: 17-17
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/idempotency/IdempotencyAspect.kt (4)
1-24: LGTM!패키지 선언, 임포트, 클래스 구조 모두 적절합니다.
IdempotencyStore를 생성자 주입으로 받아 테스트 용이성을 확보한 점이 좋습니다.
48-65: LGTM!
Throwable을 잡아 모든 실패 경로에서saveFailed가 호출되도록 처리한 점이 좋습니다. 락 획득 → 실행 → 성공/실패 저장 흐름이 명확합니다.
81-86: LGTM!
RequestContextHolder에서 요청을 가져오는 표준 패턴이며, 헤더 상수 정의도 적절합니다.
74-76: 이 주석은 철회합니다.
TokenProvider.extractMemberId(token: String)의 반환 타입이String이므로 타입 캐스팅 문제가 없습니다.JwtAuthenticationFilter에서 이미 String으로 저장되는 값을IdempotencyAspect에서as? String으로 캐스팅하면 정상적으로 작동합니다. 인증된 사용자의 memberId가 올바르게 처리되어 "anonymous"로 대체되지 않습니다.
관련 이슈
변경 내용
@Idempotent어노테이션을 추가하고, AOP 기반IdempotencyAspect를 구현해 멱등성 처리 흐름을 구성IdempotencyStore포트와CachedResponse,IdempotencyStatus를 정의하고, Caffeine 기반 저장소 구현체를 추가DUPLICATE_REQUEST예외 코드를 추가Video analyze,TripPlan deleteAPI에 대한 멱등성 적용 준비를 위해 컨트롤러 import 및 Swagger 문서를 반영Idempotency-Key헤더, 중복 요청 동작 방식,409응답 예시를 추가체크리스트
Summary by CodeRabbit
새로운 기능
문서