-
Notifications
You must be signed in to change notification settings - Fork 1
[SRLT-124] AI 리포트 고도화 및 의존성 정리 #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
The head ref may contain hidden characters: "SRLT-124-ai-\uB9AC\uD3EC\uD2B8-\uACE0\uB3C4\uD654-\uBC0F-\uC758\uC874\uC131-\uC815\uB9AC"
Conversation
|
Note
|
| Cohort / File(s) | 변경 요약 |
|---|---|
AI Report 채점 아키텍처 제거 (레거시) src/main/java/starlight/adapter/ai/OpenAiReportGrader.java, src/main/java/starlight/adapter/ai/infra/PromptProvider.java, src/main/java/starlight/adapter/ai/util/ChecklistCatalog.java |
기존 OpenAiReportGrader, PromptProvider 클래스 및 ChecklistCatalog 완전 삭제. 보고서/체크리스트 프롬프트 생성 로직 레거시 구현체 제거 |
AI Report 채점 아키텍처 신규 구현 (Port 기반) src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java, src/main/java/starlight/adapter/aireport/report/agent/ |
ReportGraderPort 구현. 섹션별 에이전트(SectionGradeAgent)와 전체 보고서 에이전트(FullReportGradeAgent)를 통한 다중 경로 채점. 회로 차단기 기반 오류 처리 및 2분 타임아웃 |
Checklist 채점 마이그레이션 src/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.java, src/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.java, src/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.java |
OpenAiChecklistGrader → SpringAiChecklistGrader, ChecklistCatalog → ChecklistPromptProvider로 마이그레이션. 의존성 재구성 |
Port 인터페이스 도입 (Application Layer) src/main/java/starlight/application/aireport/required/, src/main/java/starlight/application/businessplan/required/ |
AiReportCommandPort, AiReportQueryPort, BusinessPlanCommandPort, BusinessPlanQueryPort, ReportGraderPort, ChecklistGraderPort, SpellCheckerPort, OcrProviderPort, PresignedUrlProviderPort 신규 도입 |
비즈니스 플랜 아키텍처 리팩토링 src/main/java/starlight/application/businessplan/BusinessPlanService.java, src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanJpa.java |
BusinessPlanServiceImpl → BusinessPlanService (UseCase 구현). 저장/삭제 로직을 BusinessPlanCommandPort로 분리. 대량 쿼리 결과 매핑 및 의존성 포트 재구성 |
Use Case 인터페이스 이름 변경 (Provided Layer) src/main/java/starlight/application/*/provided/ |
AiReportService → AiReportUseCase, BusinessPlanService → BusinessPlanUseCase, ExpertReportServiceUseCase → ExpertReportUseCase, MemberQueryUseCase → MemberUseCase, CredentialService → CredentialUseCase 이름 일관화 |
DTO 이름 변경 (응답 타입) src/main/java/starlight/application/*/provided/dto/ |
AiReportResponse → AiReportResult, BusinessPlanResponse → BusinessPlanResult, SubSectionResponse → SubSectionResult. 내부 필드/메서드 참조 모두 업데이트 |
응답 파서 이동 및 확장 src/main/java/starlight/application/aireport/util/AiReportResponseParser.java |
starlight.adapter.ai.util → starlight.application.aireport.util로 패키지 이동. AiReportResponse → AiReportResult 타입 변경. parseSectionResponse, parseStrengthWeakness 메서드 신규 추가. JSON 복구 로직 강화 |
컨트롤러 업데이트 (웹 API Layer) src/main/java/starlight/adapter/*/webapi/ |
각 컨트롤러가 서비스 대신 UseCase 포트에 의존. 반환 타입 DTO 이름 변경 (Response → Result). ImageController, AiReportController, BusinessPlanController, SpellController, MemberController, ExpertReportController 모두 업데이트 |
Persistence 계층 Port 구현 src/main/java/starlight/adapter/aireport/persistence/AiReportJpa.java, src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanJpa.java, src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java |
기존 인터페이스에서 새로운 Port 인터페이스로 변경. AiReportJpa는 AiReportCommandPort, AiReportQueryPort 추가 구현 |
설정 및 빈 구성 src/main/java/starlight/adapter/aireport/report/config/SectionAdvisorConfig.java, src/main/java/starlight/bootstrap/AsyncConfig.java |
SectionAdvisorConfig 신규 추가 (섹션별 에이전트 리스트 빈 생성). AsyncConfig에 sectionGradingExecutor 스레드풀 빈 추가 (5 core, 20 max, 100 queue) |
비즈니스 콘텐츠 추출기 확장 src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java |
extractSectionContents 메서드 신규 추가. Map<SectionType, String>로 섹션별 콘텐츠 반환 |
Enum 확장 src/main/java/starlight/shared/enumerate/SectionType.java |
tag, sectionTypeString, scoreExtractor 필드 추가. OVERVIEW는 null, 나머지는 section-specific 스코어 추출 함수 지정 |
테스트 코드 업데이트 (삭제/신규) src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java, src/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.java |
레거시 테스트 완전 삭제 (100줄, 129줄) |
테스트 코드 업데이트 (마이그레이션/이름변경) src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java, src/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.java |
신규 테스트 추가 및 기존 테스트 클래스/메서드명 변경. Port 기반 모킹 |
통합 테스트 리팩토링 src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java, src/test/java/starlight/application/businessplan/BusinessPlanServiceIntegrationTest.java, src/test/java/starlight/application/member/ |
AiReportServiceImpl → AiReportService, BusinessPlanServiceImpl → BusinessPlanService로 클래스명 변경. Port 기반 TestBeans 재구성. 의존성 주입 패턴 변경 |
단위 테스트 리팩토링 src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java, src/test/java/starlight/application/businessplan/BusinessPlanServiceUnitTest.java |
모킹 대상을 Port 인터페이스로 변경. gradeWithSectionAgents 메서드 호출로 변경. sectionContents 추출 로직 추가 |
🔄 시퀀스 다이어그램
1️⃣ 섹션별 에이전트를 통한 보고서 채점 흐름
sequenceDiagram
participant Client
participant SpringAiReportGrader
participant ContentExtractor
participant SectionGradeAgent as SectionGradeAgent<br/>(×4 병렬)
participant Executor
participant SpringAiReportSupervisor
participant ResponseParser
Client->>SpringAiReportGrader: gradeWithSectionAgents(sectionContents, fullContent)
SpringAiReportGrader->>ContentExtractor: extractSectionContents(plan)
par 병렬 섹션 평가 (타임아웃 2분)
SpringAiReportGrader->>Executor: submit(PROBLEM_RECOGNITION)
Executor->>SectionGradeAgent: gradeSection(problemContent)
SectionGradeAgent-->>Executor: SectionGradingResult
SpringAiReportGrader->>Executor: submit(FEASIBILITY)
Executor->>SectionGradeAgent: gradeSection(feasibilityContent)
SectionGradeAgent-->>Executor: SectionGradingResult
SpringAiReportGrader->>Executor: submit(GROWTH_STRATEGY)
Executor->>SectionGradeAgent: gradeSection(growthContent)
SectionGradeAgent-->>Executor: SectionGradingResult
SpringAiReportGrader->>Executor: submit(TEAM_COMPETENCE)
Executor->>SectionGradeAgent: gradeSection(teamContent)
SectionGradeAgent-->>Executor: SectionGradingResult
end
SpringAiReportGrader->>SpringAiReportSupervisor: generateStrengths(fullContent, results)
SpringAiReportSupervisor-->>SpringAiReportGrader: strengths[]
SpringAiReportGrader->>SpringAiReportSupervisor: generateWeaknesses(fullContent, results)
SpringAiReportSupervisor-->>SpringAiReportGrader: weaknesses[]
SpringAiReportGrader->>ResponseParser: assembleReportResponse(results, strengths, weaknesses)
ResponseParser-->>SpringAiReportGrader: AiReportResult
SpringAiReportGrader-->>Client: AiReportResult
2️⃣ 전체 보고서 단일 프롬프트 채점 흐름
sequenceDiagram
participant Client
participant SpringAiReportGrader
participant FullReportGradeAgent
participant ResponseParser
Client->>SpringAiReportGrader: gradeWithFullPrompt(content)
SpringAiReportGrader->>FullReportGradeAgent: gradeFullReport(content)
FullReportGradeAgent->>FullReportGradeAgent: validate(content)
FullReportGradeAgent->>FullReportGradeAgent: createPrompt(content)
FullReportGradeAgent->>FullReportGradeAgent: chatClient.call(prompt)
FullReportGradeAgent->>ResponseParser: parse(llmResponse)
ResponseParser-->>FullReportGradeAgent: AiReportResult
FullReportGradeAgent-->>SpringAiReportGrader: AiReportResult
SpringAiReportGrader-->>Client: AiReportResult
🎯 코드 리뷰 난이도
🎯 4 (Complex) | ⏱️ ~75분
분석 근거:
- 변경 파일 수: 90개 이상 (어댑터, 애플리케이션, 도메인, 테스트 계층)
- 이질성 수준: 높음 — Port 인터페이스 도입, DTO 이름 일괄 변경, 새로운 다중 에이전트 패턴, 회로 차단기, 비동기 처리, 대량 테스트 업데이트
- 로직 밀도: 중상 — 섹션별 병렬 처리, 타임아웃 관리, 회로 차단기 상태 전이, 슈퍼바이저 패턴
- 구조적 변경: 상당함 — 레이어 간 의존성 역전 (Port 기반), 기존 구현체 제거, 새로운 빈 구성
- 테스트 변경: 광범위 — 단위/통합 테스트 클래스명 변경, 모킹 대상 변경, 테스트 데이터 구조 변경
📌 관련 PR
- [SRLT-11] 사업 계획서 AI 리포트 생성 및 조회 개발 #31: AI 채점 컴포넌트 리팩토링 (OpenAiReportGrader, PromptProvider 등 레거시 제거의 선행 작업)
- [SRLT-113] AiReport, Expert, ExpertReport/Application, Wallet 도메인의 패키지 구조를 정리한다 #68: Aireport 어댑터의 Port 리팩토링 (ClovaOcrProvider, PresignedUrlProvider 등 Port 명명 통일)
- [SLRT-71] 사업계획서 제목 조회 및 DTO 관련 리팩토링 #47: 비즈니스플랜 DTO 및 컨트롤러 리팩토링 (BusinessPlanResponse → BusinessPlanResult 변경)
🏷️ 제안 라벨
🧵 REFACTOR, ✨ FEAT
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 18.32% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | PR 제목은 'SRLT-124 AI 리포트 고도화 및 의존성 정리'로, 변경사항의 핵심을 명확하게 요약하고 있습니다. 섹션별 Agent 패턴, Supervisor, Circuit Breaker 도입 및 헥사고날 아키텍처를 통한 의존성 정리라는 주요 변경사항을 효과적으로 반영하고 있습니다. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
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 @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Fix all issues with AI Agents 🤖
In
@src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java:
- Around line 76-88: The recordFailure() method has a concurrency bug: when
state is HALF_OPEN it may reuse failureCount accumulated from CLOSED and
immediately trip to OPEN; update the logic so that when transitioning into
HALF_OPEN you reset failureCount (and optionally lastFailureTime) to zero/now,
or explicitly treat the first failure in HALF_OPEN as a single failure by
resetting failureCount before incrementing in recordFailure(); locate
recordFailure(), the state transition that sets State.HALF_OPEN, and the
AtomicInteger failureCount/lastFailureTime fields and ensure failureCount is
cleared at the moment of entering HALF_OPEN (or handle HALF_OPEN in
recordFailure() to reset before comparing to FAILURE_THRESHOLD) to prevent stale
counts from causing an immediate OPEN transition.
- Around line 99-110: recordSuccess and recordFailure currently ignore calls
when circuitStates.get(sectionType) is null, risking missed state updates; make
them mirror allowRequest by using circuitStates.computeIfAbsent(sectionType,
...) to create or retrieve the CircuitState before invoking
recordSuccess()/recordFailure(), ensuring the same CircuitState creation logic
and thread-safe behavior as allowRequest (use the same constructor/factory used
elsewhere for CircuitState).
In @src/main/java/starlight/application/aireport/AiReportService.java:
- Line 10: AiReportService in the application layer is importing the adapter
class AiReportResponseParser, violating hexagonal architecture; create a port
interface AiReportParserPort in package application.aireport.required, declare
the needed methods (convertToJsonNode, toResponse, parse), have the adapter
class AiReportResponseParser implement AiReportParserPort, and update
AiReportService to depend only on AiReportParserPort (inject via constructor or
setter) so the application layer no longer imports the adapter.
In
@src/main/java/starlight/application/businessplan/required/ChecklistGraderPort.java:
- Around line 7-10: Add JavaDoc to the ChecklistGraderPort interface and its
check method to restore the removed contract: document the purpose/behavior of
ChecklistGraderPort and the check(SubSectionType subSectionType, String content)
method, describe the meaning and constraints of the subSectionType and content
parameters (e.g., allowed values, non-null expectations, trimming rules),
specify the structure and nullability of the returned List<Boolean> (e.g.,
number of elements, element meanings, whether null or empty lists are
permitted), and list any exceptions the method may throw or error conditions
callers should handle.
In
@src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java:
- Around line 129-142: The extractSectionContentsFromText method currently
assigns the entire document to every section, breaking per-section grading and
wasting API tokens; change the routing so PDF inputs do not call
extractSectionContentsFromText but instead are handled by FullReportGradeAgent
(i.e., adjust callers to detect PDF and route to FullReportGradeAgent or stop
invoking extractSectionContentsFromText for PDFs), and keep
extractSectionContents(BusinessPlan) for non-PDF flows or alternatively
implement real parsing by section headers if you want true section splitting;
update callers that currently call
BusinessPlanContentExtractor.extractSectionContentsFromText to perform the PDF
check and route accordingly.
In @src/main/java/starlight/shared/enumerate/SectionType.java:
- Around line 27-32: In SectionType.extractScore(AiReportResult response) you
currently call scoreExtractor.apply(response) without validating the response,
which can cause an NPE when response is null; update extractScore to check for
null response (e.g., if response == null) and return a safe default (such as 0)
before calling scoreExtractor, or adjust the logic to handle nulls inside the
scoreExtractor invocation (using Optional or a null-safe wrapper) so
scoreExtractor is only invoked when both scoreExtractor and response are
non-null.
In @src/test/java/starlight/adapter/ai/AiChecklistGraderTest.java:
- Line 21: The tests currently mock SpringAiFullReportGradeAgent (e.g., variable
generator in AiChecklistGraderTest) but that class lacks generateChecklistArray
and cannot be assigned where a SpringAiChecklistAgent is expected; replace all
uses/mocks of SpringAiFullReportGradeAgent with the correct agent type
SpringAiChecklistAgent (update imports and mock(...) calls for lines referenced
and any other occurrences at 31, 48, 58) so the mocked object exposes
generateChecklistArray and matches the expected parameter/type in
AiChecklistGraderTest.
In @src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java:
- Around line 45-46: The test is mocking a non-existent method generateReport on
SpringAiFullReportGradeAgent; update the mock to use the actual method
gradeFullReport(String) so the stub matches the implementation (replace
when(generator.generateReport(content)).thenReturn(llmResponse) with
when(generator.gradeFullReport(content)).thenReturn(llmResponse)), and while
here verify the test uses SpringAiFullReportGradeAgent and FullReportGradeAgent
types consistently and adjust any references to response/variable names if
necessary.
In
@src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java:
- Around line 165-202: The test references a missing port interface LlmGenerator
which causes compilation failure; add a new interface named LlmGenerator under
package starlight.application.aireport.required (matching the project's
hexagonal port naming) that declares the methods used by the test:
generateChecklistArray(...) returning java.util.List<Boolean> and
generateReport(String) returning String, with appropriate parameter types
(starlight.domain.businessplan.enumerate.SubSectionType, List<String> criteria,
List<String> detailedCriteria) so the anonymous test bean can compile against
it. Ensure the interface is public and placed in the package expected by the
import/usage in AiReportServiceImplIntegrationTest.
In
@src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java:
- Line 84: The test fails to compile because AiReportService's constructor now
requires BusinessPlanCommandPort and AiReportCommandPort; add mocks for these
ports (e.g., businessPlanCommand and aiReportCommand using
mock(BusinessPlanCommandPort.class) and mock(AiReportCommandPort.class)) in
AiReportServiceImplUnitTest and update the SUT instantiation to pass these two
mocks into the AiReportService(...) constructor along with the existing mocks
(businessPlanQuery, businessPlanService, aiReportQuery, aiReportGrader,
objectMapper, ocrProvider, responseParser, contentExtractor).
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/main/java/starlight/application/aireport/provided/dto/AiReportResult.java (2)
10-21: AiReportResult 리네임 마이그레이션 상태 검증 완료
AiReportResponse→AiReportResult리네임이 코드 수준에서는 완전히 적용되었습니다. SpringAiReportGrader, AiReportService 등 핵심 계층에서 올바르게AiReportResult를 사용 중입니다.다만 일관성 차원에서 다음 항목들의 문서/주석 업데이트가 필요합니다:
- SpringAiReportGrader.java:197의 주석에서 "
AiReportResponse.fromGradingResult"를 "AiReportResult.fromGradingResult"로 변경- 테스트 클래스의
@DisplayName문자열들에서 "AiReportResponse"를 "AiReportResult"로 갱신 (AiReportServiceImplUnitTest, AiReportServiceImplIntegrationTest, OpenAiReportGraderTest)- AiReportResponseParser.java의 JavaDoc 주석에서 클래스명 참조 업데이트 (LLM 응답을 파싱하여 AiReportResult로 변환... 등)
기능적으로는 문제없지만, 헥사고날 아키텍처와 도메인 일관성을 위해 문서화 정비를 권장합니다.
60-65: 섹션 점수 null 처리의 비즈니스 의미 재검토 필요PR 목표인 "섹션별 응답 파싱 개선(0과 null 구분)"을 달성하기 위해서는 현재 구현을 재고찰해야 합니다.
현재 플로우를 보면:
SectionGradingResult.failure(): 실패 시score=0으로 명시적 설정extractScore()(라인 208-213): 찾지 못한 섹션도orElse(0)처리- 결과:
sumTotalScore()에 도달하는 값은 절대 null이 아님 (이미 0으로 변환)따라서
sumTotalScore()의 null 체크는 실제로는 도달하지 않는 방어 코드이며, 실제 문제는extractScore()에서 발생합니다.Circuit Breaker로 인해 일부 섹션이 실패하면:
- 현재: 실패 = 0점 → 4개 섹션 중 1개 실패 시 총점이 3개 섹션 합으로 계산됨
- PR 목표: 0점과 실패(null)를 구분 → 4개 섹션 중 1개 실패 시 총점을 null 또는 부분 점수로 표시
sumTotalScore()의 null 체크를 추가하는 것보다,extractScore()에서 실패한 섹션에 대해 null을 반환하도록 변경하여 상위 계층에서 부분 채점 상황을 정확히 인지할 수 있어야 합니다. 아니면 모든 섹션 채점이 성공한 경우에만 총점을 계산하는 전략도 고려해보세요.src/test/java/starlight/adapter/ai/util/AiReportResponseParserTest.java (1)
98-118: 0과 null 구분 로직 구현 확인 필요PR 목표 "섹션별 응답 파싱 개선(0과 null 구분)"이 완전히 구현되지 않았습니다.
발견된 문제점:
파서의 null 체크 미흡:
parseSectionResponse()에서!jsonNode.path("...").isNull()체크는 있지만, 바로.asInt(0)으로 변환되어 null이 0으로 강제됩니다.실패 케이스도 0으로 반환: 병렬 처리 중 섹션 채점 실패 시
SectionGradingResult.failure(sectionType, 0, null, ...)로 0이 반환되므로, 다음을 구분할 수 없습니다:
- 실제 0점 (유효한 응답)
- 채점 실패 (null이어야 할 값)
Supervisor의 한계:
assembleReportResponse()에서extractScore()호출 시 실패 섹션도 0으로 처리되므로, Supervisor는 실제 점수와 실패를 구분할 수 없습니다.개선 필요사항:
SectionGradingResult의 실패 케이스를 score=0이 아닌 score=null로 변경- 파서에서 null을 명시적으로 보존하는 방식 검토
extractScore()로직 수정으로 실패 섹션 처리 명확화src/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.java (1)
21-53: 테스트가 잘못된 컴포넌트를 사용하고 있습니다.이 테스트는 체크리스트 채점 기능(
generateChecklistArray)을 테스트하고 있지만, 전체 리포트 채점용 컴포넌트를 사용하고 있습니다:문제점:
- Line 36-37:
ReportPromptProvider는createChecklistGradingPrompt메서드가 없습니다 (컴파일 오류 원인)- Line 44:
SpringAiFullReportGradeAgent는 전체 리포트 채점용이며, 체크리스트 채점에는SpringAiChecklistAgent를 사용해야 합니다올바른 구성:
SpringAiChecklistAgent+ChecklistPromptProvider조합 사용 (SpringAiChecklistGrader.java 참고)🔎 수정 제안
- ReportPromptProvider reportPromptProvider = mock(ReportPromptProvider.class); - when(reportPromptProvider.createChecklistGradingPrompt(any(SubSectionType.class), anyString(), anyList(), anyList())) + ChecklistPromptProvider checklistPromptProvider = mock(ChecklistPromptProvider.class); + when(checklistPromptProvider.createChecklistGradingPrompt(any(SubSectionType.class), anyString(), anyList(), anyList())) .thenReturn(mock(Prompt.class)); SpringAiAdvisorProvider advisorProvider = mock(SpringAiAdvisorProvider.class); when(advisorProvider.getSimpleLoggerAdvisor()) .thenReturn(mock(org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor.class)); - SpringAiFullReportGradeAgent sut = new SpringAiFullReportGradeAgent(builder, reportPromptProvider, advisorProvider); + SpringAiChecklistAgent sut = new SpringAiChecklistAgent(builder, checklistPromptProvider, advisorProvider, new ObjectMapper());임포트도 수정 필요:
-import starlight.adapter.aireport.report.agent.impl.SpringAiFullReportGradeAgent; -import starlight.adapter.aireport.report.provider.ReportPromptProvider; +import starlight.adapter.businessplan.checklist.agent.SpringAiChecklistAgent; +import starlight.adapter.businessplan.checklist.provider.ChecklistPromptProvider; +import com.fasterxml.jackson.databind.ObjectMapper;src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java (1)
1-100: 테스트 파일 전체를 새로운 아키텍처에 맞춰 재작성해야 합니다.이 PR에서 모놀리식 OpenAI 기반 채점을 섹션별 에이전트 + 슈퍼바이저 패턴으로 전환했으므로, 테스트도 다음을 반영해야 합니다:
- 새 의존성 구조: SpringAiReportGrader는 이제 에이전트 리스트, 슈퍼바이저, 컨텐츠 추출기, Executor를 주입받습니다.
- 병렬 처리 검증: CompletableFuture 기반 병렬 채점 로직 테스트
- Circuit Breaker 동작: 실패 시나리오와 회로 차단 동작 검증
- 섹션별 채점 vs 전체 채점: PDF는 전체 채점, 텍스트는 섹션별 채점 분기 로직 테스트
PR 목표에 "테스트 코드는 별도 PR로 추가 예정"이라고 명시되어 있으니, 현재 테스트를 삭제하거나 @disabled 처리하고 새 테스트 PR에서 재작성하는 것을 권장합니다.
새로운 아키텍처에 맞는 통합 테스트 구조를 제안해 드릴까요?
src/test/java/starlight/application/businessplan/BusinessPlanServiceImplUnitTest.java (2)
39-52:BusinessPlanQueryPort에save()메서드가 없어 컴파일 실패파이프라인 로그에서 확인된 대로,
BusinessPlanQueryPort는 조회(Query) 전용 포트이므로save()메서드가 없습니다. 헥사고날 아키텍처에서 CQRS 패턴을 따르고 있으므로,save()와delete()작업은BusinessPlanCommandPort를 통해 수행해야 합니다.
BusinessPlanService.java를 보면businessPlanCommandPort.save()를 사용하고 있으므로, 테스트에서도BusinessPlanCommandPort목을 추가해야 합니다.🔎 수정 제안
@Mock private BusinessPlanQueryPort businessPlanQuery; +@Mock +private BusinessPlanCommandPort businessPlanCommand; + @Mock private ChecklistGraderPort checklistGrader;그리고 모든
businessPlanQuery.save()호출을businessPlanCommand.save()로,businessPlanQuery.delete()호출을businessPlanCommand.delete()로 변경해야 합니다:-when(businessPlanQuery.save(any(BusinessPlan.class))) +when(businessPlanCommand.save(any(BusinessPlan.class))) .thenAnswer(invocation -> invocation.getArgument(0));
74-83:businessPlanCommand포트 사용 필요이 테스트 메서드와 파일 전체에서
businessPlanQuery.save()를 사용하고 있지만, 실제 서비스는businessPlanCommandPort.save()를 사용합니다.영향받는 라인들:
- Line 75-76, 88-89, 109-110, 143, 154-155, 178, 193-194, 210, 280-281, 290, 375-376, 438-439, 486-488, 514-516, 548-550
모든
save()및delete()스텁과 검증을businessPlanCommand로 변경해야 합니다.
♻️ Duplicate comments (2)
src/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.java (1)
55-87: 동일한 컴포넌트 오류가 반복됩니다.이 테스트도 첫 번째 테스트와 동일한 문제를 가지고 있습니다.
SpringAiChecklistAgent+ChecklistPromptProvider를 사용하도록 수정해야 합니다.src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java (1)
113-119: 동일한 시그니처 불일치 문제 반복Lines 65, 84와 동일한 문제가 이 테스트 케이스에서도 반복됩니다. 모든 테스트 메서드에서 일관되게 수정이 필요합니다.
🧹 Nitpick comments (45)
src/main/java/starlight/application/businessplan/required/SpellCheckerPort.java (1)
1-11: 헥사고날 아키텍처 의존성 방향 위반: Port 인터페이스가 Adapter 레이어의 DTO를 참조하고 있습니다.
SpellCheckerPort는 application 레이어에 위치한 아웃바운드 포트인데, adapter 레이어의starlight.adapter.businessplan.spellcheck.dto.Finding을 import하고 있습니다. 헥사고날 아키텍처에서는 내부 레이어(application)가 외부 레이어(adapter)에 의존하면 안 됩니다.
FindingDTO를 application 레이어 또는 도메인 레이어로 이동시키는 것을 권장합니다.🔎 권장 구조 예시
starlight/ ├── application/ │ └── businessplan/ │ └── required/ │ ├── SpellCheckerPort.java │ └── dto/ │ └── Finding.java ← 여기로 이동 └── adapter/ └── businessplan/ └── spellcheck/ └── DaumSpellChecker.java ← application.dto.Finding 사용또는 Port 인터페이스 전용 DTO를 정의하고 Adapter에서 매핑하는 방식도 가능합니다.
src/test/java/starlight/adapter/businessplan/webapi/SpellControllerTest.java (1)
53-75: 주석 처리된 테스트 코드 정리 필요PR 목표에 "테스트 코드는 별도 PR로 추가 예정"이라고 명시되어 있지만, 주석 처리된 코드를 그대로 두는 것보다 제거하거나 TODO 주석을 추가하는 것이 유지보수에 더 좋습니다.
🔎 권장 방안
- 제거: 테스트 코드를 삭제하고, 별도 PR에서 새로 작성
- TODO 추가: 주석 상단에 의도를 명시
+ // TODO: SRLT-XXX - 통합 테스트 별도 PR에서 활성화 예정 // @Test // @DisplayName("End-to-End - 가짜 맞춤법 검사기로 통합 테스트") // void endToEnd_withFakeSpellChecker() throws Exception {src/test/java/starlight/application/member/auth/AuthServiceImplIntegrationTest.java (1)
31-34: 필드명과 타입명의 일관성 개선 고려필드명이 이전 명명 규칙을 따르고 있어 타입명과 불일치합니다:
memberQueryUseCase필드 →MemberUseCase타입 (필드명을memberUseCase로 변경 고려)credentialService필드 →CredentialUseCase타입 (필드명을credentialUseCase로 변경 고려)일관성을 위해 필드명도 새로운 타입명에 맞춰 정렬하는 것을 권장합니다.
🔎 필드명 정렬 제안
@MockitoBean -MemberUseCase memberQueryUseCase; +MemberUseCase memberUseCase; @MockitoBean -CredentialUseCase credentialService; +CredentialUseCase credentialUseCase;테스트 메서드 내 모든 참조도 함께 업데이트해야 합니다.
src/main/java/starlight/application/member/auth/AuthServiceImpl.java (1)
28-29: 필드명과 타입명의 일관성 개선 권장프로덕션 코드에서 필드명이 새로운 타입명과 일치하지 않습니다:
memberQueryUseCase필드 →MemberUseCase타입credentialService필드 →CredentialUseCase타입코드 가독성과 유지보수성을 위해 필드명을 타입명에 맞춰 정렬하는 것을 권장합니다.
🔎 필드명 정렬 제안
-private final MemberUseCase memberQueryUseCase; -private final CredentialUseCase credentialService; +private final MemberUseCase memberUseCase; +private final CredentialUseCase credentialUseCase; private final TokenProvider tokenProvider; private final KeyValueMap redisClient;메서드 내 모든 참조(lines 45, 46, 65, 66, 110)도 함께 업데이트해야 합니다.
src/test/java/starlight/application/member/MemberQueryServiceIntegrationTest.java (1)
21-25: 테스트 설정 업데이트 확인테스트가 새로운
MemberService구현체를 올바르게 임포트하고 테스트하도록 업데이트되었습니다.테스트 파일명(
MemberQueryServiceIntegrationTest)이 테스트 대상 클래스(MemberService)와 일치하지 않습니다. 일관성을 위해 파일명을MemberServiceIntegrationTest.java로 변경하는 것을 고려해보세요.src/test/java/starlight/application/member/MemberQueryServiceUnitTest.java (1)
21-21: 테스트 클래스명을 실제 테스트 대상과 일치시켜 주세요.테스트 대상이
MemberService로 변경되었지만 테스트 클래스명은 여전히MemberQueryServiceUnitTest입니다. 일관성을 위해MemberServiceUnitTest로 변경하는 것을 권장합니다.🔎 제안하는 수정
-class MemberQueryServiceUnitTest { +class MemberServiceUnitTest {Also applies to: 25-26
src/test/java/starlight/application/member/CredentialServiceImplUnitTest.java (1)
17-17: 테스트 클래스명을 실제 테스트 대상과 일치시켜 주세요.테스트 대상이
CredentialService로 변경되었지만 테스트 클래스명은 여전히CredentialServiceImplUnitTest입니다. 일관성을 위해CredentialServiceUnitTest로 변경하는 것을 권장합니다.🔎 제안하는 수정
-class CredentialServiceImplUnitTest { +class CredentialServiceUnitTest {Also applies to: 20-21
src/main/java/starlight/adapter/member/webapi/MemberController.java (1)
21-21: 필드명을 타입과 일치시키면 더 명확합니다.필드 타입이
MemberUseCase로 변경되었지만 필드명은 여전히memberQueryUseCase입니다. "Query"가 제거된 새로운 네이밍 컨벤션에 맞춰memberUseCase로 변경하는 것을 권장합니다.🔎 제안하는 수정
- private final MemberUseCase memberQueryUseCase; + private final MemberUseCase memberUseCase;그리고 Line 28의 사용처도 함께 업데이트:
- memberQueryUseCase.getUserById(authenticatedMember.getMemberId()) + memberUseCase.getUserById(authenticatedMember.getMemberId())src/test/java/starlight/application/member/auth/AuthServiceImplUnitTest.java (1)
27-30: Mock 필드명을 UseCase 타입과 일치시켜 주세요.Mock 필드의 타입이
MemberUseCase와CredentialUseCase로 올바르게 변경되었지만, 필드명이 일관성 있게 업데이트되지 않았습니다:
memberQueryUseCase→memberUseCase(또는memberService)credentialService→credentialUseCase새로운 UseCase 기반 네이밍 컨벤션과의 일관성을 위해 필드명 변경을 권장합니다.
🔎 제안하는 수정
@Mock - MemberUseCase memberQueryUseCase; + MemberUseCase memberUseCase; @Mock - CredentialUseCase credentialService; + CredentialUseCase credentialUseCase;그리고 테스트 메서드 내 사용처도 함께 업데이트 필요 (Lines 46, 51, 66 등):
- when(memberQueryUseCase.getUserByEmail("[email protected]")).thenReturn(member); + when(memberUseCase.getUserByEmail("[email protected]")).thenReturn(member);- verify(credentialService).checkPassword(member, "pw"); + verify(credentialUseCase).checkPassword(member, "pw");src/test/java/starlight/application/member/CredentialServiceImplIntegrationTest.java (1)
20-20: 테스트 와이어링이 리팩터링된 클래스명과 올바르게 동기화되었습니다.
@ContextConfiguration과@Autowired필드가CredentialService로 정확히 업데이트되어 프로덕션 코드의 변경사항을 반영하고 있습니다.다만, 테스트 클래스명이 여전히
CredentialServiceImplIntegrationTest로 남아있어 테스트 대상(CredentialService)과 불일치합니다. 일관성 향상을 위해CredentialServiceIntegrationTest로 변경을 검토해보세요.🔎 테스트 클래스명 일관성 개선 제안
테스트 파일명과 클래스명을 함께 변경:
파일명 변경:
CredentialServiceImplIntegrationTest.java→CredentialServiceIntegrationTest.java클래스명 변경:
-class CredentialServiceImplIntegrationTest { +class CredentialServiceIntegrationTest {Also applies to: 29-29
src/main/java/starlight/application/aireport/required/PresignedUrlProviderPort.java (1)
5-10: 포트 인터페이스 이름 변경이 헥사고날 아키텍처 규칙을 잘 따르고 있습니다.
PresignedUrlProvider에서PresignedUrlProviderPort로 변경하여 아웃바운드 포트임을 명확히 표현했습니다.required패키지 위치도 적절합니다.선택적 개선사항: 포트 인터페이스와 메서드에 JavaDoc을 추가하면 구현체 작성 시 계약을 더 명확히 이해할 수 있습니다:
getPreSignedUrl: PreSigned URL 생성 목적, 파라미터 의미, 실패 시 동작makePublic: 객체를 공개로 전환하는 목적과 반환값의 의미src/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.java (1)
19-20: 필드 명명이 역할을 명확히 반영하지 못합니다.Line 20의
checklistCatalog변수명이 혼란스럽습니다:
- 타입은
ChecklistPromptProvider인데 변수명은catalog로, 마치 데이터 저장소처럼 보입니다- 실제로는 프롬프트를 생성하고 기준을 제공하는 역할입니다
Line 19의
generator도 좀 더 구체적일 수 있습니다.🔎 명명 개선 제안
- private final SpringAiChecklistAgent generator; - private final ChecklistPromptProvider checklistCatalog; + private final SpringAiChecklistAgent checklistAgent; + private final ChecklistPromptProvider promptProvider;그리고 메서드 내부에서도:
- List<String> criteria = checklistCatalog.getCriteriaBySubSectionType(subSectionType); - List<String> detailedCriteria = checklistCatalog.getDetailedCriteriaBySubSectionType(subSectionType); + List<String> criteria = promptProvider.getCriteriaBySubSectionType(subSectionType); + List<String> detailedCriteria = promptProvider.getDetailedCriteriaBySubSectionType(subSectionType); - List<Boolean> result = generator.generateChecklistArray(subSectionType, content, criteria, detailedCriteria); + List<Boolean> result = checklistAgent.generateChecklistArray(subSectionType, content, criteria, detailedCriteria);src/main/java/starlight/application/expertReport/provided/ExpertReportUseCase.java (1)
10-19: 유스케이스 인터페이스 이름 변경이 아키텍처 규칙을 잘 따릅니다.
ExpertReportServiceUseCase에서ExpertReportUseCase로 변경하여 네이밍이 간결해지고 인바운드 포트(제공 인터페이스)임을 명확히 표현합니다.provided패키지 위치도 적절합니다.선택적 개선사항: 각 메서드의 목적과 파라미터를 설명하는 JavaDoc을 추가하면 클라이언트(컨트롤러 등)에서 사용 시 더 명확합니다. 특히:
createExpertReportLink: 전문가 리포트 링크 생성 목적saveReport: SaveType에 따른 저장 동작 차이- 각 메서드의 예외 발생 조건
src/main/java/starlight/adapter/aireport/report/agent/FullReportGradeAgent.java (1)
5-8: 인터페이스 설계 승인 및 JavaDoc 추가 권장에이전트 패턴을 적용한 인터페이스 설계가 명확하고 단일 책임 원칙을 잘 따르고 있습니다. 다만 공개 인터페이스에 JavaDoc을 추가하면 계약(contract)과 사용 의도를 더 명확히 전달할 수 있습니다.
🔎 JavaDoc 추가 제안
+/** + * 전체 사업계획서 콘텐츠를 AI로 채점하는 에이전트 인터페이스. + * PDF 업로드 등 섹션 분리가 불가능한 경우 사용됩니다. + */ public interface FullReportGradeAgent { + /** + * 전체 사업계획서 콘텐츠를 채점합니다. + * + * @param content 채점할 사업계획서 전체 내용 + * @return AI 채점 결과 + */ AiReportResult gradeFullReport(String content); }src/main/java/starlight/application/aireport/required/AiReportCommandPort.java (1)
5-7: CQRS 패턴의 Command Port 설계 승인AI 리포트 저장을 담당하는 커맨드 포트 인터페이스가 명확하게 정의되었습니다. CQRS 패턴에 따라 Query와 Command를 분리하는 설계가 잘 적용되었습니다.
🔎 JavaDoc 추가 제안
+/** + * AI 리포트 저장(Command) 작업을 위한 아웃바운드 포트. + * CQRS 패턴의 Command 측면을 담당합니다. + */ public interface AiReportCommandPort { + /** + * AI 리포트를 저장합니다. + * + * @param aiReport 저장할 AI 리포트 엔티티 + * @return 저장된 AI 리포트 엔티티 + */ AiReport save(AiReport aiReport); }src/main/java/starlight/adapter/aireport/webapi/ImageController.java (1)
18-18: 포트 기반 의존성 전환 승인 및 네이밍 개선 제안
PresignedUrlProviderPort로의 전환이 올바르게 적용되었습니다. 다만 필드명presignedUrlReader는 읽기 전용 작업을 암시하지만, 실제로는makePublic()같은 쓰기 작업도 수행합니다.presignedUrlProvider로 변경하면 더 정확한 의미 전달이 가능합니다.🔎 필드명 개선 제안
- private final PresignedUrlProviderPort presignedUrlReader; + private final PresignedUrlProviderPort presignedUrlProvider; @GetMapping(value = "/upload-url", produces = MediaType.APPLICATION_JSON_VALUE) public ApiResponse<PreSignedUrlResponse> getPresignedUrl( @AuthenticationPrincipal AuthenticatedMember authenticatedMember, @RequestParam String fileName ) { - return ApiResponse.success(presignedUrlReader.getPreSignedUrl(authenticatedMember.getMemberId(), fileName)); + return ApiResponse.success(presignedUrlProvider.getPreSignedUrl(authenticatedMember.getMemberId(), fileName)); } @PostMapping("/upload-url/public") public ApiResponse<String> finalizePublic(@RequestParam String objectUrl) { - return ApiResponse.success(presignedUrlReader.makePublic(objectUrl)); + return ApiResponse.success(presignedUrlProvider.makePublic(objectUrl)); }src/main/java/starlight/domain/aireport/exception/AiReportErrorType.java (1)
15-17: 새로운 에러 타입 추가 확인 완료섹션별 채점 실패를 처리하기 위한
AI_GRADING_FAILED추가가 적절합니다:
INTERNAL_SERVER_ERROR상태 코드는 AI 시스템 장애에 적합합니다- 사용자 친화적인 한글 메시지를 제공합니다
- Circuit Breaker 및 섹션별 에이전트의 실패 처리에 활용될 것으로 보입니다
선택적 개선: 불필요한 세미콜론 정리
Line 17의 독립적인 세미콜론은 문법적으로 유효하지만 불필요합니다:
AI_RESPONSE_PARSING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 파싱에 실패했습니다."), - AI_GRADING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AI 채점에 실패했습니다."); - ; + AI_GRADING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AI 채점에 실패했습니다."); private final HttpStatus status;src/main/java/starlight/application/businessplan/required/BusinessPlanCommandPort.java (1)
5-10: 포트 인터페이스에 JavaDoc 추가를 권장합니다.헥사고날 아키텍처에서 포트는 도메인과 어댑터 간의 명확한 계약(contract)을 정의합니다. 각 메서드의 책임, 반환값의 의미, 예외 발생 조건 등을 문서화하면 구현체(어댑터)의 일관성을 확보하고 유지보수성을 높일 수 있습니다.
🔎 JavaDoc 추가 예시
+/** + * 사업계획서 쓰기 작업을 위한 커맨드 포트. + * 이 포트는 사업계획서 도메인의 영속성 쓰기 작업을 추상화합니다. + */ public interface BusinessPlanCommandPort { + /** + * 사업계획서를 저장합니다. + * + * @param businessPlan 저장할 사업계획서 엔티티 + * @return 저장된 사업계획서 (생성된 ID 포함) + * @throws IllegalArgumentException businessPlan이 null인 경우 + */ BusinessPlan save(BusinessPlan businessPlan); + /** + * 사업계획서를 삭제합니다. + * + * @param businessPlan 삭제할 사업계획서 엔티티 + * @throws IllegalArgumentException businessPlan이 null인 경우 + */ void delete(BusinessPlan businessPlan); }src/main/java/starlight/application/aireport/required/AiReportQueryPort.java (1)
7-9: 쿼리 포트에도 JavaDoc 추가를 권장합니다.Command 포트와 마찬가지로, Query 포트의 계약을 명확히 문서화하면 어댑터 구현의 일관성을 보장할 수 있습니다. 특히
Optional반환 시나리오(존재하는 경우 vs 없는 경우)를 명시하면 클라이언트 코드의 오해를 방지할 수 있습니다.🔎 JavaDoc 추가 예시
+/** + * AI 리포트 조회 작업을 위한 쿼리 포트. + * 이 포트는 AI 리포트 도메인의 읽기 전용 작업을 추상화합니다. + */ public interface AiReportQueryPort { + /** + * 사업계획서 ID로 AI 리포트를 조회합니다. + * + * @param businessPlanId 사업계획서 ID + * @return AI 리포트가 존재하면 Optional로 감싼 AiReport, 없으면 Optional.empty() + * @throws IllegalArgumentException businessPlanId가 null인 경우 + */ Optional<AiReport> findByBusinessPlanId(Long businessPlanId); }src/main/java/starlight/application/aireport/provided/AiReportUseCase.java (1)
5-11: 유스케이스 인터페이스에 JavaDoc 추가를 적극 권장합니다.
AiReportUseCase는 애플리케이션의 핵심 진입점(인바운드 포트)으로, 컨트롤러 등 외부 어댑터에서 직접 사용됩니다. 각 유스케이스의 비즈니스 의도, 전제조건(예: 인증 필요 여부, 권한 검증), 예외 발생 시나리오를 명확히 문서화하면 API 사용자의 오용을 방지하고 유지보수성을 크게 향상시킬 수 있습니다.특히
createAndGradePdfBusinessPlan은 생성과 채점이라는 두 가지 작업을 수행하므로, 각 단계의 실패 처리와 트랜잭션 범위를 문서화하는 것이 중요합니다.🔎 JavaDoc 추가 예시
+/** + * AI 리포트 생성 및 조회를 위한 유스케이스 인터페이스. + * 이 인터페이스는 애플리케이션의 인바운드 포트로, 외부 어댑터(컨트롤러 등)에서 호출됩니다. + */ public interface AiReportUseCase { + /** + * 기존 사업계획서에 대해 AI 채점을 수행합니다. + * + * @param businessPlanId 채점 대상 사업계획서 ID + * @param memberId 요청자 회원 ID (권한 검증 용도) + * @return AI 채점 결과 + * @throws IllegalArgumentException ID가 null이거나 유효하지 않은 경우 + * @throws ForbiddenException 회원이 해당 사업계획서에 접근 권한이 없는 경우 + * @throws NotFoundException 사업계획서를 찾을 수 없는 경우 + */ AiReportResult gradeBusinessPlan(Long businessPlanId, Long memberId); + /** + * PDF URL로부터 사업계획서를 생성하고 AI 채점을 수행합니다. + * PDF 다운로드 → OCR 처리 → 사업계획서 생성 → AI 채점의 전체 워크플로우를 실행합니다. + * + * @param title 사업계획서 제목 + * @param pdfUrl 접근 가능한 PDF URL + * @param memberId 요청자 회원 ID + * @return AI 채점 결과 (생성된 사업계획서 정보 포함) + * @throws OcrException PDF 처리 또는 OCR 실패 시 + * @throws IllegalArgumentException 파라미터가 null이거나 유효하지 않은 경우 + */ AiReportResult createAndGradePdfBusinessPlan(String title, String pdfUrl, Long memberId); + /** + * 기존에 생성된 AI 리포트를 조회합니다. + * + * @param businessPlanId 사업계획서 ID + * @param memberId 요청자 회원 ID (권한 검증 용도) + * @return AI 리포트 결과 + * @throws IllegalArgumentException ID가 null이거나 유효하지 않은 경우 + * @throws ForbiddenException 회원이 해당 리포트에 접근 권한이 없는 경우 + * @throws NotFoundException AI 리포트를 찾을 수 없는 경우 + */ AiReportResult getAiReport(Long businessPlanId, Long memberId); }src/main/java/starlight/adapter/aireport/persistence/AiReportJpa.java (1)
34-49: 배치 조회 로직은 적절하나, 파싱 성능 모니터링을 권장합니다.현재 구현은
findAllByBusinessPlanIdIn으로 N+1 문제를 회피하고 있어 좋습니다. 다만 반복문 내부에서responseParser.toResponse(report)를 매번 호출하므로, 파싱 로직이 복잡할 경우 누적 비용이 발생할 수 있습니다.학습 자료에 따르면 회원당 20-30개 수준으로 제한되어 있어 현재는 문제가 되지 않지만, 향후 규모가 커지거나 파싱 로직이 복잡해질 경우 성능 모니터링 및 최적화(배치 파싱, 캐싱 등)를 고려해 주세요.
Based on learnings, 회원당 AI 리포트는 20-30개 수준으로 제한되므로 현재 구현은 적절합니다.
src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java (1)
3-3: @slf4j 어노테이션이 사용되지 않고 있습니다.현재 코드에서
log사용이 없는데 Lombok의@Slf4j를 추가했습니다. 향후 로깅을 추가할 계획이 아니라면 제거하는 것이 좋습니다.🔎 제안: 미사용 어노테이션 제거
-import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;-@Slf4j @Component public class BusinessPlanContentExtractor {Also applies to: 19-19
src/main/java/starlight/adapter/aireport/report/agent/SectionGradeAgent.java (2)
15-24: buildFilterExpression에서 잠재적 NPE 가능성을 방어해야 합니다.Line 16에서
getSectionType()을 호출하고 line 17에서 바로.getTag()를 호출하는데, 만약getSectionType()이null을 반환하면 NPE가 발생합니다.인터페이스 설계상
getSectionType()은 항상 non-null을 반환해야 하지만, 디폴트 메서드에서 방어 로직을 추가하면 더 안전합니다.🔎 제안: Null 방어 로직 추가
default String buildFilterExpression() { SectionType sectionType = getSectionType(); + if (sectionType == null) { + return null; + } + String tag = sectionType.getTag(); if (tag == null || tag.isBlank()) { return null; } return "tag == '" + tag + "'"; }
23-23: 문자열 연결 대신 String.format을 고려할 수 있습니다.현재 단순 연결로 구현되어 있어 큰 문제는 없지만, SQL Injection 등의 위험이 있는 filter expression 생성에서는
String.format이나 template 방식이 더 명확할 수 있습니다.🔎 제안: String.format 사용
- return "tag == '" + tag + "'"; + return String.format("tag == '%s'", tag);src/main/java/starlight/adapter/aireport/report/dto/SectionGradingResult.java (1)
21-23: 현재 구현에서는success플래그로 이미 성공/실패를 완벽하게 구분하고 있어 코드 동작에는 문제가 없습니다.다만 의미론적으로는 개선 여지가 있습니다:
- ReportSupervisor (line 86-94)는
result.success()로 성공/실패를 판단하며, 실패 시 점수를 사용하지 않음- SpringAiReportGrader와 SpringAiSectionGradeAgent의 필터링도 모두
success()boolean 기반- 코드베이스 전체에서 score 값을 직접 비교하는 로직이 없음
따라서 failure() 메서드의 score를 null로 변경해도 기존 로직이 깨지지 않습니다. 오히려 다음과 같은 이점이 있습니다:
- 의미 명확화: score=null은 "점수 없음(채점 실패)"을, score=0은 "채점 완료, 0점"을 더 명확히 표현
- 로깅 개선: 현재 로그에 "score=0"이 출력되는 것은 혼란의 여지가 있음
변경을 권장합니다:
public static SectionGradingResult failure(SectionType sectionType, String errorMessage) { return new SectionGradingResult(sectionType, null, null, false, errorMessage); }다만, 로깅 위치(SpringAiReportGrader:136, SpringAiSectionGradeAgent:76)에서
result.score()를 출력할 때 null 안전성을 추가해야 합니다.src/main/java/starlight/application/aireport/required/ReportGraderPort.java (1)
8-11: Port 인터페이스 설계가 적절합니다.헥사고날 아키텍처의 아웃바운드 포트로서
application.aireport.required패키지에 적절히 위치했습니다. 섹션별 에이전트 채점과 전체 프롬프트 채점을 구분하여 PDF 채점과 일반 채점 플로우를 모두 지원합니다.
gradeWithSectionAgents의fullContent파라미터는 Supervisor가 종합 평가 시 전체 맥락을 참조하기 위한 것으로 보입니다. 이 의도가 맞다면, 메서드에 간단한 Javadoc을 추가하면 향후 유지보수에 도움이 될 것입니다.src/main/java/starlight/adapter/aireport/report/config/SectionAdvisorConfig.java (1)
29-43: 섹션별 에이전트 생성 설정이 잘 구성되었습니다.
SectionType.values()를 순회하며 각 섹션별 에이전트를 동적으로 생성하는 방식이 확장성 있습니다. 새로운 섹션 타입이 추가되더라도 자동으로 에이전트가 생성됩니다.다만,
getTag() != null조건으로 OVERVIEW를 제외하는 방식은 다소 암묵적입니다. 명시성을 높이기 위해 아래와 같이 리팩터링하거나 주석을 보완하는 것을 고려해 주세요.🔎 필터링 로직 명시화 제안
// 채점 대상이 아닌 OVERVIEW를 제외한 모든 SectionType에 대해 Advisor 생성 return Arrays.stream(SectionType.values()) - .filter(sectionType -> sectionType.getTag() != null) // OVERVIEW 제외 + .filter(SectionType::isGradable) // OVERVIEW 등 비채점 대상 제외 .map(sectionType -> new SpringAiSectionGradeAgent(또는
SectionType에isGradable()메서드를 추가하여 채점 가능 여부를 명시적으로 표현할 수 있습니다.src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java (2)
44-47: LocalDateTime 대신 monotonic 시계 사용을 권장합니다.
LocalDateTime.now()는 시스템 시계에 의존하여 NTP 동기화나 시간대 변경 시 오동작할 수 있습니다. 경과 시간 측정에는System.nanoTime()또는Instant.now()가 더 적합합니다.🔎 Instant 사용 제안
- private final AtomicReference<LocalDateTime> lastFailureTime = new AtomicReference<>(); + private final AtomicReference<Instant> lastFailureTime = new AtomicReference<>(); public boolean allowRequest() { // ... if (current == State.OPEN) { - LocalDateTime lastFailure = lastFailureTime.get(); + Instant lastFailure = lastFailureTime.get(); if (lastFailure != null && - java.time.Duration.between(lastFailure, LocalDateTime.now()) + java.time.Duration.between(lastFailure, Instant.now()) .getSeconds() >= HALF_OPEN_TIMEOUT_SECONDS) {
Instant는 시간대에 독립적이며, 시스템 시계 변경에 덜 민감합니다.
15-27: 커스텀 서킷 브레이커 구현이 잘 설계되었습니다.섹션별 독립적인 상태 관리를 위해
ConcurrentHashMap<SectionType, CircuitState>를 사용한 설계가 적절합니다. 한 섹션의 실패가 다른 섹션에 영향을 주지 않아 장애 격리가 잘 됩니다.PR 목표에 명시된 대로 5회 실패 → OPEN, 2회 성공 → CLOSED, 60초 HALF_OPEN 타임아웃 설정이 적용되었습니다. 운영 환경에서 모니터링 후 임계값 조정이 필요할 수 있으니, 향후 이 값들을 설정 파일로 외부화하는 것을 고려해 주세요.
src/main/java/starlight/adapter/businessplan/webapi/BusinessPlanController.java (1)
36-36: 필드명과 타입명의 일관성을 개선하면 좋겠습니다.필드 타입이
BusinessPlanUseCase로 변경되었지만 필드명은 여전히businessPlanService입니다. 헥사고날 아키텍처 도입으로 Service → UseCase 전환을 명확히 하려면 필드명도 일치시키는 것이 좋습니다.🔎 필드명 일관성 개선 제안
-private final BusinessPlanUseCase businessPlanService; +private final BusinessPlanUseCase businessPlanUseCase;그리고 모든 메서드 내부의
businessPlanService.xxx()호출을businessPlanUseCase.xxx()로 변경합니다.src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.java (1)
59-63: 예외 처리 시 원본 예외 정보가 손실됩니다.Line 61-62에서
Exception e를 catch하여 새로운AiReportException으로 래핑하는데, 원본 예외의 cause chain이 끊어집니다. 디버깅과 모니터링을 위해 원본 예외를 로깅하거나 cause로 전달하는 것이 좋습니다.🔎 예외 처리 개선 제안
} catch (AiReportException e) { throw e; } catch (Exception e) { + log.error("AI 리포트 채점 중 예상치 못한 오류 발생: {}", e.getMessage(), e); - throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED); + throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED, e); }단,
AiReportException생성자가 cause를 받을 수 있는지 확인이 필요합니다.src/main/java/starlight/adapter/aireport/report/provider/ReportPromptProvider.java (2)
61-72: default 케이스에서 빈 문자열 반환은 위험할 수 있습니다.Line 67의
default -> ""는 OVERVIEW나 예상치 못한 SectionType이 전달될 때 섹션별 프롬프트가 누락된 채 채점이 진행될 수 있습니다. SectionAdvisorConfig에서 OVERVIEW를 필터링하더라도, 방어적 코딩 차원에서 명시적으로 예외를 던지는 것이 안전합니다.🔎 방어적 예외 처리 제안
private String getSectionGradingSystemPrompt(SectionType sectionType) { String sectionSpecificPrompt = switch (sectionType) { case PROBLEM_RECOGNITION -> problemRecognitionSystemPrompt; case FEASIBILITY -> feasibilitySystemPrompt; case GROWTH_STRATEGY -> growthStrategySystemPrompt; case TEAM_COMPETENCE -> teamCompetenceSystemPrompt; - default -> ""; // 기본값 + default -> throw new IllegalArgumentException( + "지원하지 않는 섹션 타입입니다: " + sectionType + ); }; // 공통 프롬프트와 섹션별 프롬프트를 합침 return sectionDefaultSystemPrompt + "\n\n" + sectionSpecificPrompt; }OVERVIEW가 전달되는 것이 설계상 불가능하다면, 이 예외는 개발 중 버그를 조기에 발견하는 안전장치가 됩니다.
16-35: 프롬프트 속성 주입 방식을 검토하면 좋겠습니다.현재 6개의 프롬프트 문자열을 @value로 주입받고 있습니다. 프롬프트가 더 추가되거나 변경될 때마다 필드가 늘어날 수 있으므로, 향후 프롬프트 관리 개선을 고려해볼 수 있습니다:
- Properties 객체로 그룹화하여
@ConfigurationProperties사용- 프롬프트 템플릿 파일을 외부 리소스로 관리
현재 구조도 동작하지만, 프롬프트가 10개 이상으로 늘어나면 리팩터링을 권장합니다.
src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java (1)
137-140: 전체 테스트 파일에 걸친 시그니처 수정 필요이 파일의 모든 테스트 메서드에서
AiReportService생성자 호출과gradeWithSectionAgentsmock 설정이 동일하게 수정되어야 합니다. 테스트 가독성과 유지보수를 위해@BeforeEach에서 공통 설정을 처리하는 것을 권장합니다.🔎 @beforeeach 활용 제안
@BeforeEach void setUp() { sut = new AiReportService( businessPlanCommand, businessPlanQuery, businessPlanService, aiReportQuery, aiReportCommand, aiReportGrader, objectMapper, ocrProvider, responseParser, contentExtractor ); }Also applies to: 159-159, 179-179, 217-220, 241-241
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (2)
91-126: 병렬 채점 로직 잘 구현됨4개 섹션의 병렬 채점을
CompletableFuture와 커스텀 Executor로 구현한 것이 적절합니다. 각 섹션별로 예외를 catch하여 실패 결과로 변환하는 방어적 처리가 잘 되어 있습니다.다만,
Arrays.asList(...).stream().collect(Collectors.toMap(...))보다EnumMap을 사용하면 메모리 효율성이 향상됩니다.🔎 EnumMap 활용 제안 (선택사항)
- Map<SectionType, CompletableFuture<SectionGradingResult>> futureMap = Arrays.asList( - SectionType.PROBLEM_RECOGNITION, - SectionType.FEASIBILITY, - SectionType.GROWTH_STRATEGY, - SectionType.TEAM_COMPETENCE).stream() - .collect(Collectors.toMap( - sectionType -> sectionType, - sectionType -> { ... })); + Map<SectionType, CompletableFuture<SectionGradingResult>> futureMap = new EnumMap<>(SectionType.class); + for (SectionType sectionType : List.of( + SectionType.PROBLEM_RECOGNITION, + SectionType.FEASIBILITY, + SectionType.GROWTH_STRATEGY, + SectionType.TEAM_COMPETENCE)) { + // ... future creation logic + futureMap.put(sectionType, future); + }
29-49: 불필요한 의존성 제거:contentExtractor미사용생성자에서 주입되지만 클래스의 어떤 메소드에서도 사용되지 않는
contentExtractor필드를 제거하세요.gradeWithFullPrompt()와gradeWithSectionAgents()모두 이 의존성이 필요 없으며, import 문도 함께 정리할 수 있습니다.src/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.java (1)
52-57: 파싱 실패 시 로깅 추가 권장체크리스트 파싱 실패 시 기본값
[false, false, false, false, false]를 반환하는 것은 방어적 처리로 적절하지만, 예외 정보를 로깅하지 않으면 문제 진단이 어렵습니다.🔎 로깅 추가 제안
try { return objectMapper.readValue(output, new TypeReference<List<Boolean>>() { }); } catch (Exception e) { + log.warn("체크리스트 응답 파싱 실패. subSectionType={}, response={}", subSectionType, output, e); return List.of(false, false, false, false, false); }src/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.java (1)
101-107: criteria와 detailedCriteria 크기 불일치 가능성 확인
criteria.size()와detailedCriteria.size()가 다를 경우, 더 작은 쪽 기준으로 루프가 종료되어 일부 항목이 누락될 수 있습니다. 설정 오류 시 조기에 감지하기 어려울 수 있습니다.설정 시점에 크기 일치 여부를 검증하거나, 불일치 시 경고 로그를 남기는 것을 고려해 주세요.
🔎 크기 검증 로깅 추가 제안
+ if (criteria.size() != detailedCriteria.size()) { + log.warn("criteria와 detailedCriteria 크기 불일치: {} vs {}", + criteria.size(), detailedCriteria.size()); + } for (int i = 0; i < criteria.size() && i < detailedCriteria.size(); i++) {src/main/java/starlight/adapter/aireport/report/supervisor/ReportSupervisor.java (1)
49-76: LLM 호출 및 예외 처리 구현 양호Supervisor LLM 호출 실패 시 빈 리스트를 반환하는 것은 강점/약점이 필수가 아닌 부가 정보이므로 적절한 graceful degradation입니다.
temperature: 0.0으로 결정론적 출력을 유도하는 설정도 적합합니다.다만, 빈 리스트 반환 시 사용자에게 강점/약점이 누락된 이유가 전달되지 않으므로, 상위 레벨에서 이를 인지할 수 있도록 로깅 레벨을
warn으로 상향하는 것도 고려해 주세요.src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java (1)
312-314: DisplayName과 실제 반환 타입 불일치
@DisplayName에 "AiReportResponse"라고 명시되어 있지만, 실제 반환 타입은AiReportResult입니다. DTO 이름 변경에 맞춰 DisplayName도 업데이트하면 좋겠습니다.🔎 수정 제안
@Test - @DisplayName("리포트 조회 성공 시 AiReportResponse를 반환한다") + @DisplayName("리포트 조회 성공 시 AiReportResult를 반환한다") void getAiReport_returnsResponse() {src/main/java/starlight/application/aireport/AiReportService.java (1)
139-143:RuntimeException대신 도메인 예외 사용 권장JSON 변환 실패 시
RuntimeException을 던지고 있습니다. 일관된 예외 처리를 위해AiReportException을 사용하는 것이 좋습니다.🔎 수정 제안
try { rawJsonString = objectMapper.writeValueAsString(gradingJsonNode); } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to convert JsonNode to string", e); + log.error("Failed to convert grading result to JSON string", e); + throw new AiReportException(AiReportErrorType.AI_RESPONSE_PARSING_FAILED); }src/main/java/starlight/application/businessplan/BusinessPlanService.java (2)
248-257:SectionTypeswitch 표현식의default케이스 검토
SectionType이 5개의 값을 가진 enum이라면, 모든 케이스를 명시적으로 처리하고 있으므로default케이스가 도달 불가능합니다. Java 21의 sealed/exhaustive switch를 활용하면 컴파일 타임에 누락된 케이스를 감지할 수 있습니다.다만, 향후 enum 확장에 대한 방어적 코딩 의도라면 현재 코드도 괜찮습니다.
🔎 대안
private BaseSection getSectionByPlanAndType(BusinessPlan plan, SectionType type) { return switch (type) { case OVERVIEW -> plan.getOverview(); case PROBLEM_RECOGNITION -> plan.getProblemRecognition(); case FEASIBILITY -> plan.getFeasibility(); case GROWTH_STRATEGY -> plan.getGrowthTactic(); case TEAM_COMPETENCE -> plan.getTeamCompetence(); - default -> throw new IllegalArgumentException("Unsupported section: " + type); }; }
150-154: 상태 변경 시 메시지 덮어쓰기
areWritingCompleted()조건을 만족하면 message가 "Subsection writing completed"로 덮어써져서, 원래 "Subsection created" 또는 "Subsection updated" 정보가 손실됩니다. 클라이언트에게 더 상세한 정보를 제공하려면 메시지를 조합하는 것도 고려해볼 수 있습니다.🔎 대안
if (plan.areWritingCompleted()) { plan.updateStatus(PlanStatus.WRITTEN_COMPLETED); - message = "Subsection writing completed"; + message += " (Writing completed)"; }src/main/java/starlight/adapter/aireport/report/util/AiReportResponseParser.java (2)
274-286: 정규식 패턴의 Greedy 매칭 주의
"(.*)\"패턴은 greedy하게 매칭하여 의도치 않게 더 많은 내용을 캡처할 수 있습니다. 특히 JSON에 여러 필드가 있을 경우 문제가 될 수 있습니다.🔎 수정 제안
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( - "\"text\"\\s*:\\s*\"(.*)\"", + "\"text\"\\s*:\\s*\"(.*?)\"", java.util.regex.Pattern.DOTALL);Non-greedy
.*?를 사용하면 첫 번째 닫는 따옴표에서 매칭이 종료됩니다.
18-20: Javadoc에서 이전 타입명 참조클래스 및 메서드 Javadoc에서 여전히
AiReportResponse를 참조하고 있습니다. DTO 이름이AiReportResult로 변경되었으므로 문서도 업데이트하면 좋겠습니다.🔎 수정 제안
/** - * LLM 응답을 파싱하여 AiReportResponse로 변환하는 컴포넌트 + * LLM 응답을 파싱하여 AiReportResult로 변환하는 컴포넌트 */
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (80)
src/main/java/starlight/adapter/ai/OpenAiReportGrader.javasrc/main/java/starlight/adapter/ai/infra/PromptProvider.javasrc/main/java/starlight/adapter/ai/util/ChecklistCatalog.javasrc/main/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProvider.javasrc/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.javasrc/main/java/starlight/adapter/aireport/persistence/AiReportJpa.javasrc/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.javasrc/main/java/starlight/adapter/aireport/report/agent/FullReportGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/agent/SectionGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.javasrc/main/java/starlight/adapter/aireport/report/config/SectionAdvisorConfig.javasrc/main/java/starlight/adapter/aireport/report/dto/SectionGradingResult.javasrc/main/java/starlight/adapter/aireport/report/provider/ReportPromptProvider.javasrc/main/java/starlight/adapter/aireport/report/provider/SpringAiAdvisorProvider.javasrc/main/java/starlight/adapter/aireport/report/supervisor/ReportSupervisor.javasrc/main/java/starlight/adapter/aireport/report/util/AiReportResponseParser.javasrc/main/java/starlight/adapter/aireport/webapi/AiReportController.javasrc/main/java/starlight/adapter/aireport/webapi/ImageController.javasrc/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.javasrc/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.javasrc/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.javasrc/main/java/starlight/adapter/businessplan/persistence/BusinessPlanJpa.javasrc/main/java/starlight/adapter/businessplan/spellcheck/DaumSpellChecker.javasrc/main/java/starlight/adapter/businessplan/webapi/BusinessPlanController.javasrc/main/java/starlight/adapter/businessplan/webapi/SpellController.javasrc/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.javasrc/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.javasrc/main/java/starlight/adapter/member/webapi/MemberController.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/application/aireport/AiReportServiceImpl.javasrc/main/java/starlight/application/aireport/provided/AiReportService.javasrc/main/java/starlight/application/aireport/provided/AiReportUseCase.javasrc/main/java/starlight/application/aireport/provided/dto/AiReportResult.javasrc/main/java/starlight/application/aireport/required/AiReportCommandPort.javasrc/main/java/starlight/application/aireport/required/AiReportGrader.javasrc/main/java/starlight/application/aireport/required/AiReportQueryPort.javasrc/main/java/starlight/application/aireport/required/OcrProviderPort.javasrc/main/java/starlight/application/aireport/required/PresignedUrlProviderPort.javasrc/main/java/starlight/application/aireport/required/ReportGraderPort.javasrc/main/java/starlight/application/businessplan/BusinessPlanService.javasrc/main/java/starlight/application/businessplan/provided/BusinessPlanService.javasrc/main/java/starlight/application/businessplan/provided/BusinessPlanUseCase.javasrc/main/java/starlight/application/businessplan/provided/dto/BusinessPlanResult.javasrc/main/java/starlight/application/businessplan/provided/dto/SubSectionResult.javasrc/main/java/starlight/application/businessplan/required/BusinessPlanCommandPort.javasrc/main/java/starlight/application/businessplan/required/BusinessPlanQueryPort.javasrc/main/java/starlight/application/businessplan/required/ChecklistGrader.javasrc/main/java/starlight/application/businessplan/required/ChecklistGraderPort.javasrc/main/java/starlight/application/businessplan/required/SpellCheckerPort.javasrc/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.javasrc/main/java/starlight/application/expertApplication/ExpertApplicationCommandService.javasrc/main/java/starlight/application/expertReport/ExpertReportService.javasrc/main/java/starlight/application/expertReport/provided/ExpertReportUseCase.javasrc/main/java/starlight/application/infrastructure/provided/LlmGenerator.javasrc/main/java/starlight/application/member/CredentialService.javasrc/main/java/starlight/application/member/MemberService.javasrc/main/java/starlight/application/member/auth/AuthServiceImpl.javasrc/main/java/starlight/application/member/provided/CredentialUseCase.javasrc/main/java/starlight/application/member/provided/MemberUseCase.javasrc/main/java/starlight/bootstrap/AsyncConfig.javasrc/main/java/starlight/domain/aireport/exception/AiReportErrorType.javasrc/main/java/starlight/shared/enumerate/SectionType.javasrc/test/java/starlight/adapter/ai/AiChecklistGraderTest.javasrc/test/java/starlight/adapter/ai/OpenAiReportGraderTest.javasrc/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.javasrc/test/java/starlight/adapter/ai/util/AiReportResponseParserTest.javasrc/test/java/starlight/adapter/aireport/infrastructure/webapi/ImageControllerIntegrationTest.javasrc/test/java/starlight/adapter/businessplan/webapi/SpellControllerTest.javasrc/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.javasrc/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.javasrc/test/java/starlight/application/businessplan/BusinessPlanServiceImplIntegrationTest.javasrc/test/java/starlight/application/businessplan/BusinessPlanServiceImplUnitTest.javasrc/test/java/starlight/application/member/CredentialServiceImplIntegrationTest.javasrc/test/java/starlight/application/member/CredentialServiceImplUnitTest.javasrc/test/java/starlight/application/member/MemberQueryServiceIntegrationTest.javasrc/test/java/starlight/application/member/MemberQueryServiceUnitTest.javasrc/test/java/starlight/application/member/auth/AuthServiceImplIntegrationTest.javasrc/test/java/starlight/application/member/auth/AuthServiceImplUnitTest.java
💤 Files with no reviewable changes (9)
- src/main/java/starlight/application/aireport/provided/AiReportService.java
- src/main/java/starlight/application/infrastructure/provided/LlmGenerator.java
- src/main/java/starlight/adapter/ai/util/ChecklistCatalog.java
- src/main/java/starlight/adapter/ai/OpenAiReportGrader.java
- src/main/java/starlight/application/aireport/AiReportServiceImpl.java
- src/main/java/starlight/application/aireport/required/AiReportGrader.java
- src/main/java/starlight/application/businessplan/required/ChecklistGrader.java
- src/main/java/starlight/adapter/ai/infra/PromptProvider.java
- src/main/java/starlight/application/businessplan/provided/BusinessPlanService.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-01T07:34:02.762Z
Learnt from: SeongHo5356
Repo: StartUpLight/STARLIGHT_BE PR: 71
File: src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanRepository.java:27-33
Timestamp: 2026-01-01T07:34:02.762Z
Learning: `ExpertAiReportQueryService`에서 사용하는 `findAllByMemberIdOrderByLastSavedAt` 메서드는 AI 리포트를 생성한 사업계획서만 조회 대상이므로 회원당 20~30개 수준으로 제한되어, 전체 조회 방식(페이징 없음)이 적절합니다. 향후 50개 이상으로 증가 시 페이징 추가 예정입니다.
Applied to files:
src/main/java/starlight/adapter/aireport/webapi/AiReportController.javasrc/main/java/starlight/application/aireport/required/AiReportQueryPort.javasrc/main/java/starlight/application/aireport/provided/dto/AiReportResult.javasrc/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/adapter/aireport/report/util/AiReportResponseParser.java
🧬 Code graph analysis (7)
src/main/java/starlight/application/businessplan/required/ChecklistGraderPort.java (1)
src/main/java/starlight/application/businessplan/required/ChecklistGrader.java (2)
ChecklistGrader(7-20)check(16-19)
src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java (1)
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java (1)
Slf4j(19-111)
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (4)
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java (1)
Slf4j(19-111)src/main/java/starlight/adapter/aireport/report/supervisor/ReportSupervisor.java (1)
Slf4j(21-105)src/main/java/starlight/adapter/aireport/report/util/AiReportResponseParser.java (1)
Slf4j(21-479)src/main/java/starlight/adapter/aireport/report/provider/ReportPromptProvider.java (1)
Component(13-73)
src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java (3)
src/main/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProvider.java (1)
Slf4j(18-69)src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java (1)
Slf4j(22-119)src/main/java/starlight/bootstrap/AsyncConfig.java (1)
Slf4j(15-57)
src/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.java (1)
src/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.java (1)
Slf4j(18-59)
src/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.java (1)
src/main/java/starlight/adapter/aireport/report/provider/ReportPromptProvider.java (1)
Component(13-73)
src/main/java/starlight/adapter/businessplan/checklist/agent/SpringAiChecklistAgent.java (2)
src/main/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGrader.java (1)
Slf4j(14-45)src/main/java/starlight/adapter/businessplan/checklist/provider/ChecklistPromptProvider.java (1)
Component(20-118)
🪛 GitHub Actions: Deployment Workflow
src/test/java/starlight/adapter/ai/AiChecklistGraderTest.java
[error] 31-31: SpringAiFullReportGradeAgent cannot be assigned to SpringAiChecklistAgent in test setup; type incompatibility in mocks.
src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java
[error] 57-57: SpringAiReportGrader constructor arguments do not match: expected (List, FullReportGradeAgent, ReportSupervisor, BusinessPlanContentExtractor, Executor) but tests provide (SpringAiFullReportGradeAgent, AiReportResponseParser).
src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java
[error] 166-166: LlmGenerator class not found in test fabric; unresolved reference in integration test bean provision.
src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java
[error] 65-65: AiReportGrader.gradeWithSectionAgents expects (Map<SectionType,String>, String) but test calls with a single String. Argument mismatch in test setup.
[error] 84-84: AiReportService constructor arguments do not match usage in test. Provided: (BusinessPlanQueryPort, BusinessPlanUseCase, AiReportQueryPort, ReportGraderPort, ObjectMapper, OcrProviderPort, AiReportResponseParser, BusinessPlanContentExtractor) expected: (BusinessPlanCommandPort, BusinessPlanQueryPort, BusinessPlanUseCase, AiReportQueryPort, AiReportCommandPort, ReportGraderPort, ObjectMapper, OcrProviderPort, AiReportResponseParser, BusinessPlanContentExtractor).
[error] 119-119: AiReportGrader.gradeWithSectionAgents argument mismatch (Map<SectionType,String>,String) vs test-provided arguments.
src/test/java/starlight/application/businessplan/BusinessPlanServiceImplUnitTest.java
[error] 75-75: BusinessPlanQueryPort.save(BusinessPlan) method not found or not visible in test doubles; cannot stub or verify save calls.
🪛 GitHub Actions: PR Test
src/test/java/starlight/adapter/ai/AiChecklistGraderTest.java
[error] 22-22: cannot find symbol: method generateChecklistArray(SubSectionType,String,List,List) on SpringAiFullReportGradeAgent
src/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.java
[error] 37-37: cannot find symbol: method createChecklistGradingPrompt(SubSectionType,String,List,List) on ReportPromptProvider
src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java
[error] 46-46: cannot find symbol: method generateReport(content) on SpringAiFullReportGradeAgent
src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java
[error] 166-166: LlmGenerator llmGenerator() { ... } references undefined symbol: LlmGenerator
[error] 1-1: Gradle test compilation failed. See log for details.
src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java
[error] 65-65: method gradeWithSectionAgents in interface ReportGraderPort cannot be applied to given types; required: Map<SectionType,String>,String; found: String
[error] 84-84: constructor AiReportService in class AiReportService cannot be applied to given types; found: BusinessPlanQueryPort,BusinessPlanUseCase,AiReportQueryPort,ReportGraderPort,ObjectMapper,OcrProviderPort,AiResponseParser,BusinessPlanContentExtractor; required: (full constructor signature as logged)
src/test/java/starlight/application/businessplan/BusinessPlanServiceImplUnitTest.java
[error] 75-75: when(businessPlanQuery.save(any(BusinessPlan.class)))...; cannot find symbol: method save(BusinessPlan) in BusinessPlanQueryPort
...main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java
Show resolved
Hide resolved
...main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java
Outdated
Show resolved
Hide resolved
src/main/java/starlight/application/aireport/AiReportService.java
Outdated
Show resolved
Hide resolved
src/main/java/starlight/application/businessplan/required/ChecklistGraderPort.java
Show resolved
Hide resolved
src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java
Outdated
Show resolved
Hide resolved
src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java
Outdated
Show resolved
Hide resolved
src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java
Outdated
Show resolved
Hide resolved
src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java
Outdated
Show resolved
Hide resolved
…nessPlanCreationPort)를 통해서 사업계획서 생성 어댑터(BusinessPlanCreationAdapter) 구현
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In @src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java:
- Around line 35-49: The current construction of sectionGradeAgentMap from
sectionGradeAgentList using Collectors.toMap(SectionGradeAgent::getSectionType,
advisor -> advisor) can throw IllegalStateException on duplicate SectionType;
update the collection to handle duplicates defensively by supplying a merge
function or pre-validating the list: change the stream collect to use
Collectors.toMap(SectionGradeAgent::getSectionType, advisor -> advisor,
(existing, incoming) -> { log a warning including the duplicate SectionType and
chosen agent; return existing (or incoming) per desired policy }) or
alternatively detect duplicates in sectionGradeAgentList before collecting and
throw a clear exception or log and skip duplicates so sectionGradeAgentMap is
built deterministically.
- Around line 90-147: The code calls future.get(2, TimeUnit.MINUTES)
sequentially over futureMap which can block up to 2 minutes per section (total
up to 8 minutes) and doesn't cancel timed-out futures; change to wait for all
section futures concurrently with a single global timeout and cancel any
unfinished futures on timeout. Specifically, create a CompletableFuture<Void>
all = CompletableFuture.allOf(futureMap.values().toArray(new
CompletableFuture[0])) and call all.get(2, TimeUnit.MINUTES) (or use
all.orTimeout(...) / all.join with timeout handling), then for each entry in
futureMap: if future.isDone() obtain its result normally, otherwise on
TimeoutException or if not done call future.cancel(true) and return
SectionGradingResult.failure(sectionType, "타임아웃"); ensure exceptions from
completed futures are handled as before and keep logging (refer to futureMap,
sectionGradingExecutor, SectionGradingResult and SectionType).
In
@src/main/java/starlight/adapter/businessplan/creation/BusinessPlanCreationAdapter.java:
- Around line 20-27: The method
BusinessPlanCreationAdapter.createBusinessPlanWithPdf uses the result of
businessPlanUseCase.createBusinessPlanWithPdf without null-safety checks; add
defensive validation to ensure the returned BusinessPlanResult.Result and its
businessPlanId() are non-null (following the existing Assert.notNull() /
defensive pattern used in AiReport.create() and ExpertReport.create()), and if
either is null throw an IllegalStateException with a clear message before
returning result.businessPlanId().
In @src/main/java/starlight/application/aireport/AiReportService.java:
- Around line 141-155: The upsertAiReportWithRawJsonStr method risks TOCTOU
races because it reads via aiReportQueryPort.findByBusinessPlanId and then
conditionally calls AiReport.create or update; fix by enforcing a
single-source-of-truth: add a DB-level unique constraint on business_plan_id
(e.g., @Table(uniqueConstraints = ... ) or migration) and/or protect the
read-modify-write with locking (use optimistic @Version on AiReport or load the
BusinessPlan/AiReport with @Lock(LockModeType.PESSIMISTIC_WRITE) before
create/update), and/or catch unique-constraint violations on save
(businessPlanCommandPort.save/aiReport repository save) to retry the lookup and
return the existing AiReport; apply changes around upsertAiReportWithRawJsonStr,
aiReportQueryPort.findByBusinessPlanId, AiReport.create/AiReport.update and the
save call to ensure only one record per businessPlanId.
In @src/main/java/starlight/shared/enumerate/SectionType.java:
- Around line 5-7: SectionType currently depends on
application.aireport.provided.dto.AiReportResult via the scoreExtractor; remove
that dependency by deleting the AiReportResult import and the scoreExtractor
logic from the SectionType enum, then implement a new mapper in the application
layer (e.g., SectionScoreExtractor) that holds the Map<SectionType,
Function<AiReportResult,Integer>> and exposes a static extractScore(SectionType,
AiReportResult) method; finally update all call sites to use
SectionScoreExtractor.extractScore(...) and remove any direct AiReportResult
references from shared.enumerate.SectionType so shared/domain no longer depends
on application.
In
@src/test/java/starlight/application/member/CredentialServiceIntegrationTest.java:
- Line 20: @ContextConfiguration의 classes 배열에 인터페이스(CredentialService)가 들어가 있어
스프링이 빈을 생성하지 못할 수 있습니다; TestBeans 대신 또는 함께 실제 구현 클래스를 지정하도록 변경하여 컨텍스트가 구체 클래스(예:
CredentialServiceImpl)를 로드하게 하세요. 즉, @ContextConfiguration(classes =
{CredentialServiceImpl.class,
CredentialServiceIntegrationTest.TestBeans.class})처럼 구체 구현을 추가하거나 TestBeans 내부에서
CredentialServiceImpl을 @Bean으로 등록해 CredentialService sut가 인터페이스 타입으로 정상 주입되도록
수정하세요.
In @src/test/java/starlight/application/member/CredentialServiceUnitTest.java:
- Around line 20-21: The test uses @InjectMocks on the CredentialService
interface which cannot be instantiated; replace the injected field with the
concrete implementation class (e.g., CredentialServiceImpl) so Mockito can
create and inject it (update the import and the declaration from
CredentialService to CredentialServiceImpl), or alternatively remove
@InjectMocks and manually construct a CredentialServiceImpl in the test setup by
passing mocked dependencies (the mocks you already declared) to the
implementation's constructor and use those for injection.
🧹 Nitpick comments (26)
src/test/java/starlight/application/businessplan/BusinessPlanServiceIntegrationTest.java (1)
43-45: ChecklistGraderPort 스텁이 서브섹션별 체크리스트 길이 변화에 취약합니다.현재는 무조건 길이 5의 리스트를 반환해서,
SubSectionType별 체크리스트 문항 수가 달라지거나 확장되면(또는 검증 로직이 추가되면) 테스트가 의미 없이 통과/실패할 수 있습니다. “길이”를 도메인/정책에서 가져오거나 최소한 상수로 의도를 고정하는 편이 안전합니다.제안 diff
@@ import org.springframework.context.annotation.Import; @@ +import java.util.Collections; import java.util.List; @@ @Bean ChecklistGraderPort checklistGrader() { - return (subSectionType, content) -> List.of(false, false, false, false, false); + final int checklistSize = 5; // TODO: SubSectionType별 정책/상수로 대체 고려 + return (subSectionType, content) -> Collections.nCopies(checklistSize, false); }src/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.java (1)
45-61: 패딩 동작 테스트는 좋으나, Mock 검증 일관성을 높이면 더 좋습니다.LLM 결과가 5개 미만일 때
false로 채우는 중요한 엣지 케이스를 테스트하고 있습니다. 다만 첫 번째 테스트(check_returnsFromLlm)와 달리 Mock 상호작용 검증(verify())이 없어 일관성이 떨어집니다.♻️ Mock 검증 추가 제안
첫 번째 테스트처럼 명시적인 검증을 추가하면 테스트 일관성과 신뢰도가 높아집니다:
SpringAiChecklistGrader sut = new SpringAiChecklistGrader(generator, catalog); List<Boolean> result = sut.check(SubSectionType.OVERVIEW_BASIC, "input text"); assertThat(result).containsExactly(true, false, false, false, false); + verify(generator).generateChecklistArray( + eq(SubSectionType.OVERVIEW_BASIC), + eq("input text"), + eq(List.of("c1", "c2", "c3", "c4", "c5")), + eq(List.of("d1", "d2", "d3", "d4", "d5")) + ); + verify(catalog).getCriteriaBySubSectionType(SubSectionType.OVERVIEW_BASIC); + verify(catalog).getDetailedCriteriaBySubSectionType(SubSectionType.OVERVIEW_BASIC); }src/test/java/starlight/application/member/MemberServiceIntegrationTest.java (1)
19-26:@DataJpaTest슬라이스 테스트인데 “IntegrationTest”/구체 타입 의존이 헷갈릴 수 있어요
지금 구성은 “JPA 슬라이스 + 서비스/어댑터만 수동 Import”라서, 테스트 의도가 “서비스 레벨 통합”이라면@SpringBootTest가 더 맞고, 의도가 “영속성 포함 서비스 단위(슬라이스)”라면 클래스명/주입 타입을 포트(예:MemberUseCase) 기준으로 맞추는 편이 헥사고날 톤과 일관적입니다. 또 서비스가 분리 트랜잭션(REQUIRES_NEW등)을 쓰면save()만으로는 조회 가시성이 흔들릴 수 있으니flush()로 결정성을 올려두는 걸 권합니다.제안 diff (결정성 + 포트 기준 주입 예시)
@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) @Import({MemberService.class, MemberJpa.class}) class MemberServiceIntegrationTest { @Autowired - MemberService sut; + MemberUseCase sut; @Autowired MemberRepository memberRepository; @Test void getUserByEmail_DB저장후_정상조회() { Member preSaved = Member.create("name", "[email protected]", null, MemberType.FOUNDER, null, "img.png"); memberRepository.save(preSaved); + memberRepository.flush(); Member found = sut.getUserByEmail("[email protected]"); assertEquals("[email protected]", found.getEmail()); }확인 부탁:
MemberService가 내부에서 추가 빈(예: 외부 Port, Encoder 등)을 요구하지 않도록 현재 슬라이스 구성이 충분한지(테스트 컨텍스트 부팅 안정성), 그리고 트랜잭션 전파가 기본(REQUIRED)인지 확인해 주세요.src/test/java/starlight/application/businessplan/BusinessPlanServiceUnitTest.java (3)
12-16: 포트/DTO 리네이밍 반영은 깔끔합니다(다만 Mock 필드명 일관성만 맞추면 더 좋아요).
BusinessPlanCommandPort/BusinessPlanQueryPort/ChecklistGraderPort,BusinessPlanResult/SubSectionResult로의 변경 반영 방향은 좋습니다. 다만 현재 필드명이checklistGrader로 남아있어 “Port” 기준 네이밍 일관성이 살짝 깨집니다(리뷰 요청사항과도 연결).제안 diff (테스트 가독성/일관성)
- private ChecklistGraderPort checklistGrader; + private ChecklistGraderPort checklistGraderPort; @@ - when(checklistGrader.check( + when(checklistGraderPort.check( eq(SubSectionType.OVERVIEW_BASIC), eq("updated content"))).thenReturn(updatedChecks);
37-38: Mockito Strictness.LENIENT는 테스트 품질 저하/누락 스텁 은닉 리스크가 있어 최소화 권장합니다.
클래스 전체를 LENIENT로 두면 “실제로는 안 쓰는 스텁/잘못된 호출”이 테스트에서 조용히 지나갈 수 있습니다. 필요 케이스만lenient()로 국소 적용하거나 기본은STRICT_STUBS유지가 안전합니다.
79-80:businessPlanCommand.save(...).thenAnswer(...)스텁이 반복돼서 유지보수 비용이 큽니다.
여러 테스트에서 동일한 “save는 인자로 받은 엔티티를 그대로 반환” 스텁이 반복됩니다.@BeforeEach에 기본 스텁으로 올려두고, 예외적인 테스트만 override 하는 편이 변경에 강합니다.제안 diff (중복 제거)
@BeforeEach void setup() { + when(businessPlanCommand.save(any(BusinessPlan.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); when(objectMapper.valueToTree(any())) .thenReturn(new com.fasterxml.jackson.databind.ObjectMapper().createObjectNode()); @@ void createBusinessPlan_savesRoot() { - when(businessPlanCommand.save(any(BusinessPlan.class))) - .thenAnswer(invocation -> invocation.getArgument(0)); - BusinessPlanResult.Result created = sut.createBusinessPlan(1L);Also applies to: 92-93, 113-114, 158-159, 197-198, 284-285, 379-380, 442-443, 491-492, 519-520, 553-554
src/main/java/starlight/adapter/businessplan/creation/BusinessPlanCreationAdapter.java (1)
9-12: JavaDoc의 설명이 다소 모호합니다."필요한 기능만 노출합니다"라고 했지만, 실제로는
BusinessPlanUseCase.createBusinessPlanWithPdf()의 모든 기능을 그대로 위임하고 있습니다. 이 어댑터의 진짜 역할은 Anti-Corruption Layer로서 AiReport 컨텍스트와 BusinessPlan 컨텍스트 간의 경계를 명확히 하고, 반환 타입을 변환(Result → Long)하는 것입니다.📝 JavaDoc 개선 제안
/** - * BusinessPlanCreationPort의 구현체 - * BusinessPlanUseCase를 래핑하여 필요한 기능만 노출합니다. + * AiReport 컨텍스트를 위한 BusinessPlanCreationPort 구현체 + * + * BusinessPlan 컨텍스트의 BusinessPlanUseCase를 호출하여 + * 사업계획서 생성 기능을 제공하는 Anti-Corruption Layer입니다. + * BusinessPlanResult를 Long ID로 변환하여 컨텍스트 간 결합도를 낮춥니다. */src/main/java/starlight/application/aireport/required/BusinessPlanCreationPort.java (1)
3-6: 헥사고날 아키텍처의 의존성 역전 원칙을 잘 적용했습니다.AiReport 컨텍스트가 BusinessPlan 생성 기능을 Required Port로 정의하여 의존성 방향을 올바르게 설정했습니다. 검증 결과, BusinessPlan 컨텍스트에서 AiReport로의 역참조가 없으므로 순환 의존성 우려는 없습니다.
한 가지 개선 사항을 제안합니다:
JavaDoc 추가 권장: 현재 프로젝트의 Port 인터페이스들이 모두 문서화가 없는 상태이지만, 이 Port는 다른 컨텍스트에서 구현되는 경계점이므로 메서드 파라미터와 반환값에 대한 문서화를 추가하면 구현체 개발자에게 도움이 될 것입니다.
📝 JavaDoc 추가 제안
public interface BusinessPlanCreationPort { + /** + * PDF를 포함한 사업계획서를 생성합니다. + * + * @param title 사업계획서 제목 + * @param pdfUrl PDF 파일 URL + * @param memberId 회원 ID + * @return 생성된 사업계획서 ID + */ Long createBusinessPlanWithPdf(String title, String pdfUrl, Long memberId); }src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java (1)
130-131: ExecutorService 리소스 누수 가능성테스트에서
Executors.newFixedThreadPool(4)를 사용하고 있으나, 테스트 종료 후 ExecutorService를 shutdown하지 않아 스레드 리소스가 누수될 수 있습니다.@AfterEach에서 shutdown하거나, 동기 실행을 위한MoreExecutors.directExecutor()(Guava) 또는Runnable::run기반 직접 실행 Executor 사용을 권장합니다.♻️ 테스트용 동기 Executor 사용 제안
- // 실제 Executor 사용 (비동기 실행을 위해) - Executor executor = Executors.newFixedThreadPool(4); + // 테스트용 동기 Executor 사용 + Executor executor = Runnable::run;또는 비동기 테스트가 필요하다면:
private ExecutorService executor; @BeforeEach void setUp() { executor = Executors.newFixedThreadPool(4); } @AfterEach void tearDown() { executor.shutdown(); }src/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.java (3)
19-19: 미사용 @slf4j 어노테이션
@Slf4j어노테이션이 추가되었으나 현재 파일에서log사용이 보이지 않습니다. 실제 로깅이 필요한 경우가 아니라면 제거하거나, 향후 사용 계획이 있다면 해당 로깅 로직도 함께 추가해주세요.
99-123: LGTM!섹션별 컨텐츠 추출 로직이 기존
extractSectionContent메서드를 잘 재사용하고 있습니다. 헥사고날 아키텍처에서 섹션별 채점을 위한 입력 데이터 준비에 적합한 구조입니다.성능 최적화가 필요하다면
HashMap대신EnumMap<SectionType, String>을 사용할 수 있습니다.
125-148: 주석 처리된 코드 정리 필요PDF 파싱을 위한
extractSectionContentsFromText메서드가 주석 처리되어 있습니다. PR 목적에 따르면 PDF는 현재 FullReportGradeAgent를 사용하므로 의도된 것이지만, 주석 코드는 별도 이슈로 추적하거나 완전히 제거하는 것이 좋습니다.이 TODO를 추적하기 위한 GitHub 이슈를 생성해 드릴까요?
src/main/java/starlight/adapter/aireport/report/config/SectionAdvisorConfig.java (1)
29-43: LGTM!SectionType을 기반으로 동적으로 SectionGradeAgent 빈을 생성하는 구성이 잘 되어 있습니다. OVERVIEW 제외 로직도 명확합니다.
빈 이름을
sectionAdvisors에서sectionGradeAgents로 변경하면 실제 반환 타입(List<SectionGradeAgent>)과 더 일관성 있는 명명이 됩니다.@Bean - public List<SectionGradeAgent> sectionAdvisors() { + public List<SectionGradeAgent> sectionGradeAgents() {src/main/java/starlight/application/aireport/required/ReportGraderPort.java (1)
8-13: LGTM!헥사고날 아키텍처의 아웃바운드 포트로서 잘 정의되어 있습니다. 두 가지 채점 방식(섹션별/전체 프롬프트)을 명확하게 분리하고 있습니다.
각 메서드의 사용 시점(섹션별 vs PDF 전체 프롬프트)에 대한 Javadoc 추가를 권장합니다.
public interface ReportGraderPort { + /** + * 섹션별 에이전트를 사용한 병렬 채점 (BusinessPlan 입력용) + */ AiReportResult gradeWithSectionAgents(Map<SectionType, String> sectionContents, String fullContent); + /** + * 전체 프롬프트를 사용한 채점 (PDF 입력용) + */ AiReportResult gradeWithFullPrompt(String content); }src/main/java/starlight/shared/enumerate/SectionType.java (1)
27-32: scoreExtractor 반환값 null 처리 필요
scoreExtractor.apply(response)가 null을 반환할 경우(예: AiReportResult의 score 필드가 null인 경우) 그대로 null이 반환됩니다. 일관성을 위해 null 체크를 추가하는 것이 좋습니다.public Integer extractScore(AiReportResult response) { if (scoreExtractor == null || response == null) { return 0; } - return scoreExtractor.apply(response); + Integer score = scoreExtractor.apply(response); + return score != null ? score : 0; }src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.java (2)
59-63: 예외 처리 시 원인(cause) 정보 손실일반 예외를
AiReportException으로 래핑할 때 원인 예외가 보존되지 않아 디버깅이 어려울 수 있습니다.} catch (AiReportException e) { throw e; } catch (Exception e) { - throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED); + log.error("AI grading failed", e); + throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED, e); }
AiReportException이 cause를 받는 생성자를 지원하는지 확인해주세요.
39-41: 매직 넘버를 상수 또는 설정으로 추출
getQuestionAnswerAdvisor(0.6, 3, null)의 파라미터들이 매직 넘버로 사용되고 있습니다. 의미를 명확히 하기 위해 상수나 설정 프로퍼티로 추출하는 것이 좋습니다.+ private static final double SIMILARITY_THRESHOLD = 0.6; + private static final int TOP_K = 3; + @Override public AiReportResult gradeFullReport(String content) { // ... - QuestionAnswerAdvisor qaAdvisor = advisorProvider - .getQuestionAnswerAdvisor(0.6, 3, null); + QuestionAnswerAdvisor qaAdvisor = advisorProvider + .getQuestionAnswerAdvisor(SIMILARITY_THRESHOLD, TOP_K, null);src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (1)
29-34:contentExtractor필드가 현재 클래스에서 사용되지 않음(Line 32) 현재 구현만 보면 주입/보관이 불필요해 보입니다. 의도(추후 확장) 아니라면 제거해도 됩니다.
src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java (3)
35-61: 시간 기준을LocalDateTime.now()로 직접 박아두면 테스트/운영(Clock 변경) 대응이 약함서킷브레이커는 보통
Instant+Clock주입(또는System.currentTimeMillis) 형태가 운영/테스트에서 다루기 쉽습니다.
63-76: HALF_OPEN에서 CLOSED로 전환 시successCount도 리셋하는 게 상태 해석이 명확함현재는 (Line 68-71)
failureCount만 0으로 만들고successCount는 유지됩니다. 동작엔 큰 문제는 없지만, 상태/로그 해석이 깔끔해집니다.Proposed diff
if (state.compareAndSet(State.HALF_OPEN, State.CLOSED)) { failureCount.set(0); + successCount.set(0); log.info("Circuit breaker CLOSED after successful recovery"); }
114-123:recordFailure(sectionType, errorMessage)파라미터가 로직/로그 어디에도 반영되지 않음에러 메시지를 받는 API라면 로그에 포함하거나, 아니면 오버로드를 단순화하는 편이 낫습니다.
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java (1)
86-105:sectionResponse.sectionScores()가 null이면 NPE 가능현재 (Line 98)에서 바로
.stream()을 타고 있어, 파서가 항상 non-null 리스트를 보장하지 않으면 파싱 성공 케이스에서도 실패로 떨어질 수 있습니다.Proposed diff
- sectionScore = sectionResponse.sectionScores().stream() + sectionScore = java.util.Optional.ofNullable(sectionResponse.sectionScores()) + .orElseGet(java.util.List::of) + .stream() .filter(ss -> sectionTypeString.equals(ss.sectionType())) .findFirst() .orElse(null);src/main/java/starlight/application/aireport/util/AiReportResponseParser.java (1)
258-288:"text"필드 정규식이 greedy라 오탐/과캡처로 파싱 실패를 유발할 수 있음(Line 271-273)의
"(.*)"는 마지막 따옴표까지 먹을 수 있어, 뒤에 다른 필드가 있거나\"가 섞인 응답에서 깨질 확률이 있습니다.Proposed diff (non-greedy)
- java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( - "\"text\"\\s*:\\s*\"(.*)\"", - java.util.regex.Pattern.DOTALL); + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( + "\"text\"\\s*:\\s*\"(.*?)\"", + java.util.regex.Pattern.DOTALL);src/main/java/starlight/application/aireport/AiReportService.java (1)
130-139:JsonProcessingException을RuntimeException으로 던지면 도메인 에러타입 일관성이 깨짐(Line 133-137)에서 직렬화 실패는 “AI 채점 실패/저장 실패”로 묶여야 운영에서 원인 추적/에러 응답이 일관됩니다.
AiReportException으로 변환하는 것을 권장합니다.Proposed diff
try { rawJsonString = objectMapper.writeValueAsString(gradingJsonNode); } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to convert JsonNode to string", e); + log.error("Failed to serialize ai report grading result", e); + throw new AiReportException(AiReportErrorType.AI_GRADING_FAILED); }src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java (1)
107-115: 테스트에서ObjectMapper를 2개 만들면(서로 설정 다를 수 있음) 환경 차이로 흔들릴 수 있음(Line 112-115)
responseParser()가 별도new ObjectMapper()를 쓰고 있어, 추후 커스텀 설정이 들어가면 테스트/운영 결과가 달라질 수 있습니다.Proposed diff
@Bean - AiReportResponseParser responseParser() { - return new AiReportResponseParser(new ObjectMapper()); + AiReportResponseParser responseParser(ObjectMapper objectMapper) { + return new AiReportResponseParser(objectMapper); }src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java (1)
63-109: 회귀 방지:gradeWithSectionAgents(sectionContents, extractedContent)호출 인자까지 verify 해두면 좋음현재는 save/updateStatus 위주로만 검증되어, “섹션 추출/전체 본문 전달” 계약이 깨져도 테스트가 통과할 수 있습니다.
Proposed diff
// then assertThat(result).isNotNull(); + verify(aiReportGrader).gradeWithSectionAgents(eq(sectionContents), eq(extractedContent)); verify(plan).updateStatus(PlanStatus.AI_REVIEWED); verify(aiReportCommand).save(any(AiReport.class));
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
src/main/java/starlight/adapter/aireport/persistence/AiReportJpa.javasrc/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.javasrc/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.javasrc/main/java/starlight/adapter/aireport/report/config/SectionAdvisorConfig.javasrc/main/java/starlight/adapter/aireport/report/supervisor/SpringAiReportSupervisor.javasrc/main/java/starlight/adapter/businessplan/creation/BusinessPlanCreationAdapter.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/application/aireport/required/AiReportCommandPort.javasrc/main/java/starlight/application/aireport/required/AiReportQueryPort.javasrc/main/java/starlight/application/aireport/required/BusinessPlanCreationPort.javasrc/main/java/starlight/application/aireport/required/ReportGraderPort.javasrc/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/main/java/starlight/application/businessplan/util/BusinessPlanContentExtractor.javasrc/main/java/starlight/shared/enumerate/SectionType.javasrc/test/java/starlight/adapter/ai/OpenAiReportGraderTest.javasrc/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.javasrc/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.javasrc/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.javasrc/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.javasrc/test/java/starlight/application/aireport/AiReportServiceUnitTest.javasrc/test/java/starlight/application/aireport/util/AiReportResponseParserTest.javasrc/test/java/starlight/application/businessplan/BusinessPlanServiceIntegrationTest.javasrc/test/java/starlight/application/businessplan/BusinessPlanServiceUnitTest.javasrc/test/java/starlight/application/member/CredentialServiceIntegrationTest.javasrc/test/java/starlight/application/member/CredentialServiceUnitTest.javasrc/test/java/starlight/application/member/MemberServiceIntegrationTest.javasrc/test/java/starlight/application/member/MemberServiceUnitTest.java
💤 Files with no reviewable changes (2)
- src/test/java/starlight/adapter/ai/OpenAiReportGraderTest.java
- src/test/java/starlight/adapter/ai/infra/OpenAiGeneratorTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/starlight/adapter/aireport/persistence/AiReportJpa.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-01T07:34:02.762Z
Learnt from: SeongHo5356
Repo: StartUpLight/STARLIGHT_BE PR: 71
File: src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanRepository.java:27-33
Timestamp: 2026-01-01T07:34:02.762Z
Learning: `ExpertAiReportQueryService`에서 사용하는 `findAllByMemberIdOrderByLastSavedAt` 메서드는 AI 리포트를 생성한 사업계획서만 조회 대상이므로 회원당 20~30개 수준으로 제한되어, 전체 조회 방식(페이징 없음)이 적절합니다. 향후 50개 이상으로 증가 시 페이징 추가 예정입니다.
Applied to files:
src/main/java/starlight/application/aireport/required/AiReportQueryPort.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java
🧬 Code graph analysis (5)
src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java (3)
src/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.java (1)
DisplayName(15-62)src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java (1)
DisplayName(34-272)src/test/java/starlight/application/aireport/util/AiReportResponseParserTest.java (1)
DisplayName(14-151)
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java (1)
src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java (1)
Slf4j(13-124)
src/main/java/starlight/adapter/aireport/report/circuitbreaker/SectionGradingCircuitBreaker.java (1)
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java (1)
Slf4j(19-111)
src/test/java/starlight/application/aireport/AiReportServiceIntegrationTest.java (1)
src/test/java/starlight/application/aireport/util/AiReportResponseParserTest.java (1)
DisplayName(14-151)
src/test/java/starlight/application/aireport/AiReportServiceUnitTest.java (4)
src/main/java/starlight/domain/aireport/exception/AiReportException.java (1)
AiReportException(6-11)src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java (1)
DisplayName(25-169)src/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.java (1)
DisplayName(15-62)src/test/java/starlight/application/aireport/util/AiReportResponseParserTest.java (1)
DisplayName(14-151)
⏰ 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). (2)
- GitHub Check: test
- GitHub Check: build-and-push
🔇 Additional comments (23)
src/test/java/starlight/application/businessplan/BusinessPlanServiceIntegrationTest.java (2)
29-35: @DataJpaTest 슬라이스에 Application Service(@import BusinessPlanService) 주입은 깨지기 쉬우니 의도 확인이 필요합니다.
@DataJpaTest는 JPA 슬라이스라서,BusinessPlanService가 트랜잭션/이벤트/스케줄러/Executor/외부 어댑터 등 “JPA 외” 빈에 의존하기 시작하면 테스트 컨텍스트가 쉽게 깨질 수 있어요. 현재 PR에서 병렬 처리/서킷브레이커/헥사고날 확장이 있는 만큼, 이 테스트가 “JPA + 최소 Port Stub” 범위로 계속 유지될 수 있는지 확인 부탁드립니다.
추가로 PR 목표가 “Service→UseCase”라면 여기서도BusinessPlanService가 인바운드 유스케이스인지(혹은 네이밍 예외인지) 일관성 체크가 필요해 보입니다.
14-15: Port 타입으로 테스트 wiring 정리한 방향은 좋습니다.
ChecklistGraderPort로 전환한 덕분에 테스트가 구현체에 덜 결합되고(헥사고날 의도에 부합), 향후 어댑터 교체/확장에도 안정적입니다.src/test/java/starlight/adapter/businessplan/checklist/SpringAiChecklistGraderTest.java (2)
1-16: 패키지 재구성 및 헥사고날 아키텍처 적용이 잘 반영되었습니다.패키지명 변경(
adapter.ai→adapter.businessplan.checklist)과 새로운 의존성(SpringAiChecklistAgent,ChecklistPromptProvider) 도입이 PR 목표와 일치합니다.DisplayName추가로 테스트 가독성도 개선되었네요.
18-43: 체크리스트 채점 흐름이 명확하게 테스트되었습니다.Mock 설정과 검증이 체계적입니다.
SpringAiChecklistAgent의 생성 로직과ChecklistPromptProvider에서 가져온 기준 항목들이 올바르게 조합되는지 상세히 검증하고 있어 좋습니다.src/test/java/starlight/application/member/MemberServiceUnitTest.java (2)
28-51: 테스트 커버리지와 구조가 잘 구성되어 있습니다.Command(createUser)와 Query(getUserByEmail) 작업 모두에 대해 정상 케이스와 예외 케이스를 적절히 테스트하고 있습니다. Port 인터페이스를 mock으로 사용하는 구조도 헥사고날 아키텍처 패턴에 잘 부합합니다.
기존
MemberQueryServiceUnitTest에서MemberServiceUnitTest로의 리네이밍도 테스트 범위(Command + Query)를 고려할 때 더 적합한 명명입니다.Also applies to: 53-64
21-21: 현재 구조는 헥사고날 아키텍처 명명 규칙을 이미 준수하고 있습니다.MemberService는 이미 MemberUseCase 인터페이스를 구현하는 구조로 되어 있습니다. 이는 StarLight 전체에서 일관되게 따르는 패턴입니다:
- 인터페이스:
MemberUseCase(provided 패키지)- 구현 클래스:
MemberService(application 패키지)다른 도메인들도 동일한 패턴을 따르고 있어서(BusinessPlanService ← BusinessPlanUseCase, AiReportService ← AiReportUseCase 등), 테스트에서 구현 클래스인 MemberService를 SUT로 사용하는 것은 올바릅니다.
Likely an incorrect or invalid review comment.
src/test/java/starlight/application/businessplan/BusinessPlanServiceUnitTest.java (1)
350-355: 정렬 순서 확정이 필요하지 않습니다. 현재 테스트는 안전합니다.
SubSectionType.values()는 Java enum의 선언 순서를 보장하며, Stream의filter()및toList()도 순서를 유지합니다. Map/Set 순회와는 무관하므로containsExactly(...)로 순서를 검증하는 것이 적절합니다.Likely an incorrect or invalid review comment.
src/main/java/starlight/application/aireport/required/AiReportQueryPort.java (1)
7-10: 깔끔한 Query Port 분리입니다!Command와 Query 책임을 명확히 분리한 포트 인터페이스 설계가 훌륭합니다.
save()메서드를AiReportCommandPort로 이동시켜 CQRS 패턴을 잘 따르고 있습니다.src/main/java/starlight/adapter/businessplan/creation/BusinessPlanCreationAdapter.java (1)
1-1: 어댑터의 패키지 위치를 재검토해주세요.현재 이 어댑터는
adapter.businessplan.creation패키지에 위치하지만, AiReport 컨텍스트가 BusinessPlan 기능을 사용하기 위한 어댑터이므로adapter.aireport.*패키지 아래에 위치하는 것이 헥사고날 아키텍처 관점에서 더 적절해 보입니다.근거:
- 이 어댑터는 AiReport 컨텍스트의 Required Port(
BusinessPlanCreationPort)를 구현합니다- 헥사고날 아키텍처에서 어댑터는 자신이 속한 컨텍스트 내에 위치해야 합니다
- 현재 위치는 BusinessPlan 컨텍스트 영역인데, 실제로는 AiReport 컨텍스트를 위한 Anti-Corruption Layer입니다
제안하는 패키지 구조:
starlight.adapter.aireport.businessplan.BusinessPlanCreationAdapter또는starlight.adapter.aireport.external.BusinessPlanCreationAdapter팀 내 컨벤션을 확인하여 적절한 위치를 결정해주세요.
src/main/java/starlight/application/aireport/required/AiReportCommandPort.java (1)
5-8: Command/Query 분리가 완벽합니다!
AiReportCommandPort와AiReportQueryPort로 명령과 조회 책임을 명확히 분리한 설계가 훌륭합니다. CQRS 패턴을 따라 각 포트가 단일 책임을 가지도록 잘 구성했습니다.src/test/java/starlight/application/aireport/util/AiReportResponseParserTest.java (7)
1-19: 패키지 및 타입 변경이 정확히 반영되었습니다.
AiReportResponse에서AiReportResult로의 타입 변경과 패키지 재구성이 테스트 코드에 일관되게 적용되었습니다. 테스트 설정도 깔끔합니다.
20-57: 유효한 JSON 파싱 테스트가 포괄적입니다.모든 필수 필드(4개 점수 필드, 강점/약점, 섹션별 상세 점수)를 검증하고 있어 훌륭합니다. 특히
gradingListScores가 JSON 문자열로 이스케이프되어 있는 실제 응답 형식을 반영한 점이 좋습니다.
59-77: Null 안전성 테스트가 적절합니다.Null 입력과 빈 문자열에 대해
AI_RESPONSE_PARSING_FAILED예외를 발생시키는 방어 로직을 잘 검증하고 있습니다.
79-95: 필수 필드 누락 검증이 견고합니다.점수 필드가 없는 부분 응답에 대해 파싱 실패를 보장하는 것은 AI 응답의 무결성을 지키는 중요한 검증입니다.
97-118: 기본값 응답 거부 로직이 인상적입니다!모든 점수가 0인 "기본값" 응답을 거부하는 것은 AI가 실제로 채점하지 않고 템플릿 응답만 반환하는 경우를 방지하는 훌륭한 방어 로직입니다. PR 요약에 언급된 "0과 null 구분" 개선이 이런 맥락에서 중요하다는 것을 잘 보여줍니다.
120-137: 중첩된text필드 처리가 유연합니다.일부 AI 응답이
{"text": "{실제JSON}"}으로 래핑되는 경우를 처리하는 것은 실전적인 방어 코드입니다. 프로덕션에서 발생할 수 있는 예외 케이스를 잘 고려했습니다.
139-151: 잘못된 JSON 형식 처리가 완벽합니다.파싱 불가능한 입력에 대한 예외 처리를 검증하여 테스트 커버리지를 완성했습니다. 전체적으로
AiReportResponseParser의 모든 주요 시나리오를 다루는 견고한 테스트 스위트입니다!src/test/java/starlight/adapter/aireport/report/SpringAiReportGraderTest.java (2)
28-66: LGTM!전체 프롬프트 채점 경로에 대한 테스트가 잘 구성되어 있습니다. AAA 패턴을 따르고, FullReportGradeAgent의 호출을 검증하며, 모든 주요 필드에 대한 assertion이 포함되어 있습니다.
68-168: LGTM!섹션별 에이전트 채점 경로에 대한 테스트가 잘 구성되어 있습니다. 4개의 섹션 에이전트와 Supervisor 모킹이 적절하며, 각 에이전트가 올바른 섹션 내용으로 호출되는지 검증하고 있습니다.
src/main/java/starlight/adapter/aireport/report/supervisor/SpringAiReportSupervisor.java (3)
72-75: 실패 시 빈 리스트 반환에 대한 검토 필요예외 발생 시 빈 리스트를 반환하면 호출자가 실패를 인지하기 어렵습니다. 강점/약점이 없는 정상 결과와 실패를 구분할 수 없게 됩니다.
의도된 동작인지 확인해주세요. 대안으로:
- 실패 정보를 포함한 래퍼 객체 반환
- 예외를 상위로 전파하고 호출자가 처리
- 최소한 WARN 레벨 로깅 (현재 ERROR 사용 중이므로 양호)
현재 구현이 섹션 채점 실패와 독립적으로 동작해야 한다면, 빈 리스트 반환은 합리적인 선택입니다.
54-68: LGTM!Spring AI의 ChatClient 사용 패턴이 적절합니다. temperature 0.0과 topP 0.1 설정으로 결정적(deterministic) 응답을 유도하고 있습니다.
78-104: LGTM!섹션 채점 결과를 포맷팅하고 PromptTemplate을 활용한 프롬프트 구성이 잘 되어 있습니다. 성공/실패 케이스 모두 적절하게 처리하고 있습니다.
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiFullReportGradeAgent.java (1)
29-57: LGTM!전체 리포트 채점 로직이 잘 구현되어 있습니다. 입력 검증 → 프롬프트 생성 → LLM 호출 → 응답 검증 → 파싱의 흐름이 명확합니다.
src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java
Show resolved
Hide resolved
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java
Show resolved
Hide resolved
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java
Show resolved
Hide resolved
src/main/java/starlight/adapter/businessplan/creation/BusinessPlanCreationAdapter.java
Show resolved
Hide resolved
src/main/java/starlight/application/aireport/util/AiReportResponseParser.java
Show resolved
Hide resolved
src/test/java/starlight/application/member/CredentialServiceIntegrationTest.java
Show resolved
Hide resolved
src/test/java/starlight/application/member/CredentialServiceUnitTest.java
Show resolved
Hide resolved
- CompletableFuture.allOf()를 통해 병렬처리 결과를 한번에 받을 수 있도록 하게 함 - 타임아웃 발생시에 cancel()을 통해 스레드 자원을 해제시키도록 함
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@src/main/java/starlight/application/aireport/util/AiReportResponseParser.java`:
- Around line 191-202: The section-score parsing currently uses
jsonNode.path(...).asInt(0) and a later ternary that coerces nulls to 0, which
hides the difference between "field missing/null" and "explicitly 0"; change the
score fields (problemRecognitionScore, feasibilityScore, growthStrategyScore,
teamCompetenceScore) from primitive int to Integer so they can hold null, remove
the redundant asInt(0) default (use jsonNode.has(...) &&
!jsonNode.path(...).isNull() then jsonNode.path(...).asInt() without a default),
and then adjust the later conversion/ternary logic that currently maps null to 0
so it either preserves nulls (if you need to distinguish missing vs zero) or
explicitly sets 0 at the final output site with a clear, single place decision;
update AiReportResponseParser to reflect these changes.
♻️ Duplicate comments (4)
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (2)
43-51: 중복 SectionType 에이전트 처리 개선됨
IllegalStateException을 catch하여 명시적인AiReportException으로 변환하는 방식이 적용되었습니다. 이전 리뷰에서 제안된 방어적 처리가 반영되었습니다.다만, 로깅 후 커스텀 예외를 던지는 현재 방식은 fail-fast 전략으로 적절하지만, 운영 환경에서 어떤 에이전트가 중복인지 추적하기 어려울 수 있습니다.
135-155: 전체 타임아웃 및 Future 취소 로직 적용됨이전 리뷰에서 지적된 "섹션별 2분 × 4 = 최대 8분" 문제가 해결되었습니다.
CompletableFuture.allOf를 사용한 전체 2분 타임아웃과 타임아웃 시 미완료 Future 취소 로직이 적용되어 스레드 자원이 적절히 해제됩니다.src/main/java/starlight/application/aireport/util/AiReportResponseParser.java (1)
159-161: 로깅 민감도 개선됨이전 리뷰에서 지적된 원문 LLM 응답 로깅 문제가 해결되었습니다. 예외 발생 시 원문 대신 예외만 전파하여 개인정보 유출 리스크가 줄었습니다.
src/main/java/starlight/application/aireport/AiReportService.java (1)
141-156: 유니크 제약 위반 예외 처리 필요
AiReport엔티티에business_plan_id유니크 제약이 추가되어 데이터 무결성은 보장되지만, 동시 요청 시DataIntegrityViolationException이 발생하면 500 에러로 이어집니다.유니크 제약 위반 예외를 catch하여 기존 레코드를 조회 후 업데이트하는 재시도 로직을 추가하면 더 견고해집니다.
🔧 예외 처리 추가 제안
private AiReport upsertAiReportWithRawJsonStr(String rawJsonString, BusinessPlan plan) { - Optional<AiReport> existingReport = aiReportQueryPort.findByBusinessPlanId(plan.getId()); - - AiReport aiReport; - if (existingReport.isPresent()) { - aiReport = existingReport.get(); - aiReport.update(rawJsonString); - } else { - aiReport = AiReport.create(plan.getId(), rawJsonString); - } - - plan.updateStatus(PlanStatus.AI_REVIEWED); - businessPlanCommandPort.save(plan); - - return aiReportCommandPort.save(aiReport); + try { + Optional<AiReport> existingReport = aiReportQueryPort.findByBusinessPlanId(plan.getId()); + + AiReport aiReport; + if (existingReport.isPresent()) { + aiReport = existingReport.get(); + aiReport.update(rawJsonString); + } else { + aiReport = AiReport.create(plan.getId(), rawJsonString); + } + + plan.updateStatus(PlanStatus.AI_REVIEWED); + businessPlanCommandPort.save(plan); + + return aiReportCommandPort.save(aiReport); + } catch (org.springframework.dao.DataIntegrityViolationException e) { + // 동시 생성으로 인한 유니크 제약 위반 시 재조회 후 업데이트 + log.warn("유니크 제약 위반으로 재시도. planId: {}", plan.getId()); + AiReport existingReport = aiReportQueryPort.findByBusinessPlanId(plan.getId()) + .orElseThrow(() -> new AiReportException(AiReportErrorType.AI_GRADING_FAILED)); + existingReport.update(rawJsonString); + return aiReportCommandPort.save(existingReport); + } }
🧹 Nitpick comments (5)
src/main/java/starlight/application/aireport/util/SectionScoreExtractor.java (2)
12-19: 불변Map.of()사용을 권장합니다.
HashMap과 static 초기화 블록 대신 Java 9+의Map.of()를 사용하면 코드가 더 간결해지고, 불변성이 명시적으로 보장됩니다.♻️ 리팩토링 제안
- private static final Map<SectionType, Function<AiReportResult, Integer>> SCORE_EXTRACTORS = new HashMap<>(); - - static { - SCORE_EXTRACTORS.put(SectionType.PROBLEM_RECOGNITION, AiReportResult::problemRecognitionScore); - SCORE_EXTRACTORS.put(SectionType.FEASIBILITY, AiReportResult::feasibilityScore); - SCORE_EXTRACTORS.put(SectionType.GROWTH_STRATEGY, AiReportResult::growthStrategyScore); - SCORE_EXTRACTORS.put(SectionType.TEAM_COMPETENCE, AiReportResult::teamCompetenceScore); - } + private static final Map<SectionType, Function<AiReportResult, Integer>> SCORE_EXTRACTORS = Map.of( + SectionType.PROBLEM_RECOGNITION, AiReportResult::problemRecognitionScore, + SectionType.FEASIBILITY, AiReportResult::feasibilityScore, + SectionType.GROWTH_STRATEGY, AiReportResult::growthStrategyScore, + SectionType.TEAM_COMPETENCE, AiReportResult::teamCompetenceScore + );
26-29: 알 수 없는SectionType에 대한 silent 실패가 버그를 숨길 수 있습니다.현재 구현에서
SCORE_EXTRACTORS에 등록되지 않은SectionType이 전달되면 조용히 0을 반환합니다. 향후 새로운SectionType이 enum에 추가되었지만 이 맵에 등록되지 않으면, 실제 점수가 아닌 0이 반환되어 버그를 추적하기 어려워질 수 있습니다.최소한 경고 로그를 남기거나, 개발 단계에서 빠르게 발견할 수 있도록 예외를 던지는 것을 고려해 주세요.
♻️ 경고 로그 추가 예시
+import lombok.extern.slf4j.Slf4j; + +@Slf4j public class SectionScoreExtractor { // ... public static Integer extractScore(SectionType sectionType, AiReportResult result) { if (sectionType == null || result == null) { return 0; } Function<AiReportResult, Integer> extractor = SCORE_EXTRACTORS.get(sectionType); if (extractor == null) { + log.warn("Unknown SectionType: {}. Returning default score 0.", sectionType); return 0; } Integer score = extractor.apply(result); return score != null ? score : 0; } }src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (1)
162-171: 결과 수집 시getNow()사용 권장
future.get(0, TimeUnit.SECONDS)대신future.getNow(defaultValue)를 사용하면 더 명확합니다.allOf.get()이후에는 모든 Future가 완료 또는 취소 상태이므로, 블로킹 호출보다 즉시 값을 가져오는 방식이 의도를 더 잘 표현합니다.♻️ 선택적 리팩토링 제안
- try { - if (future.isCancelled()) { - return SectionGradingResult.failure(sectionType, "타임아웃"); - } - return future.get(0, TimeUnit.SECONDS); - } catch (java.util.concurrent.TimeoutException e) { - return SectionGradingResult.failure(sectionType, "타임아웃"); - } catch (Exception e) { - return SectionGradingResult.failure(sectionType, "예외: " + e.getMessage()); - } + if (future.isCancelled()) { + return SectionGradingResult.failure(sectionType, "타임아웃"); + } + return future.getNow( + SectionGradingResult.failure(sectionType, "미완료"));src/main/java/starlight/application/aireport/util/AiReportResponseParser.java (1)
290-364: 불완전한 JSON 복구 로직의 한계 인지 필요LLM 응답이 잘리는 경우를 대비한 방어적 코드입니다. 괄호 균형 맞추기는 구문적으로 유효한 JSON을 만들지만, 의미적으로 올바른지는 보장하지 않습니다.
예를 들어,
{"title": "잘린 텍스→{"title": "잘린 텍스"}로 복구 시 닫히지 않은 문자열이 그대로 남습니다.현재 로깅(
log.warn)으로 복구 발생을 추적하는 것은 좋지만, 복구된 결과가 파싱에 실패하면 기본값으로 대체되므로 운영 환경에서 모니터링이 필요합니다.src/main/java/starlight/application/aireport/AiReportService.java (1)
130-139: RuntimeException 대신 도메인 예외 사용 권장
JsonProcessingException발생 시RuntimeException을 던지고 있습니다. 일관성을 위해AiReportException을 사용하는 것이 좋습니다.♻️ 도메인 예외로 변경
private String getRawJsonAiReportResponseFromGradingResult(AiReportResult gradingResult) { JsonNode gradingJsonNode = responseParser.convertToJsonNode(gradingResult); String rawJsonString; try { rawJsonString = objectMapper.writeValueAsString(gradingJsonNode); } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to convert JsonNode to string", e); + log.error("채점 결과 JSON 변환 실패", e); + throw new AiReportException(AiReportErrorType.AI_RESPONSE_PARSING_FAILED); } return rawJsonString; }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.javasrc/main/java/starlight/adapter/aireport/report/agent/SectionGradeAgent.javasrc/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/main/java/starlight/application/aireport/util/SectionScoreExtractor.javasrc/main/java/starlight/domain/aireport/entity/AiReport.javasrc/main/java/starlight/domain/aireport/exception/AiReportErrorType.javasrc/main/java/starlight/shared/enumerate/SectionType.java
🚧 Files skipped from review as they are similar to previous changes (4)
- src/main/java/starlight/adapter/aireport/report/agent/impl/SpringAiSectionGradeAgent.java
- src/main/java/starlight/shared/enumerate/SectionType.java
- src/main/java/starlight/domain/aireport/exception/AiReportErrorType.java
- src/main/java/starlight/adapter/aireport/report/agent/SectionGradeAgent.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-11T16:13:19.742Z
Learnt from: 2ghrms
Repo: StartUpLight/STARLIGHT_BE PR: 77
File: src/test/java/starlight/application/member/CredentialServiceUnitTest.java:20-21
Timestamp: 2026-01-11T16:13:19.742Z
Learning: StarLight 프로젝트에서 헥사고날 아키텍처 도입 시 인터페이스 및 구현체 네이밍 규칙을 적용합니다. 기존 Service 인터페이스는 UseCase로 이름을 변경하고, ServiceImpl 구현 클래스는 Service로 이름을 변경합니다. 예: CredentialService 인터페이스 → CredentialUseCase, CredentialServiceImpl → CredentialService. 이 규칙은 프로젝트의 모든 Java 파일에 일관되게 적용되어야 하며, 관련 참조 및 코드 호출부도 동일한 네이밍으로 업데이트해야 합니다.
Applied to files:
src/main/java/starlight/application/aireport/util/SectionScoreExtractor.javasrc/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.javasrc/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/domain/aireport/entity/AiReport.java
📚 Learning: 2026-01-11T16:13:35.380Z
Learnt from: 2ghrms
Repo: StartUpLight/STARLIGHT_BE PR: 77
File: src/test/java/starlight/application/member/CredentialServiceIntegrationTest.java:20-20
Timestamp: 2026-01-11T16:13:35.380Z
Learning: Enforce the naming convention introduced by SRLT-124: all use-case interfaces must end with the suffix UseCase, while their concrete implementations must drop the Impl suffix and use the base name without UseCase (e.g., CredentialUseCase as the interface, CredentialService as the concrete implementation). In code reviews, verify that: - interface names end with UseCase; - concrete classes implementing those interfaces are named [BaseName]Service; - any class named [BaseName]UseCase or [BaseName]Impl is avoided in implementation files; - relationship between interface and implementation is clear and consistent across src/main/java. In test files, ensure integration tests reference the concrete *Service* implementations (e.g., CredentialService), not interfaces, unless explicitly testing the interface type.
Applied to files:
src/main/java/starlight/application/aireport/util/SectionScoreExtractor.javasrc/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.javasrc/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/main/java/starlight/application/aireport/AiReportService.javasrc/main/java/starlight/domain/aireport/entity/AiReport.java
📚 Learning: 2026-01-01T07:34:02.762Z
Learnt from: SeongHo5356
Repo: StartUpLight/STARLIGHT_BE PR: 71
File: src/main/java/starlight/adapter/businessplan/persistence/BusinessPlanRepository.java:27-33
Timestamp: 2026-01-01T07:34:02.762Z
Learning: `ExpertAiReportQueryService`에서 사용하는 `findAllByMemberIdOrderByLastSavedAt` 메서드는 AI 리포트를 생성한 사업계획서만 조회 대상이므로 회원당 20~30개 수준으로 제한되어, 전체 조회 방식(페이징 없음)이 적절합니다. 향후 50개 이상으로 증가 시 페이징 추가 예정입니다.
Applied to files:
src/main/java/starlight/application/aireport/util/AiReportResponseParser.javasrc/main/java/starlight/application/aireport/AiReportService.java
🧬 Code graph analysis (1)
src/main/java/starlight/application/aireport/AiReportService.java (1)
src/main/java/starlight/domain/aireport/exception/AiReportException.java (1)
AiReportException(6-11)
⏰ 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). (2)
- GitHub Check: test
- GitHub Check: build-and-push
🔇 Additional comments (6)
src/main/java/starlight/domain/aireport/entity/AiReport.java (1)
14-21: LGTM! TOCTOU 경쟁 조건 해결을 위한 유니크 제약 추가
business_plan_id에 대한 DB 레벨 유니크 제약이 추가되어 동시 요청 시 중복 레코드 생성을 방지합니다. 이전 리뷰에서 지적된 upsert 경쟁 조건 문제를 해결하는 올바른 접근입니다.단,
AiReportService.upsertAiReportWithRawJsonStr메소드에서 유니크 제약 위반 예외(DataIntegrityViolationException)를 적절히 처리하여 재시도 로직을 추가하면 더 견고해집니다.src/main/java/starlight/adapter/aireport/report/SpringAiReportGrader.java (1)
235-241: LGTM!섹션 점수 추출 헬퍼 메소드가 깔끔하게 구현되었습니다. 없는 섹션에 대해 0을 반환하는 것은 합계 계산 시 적절한 기본값입니다.
src/main/java/starlight/application/aireport/util/AiReportResponseParser.java (1)
1-1: 헥사고날 아키텍처 준수: Application 계층으로 이동됨
AiReportResponseParser가starlight.application.aireport.util패키지로 이동되어 이전 리뷰에서 지적된 Application → Adapter 의존성 문제가 해결되었습니다.src/main/java/starlight/application/aireport/AiReportService.java (3)
31-35: 헥사고날 아키텍처 네이밍 규칙 준수
AiReportUseCase인터페이스를 구현하는AiReportService클래스로 프로젝트의 네이밍 규칙(UseCase 접미사 인터페이스, Service 구현체)을 올바르게 따르고 있습니다. Based on learnings.
174-185: LGTM!채점 결과 검증 로직이 명확합니다. 모든 점수가 0이고 모든 배열이 비어있는 경우에만 유효하지 않은 결과로 판단하여, 부분적으로 성공한 채점 결과도 허용합니다.
48-82: LGTM!
gradeBusinessPlan메소드의 전체 흐름이 명확합니다:
- 소유권 및 상태 검증
- 섹션별/전체 내용 추출
- 섹션 에이전트를 통한 채점
- 결과 검증 및 저장
- 응답 반환
로깅도 적절한 수준으로 되어 있어 운영 시 디버깅에 도움이 됩니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| if (jsonNode.has("problemRecognitionScore") && !jsonNode.path("problemRecognitionScore").isNull()) { | ||
| problemRecognitionScore = jsonNode.path("problemRecognitionScore").asInt(0); | ||
| } | ||
| if (jsonNode.has("feasibilityScore") && !jsonNode.path("feasibilityScore").isNull()) { | ||
| feasibilityScore = jsonNode.path("feasibilityScore").asInt(0); | ||
| } | ||
| if (jsonNode.has("growthStrategyScore") && !jsonNode.path("growthStrategyScore").isNull()) { | ||
| growthStrategyScore = jsonNode.path("growthStrategyScore").asInt(0); | ||
| } | ||
| if (jsonNode.has("teamCompetenceScore") && !jsonNode.path("teamCompetenceScore").isNull()) { | ||
| teamCompetenceScore = jsonNode.path("teamCompetenceScore").asInt(0); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "AiReportResponseParser.java" | head -5Repository: StartUpLight/STARLIGHT_BE
Length of output: 148
🏁 Script executed:
wc -l src/main/java/starlight/application/aireport/util/AiReportResponseParser.javaRepository: StartUpLight/STARLIGHT_BE
Length of output: 150
🏁 Script executed:
sed -n '180,230p' src/main/java/starlight/application/aireport/util/AiReportResponseParser.javaRepository: StartUpLight/STARLIGHT_BE
Length of output: 2460
🏁 Script executed:
# SectionGradingResult 클래스 찾기
fd -t f "SectionGradingResult.java"Repository: StartUpLight/STARLIGHT_BE
Length of output: 146
🏁 Script executed:
cat -n src/main/java/starlight/adapter/aireport/report/dto/SectionGradingResult.javaRepository: StartUpLight/STARLIGHT_BE
Length of output: 1073
🏁 Script executed:
# 이 클래스가 어떻게 사용되는지 확인
rg "SectionGradingResult" --type java -A 3 -B 3 | head -80Repository: StartUpLight/STARLIGHT_BE
Length of output: 9725
🏁 Script executed:
# AiReportResult 클래스의 fromGradingResult 메서드 확인
ast-grep --pattern 'fromGradingResult($$$)'Repository: StartUpLight/STARLIGHT_BE
Length of output: 51
🏁 Script executed:
# asInt(0) 호출 후 실제 0 값이 어떻게 처리되는지 확인하기 위해 jackson 문서 검색
# 그리고 실제 테스트 케이스 확인
fd -t f "AiReportResponseParserTest" "Test.java" | head -10Repository: StartUpLight/STARLIGHT_BE
Length of output: 165
🏁 Script executed:
# PR 관련 문서나 주석 확인
rg "0과 null|null vs 0|distinction" --type java src/main/java/starlight/application/aireport/Repository: StartUpLight/STARLIGHT_BE
Length of output: 51
🏁 Script executed:
# asInt() 메서드가 null 값에 대해 어떻게 작동하는지 확인
# 전체 메서드 흐름을 더 자세히 보기
sed -n '160,240p' src/main/java/starlight/application/aireport/util/AiReportResponseParser.javaRepository: StartUpLight/STARLIGHT_BE
Length of output: 3415
섹션 점수 파싱의 null/0 구분 로직 정리 필요
현재 파싱 로직에서는 has() && !isNull() 체크 후 asInt(0) 기본값이 기술적으로 작동하지 않습니다. null이 이미 필터링되었기 때문입니다.
더 중요한 점은 라인 216-219의 ternary 연산자로 모든 null을 0으로 변환하면서, "필드가 없거나 null인 경우"와 "명시적으로 0이 설정된 경우"를 구분할 수 없게 됩니다. 이것이 의도적인 설계(점수가 없으면 0으로 통일)인지, 아니면 각 상태를 구분하여 처리해야 하는지 명확히 하고, 필요시 로직을 정리해 주세요.
구체적으로:
asInt(0)기본값 제거 (redundant)- null/0 구분이 필요한 경우, 반환 전 변환 로직 재검토
🤖 Prompt for AI Agents
In
`@src/main/java/starlight/application/aireport/util/AiReportResponseParser.java`
around lines 191 - 202, The section-score parsing currently uses
jsonNode.path(...).asInt(0) and a later ternary that coerces nulls to 0, which
hides the difference between "field missing/null" and "explicitly 0"; change the
score fields (problemRecognitionScore, feasibilityScore, growthStrategyScore,
teamCompetenceScore) from primitive int to Integer so they can hold null, remove
the redundant asInt(0) default (use jsonNode.has(...) &&
!jsonNode.path(...).isNull() then jsonNode.path(...).asInt() without a default),
and then adjust the later conversion/ternary logic that currently maps null to 0
so it either preserves nulls (if you need to distinguish missing vs zero) or
explicitly sets 0 at the final output site with a clear, single place decision;
update AiReportResponseParser to reflect these changes.
SeongHo5356
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요즘 정신없었을텐데 너무 고생했어~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 사용하지 않고 있는 것 같아서 삭제하는 방향으로 논의하려고 했는데 했네 굳굳!!
| private final ExpertApplicationQueryPort applicationQueryPort; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
| private final ExpertReportServiceUseCase expertReportUseCase; | ||
| private final ExpertReportUseCase expertReportUseCase; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 네이밍 관련 수정사항은 이전 내 PR에 리뷰로 남겨서 논의했으면 좋았을 것 같다!
| private final ChecklistGrader checklistGrader; | ||
| private final BusinessPlanCommandPort businessPlanCommandPort; | ||
| private final BusinessPlanQueryPort businessPlanQueryPort; | ||
| private final MemberQueryPort memberQueryPort; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Member 도메인의 포트를 가져다 쓰기보단, 해당 도메인에서 포트를 정의해서 사용하면 도메인간 결합도를 낮출 수 있을 것 같다! 저번에 논의했다싶이 LookUpPort로 활용하면 좋을 것 같으
| BusinessPlan plan = getOwnedBusinessPlanOrThrow(planId, memberId); | ||
|
|
||
| return BusinessPlanResponse.Result.from(plan, "Business plan retrieved"); | ||
| return BusinessPlanResult.Result.from(plan, "Business plan retrieved"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
~ Result로 네이밍 통일한 거 좋와요
| BusinessPlan save(BusinessPlan businessPlan); | ||
|
|
||
| void delete(BusinessPlan businessPlan); | ||
| BusinessPlan findWithAllSubSectionsOrThrow(Long id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
findByIdWithAllSubSections 이런식으로, 어떤 걸 통해서 어떤 걸 찾아왔는지 네이밍하면 좋을 것 같은데, 이 부분은 따로 논의해보면 좋을 것 같다
| package starlight.application.aireport.required; | ||
|
|
||
| public interface BusinessPlanCreationPort { | ||
|
|
||
| Long createBusinessPlanWithPdf(String title, String pdfUrl, Long memberId); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AiReportService 에 남긴 코멘트에 따라서 이건 지울 수 있는 방향으로 개선되면 좋겠다!
|
|
||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class SectionAdvisorConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bootStrap 쪽으로 이동해도 될 것 같은데 어때?
| private final ReportGraderPort reportGrader; | ||
| private final ObjectMapper objectMapper; | ||
| private final OcrProviderPort ocrProvider; | ||
| private final AiReportResponseParser responseParser; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의존성 방향이 반대로 흐르고 있는 것 같다!
| public ApiResponse<AiReportResponse> createAndGradeBusinessPlan( | ||
| public ApiResponse<AiReportResult> createAndGradeBusinessPlan( | ||
| @AuthenticationPrincipal AuthDetails authDetails, | ||
| @Valid @RequestBody BusinessPlanCreateWithPdfRequest request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 도메인 adapter DTO(BusinessPlanCreateWithPdfRequest)를 직접 사용하고 있는 것 같다. 이 api 자체의 endpoint를 사업계획서로 옮기는 게 좋아보여
| private final BusinessPlanCommandPort businessPlanCommandPort; | ||
| private final BusinessPlanQueryPort businessPlanQueryPort; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 도메인의 포트를 직접 사용하면 도메인간 결합도가 높아질 것 같아. 포트는 소비자쪽에서 정의해서 사용하면 좋을 것 같다
🚀 Why - 해결하려는 문제가 무엇인가요?
기존 AI 리포트 채점 시스템의 문제점
배경
✅ What - 무엇이 변경됐나요?
구현한 기능 요약
adapter.ai패키지를adapter.aireport.report로 재구성하여 도메인별 명확한 구조화주요 변경사항
AiReportService,BusinessPlanService등이 UseCase 인터페이스를 구현하도록 변경AiReportResponseParser를 Adapter Layer에서 Application Layer로 이동)ReportGraderPort,AiReportQueryPort등 Port 인터페이스를 구현하도록 변경BusinessCreationAdapter를 추가하여서 AI리포트 도메인에서 사업계획서 도메인 서비스를 사용하지 않도록 함)SectionGradeAgent)와 전체 리포트 Agent(FullReportGradeAgent)로 분리ReportPromptProvider에서 관리하고, 공통 프롬프트와 섹션별 프롬프트를 조합AiReportResponse→AiReportResult,BusinessPlanResponse→BusinessPlanResult로 명명 변경Service→UseCase,ServiceImpl→Service로 변경하여 명확한 역할 구분🛠️ How - 어떻게 해결했나요?
핵심 로직 설명
섹션별 병렬 채점
// 4개 섹션을 CompletableFuture로 병렬 실행
Map<SectionType, CompletableFuture> futureMap = ...
// 각 섹션별 Agent가 독립적으로 채점 수행
// 최대 2분 타임아웃으로 모든 결과 수집
2. 섹션별 에이전트 구조
SectionGradeAgent인터페이스: 각 섹션별 채점 책임SpringAiSectionGradeAgent: Spring AI를 활용한 실제 구현체SectionType의tag를 활용하여 Vector Store 필터링으로 섹션별 컨텍스트 제공슈퍼바이저 패턴
ReportSupervisor가 전체 사업계획서 내용과 섹션별 점수를 바탕으로 종합 분석Circuit Breaker
헥사고날 아키텍처
AiReportUseCase,BusinessPlanUseCase등ReportGraderPort,AiReportQueryPort등섹션별 응답 파싱 개선
// 필드 존재 여부와 null 여부를 모두 확인
if (jsonNode.has("feasibilityScore") && !jsonNode.path("feasibilityScore").isNull()) {
feasibilityScore = jsonNode.path("feasibilityScore").asInt(0);
}
- 0점과 null을 정확히 구분하여 파싱 오류 방지
예외 사항, 고민 포인트
PDF 채점 방식
gradeWithFullPrompt) 유지병렬 처리 최적화
sectionGradingExecutor의 스레드 풀 크기 조정 (core: 5, max: 20)Circuit Breaker 임계값
프롬프트 관리
prompt.report.section.default.system)와 섹션별 프롬프트를 조합하여 중복 제거🖼️ Attachment
아키텍처 변경 전후 비교
성능 개선 예상
💬 기타 코멘트
리뷰 요청 사항
ExpertReportServiceUseCase→ExpertReportUseCase,CredentialService→CredentialUseCase,MemberQueryUseCase→MemberUseCase등의 변경이 포함되어 있습니다.테스트 코드
주의할 점
향후 개선 사항
Summary by CodeRabbit
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.