Conversation
WalkthroughSpring AI 의존성 및 Gemini(ChatClient) 통합을 추가합니다. AI 독후감 생성 포트·어댑터·설정과 관련 유스케이스·서비스·컨트롤러·DTO·영속성 확장이 도입되며, 사용자 엔티티에 recordReviewCount 필드와 DB 마이그레이션 및 통합 테스트가 추가됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant WC as RoomPostCommandController
participant S as RecordReviewCreateService
participant V as RoomParticipantValidator
participant RQ as RecordQueryPort
participant U as UserCommandPort
participant B as BookCommandPort
participant G as GeminiLoadPort
C->>WC: POST /rooms/{roomId}/record/ai-review
WC->>S: createAiRecordReview(roomId, userId)
S->>V: validate(userId, roomId)
S->>RQ: findAllByRoomIdAndUserId(roomId, userId)
S->>B: findByRoomId(roomId)
S->>U: findById(userId)
alt records < 2
S-->>WC: throw RECORD_REVIEW_NOT_ENOUGH_RECORDS
WC-->>C: 400 error
else
S->>U: increaseRecordReviewCount()
S->>G: generateRecordReview(user, records, book, min,max)
alt Gemini error
G-->>S: throw GEMINI_API_*_ERROR
S-->>WC: propagate 500
WC-->>C: 500 error
else success
S->>U: save(updated user)
S-->>WC: RecordReviewCreateResult(content, count)
WC-->>C: 200 BaseResponse
end
end
sequenceDiagram
autonumber
participant C as Client
participant WQ as RoomPostQueryController
participant S as RecordAiUsageService
participant V as RoomParticipantValidator
participant RQ as RecordQueryPort
participant U as UserCommandPort
C->>WQ: GET /rooms/{roomId}/users/ai-usage
WQ->>S: getUserAiUsage(userId, roomId)
S->>V: validate(userId, roomId)
S->>RQ: countAllByRoomIdAndUserId(roomId, userId)
S->>U: findById(userId)
S-->>WQ: RecordAiUsageResult(reviewCount, recordCount)
WQ-->>C: 200 BaseResponse
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/resources/db/migration/V251012__Add_record_review_count.sql (1)
4-6: 불필요한 UPDATE 문을 제거하세요.
NOT NULL DEFAULT 0제약조건과 함께 컬럼을 추가하면, MySQL이 자동으로 기존의 모든 행에 대해 값을 0으로 설정합니다. 따라서WHERE record_review_count IS NULL조건은 절대 매칭되지 않으며, 이 UPDATE 문은 불필요합니다.다음 diff를 적용하여 불필요한 코드를 제거하세요:
ALTER TABLE users ADD COLUMN record_review_count INT NOT NULL DEFAULT 0 AFTER follower_count; - -UPDATE users -SET record_review_count = 0 -WHERE record_review_count IS NULL;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (25)
build.gradle(1 hunks)src/main/java/konkuk/thip/common/ai/adapter/out/GeminiAdapter.java(1 hunks)src/main/java/konkuk/thip/common/ai/application/out/GeminiLoadPort.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(3 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/config/GeminiAiConfig.java(1 hunks)src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java(2 hunks)src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java(3 hunks)src/main/java/konkuk/thip/roompost/adapter/in/web/request/RecordAiUsageResponse.java(1 hunks)src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReviewCreateResponse.java(1 hunks)src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java(3 hunks)src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java(1 hunks)src/main/java/konkuk/thip/roompost/application/port/in/RecordAiUsageUseCase.java(1 hunks)src/main/java/konkuk/thip/roompost/application/port/in/RecordReviewCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordAiUsageResult.java(1 hunks)src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReviewCreateResult.java(1 hunks)src/main/java/konkuk/thip/roompost/application/port/out/RecordQueryPort.java(2 hunks)src/main/java/konkuk/thip/roompost/application/service/RecordAiUsageService.java(1 hunks)src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java(5 hunks)src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java(2 hunks)src/main/java/konkuk/thip/user/domain/User.java(3 hunks)src/main/resources/db/migration/V251012__Add_record_review_count.sql(1 hunks)src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiReviewCreateApiTest.java(1 hunks)src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiUsageApiTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiReviewCreateApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(35-403)
src/main/java/konkuk/thip/common/ai/adapter/out/GeminiAdapter.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
InternalServerException(5-17)
src/test/java/konkuk/thip/roompost/adapter/in/web/RecordAiUsageApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(35-403)
⏰ 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: build
🔇 Additional comments (19)
src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java (1)
18-18: LGTM!도메인 엔티티와 JPA 엔티티 간의
recordReviewCount필드 매핑이 양방향으로 정확하게 구현되었습니다.Also applies to: 31-31
src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordReviewCreateResult.java (1)
3-6: LGTM!AI 기반 독후감 생성 결과를 담는 간결하고 명확한 DTO입니다.
src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordAiUsageResult.java (1)
3-6: LGTM!AI 사용량 정보를 담는 간결하고 명확한 DTO입니다.
src/main/java/konkuk/thip/roompost/application/port/in/RecordAiUsageUseCase.java (1)
5-7: LGTM!AI 사용량 조회를 위한 명확한 유스케이스 인터페이스입니다.
src/main/java/konkuk/thip/config/GeminiAiConfig.java (1)
8-14: LGTM!Gemini AI ChatClient를 위한 명확하고 간결한 Spring 설정입니다.
src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordReviewCreateResponse.java (1)
5-11: LGTM!응용 계층의 결과를 웹 응답으로 변환하는 정확한 매핑이 구현되었습니다.
src/main/java/konkuk/thip/user/domain/User.java (1)
32-32: LGTM!
recordReviewCount필드가 적절하게 추가되었고, 새 사용자 생성 시 0으로 올바르게 초기화됩니다.Also applies to: 44-44
src/main/java/konkuk/thip/common/ai/application/out/GeminiLoadPort.java (1)
9-11: LGTM! 깔끔한 포트 인터페이스입니다.헥사고날 아키텍처 패턴을 잘 따르고 있으며, 메서드 시그니처가 명확합니다.
minLength와maxLength파라미터로 생성될 독후감의 길이를 제어할 수 있어 좋습니다.src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostCommandController.java (2)
36-36: 의존성 주입이 올바르게 추가되었습니다.새로운 AI 독후감 생성 기능을 위한 use case가 적절히 주입되었습니다.
185-199: 새로운 AI 독후감 생성 엔드포인트가 잘 구현되었습니다.
- RESTful한 경로 설계 (
POST /rooms/{roomId}/record/ai-review)- 보안 컨텍스트에서 사용자 ID 추출 (
@UserId)- Swagger 문서화 포함
- Use case로의 적절한 위임
관심사의 분리가 잘 이루어져 있습니다.
src/main/java/konkuk/thip/roompost/adapter/in/web/request/RecordAiUsageResponse.java (1)
5-14: LGTM! 간결하고 명확한 DTO입니다.Java record를 활용한 불변 DTO로 잘 설계되었습니다. 정적 팩토리 메서드
of()를 통한 변환 로직도 깔끔합니다.src/main/java/konkuk/thip/roompost/application/port/out/RecordQueryPort.java (1)
20-22: 새로운 쿼리 메서드가 적절히 추가되었습니다.AI 독후감 생성을 위해 필요한 레코드 조회 및 카운트 메서드가 포트에 잘 정의되었습니다. 메서드 이름이 명확하고 반환 타입도 적절합니다.
구현체에서
isOverview = false필터링이 올바르게 적용되었는지 확인하세요. AI 독후감 생성 시 개요 레코드는 제외되어야 합니다.src/main/java/konkuk/thip/roompost/application/port/in/RecordReviewCreateUseCase.java (1)
5-8: LGTM! 단일 책임 원칙을 잘 따르는 use case 인터페이스입니다.명확한 메서드 시그니처와 반환 타입으로 AI 독후감 생성의 의도를 잘 표현하고 있습니다.
src/main/java/konkuk/thip/roompost/application/service/RecordAiUsageService.java (1)
20-27: 서비스 로직이 잘 구현되었습니다.방 멤버십 검증 후 필요한 데이터를 조회하는 흐름이 올바릅니다.
countAllByRoomIdAndUserId메서드를 사용하여 효율적으로 카운트를 가져오고 있습니다.
recordCount와recordReviewCount가Integer타입이므로 null이 될 수 있습니다. 포트 구현체에서 null 대신 0을 반환하도록 보장되어 있는지 확인하세요.src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (3)
50-52: 새 필드가 올바르게 추가되었습니다.
recordReviewCount필드가 적절히 정의되었습니다:
@Builder.Default = 0으로 기본값 설정nullable = false제약조건 추가- 컬럼명 명시 (
record_review_count)
62-77: 엔티티 업데이트 메서드가 일관성 있게 수정되었습니다.
updateIncludeAliasFrom과updateFrom두 메서드 모두에서recordReviewCount필드를 동기화하고 있어 데이터 일관성이 보장됩니다.
87-90: 테스트 전용 setter가 적절히 제한되었습니다.
@VisibleForTesting어노테이션을 사용하여 테스트 목적임을 명확히 했습니다. 이는 프로덕션 코드에서 실수로 사용되는 것을 방지하는 좋은 패턴입니다.src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (2)
26-31: 레코드 조회 쿼리가 잘 작성되었습니다.AI 독후감 생성에 필요한 조건을 정확히 구현했습니다:
isOverview = false: 개요 레코드 제외ORDER BY r.page ASC: 페이지 순서대로 정렬하여 독후감 생성 시 맥락 유지- 명시적인 JOIN 조건으로 정확한 데이터 조회
33-37: 카운트 쿼리가 조회 쿼리와 일관성 있게 작성되었습니다.
findAllByRoomIdAndUserIdOrderByPageAsc와 동일한 필터 조건(isOverview = false)을 사용하여 카운트와 조회 결과가 일치하도록 보장합니다.
| // Spring AI - Google AI(Gemini) 연동 | ||
| implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6") | ||
| implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter' |
There was a problem hiding this comment.
잘못된 Spring AI 스타터 선택으로 컴파일/런타임 실패 가능
구글 Gemini 연동인데 spring-ai-openai-spring-boot-starter를 추가하면 OpenAI 전용 오토컨피그만 제공되어, GeminiAiConfig에서 필요한 org.springframework.ai.googleai.gemini 관련 빈을 못 찾아 컴파일이 깨지거나 런타임에 빈 생성이 실패합니다. Google Gemini용 스타터로 교체해야 합니다.
- implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
+ implementation 'org.springframework.ai:spring-ai-google-ai-gemini-spring-boot-starter'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Spring AI - Google AI(Gemini) 연동 | |
| implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6") | |
| implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter' | |
| // Spring AI - Google AI(Gemini) 연동 | |
| implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-M6") | |
| implementation 'org.springframework.ai:spring-ai-google-ai-gemini-spring-boot-starter' |
🤖 Prompt for AI Agents
build.gradle lines 102-104: 현재 Google Gemini 연동인데 잘못된 OpenAI 스타터가 추가되어 있어 빈 생성
실패/컴파일 오류가 발생할 수 있으니, implementation
'org.springframework.ai:spring-ai-openai-spring-boot-starter' 를 제거하고 Google
Gemini용 스타터로 교체하십시오; 예를 들어 implementation
'org.springframework.ai:spring-ai-googleai-spring-boot-starter' (또는 프로젝트/버전 정책에
맞는 공식 Google AI 스타터 아티팩트와 버전)를 사용해 Gemini 관련
빈(org.springframework.ai.googleai.gemini 등)이 올바르게 로드되도록 수정하세요.
There was a problem hiding this comment.
google ai로 하면 빈 등록이 안되어서 openai 의존성을 주입하고 내부 모델로 google ai의 gemini 모델을 사용한거야.
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Spring AI 1.0.0-M6 Google Gemini integration setup bean registration
💡 Result:
Short answer — in Spring AI 1.0.0-M6 you register Gemini (Vertex AI) clients and functions the same way you register other Spring AI beans: rely on the VertexAiGemini auto-configuration and expose beans for the ChatClient and any function callbacks.
Key steps (minimal example)
- Ensure Vertex/Google credentials are available (ADC / service account) and Spring AI Vertex auto-config is on.
- Expose a ChatClient bean using the provided ChatClient.Builder:
- Example:
- @configuration
public class ChatClientConfig {
@bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}
- @configuration
- Example:
- Register callable functions (for Gemini function-calling) as normal Spring beans:
- Option A — simple POJO/Function bean:
- @bean
public Function<MyRequest,MyResponse> currentWeather() { ... }
- @bean
- Option B — explicit FunctionCallback wrapper (required when you need OpenAPI schema / descriptions):
- @bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallback.builder()
.function("CurrentWeather", new MyWeatherService())
.description("Get the current weather")
.schemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT for Gemini function calling
.inputType(MyWeatherService.Request.class)
.build();
}
- @bean
- Option A — simple POJO/Function bean:
- If you want ChatClient preconfigured with registered tools, inject ToolCallbackProvider into your chatClient bean and call defaultTools(tools) before build() (M6 docs show this pattern).
- Note: M6’s “Function Calling” docs are marked deprecated in later releases; later milestones (M7+) changed tool/callback wiring (see migration notes) — if you upgrade you may need to enable tool callback support via properties or follow the newer Tool Calling APIs.
References
- VertexAi / Gemini function registration and bean examples (Function bean and FunctionCallback wrapper). [1]
- General M6 function-calling docs + examples for registering Function beans and ChatClient usage. [2]
- Community tutorial showing ChatClient bean + ToolCallbackProvider usage and notes about changes in later milestones. [3]
If you want, I can paste a ready-to-run small code example (pom/deps + config + sample function) tailored to M6.
🌐 Web query:
Spring AI 1.0.0-M6 spring-ai-google-ai-gemini-spring-boot-starter artifact maven
💡 Result:
Maven coordinates (Spring AI 1.0.0‑M6):
- GroupId: org.springframework.ai
- ArtifactId: spring-ai-vertex-ai-gemini-spring-boot-starter
- Version: 1.0.0-M6
Maven dependency:
org.springframework.ai
spring-ai-vertex-ai-gemini-spring-boot-starter
1.0.0-M6
Sources: Maven Central index and mvnrepository listing for the 1.0.0‑M6 starter. [1][2]
References
[1] Maven Central index for org.springframework.ai/spring-ai-vertex-ai-gemini-spring-boot-starter (1.0.0‑M6). (repo1.maven.org)
[2] mvnrepository entry for spring-ai-vertex-ai-gemini-spring-boot-starter 1.0.0‑M6. (mvnrepository.com)
Citations:
- 1: https://repo1.maven.org/maven2/org/springframework/ai/spring-ai-vertex-ai-gemini-spring-boot-starter/?utm_source=openai
- 2: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-vertex-ai-gemini-spring-boot-starter/1.0.0-M6?utm_source=openai
🌐 Web query:
Spring AI Google AI Gemini API spring-boot-starter artifact not Vertex AI 1.0.0-M6
💡 Result:
Short answer: Spring AI doesn't provide a standalone "Google AI Gemini" starter separate from Vertex AI in 1.0.0‑M6. The provided starters target Gemini via Vertex AI — use the Vertex/Gemini starter coordinates.
Maven (example):
- org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter:1.0.0-M6. [1][2]
Alternate/newer autoconfiguration name shown in docs:
- org.springframework.ai:spring-ai-starter-model-vertex-ai-gemini (documented as the model starter). [3]
Sources:
[1] mvnrepository listing for spring-ai-vertex-ai-gemini-spring-boot-starter 1.0.0‑M6.
[2] Spring AI 1.0.0‑M6 release notes.
[3] Spring AI reference (VertexAI Gemini Chat / starter naming).
🌐 Web query:
Spring AI ChatClient OpenAI starter with Google Gemini model configuration baseUrl
💡 Result:
You can use the Spring AI OpenAI starter but point its OpenAI base-url at Vertex AI’s OpenAI-compatible endpoint and set the Gemini model name. Also supply a Google Cloud access token (refreshable) as the OpenAI api-key. Example application.yml:
- application.yml
spring:
ai:
openai:
# use Vertex AI OpenAI-compatible endpoint (replace project_id + location)
base-url: https://aiplatform.googleapis.com/v1/projects/YOUR_PROJECT_ID/locations/YOUR_LOCATION/endpoints/openapi
# pass a Google Cloud access token here (refresh before it expires)
api-key: YOUR_GOOGLE_OAUTH_ACCESS_TOKEN
chat:
completions-path: /v1/chat/completions
options:
model: google/gemini-2.0-flash-001
temperature: 0.7
Notes:
- The Google Cloud access token is used in place of an OpenAI API key and must be refreshed (tokens typically expire ~1 hour). [1]
- The base-url above matches Google’s OpenAI-compatible endpoint example; choose the Gemini model name supported by your project/location. [1]
- Spring AI’s OpenAI starter supports overriding base-url and chat options via spring.ai.openai.* properties. [2]
References:
[1] Google Cloud — OpenAI compatibility / Vertex AI (examples showing base_url and use of Google credentials).
[2] Spring AI — OpenAI Chat configuration properties (spring.ai.openai.base-url, chat options).
🏁 Script executed:
#!/bin/bash
# Find Spring AI configuration in application properties/yml files
echo "=== Checking application configuration files ==="
fd -e yml -e yaml -e properties . src/main/resources src/test/resources 2>/dev/null | while read f; do
echo "--- File: $f ---"
cat "$f"
echo ""
done
echo ""
echo "=== Searching for ChatModel bean definitions ==="
rg -n "ChatModel|GoogleAi|VertexAi|GeminiChatModel" --type java -C 3
echo ""
echo "=== Checking GeminiAiConfig implementation ==="
cat src/main/java/konkuk/thip/config/GeminiAiConfig.java
Length of output: 1773
🏁 Script executed:
#!/bin/bash
# Search for application.yml or application.properties with AI configuration
echo "=== Looking for Spring AI properties configuration ==="
rg -n "spring\.ai\.|openai|gemini|google" --type yaml --type properties src/main src/test 2>/dev/null || echo "No properties found with rg"
echo ""
echo "=== Checking if application.yml exists in main resources ==="
if [ -f "src/main/resources/application.yml" ]; then
cat src/main/resources/application.yml
else
echo "application.yml not found"
fi
echo ""
echo "=== Checking if application.properties exists ==="
if [ -f "src/main/resources/application.properties" ]; then
cat src/main/resources/application.properties
else
echo "application.properties not found"
fi
echo ""
echo "=== Searching for any yml/properties files in resources ==="
find src/main/resources -name "*.yml" -o -name "*.properties" 2>/dev/null | head -10
Length of output: 315
🏁 Script executed:
#!/bin/bash
# Check if there are environment-specific configuration files or external config
echo "=== Searching for profile-specific or external configuration ==="
find . -name "application-*.yml" -o -name "application-*.properties" 2>/dev/null | head -10
echo ""
echo "=== Checking docker-compose or deployment configs for AI configuration ==="
fd -e yml -e yaml docker-compose . 2>/dev/null | xargs grep -l "OPENAI\|GEMINI\|GOOGLE_AI" 2>/dev/null || echo "No docker-compose AI config found"
echo ""
echo "=== Checking if Spring AI OpenAI autoconfiguration classes are available ==="
echo "Checking what ChatModel implementation OpenAI starter provides..."
rg -n "OpenAiChatModel|ChatModel" build.gradle
echo ""
echo "=== Verifying the actual Spring AI OpenAI starter artifact name ==="
grep -A2 -B2 "spring-ai-openai" build.gradle
Length of output: 579
🌐 Web query:
Spring AI 1.0.0-M6 OpenAI starter autoconfiguration ChatModel bean registration
💡 Result:
Short answer — in Spring AI 1.0.0‑M6 the OpenAI starter auto‑configures a ChatModel bean named by the factory method openAiChatModel. The registration details:
- Auto‑configuration class: org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration. [1]
- Bean factory method: public OpenAiChatModel openAiChatModel(...) — the method is annotated @bean and @ConditionalOnMissingBean, so it only creates a bean when no other ChatModel (or compatible bean) is defined. [1][2]
- Enable/disable toggle: controlled by property spring.ai.openai.chat.enabled (havingValue="true", matchIfMissing=true) — enabled by default. [1]
- Implementation type: OpenAiChatModel implements ChatModel/StreamingChatModel. [2]
If you want to override it, define your own ChatModel/OpenAiChatModel bean or set spring.ai.openai.chat.enabled=false. References: Spring AI 1.0.0‑M6 API docs for OpenAiAutoConfiguration and OpenAiChatModel. [1][2]
Sources:
[1] Spring AI 1.0.0‑M6 — OpenAiAutoConfiguration (api docs).
[2] Spring AI 1.0.0‑M6 — OpenAiChatModel (api docs).
@buzz0331, 설명 감사합니다! 이해했습니다.
OpenAI 스타터를 사용하면서 내부적으로 Google Gemini 모델을 사용하시는 방식이군요. 다만, 현재 코드베이스에서는 Spring AI 관련 설정 파일(application.yml/application.properties)이 보이지 않는데, 외부에서 환경 변수나 설정을 통해 다음과 같이 구성하고 계신 건가요?
spring.ai.openai.base-url: Google의 OpenAI 호환 엔드포인트로 설정spring.ai.openai.api-key: Google Cloud 액세스 토큰spring.ai.openai.chat.options.model:google/gemini-2.0-flash-001등 Gemini 모델명
이 설정들이 환경 변수나 Kubernetes Secrets 등으로 주입되고 있다면 현재 방식이 동작할 것입니다.
만약 이러한 외부 설정이 아직 구성되지 않은 상태라면, OpenAiChatModel이 기본적으로 OpenAI의 공식 API(api.openai.com)로 요청을 보내게 되어 Gemini 연동이 실제로는 작동하지 않을 수 있습니다. 현재 설정 상태를 확인해 주실 수 있을까요?
✏️ Learnings added
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#323
File: build.gradle:102-104
Timestamp: 2025-10-13T08:39:43.808Z
Learning: Spring AI 1.0.0-M6에서 Google AI Gemini 전용 스타터가 빈 등록에 실패하는 경우, spring-ai-openai-spring-boot-starter를 사용하고 외부 설정(환경 변수 등)으로 spring.ai.openai.base-url을 Google의 OpenAI 호환 엔드포인트로, spring.ai.openai.api-key를 Google Cloud 액세스 토큰으로, spring.ai.openai.chat.options.model을 Gemini 모델명으로 지정하여 우회할 수 있습니다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
ㅇㅇ 너말대로 yml 파일을 secret으로 관리하고 있어.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/main/java/konkuk/thip/user/domain/User.java(3 hunks)
⏰ 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: build
🔇 Additional comments (2)
src/main/java/konkuk/thip/user/domain/User.java (2)
44-44: LGTM!새 사용자의
recordReviewCount초기값을 0으로 설정한 것이 적절합니다.
32-32: 마이그레이션 스크립트가 null 방지를 보장합니다
V251012__Add_record_review_count.sql에서INT NOT NULL DEFAULT 0과 기존 레코드 백필(UPDATE … SET … WHERE record_review_count IS NULL)이 적용되어 NPE 우려가 없습니다.
| this.oauth2Id = "deleted:" + this.oauth2Id; | ||
| } | ||
|
|
||
| public void increaseRecordReviewCount() { |
| 입력 기록(페이지 오름차순, 요약/정제됨): | ||
| {records} | ||
|
|
||
| 작성 지시: |
| User user = userCommandPort.findById(userId); | ||
| Book book = bookCommandPort.findBookByRoomId(roomId); | ||
|
|
||
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하) |
There was a problem hiding this comment.
아직 기록의 최대값은 기획적으로 미정인거죠?? 만약 한책에대해 기록이 10개이상에 각 기록의 글도 몇 천자이상일때 ai가 어떻게 잘 요약할지 궁금하네여..
There was a problem hiding this comment.
@hd0rable 엇 현재 하나의 기록의 길이 최댓값은 제한되어 있긴한데(= 최대 500자), 희진님이 말하는 기록의 최댓값이라는게 모임방에서 작성할 수 있는 기록의 개수의 최댓값을 말하시는 걸까요?
There was a problem hiding this comment.
한 모임방 내에서 기록의 갯수 제한이 딱히 없기 때문에 추후에 만약에 기록이 너무 넘쳐서 gemini 입력값을 초과하는 일이 발생한다면, 그때부터는 다음과 같은 다른 옵션을 좀 고려해봐야 할 것 같습니다.
- 페이지 순으로 정렬후 절반으로 끊어서 2번에 걸쳐서 gemini 호출
- 사용자가 원하는 기록을 선택하게끔 한 후 그 기록들에 한해서만 gemini 호출
| recordJpaRepository.save(buildRecord(true, 9, "총평")); | ||
|
|
||
| // LLM 호출 모킹 | ||
| final String AI_CONTENT = "AI가 생성한 독후감 본문"; |
seongjunnoh
left a comment
There was a problem hiding this comment.
고생하셨습니다! ai 독서감상문 service 코드에서 코드 흐름과 관련된 간단한 리뷰 남겼는데, 확인부탁드립니다!
| User user = userCommandPort.findById(userId); | ||
| Book book = bookCommandPort.findBookByRoomId(roomId); | ||
|
|
||
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하) |
There was a problem hiding this comment.
@hd0rable 엇 현재 하나의 기록의 길이 최댓값은 제한되어 있긴한데(= 최대 500자), 희진님이 말하는 기록의 최댓값이라는게 모임방에서 작성할 수 있는 기록의 개수의 최댓값을 말하시는 걸까요?
| ALTER TABLE users | ||
| ADD COLUMN record_review_count INT NOT NULL DEFAULT 0 AFTER follower_count; | ||
|
|
||
| UPDATE users | ||
| SET record_review_count = 0 | ||
| WHERE record_review_count IS NULL; No newline at end of file |
| user.increaseRecordReviewCount(); | ||
| if(records.size() < 2) { | ||
| throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS, | ||
| new IllegalArgumentException("현재 기록 개수: " + records.size())); | ||
| } |
There was a problem hiding this comment.
p3 : 별건 아니긴한데, 처음에 records 를 조회한 이후, 위 business exception 검증을 수행하는건 어떤가요?? 코드 가독성 측면에서 더 좋을 것 같습니다!!
There was a problem hiding this comment.
현재 서비스 로직에서 record를 조회한 이후 List를 반환받은 다음 business exception 검증을 수행하고 있는데 성준님이 말씀하시는 건 어떤 부분이죠?
There was a problem hiding this comment.
business exception 검증이 아니라 도메인 규칙 검증 말씀하시는건가요??
There was a problem hiding this comment.
// 1. 필요한 엔티티 조회
List<Record> records = recordQueryPort.findAllByRoomIdAndUserId(roomId, userId);
// 조회한 데이터에 대한 검증을 바로 수행
if(records.size() < 2) {
throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS,
new IllegalArgumentException("현재 기록 개수: " + records.size()));
}
// 이후 비즈니스 로직 수행
User user = userCommandPort.findById(userId);
Book book = bookCommandPort.findBookByRoomId(roomId);
// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)
user.increaseRecordReviewCount();
// 3. 독후감 생성
String reviewContent = geminiQueryPort.generateRecordReview(user, records, book, MIN_REVIEW_LENGTH, MAX_REVIEW_LENGTH);
// 4. 독후감 생성 횟수 갱신
userCommandPort.update(user);
return new RecordReviewCreateResult(reviewContent, user.getRecordReviewCount());위와 같은 흐름을 말씀드린 거였습니다!
There was a problem hiding this comment.
음 현재 user.increaseRecordReviewCount() 내부에서 유효성 검증을 수행하고 있어서 제 생각은 오히려 주석에 적힌 흐름처럼 필요한 엔티티를 한번에 조회한 뒤, 이후 단계에서 유효성 검증 로직을 함께 수행하는 방식이 가독성 측면에서 더 명확하다는 생각이 들긴 했습니다. 다만, 기록 갯수 검증 전에 recordReviewCount 증가 함수(increaseRecordReviewCount)가 호출되다 보니 가독성이 떨어질수도 있겠다는 생각이 들어서, 두 코드의 순서만 바꾸는 식으로 코드를 수정해보려고 하는데 어떠신가요?
There was a problem hiding this comment.
음 현재
user.increaseRecordReviewCount()내부에서 유효성 검증을 수행하고 있어서 제 생각은 오히려 주석에 적힌 흐름처럼 필요한 엔티티를 한번에 조회한 뒤, 이후 단계에서 유효성 검증 로직을 함께 수행하는 방식이 가독성 측면에서 더 명확하다는 생각이 들긴 했습니다. 다만, 기록 갯수 검증 전에 recordReviewCount 증가 함수(increaseRecordReviewCount)가 호출되다 보니 가독성이 떨어질수도 있겠다는 생각이 들어서, 두 코드의 순서만 바꾸는 식으로 코드를 수정해보려고 하는데 어떠신가요?
좋습니다!!
| @Query("SELECT r FROM RecordJpaEntity r " + | ||
| "WHERE r.roomJpaEntity.roomId = :roomId " + | ||
| "AND r.userJpaEntity.userId = :userId " + | ||
| "AND r.isOverview = false " + | ||
| "ORDER BY r.page ASC") | ||
| List<RecordJpaEntity> findAllByRoomIdAndUserIdOrderByPageAsc(Long roomId, Long userId); | ||
|
|
||
| @Query("SELECT COUNT(r) FROM RecordJpaEntity r " + | ||
| "WHERE r.roomJpaEntity.roomId = :roomId " + | ||
| "AND r.userJpaEntity.userId = :userId " + | ||
| "AND r.isOverview = false") | ||
| Integer countAllByRoomIdAndUserId(Long roomId, Long userId); |
| recordJpaRepository.save(buildRecord(true, 9, "총평")); | ||
|
|
||
| // LLM 호출 모킹 | ||
| final String AI_CONTENT = "AI가 생성한 독후감 본문"; |
| @Test | ||
| @DisplayName("총평 기록(isOverview=true)은 기록 개수 집계에서 제외된다.") |
|
|
||
| UserJpaEntity persisted = userJpaRepository.findById(user.getUserId()).orElseThrow(); | ||
| assertThat(returnedCount).isEqualTo(persisted.getRecordReviewCount()); | ||
| assertThat(persisted.getRecordReviewCount()).isGreaterThanOrEqualTo(1); |
There was a problem hiding this comment.
p3 : 단순 궁금증인데, isEqualTo(1) 이 아니라 isGreaterThanOrEqualTo(1) 로 작성하신 이유가 있을까요??
There was a problem hiding this comment.
엇 그러게요. 잘못 작성한거 같네요. 수정하겠습니다~
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1)
30-31: 독후감 길이 제약을 설정으로 외부화하는 것을 고려해보세요.현재 독후감의 최소/최대 길이가 하드코딩되어 있습니다. 향후 요구사항 변경 시(예: 도서 유형별 다른 길이, 사용자 선호도 반영) 코드 수정이 필요합니다.
application.yml에서 설정하거나@Value어노테이션을 통해 주입하는 방식으로 유연성을 높일 수 있습니다.예시:
@Value("${ai.review.min-length:600}") private int minReviewLength; @Value("${ai.review.max-length:900}") private int maxReviewLength;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#323
File: build.gradle:102-104
Timestamp: 2025-10-13T08:39:43.833Z
Learning: Spring AI 1.0.0-M6에서 Google AI Gemini 전용 스타터가 빈 등록에 실패하는 경우, spring-ai-openai-spring-boot-starter를 사용하고 외부 설정(환경 변수 등)으로 spring.ai.openai.base-url을 Google의 OpenAI 호환 엔드포인트로, spring.ai.openai.api-key를 Google Cloud 액세스 토큰으로, spring.ai.openai.chat.options.model을 Gemini 모델명으로 지정하여 우회할 수 있습니다.
⏰ 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: build
🔇 Additional comments (1)
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java (1)
38-38: 정렬 확인 완료
RecordQueryPersistenceAdapter의findAllByRoomIdAndUserId가 내부적으로findAllByRoomIdAndUserIdOrderByPageAsc를 호출해page오름차순 정렬을 보장합니다.
| @Override | ||
| public RecordReviewCreateResult createAiRecordReview(Long roomId, Long userId) { | ||
| roomParticipantValidator.validateUserIsRoomMember(roomId, userId); | ||
|
|
||
| // 1. 필요한 엔티티 조회 | ||
| List<Record> records = recordQueryPort.findAllByRoomIdAndUserId(roomId, userId); | ||
| User user = userCommandPort.findById(userId); | ||
| Book book = bookCommandPort.findBookByRoomId(roomId); | ||
|
|
||
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상 | ||
| if(records.size() < 2) { | ||
| throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS, | ||
| new IllegalArgumentException("현재 기록 개수: " + records.size())); | ||
| } | ||
|
|
||
| // 3. 독후감 생성 횟수 증가 (독후감 생성 횟수는 5회 이하일 경우만) | ||
| user.increaseRecordReviewCount(); | ||
|
|
||
| // 4. 독후감 생성 | ||
| String reviewContent = geminiQueryPort.generateRecordReview(user, records, book, MIN_REVIEW_LENGTH, MAX_REVIEW_LENGTH); | ||
|
|
||
| // 5. 독후감 생성 횟수 갱신 | ||
| userCommandPort.update(user); | ||
|
|
||
| return new RecordReviewCreateResult(reviewContent, user.getRecordReviewCount()); | ||
| } |
There was a problem hiding this comment.
트랜잭션 경계와 동시성 제어가 누락되었습니다.
현재 메서드에 @Transactional 어노테이션이 없어서 다음과 같은 문제가 발생할 수 있습니다:
-
데이터 일관성 문제:
- Line 49에서
user.increaseRecordReviewCount()로 메모리에서 카운트를 증가시키고 - Line 52에서 Gemini API를 호출한 후
- Line 55에서
userCommandPort.update(user)로 영속화합니다 - 만약 update 호출이 실패하면 카운트 증가가 누락되어 상태가 불일치합니다
- Line 49에서
-
동시성 race condition:
사용자 A의 현재 카운트 = 4 Request 1: findById(A) → count=4 → increment → count=5 Request 2: findById(A) → count=4 → increment → count=5 Request 1: update(count=5) Request 2: update(count=5) 결과: 두 요청 모두 성공하여 총 6회 생성 가능 (제한 위반)
해결 방법:
@Override
+@Transactional
public RecordReviewCreateResult createAiRecordReview(Long roomId, Long userId) {추가로 User 엔티티에 낙관적 락(@Version)을 추가하거나, 비관적 락(userCommandPort.findByIdWithLock)을 사용하여 동시성 문제를 해결해야 합니다.
-User user = userCommandPort.findById(userId);
+User user = userCommandPort.findByIdWithLock(userId); // 비관적 락 사용Committable suggestion skipped: line range outside the PR's diff.
| User user = userCommandPort.findById(userId); | ||
| Book book = bookCommandPort.findBookByRoomId(roomId); | ||
|
|
||
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상 |
There was a problem hiding this comment.
주석을 완성해주세요.
Line 42의 주석 "기록 개수는 2개 이상"이 불완전합니다. 실제로는 2개 이상 검증과 독후감 생성 횟수 5회 이하 검증이 모두 수행됩니다.
다음과 같이 수정하세요:
-// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상
+// 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상 | |
| // 2. 유저가 독후감 생성이 가능한 상태인지 유효성 검증 (기록 개수는 2개 이상, 독후감 생성 횟수는 5회 이하) |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/service/RecordReviewCreateService.java
around line 42, the inline comment "기록 개수는 2개 이상" is incomplete; update it to
clearly state both validations performed: check that the user has at least 2
records and that the user’s review creation count is 5 or fewer before allowing
review creation. Replace the incomplete comment with a concise description
covering both checks (record count >= 2 and review creation count <= 5).
| throw new BusinessException(ErrorCode.RECORD_REVIEW_NOT_ENOUGH_RECORDS, | ||
| new IllegalArgumentException("현재 기록 개수: " + records.size())); | ||
| } | ||
| user.increaseRecordReviewCount(); |
#️⃣ 연관된 이슈
📝 작업 내용
spring ai(google ai - gemini 2.0 flash 모델)를 활용해서 독후감 생성 기능을 구현하였습니다.
독후감 생성 기능 서비스 흐름
자세한 코드 내용은 통합 테스트 코드 확인해주세요~ (Google API 의존하지 않기 위해 GeminiLoadPort를 스텁해서 사용하였습니다.)
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit