Skip to content

[fix] 카드 생성의 비재시도 예외가 실패 로그·상태 전환을 건너뛴다 - #179

Open
theminjunchoi wants to merge 5 commits into
devfrom
fix/178-card-non-retryable
Open

[fix] 카드 생성의 비재시도 예외가 실패 로그·상태 전환을 건너뛴다#179
theminjunchoi wants to merge 5 commits into
devfrom
fix/178-card-non-retryable

Conversation

@theminjunchoi

@theminjunchoi theminjunchoi commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #178

무엇을 고쳤나

CardService 의 두 LLM 호출부가 LlmRetryExecutor.executeonExhausted 만 넘기고 onNonRetryable 은 넘기지 않았다. 같은 리팩터링을 받은 형제 서비스와 어긋나 있던 자리다.

호출부 이전 이후
CommentGenerationService:257 (댓글)
CommentGenerationService:331 (답글)
CardService extractEmotion
CardService generateMessage

빠진 쪽에서 재시도 대상이 아닌 예외로 중단되면 두 가지가 어긋났다.

  1. 실패 생성 로그가 남지 않는다. LlmRetryExecutor KDoc 이 스스로 밝힌 존재 이유가 "마지막 한 시도만 기록해 과소 집계한 버그가 있던 자리" 인데, 그 문제가 카드 경로에서 다시 열려 있었다.
  2. cardGenerationStatus 가 PENDING 으로 남는다. CAS 선점 이후라 FAILED 로 되돌리지 않으면 사용자의 재시도가 재시도 가능한 503 이 아니라 409 CARD_GENERATION_IN_PROGRESS 로 막힌다. PendingCardCleanupScheduler 가 되돌릴 때까지 최대 4분 가까이 걸린다 — 임계 pending-generation-timeout: PT3M 에 스캔 주기(fixedDelay = 60_000)가 더해져, 임계를 막 넘긴 직후 스캔을 놓치면 1분을 더 기다린다.

같은 파일의 loadUserMessagesDataAccessException 에 대해 이미 이 위험을 막고 있다 — "이 조회는 CAS 선점 이후라 실패를 그대로 던지면 상태가 PENDING으로 남아, 재시도가 재시도 가능한 503이 아니라 409(생성 중)로 막힌다". 패턴은 이미 인지하고 있었는데 재시도 실행기 경로에만 적용되지 않았다.

어떻게 고쳤나

커밋 3개로 나눴다.

  1. refactorrecordCarderror 파라미터를 CardGenerationFailedException?Throwable? 로 넓혔다. 비재시도 예외의 원인을 실을 자리를 먼저 연다. 동작 변화 없음
  2. fix — 두 호출부에 onNonRetryable 추가
  3. test — 검증

두 콜백을 각각 인라인으로 넣는 대신 failureRecorder 로 묶었다. 콜백 하나만 넘겨 나머지 경로가 조용히 빠지는 것이 이번 결함의 원인 그 자체라, 같은 실수가 이 자리에서 다시 나지 않게 하는 편이 낫다고 봤다.

val recordFailure = failureRecorder(GenerationType.CARD_EMOTION, startedAt, memberId, conversationId, tokens)
...
    onNonRetryable = recordFailure,
    onExhausted = recordFailure,

검증 — 되돌려서 실제로 잡히는지 확인했다

구현(fix 커밋)만 되돌리고 테스트를 돌렸다.

CardServiceTest > 감정 분류가 재시도 대상이 아닌 예외로 끊겨도 FAILED로 전이하고 실패 로그를 남긴다() FAILED
CardServiceTest > 대사 생성이 재시도 대상이 아닌 예외로 끊겨도 FAILED로 전이하고 실패 로그를 남긴다() FAILED
23 tests completed, 2 failed

새 테스트 2개만 실패하고 기존 21개는 그대로 통과한다 — 테스트가 실제로 이 결함을 잡는다는 것과, 기존 계약을 건드리지 않았다는 것을 함께 확인했다.

리뷰 포인트 — 예외를 변환하지 않는다

비재시도 예외는 CARD_GENERATION_FAILED 로 변환하지 않고 원래 예외를 그대로 올린다. 상태 복구와 실패 로그만 같은 계약으로 받는다.

  • LlmRetryExecutor KDoc: "어느 쪽이든 마지막에는 원래 예외를 그대로 던진다 — 호출자가 예외 타입으로 분기하는 기존 동작을 바꾸지 않기 위해서다"
  • CommentGenerationService 도 같은 계약이라 형제 서비스와 맞춘다
  • 예상 밖 결함을 업무 오류로 위장하지 않는 편이 낫다고 봤다

대신 클라이언트는 이 경우 500 을 받는다. extractEmotion KDoc 의 "어느 쪽이 실패했든 CARD_GENERATION_FAILED 하나로 재시도한다" 와는 어긋나는 면이 있어 KDoc 에 이 결정을 명시해 뒀다. 모든 실패를 CARD_GENERATION_FAILED 로 묶는 쪽이 낫다는 의견이면 알려주세요 — 다만 API 계약 변경이라 앱 팀과 맞춰야 한다.

영향 범위

  • 실제 발생 확률은 낮다. GeminiEmotionExtractor · GeminiCardMessageGenerator 가 LLM 호출을 catch (e: Exception) 으로 감싸 전부 재시도 대상 예외로 바꾸고, GenerationLogRecorder.recordrunCatching 으로 전부 삼킨다. 현실적인 탈출구는 try 바깥의 SDK 호출(response.text(), usageMetadata() 등)뿐이다
  • 마이그레이션 없음, 설정 변경 없음, API 스펙 변경 없음

배경

#177(prod 승격) 에 CodeRabbit 이 남긴 리뷰 8건 중 유일하게 반영이 필요하다고 판단한 건이다. 나머지 7건은 각 스레드에 근거를 남기고 resolve 했다.

Summary by CodeRabbit

  • 버그 수정

    • 카드 감정 분류 및 문구 생성 중 재시도할 수 없는 오류가 발생해도 실패 상태로 정상 처리됩니다.
    • 오류 원인이 기록되고 생성 상태가 FAILED로 변경됩니다.
    • 오류 발생 후 후속 처리와 카드 저장이 실행되지 않습니다.
  • 테스트

    • 감정 분류 및 문구 생성 단계의 예외 처리 동작을 검증하는 테스트를 추가했습니다.

recordCard 의 error 파라미터가 CardGenerationFailedException 만 받아, 재시도
대상이 아닌 예외로 중단될 때는 원인을 실을 자리조차 없었다. Throwable 로
넓혀 두 경우가 같은 자리에 기록되게 한다.

동작은 바뀌지 않는다 - 지금 부르는 곳은 onExhausted 뿐이다. 다음 커밋에서
onNonRetryable 이 이 자리를 쓴다.
extractEmotion·generateMessage 가 LlmRetryExecutor 에 onExhausted 만 넘기고
onNonRetryable 은 넘기지 않았다. 재시도 대상이 아닌 예외로 중단되면 실패
생성 로그가 남지 않고 cardGenerationStatus 도 PENDING 그대로 남는다. 그러면
사용자의 재시도가 재시도 가능한 503 이 아니라 409(생성 중)로 막힌다 -
PendingCardCleanupScheduler 가 되돌릴 때까지 최대 3분(PT3M)이다.

같은 리팩터링을 받은 CommentGenerationService 는 두 호출부 모두
onNonRetryable = recordFailure 를 넘긴다. 카드 쪽만 빠져 있었다.

두 콜백이 남길 것이 같아 failureRecorder 로 묶었다. 하나만 넘겨 나머지
경로가 조용히 빠지는 실수가 이 자리에서 다시 나지 않게 한다.

예외 자체는 변환하지 않고 그대로 올린다 - 예상 밖 결함을 업무 오류로
위장하지 않으려는 것이고, CommentGenerationService 도 같은 계약이다.
두 호출부(감정 분류·대사 생성)에서 재시도 대상이 아닌 예외가 나올 때
FAILED 로 전이하는지, 실패 생성 로그에 원인 클래스명이 남는지, 재시도하지
않고 원래 예외를 그대로 올리는지 확인한다.

앞 커밋의 onNonRetryable 을 되돌리고 돌리면 두 테스트만 실패한다
(23 tests completed, 2 failed) - 나머지 21개는 그대로 통과해 기존 계약을
건드리지 않았음도 함께 확인했다.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@theminjunchoi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 81a6ea74-6da6-4415-9899-76cb66d53975

📥 Commits

Reviewing files that changed from the base of the PR and between a2a7fe1 and c49f2c6.

📒 Files selected for processing (2)
  • src/main/kotlin/com/nexters/gamss/card/service/CardService.kt
  • src/test/kotlin/com/nexters/gamss/card/service/CardServiceTest.kt

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0786b462-cecb-4fd4-972f-ddd0cd0de181

📥 Commits

Reviewing files that changed from the base of the PR and between 592ae51 and a2a7fe1.

📒 Files selected for processing (2)
  • src/main/kotlin/com/nexters/gamss/card/service/CardService.kt
  • src/test/kotlin/com/nexters/gamss/card/service/CardServiceTest.kt

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

카드 생성의 감정 분류와 문구 생성에서 비재시도 예외도 실패 처리 대상이 된다. 공통 콜백은 실패 로그를 기록하고 생성 상태를 FAILED로 변경한다. 관련 테스트는 예외 전파와 후속 작업 중단을 검증한다.

Changes

카드 생성 실패 처리

Layer / File(s) Summary
공통 실패 처리 및 콜백 연결
src/main/kotlin/com/nexters/gamss/card/service/CardService.kt
감정 분류와 문구 생성이 onNonRetryableonExhaustedfailureRecorder를 공통으로 사용한다. failureRecorder는 실패 로그를 기록하고 상태를 FAILED로 변경한다. recordCard의 오류 원인 타입은 Throwable?로 확장된다.
비재시도 예외 검증
src/test/kotlin/com/nexters/gamss/card/service/CardServiceTest.kt
비재시도 IllegalStateException이 그대로 전파되는지 검증한다. 감정 분류 후 문구 생성이 호출되지 않고, 문구 생성 후 카드 저장이 호출되지 않는지 검증한다. 실패 로그와 FAILED 상태 전환도 검증한다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🔵 Low · up to a2a7f

비재시도 오류 처리로 실패 로그와 FAILED 상태 전환을 보완하지만, 실패 정리 작업 자체가 예외를 던지면 원래 오류가 가려지거나 상태가 PENDING으로 남아 사용자의 재시도가 일시적으로 막힐 수 있습니다. 범위가 제한적이므로 담당자 확인을 전제로 병합 가능합니다.

Possibly related issues

  • Nexters/GAMSS-Server#162CardService의 재시도 분류와 백오프를 다루며, 본 변경의 비재시도 예외 콜백 처리와 코드 수준의 연관성이 있다.

Possibly related PRs

  • Nexters/GAMSS-Server#136 — 감정 분류와 카드 문구 생성 오류에서 실패 로그 기록 및 FAILED 상태 전환을 다룬다.
  • Nexters/GAMSS-Server#144CardService의 실패 콜백과 관련 테스트를 변경하며, 본 변경은 해당 흐름을 공통화한다.
  • Nexters/GAMSS-Server#31CardService의 카드 생성 실패 처리를 변경하며, 본 변경은 비재시도 Throwable까지 처리 범위를 확장한다.

Suggested reviewers: kite707

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed [178]의 두 호출부 수정, Throwable 확장, 공통 실패 기록, 상태 전환과 회귀 테스트 요구사항을 모두 충족합니다.
Out of Scope Changes check ✅ Passed 변경은 [178]의 예외 처리 보완과 관련 테스트 범위에 한정되며, 별도 기능이나 설정 변경은 없습니다.
Title check ✅ Passed 제목이 카드 생성의 비재시도 예외 처리 누락과 실패 로그·상태 전환 수정을 정확히 설명합니다.
Description check ✅ Passed 필수 내용인 이슈, 변경 사항, 영향 범위, 리뷰 포인트와 검증 결과를 구체적으로 포함합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/178-card-non-retryable

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Test Results

693 tests  +3   693 ✅ +3   2m 22s ⏱️ -14s
 94 suites ±0     0 💤 ±0 
 94 files   ±0     0 ❌ ±0 

Results for commit c49f2c6. ± Comparison against base commit 592ae51.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Test Coverage

Overall Project 81.26% 🍏
Files changed 100% 🍏

File Coverage
CardService.kt 98.09% 🍏

@theminjunchoi
theminjunchoi requested a review from kite707 August 19, 2026 06:16
@kite707

kite707 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

잘 정리해주셨네요! 콜백 하나만 넘겨서 생긴 문제라 두 개를 failureRecorder로 묶어 처리한 방향이 좋은 것 같습니다!

대신 클라이언트는 이 경우 500 을 받는다. extractEmotion KDoc 의 "어느 쪽이 실패했든 CARD_GENERATION_FAILED 하나로 재시도한다" 와는 어긋나는 면이 있어 KDoc 에 이 결정을 명시해 뒀다. 모든 실패를 CARD_GENERATION_FAILED 로 묶는 쪽이 낫다는 의견이면 알려주세요 — 다만 API 계약 변경이라 앱 팀과 맞춰야 한다.

에 대해서는 예외를 변환하지 않는 쪽 그대로 두시면 될 것 같습니다. 503을 내리면 클라이언트측에서 계속 재시도할 수 있을 것 같아서요.

@kite707

kite707 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

그리고 같은 패턴이 persistCard에 하나 남아 있는 것 같아요. CardService.persistCardDataIntegrityViolationExceptionCardGenerationStateConflictException만 잡고 있어서, cardPersistenceService.save가 그 밖의 예외(커넥션 끊김, QueryTimeoutException 등)로 실패하면 CAS 선점 이후라 이번 건과 똑같이 cardGenerationStatus가 PENDING으로 남습니다. 같은 파일의 loadUserMessagesDataAccessException을 이미 막아뒀는데 저장 경로만 빠져 있는 것 같습니다. 발생 확률은 이번 건과 같은 수준이지만 여기서 함께 고치는게 좋아보여 제안드립니다!

@kite707

kite707 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

마지막으로 PR의 "최대 3분"은 실제로 4분에 가까울 것 같아요. pending-generation-timeout: PT3MPendingCardCleanupSchedulerfixedDelay = 60_000이 더해져서, 임계를 막 넘긴 직후 스캔을 놓치면 1분을 더 기다립니다. 코드 KDoc에는 숫자가 없어서 PR·이슈 본문 문구만 손보면 될 것 같습니다.

Comment thread src/main/kotlin/com/nexters/gamss/card/service/CardService.kt Outdated
recordFailure 는 CommentGenerationService 의 같은 자리와 이름이 겹치는데
하는 일이 다르다. 그쪽은 생성 로그만 남기고 FAILED 전이는 바깥 catch 가
맡지만, 여기는 상태 복구까지 한다. 이름이 같으면 두 서비스가 같은 일을
한다고 읽힌다.

failureRecorder -> failureHandler, recordFailure -> handleFailure 로
바꾸고 KDoc 에 차이를 적었다.
persistCard 가 DataIntegrityViolationException 과
CardGenerationStateConflictException 만 잡아, 커넥션 끊김이나 쿼리
타임아웃으로 저장이 실패하면 예외가 그대로 나가고 상태는 PENDING 으로
남았다. 이 자리는 CAS 선점 이후라 재시도가 재시도 가능한 503 이 아니라
409(생성 중)로 막힌다 - 앞 커밋들이 고친 것과 같은 결함이다.

같은 파일의 loadUserMessages 는 DataAccessException 을 이미 막아뒀는데
저장 경로만 빠져 있었다.

상태만 되돌리고 예외는 변환하지 않고 그대로 올린다 - failureHandler 와
같은 계약이다.

구현을 되돌리면 새 테스트만 실패한다(24 tests completed, 1 failed).
@theminjunchoi
theminjunchoi requested a review from kite707 August 19, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 카드 생성의 비재시도 예외가 실패 로그·상태 전환을 건너뛴다

2 participants