[feat] LLM 호출을 통한 댓글 생성 - #13
Conversation
Test Results155 tests +23 155 ✅ +23 56s ⏱️ -3s Results for commit de80e94. ± Comparison against base commit 895feff. This pull request removes 3 and adds 26 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Coverage
|
WalkthroughGemini SDK 기반 댓글·티키타카 생성 기능이 추가되었습니다. 메시지별 생성 상태와 루트 메시지 연결을 저장하고, 재시도·검증·영속화·비동기 API·오래된 PENDING 정리 및 KST 자정 기준 대화 조회를 구성합니다. Changes댓글 생성 기능
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ConversationController
participant CommentGenerationService
participant GeminiCommentGenerator
participant CommentPersistenceService
participant MessageRepository
Client->>ConversationController: 댓글 생성 요청
ConversationController->>CommentGenerationService: generateComments(memberId, messageId)
CommentGenerationService->>MessageRepository: PENDING 선점
CommentGenerationService->>GeminiCommentGenerator: 피드 생성 및 파싱
GeminiCommentGenerator-->>CommentGenerationService: CommentGenerationOutput
CommentGenerationService->>CommentPersistenceService: 검증된 피드 저장
CommentPersistenceService->>MessageRepository: DONE 상태 갱신
CommentGenerationService-->>ConversationController: 생성 결과
ConversationController-->>Client: 상태 및 댓글 응답
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/kotlin/com/nexters/gamss/conversation/repository/MessageRepository.kt`:
- Around line 42-50: Update the resetStalePending query to bind CommentStatus
values through named parameters instead of hard-coding the enum’s fully
qualified class name. Add the required enum parameters to resetStalePending and
bind them in both status comparisons, then update its callers to pass PENDING
and NONE.
In
`@src/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.kt`:
- Around line 55-68: Update generateComments to catch unexpected Exception
instances in addition to CommentGenerationFailedException, log the original
error with messageId context, and transition the message to CommentStatus.FAILED
using the existing pending-status guard before returning a failed
CommentGenerationResult. Preserve the current successful flow and avoid
duplicating the failure-state handling.
In
`@src/main/kotlin/com/nexters/gamss/conversation/service/PendingCommentCleanupScheduler.kt`:
- Around line 14-28: Update PendingCommentCleanupScheduler.resetStalePending to
use a distributed lock around the scheduled cleanup, such as the project’s
ShedLock configuration and annotation, so only one server instance executes the
reset at a time; preserve the existing schedule, threshold calculation,
repository update, and warning log behavior.
In `@src/main/kotlin/com/nexters/gamss/llm/CharacterSelector.kt`:
- Around line 25-30: CharacterSelector의 캐릭터 수 제한이 기획된 감정 캐릭터 3~4개 기준과 불일치합니다. 기존
요구사항을 따르는 경우 companion object의 MAX_COUNT를 4로 조정해 TIKITAKA_MAX와 일치시키고, 변경된 기획에 따른
설정이라면 현재 값 6을 유지하되 그 근거를 설명하는 주석을 추가하세요.
In `@src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt`:
- Around line 57-59: In GeminiCommentGenerator, replace the generic Exception
catches at src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt:57-59
and :46-48 with the specific Jackson parsing exception for the JSON parsing
block and the SDK’s specific base exception for the LLM call block, preserving
the existing CommentGenerationFailedException wrapping behavior.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 501af741-2c87-41b3-81b5-f5eea35ce3d8
📒 Files selected for processing (30)
build.gradle.ktssrc/main/kotlin/com/nexters/gamss/conversation/config/ConversationProperties.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/ConversationController.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/dto/CommentGenerationResponse.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/dto/GenerateCommentsRequest.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/dto/MessageResponse.ktsrc/main/kotlin/com/nexters/gamss/conversation/domain/CommentStatus.ktsrc/main/kotlin/com/nexters/gamss/conversation/domain/Conversation.ktsrc/main/kotlin/com/nexters/gamss/conversation/domain/Message.ktsrc/main/kotlin/com/nexters/gamss/conversation/repository/MessageRepository.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationResult.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentPersistenceService.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/PendingCommentCleanupScheduler.ktsrc/main/kotlin/com/nexters/gamss/global/config/SchedulingConfig.ktsrc/main/kotlin/com/nexters/gamss/global/exception/ErrorCode.ktsrc/main/kotlin/com/nexters/gamss/llm/CharacterSelector.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentFeed.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentFeedValidator.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentGenerator.ktsrc/main/kotlin/com/nexters/gamss/llm/EongttungTopicSelector.ktsrc/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.ktsrc/main/kotlin/com/nexters/gamss/llm/GeminiProperties.ktsrc/main/kotlin/com/nexters/gamss/llm/PromptCharacterId.ktsrc/main/resources/application.ymlsrc/main/resources/db/migration/V3__comment_generation.sqlsrc/test/kotlin/com/nexters/gamss/conversation/repository/MessageRepositoryTransactionTest.ktsrc/test/kotlin/com/nexters/gamss/conversation/service/CommentGenerationServiceTest.ktsrc/test/kotlin/com/nexters/gamss/llm/CommentFeedValidatorTest.ktsrc/test/resources/application.yml
theminjunchoi
left a comment
There was a problem hiding this comment.
넘 잘 작성하셨네요!
그래도 아래 3개만 확인해주세요! 수고하셨습니다 🤟🏻
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 69-70: README.md의 브랜치·커밋·PR·이슈 컨벤션 링크 목록에서 `이슈 컨벤션` 전체 텍스트가 하나의
Markdown 링크로 렌더링되도록 링크 대상과 표시 범위를 수정하고, 줄바꿈으로 `컨벤션`이 일반 텍스트로 분리되지 않게 정리하세요.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bbfcea6f-92fb-4892-83c5-afd4beb6c062
📒 Files selected for processing (4)
README.mdsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentFeedValidator.ktsrc/test/kotlin/com/nexters/gamss/llm/CommentFeedValidatorTest.kt
client.models.generateContent가 반환하는 final 타입이라 모킹이 불가능해, text -> CommentFeed 변환 로직, (parseFeed)을 client 호출과 분리해독립적으로 테스트 가능하게 만들었다.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt (1)
44-46: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win포괄적인
Exception캐치 개선두 파일 모두 최상위 예외인
Exception을 잡아 처리하고 있어,NullPointerException등 의도치 않은 런타임 오류까지 은폐하게 되어 원인 추적을 어렵게 만드는 공통된 한계가 있습니다.
src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt#L44-L46: SDK 내부 오류를 나타내는 구체적인 예외 클래스로 변경하세요.src/main/kotlin/com/nexters/gamss/llm/CommentFeedJsonParser.kt#L20-L22:tools.jackson.core.JacksonException등 파싱 관련 구체적인 예외로 변경하세요.대안 및 장단점
- 장점: 예상 가능한 장애와 코드 결함을 명확히 구분하여 문제의 조기 발견과 디버깅을 돕습니다.
- 단점: 사용 중인 라이브러리의 예외 계층 구조를 명시적으로 파악하고 지정해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt` around lines 44 - 46, Replace the broad Exception catch in GeminiCommentGenerator with the specific SDK exception type representing expected Gemini/LLM failures, while preserving CommentGenerationFailedException wrapping. In CommentFeedJsonParser, replace its broad Exception catch with the concrete Jackson parsing exception type, such as tools.jackson.core.JacksonException, and preserve the existing parser-specific failure handling; apply these changes at both listed sites.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.kt`:
- Around line 44-46: Replace the broad Exception catch in GeminiCommentGenerator
with the specific SDK exception type representing expected Gemini/LLM failures,
while preserving CommentGenerationFailedException wrapping. In
CommentFeedJsonParser, replace its broad Exception catch with the concrete
Jackson parsing exception type, such as tools.jackson.core.JacksonException, and
preserve the existing parser-specific failure handling; apply these changes at
both listed sites.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bcc478ef-ef9c-49a4-918e-5fab0e861fb4
📒 Files selected for processing (5)
src/main/kotlin/com/nexters/gamss/llm/CommentFeedJsonParser.ktsrc/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.ktsrc/main/kotlin/com/nexters/gamss/llm/PromptCharacterId.ktsrc/main/kotlin/com/nexters/gamss/llm/PromptProvider.ktsrc/test/kotlin/com/nexters/gamss/llm/CommentFeedJsonParserTest.kt
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.kt`:
- Around line 59-76: Update the exception logging in the catch block of
CommentGenerationService using a when-based type distinction: log
BusinessException without error-level reporting, retain warning logging for
CommentGenerationFailedException, and use error logging only for unexpected
exceptions. Preserve the existing FAILED status update and rethrow behavior for
BusinessException.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4280adba-effe-4c02-8580-50c27f1793b7
📒 Files selected for processing (17)
README.mdsrc/main/kotlin/com/nexters/gamss/conversation/config/ConversationProperties.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/ConversationController.ktsrc/main/kotlin/com/nexters/gamss/conversation/controller/dto/CommentGenerationResponse.ktsrc/main/kotlin/com/nexters/gamss/conversation/repository/MessageRepository.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationResult.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/CommentGenerationService.ktsrc/main/kotlin/com/nexters/gamss/conversation/service/ConversationService.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentFeedJsonParser.ktsrc/main/kotlin/com/nexters/gamss/llm/CommentGenerator.ktsrc/main/kotlin/com/nexters/gamss/llm/GeminiCommentGenerator.ktsrc/main/resources/application.ymlsrc/test/kotlin/com/nexters/gamss/conversation/config/ConversationPropertiesTest.ktsrc/test/kotlin/com/nexters/gamss/conversation/controller/ConversationControllerIntegrationTest.ktsrc/test/kotlin/com/nexters/gamss/conversation/service/CommentGenerationServiceTest.ktsrc/test/kotlin/com/nexters/gamss/conversation/service/ConversationServiceTest.ktsrc/test/resources/application.yml
💤 Files with no reviewable changes (3)
- src/test/kotlin/com/nexters/gamss/conversation/config/ConversationPropertiesTest.kt
- src/main/resources/application.yml
- src/test/resources/application.yml
🔗 연관 이슈
📌 개요
일기(사용자 메시지)에 LLM으로 캐릭터 댓글 + 티키타카를 생성하는 기능을 구현했습니다. 핵심은 멱등성(같은 메시지에 대해 한 번만 생성)과 동시성 제어(DB 조건부 UPDATE)이고, 캐릭터·소재·개수는 LLM이 아니라 서버가 정해서 프롬프트에 지시합니다.
🔧 주요 변경사항
스키마 (V3 마이그레이션)
messages에root_message_id(캐릭터 댓글·티키타카가 속한 원본 일기 참조) ·comment_status(NONE/PENDING/DONE/FAILED) ·comment_status_updated_at컬럼 추가 (root_message_id에 인덱스 추가)repliesToMessageId는 진짜 "답장"(티키타카 캐릭터 간 답장)에만 쓰고,rootMessageId는 "이 일기에 달린 피드 전체" 조회 전용으로 역할을 분리멱등성 · 동시성 제어
MessageRepository.updateCommentStatus—NONE/FAILED상태일 때만 원자적으로PENDING으로 선점하는 조건부 UPDATE(CAS). 영향 행 수(0/1)로 선점 성공 여부 판단FAILED, 성공하면 저장과 함께DONEPendingCommentCleanupScheduler— 배포 중단·크래시로 남은 고아PENDING을 주기적으로NONE으로 되돌려 재선점 가능하게 함(conversation.comment-pending-timeout, 기본 3분)MessageRepository의@Modifying메서드에@Transactional명시 — 서비스 트랜잭션 밖(스케줄러 등)에서 직접 호출해도 안전하게 동작저장 매핑
CommentPersistenceService— 1라운드(comments)를 먼저 저장해character_id → messageId맵을 만들고, 2라운드(tikitaka)의reply_to를 실제repliesToMessageId로 치환해 저장CommentGenerationService와 별도 빈으로 분리 — 같은 클래스 안에서 호출(self-invocation)하면@Transactional프록시가 안 걸리는 문제를 피하기 위함LLM 연동 (
llm패키지)CommentGenerator인터페이스 +GeminiCommentGenerator(google-genai 공식 SDK 구현) — 프로바이더 교체 가능하게 인터페이스 뒤에 숨김CharacterSelector— 캐릭터(3-6개)·tikitaka 개수(3-4개)를 서버가 랜덤 선택. LLM한테 무작위를 맡기면 통계적으로 편향된다는 근거로 서버 선택 채택EongttungTopicSelector— 엉뚱이 소재도 같은 이유로 서버가 미리 골라 프롬프트에 주입PromptCharacterId— 프롬프트가 쓰는 캐릭터 id(gippeum등) ↔ 내부EmotionType매핑 계층CommentFeedValidator—responseSchema가 못 잡는 의미 규칙(캐릭터 구성 정확히 일치, 자문자답 금지, tikitaka 개수 일치) 검증systemInstruction으로 고정 전송(프리픽스 캐싱 대상), 일기·과거 요약·이번 생성 조건만 매 호출 가변으로 붙임API
POST /api/conversations/messages/comments— 요청{messageId}, 응답{status: GENERATING|DONE|FAILED, comments?}(항상 HTTP 200, 재요청이 곧 결과 조회를 겸함)ErrorCode추가:MESSAGE_NOT_FOUND(404) ·INVALID_COMMENT_TARGET(400)공통
google-genai가 내부적으로 Jackson 2를 쓰는데, 그 jar에 딸려온 서비스 등록 파일 때문에 Hibernate의 Jackson 자동 감지가ServiceConfigurationError로 깨지는 문제 발견 →jackson-module-kotlin(Jackson 2)을runtimeOnly로 추가해 해결🌐 API·DB 영향
V3__comment_generation.sql—messages에 컬럼 3개 추가 + 인덱스 1개 (기존 데이터 영향 없음)💬 리뷰 포인트
1. 컬럼 방식(A′) vs 선점 테이블(A) 선택 — 동시성 안전성은 동일해서 테이블을 새로 안 만드는 쪽(
comment_status컬럼)을 선택했습니다. 대가로 캐릭터 댓글 행에는 의미 없는comment_status/comment_status_updated_at컬럼이 생깁니다. 스키마 취향 문제라 다른 의견 있으면 말씀해주세요.2. 과거 요약(
pastSummary)은 지금 항상 빈 값입니다 — 프론트에서 추후 내려줄 예정이라CommentGenerator인터페이스에 파라미터만 열어두고 실제 연동은 안 했습니다(CommentGenerationService에 TODO 남겨둠). 프론트 스펙 나오면 이어서 반영하겠습니다.3. 엉뚱이 소재 목록(
EongttungTopicSelector)은 자리채우기용 placeholder입니다 — 10개만 임시로 넣어뒀는데, 실제 서비스에 쓸 소재는 팀에서 다시 정하는 게 좋을 것 같습니다.4. 지수 백오프·서킷브레이커는 이번 스코프에서 뺐습니다 — DoD가 "실패 시 1회 재시도"라 그보다 큰 작업(라이브러리 도입 등)이라고 판단했습니다. 필요하면 다음 이슈로 분리하겠습니다.
5. "사용자가 캐릭터 댓글에 답글 달면 봇이 답하는" 기능은 이번 범위에 없습니다 — 트리거 시점도 다르고 상태 관리도 독립적이라 별도 이슈로 분리하는 게 맞다고 판단했습니다.
Summary by CodeRabbit
/messages/comments).