Conversation
WalkthroughAI 요금 조회가 미존재 시 자동 생성 대신 예외를 던지도록 변경하고, AI_CHARGE_NOT_FOUND 에러코드를 추가했습니다. AiCharge 저장 API 및 서비스·스케줄러가 추가되어 월별 요금 초기화 로직이 서비스로 이동했고 PR 분석 흐름이 AiChargeService 기반으로 리팩터링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant QEL as QuestionEventListener
participant PFS as PrAnalysisFacadeService
participant ACS as AiChargeService
participant AR as AiChargeRepository
QEL->>PFS: analyzePullRequest(request, token)
PFS->>ACS: getMonthlyCharge()
ACS->>AR: getByYearAndMonth(year, month)
alt 월 요금 존재
AR-->>ACS: AiCharge
ACS-->>PFS: AiCharge
else 월 요금 미존재
AR-->>ACS: throw GssException(AI_CHARGE_NOT_FOUND)
ACS-->>PFS: Exception
PFS-->>QEL: 예외 전파/처리
end
note over PFS,ACS: 분석 후 소비금액 계산
PFS->>ACS: addCharge(consumedCharge)
ACS->>AR: addCharge(year, month, consumedCharge)
AR-->>ACS: void
ACS-->>PFS: void
PFS-->>QEL: AnalyzePrResponse
sequenceDiagram
autonumber
participant SCH as AiChargeScheduler
participant ACS as AiChargeService
participant AR as AiChargeRepository
SCH->>ACS: initializeNextMonth()
ACS->>AR: save(new AiCharge(nextYear, nextMonth, 0))
AR-->>ACS: AiCharge (persisted)
ACS-->>SCH: AiCharge
note over SCH: 다음달 청구 항목 사전 생성
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Test Results0 tests 0 ✅ 0s ⏱️ Results for commit 9c82401. ♻️ This comment has been updated with latest results. |
📝 Test Coverage Report
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java (1)
6-10: 스케줄링 활성화 LGTM + 스레드풀/에러핸들러/타임존 제안기본 설정은 OK. 장기 실행 시나리오/예외 로깅/동시 실행 제어 강화를 위해 스케줄러 빈을 구성하는 편이 좋습니다. 또한 @scheduled 크론에 Asia/Seoul 등 명시적 타임존 설정을 권장합니다.
package com.devoops.config; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @Configuration @EnableScheduling +@Slf4j public class SchedulingConfig { + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(2); // 필요 시 조정 + scheduler.setThreadNamePrefix("scheduler-"); + scheduler.setRemoveOnCancelPolicy(true); + scheduler.setErrorHandler(t -> log.error("Unhandled exception in scheduler", t)); + scheduler.initialize(); + return scheduler; + } }추가 제안(클러스터 환경): 스케줄러 단일 실행 보장을 위해 ShedLock 등 분산락 도입을 고려하세요.
gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java (1)
21-24: 조회 메서드에 readOnly 트랜잭션 부여읽기 전용 쿼리이므로 트랜잭션 힌트를 명확히 해 I/O 미세 최적화 및 불필요한 flush 가능성 차단을 권장합니다.
- public AiCharge getByYearAndMonth(int year, int month) { + @Transactional(readOnly = true) + public AiCharge getByYearAndMonth(int year, int month) { return chargeJpaRepository.findByYearAndMonth(year, month) .orElseThrow(() -> new GssException(ErrorCode.AI_CHARGE_NOT_FOUND)) .toDomainEntity(); }gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java (1)
40-43: BigDecimal 비교는 double 변환 대신 BigDecimal 비교 API 사용 권장
doubleValue()로 변환하면 스케일/정밀도 이슈가 섞일 수 있습니다. 아래처럼 수정하면 타입 안전합니다.- () -> assertThat(nextMonthAiCharge.getCharge().doubleValue()).isEqualTo(0.0) + () -> assertThat(nextMonthAiCharge.getCharge()).isEqualByComparingTo(java.math.BigDecimal.ZERO)gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java (1)
13-16: 분산 환경 중복 실행 대비멀티 인스턴스에서 동시에 트리거될 수 있습니다.
initializeNextMonth()를 멱등하게 만들고(중복 삽입 무시 후 조회) 필요시 ShedLock 등의 분산락 도입을 고려하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java(2 hunks)gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java(1 hunks)gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java(1 hunks)gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java(2 hunks)gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java(1 hunks)gss-mcp-app/src/main/java/com/devoops/config/SchedulingConfig.java(1 hunks)gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java(3 hunks)gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java(1 hunks)gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisFacadeService.java(2 hunks)gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java (1)
gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisFacadeService.java (1)
Service(14-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
🔇 Additional comments (6)
gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java (1)
28-30: updateChargeById 반환 타입을 int로 변경하고 예외 처리 추가
AiChargeJpaRepository.updateChargeById가 현재void반환이라 호출부에서 업데이트 건수를 확인할 수 없습니다. JPA 쿼리 메서드를@Modifying,@Query와 함께int반환으로 변경한 뒤addCharge에서 반환값이 0일 때GssException(ErrorCode.AI_CHARGE_NOT_FOUND)를 던지세요.Likely an incorrect or invalid review comment.
gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java (2)
4-4: AssertJ thrownBy 정적 임포트 추가 LGTM예외 경로 검증 가독성 향상에 도움이 됩니다.
9-11: 예외/에러코드 임포트 LGTM테스트 의도에 부합합니다.
gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java (2)
25-25: 의존성 교체 적절
PrAnalysisService→PrAnalysisFacadeService전환 및 DI 필드 교체가 일관성 있게 반영되었습니다.
34-34: 호출부 업데이트 확인Facade로의 위임 메서드 호출로 정상 교체되었습니다. 예외 전파 정책이 이전과 동일한지만 한번 확인해 주세요.
gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java (1)
32-35: 원자적 UPDATE 및 @transactional 적용 완료
AiChargeJpaRepository.updateChargeById가@Modifying+JPQLupdate … set charge = charge + :charge where …단일 쿼리로 구현되어 있고,AiChargeRepositoryImpl.addCharge에 기본 전파 속성(REQUIRED)의@Transactional이 이미 적용되어 있어 동시성 손실이 발생하지 않습니다.
| void 가져올_요금이_없다면_에러가_발생한다() { | ||
| LocalDate localDate = LocalDate.now(); | ||
|
|
||
| AiCharge actual = chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue()); | ||
|
|
||
| assertThat(actual.getCharge().doubleValue()).isEqualTo(0.0); | ||
| assertThatThrownBy(() -> chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue())) | ||
| .isInstanceOf(GssException.class) | ||
| .hasMessage(ErrorCode.AI_CHARGE_NOT_FOUND.getMessage()); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
테스트 안정성: 날짜 고정 및 에러코드까지 검증 권장
- LocalDate.now()는 테스트 환경/시차/사전 데이터에 따라 플래키할 수 있습니다. 고정 일자를 사용하세요.
- 가능하다면 GssException의 ErrorCode도 함께 단정해 의도를 더 명확히 하세요.
- LocalDate localDate = LocalDate.now();
+ LocalDate localDate = LocalDate.of(2099, 12, 1); // 존재하지 않을 것이 보장되는 미래 날짜
-
- assertThatThrownBy(() -> chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue()))
- .isInstanceOf(GssException.class)
- .hasMessage(ErrorCode.AI_CHARGE_NOT_FOUND.getMessage());
+ assertThatThrownBy(() -> chargeRepository.getByYearAndMonth(localDate.getYear(), localDate.getMonthValue()))
+ .isInstanceOf(GssException.class)
+ .hasMessage(ErrorCode.AI_CHARGE_NOT_FOUND.getMessage());
+ // (선택) ErrorCode 필드도 검증하려면 GssException API 확인 필요
+ // .extracting("errorCode").isEqualTo(ErrorCode.AI_CHARGE_NOT_FOUND);검증 스크립트: GssException이 errorCode 게터를 노출하는지 확인
🏁 Script executed:
#!/bin/bash
rg -nP -C2 'class\s+GssException\b'
rg -nP -C2 '\bgetErrorCode\s*\('Length of output: 1175
테스트 안정성 강화 및 (선택) 에러코드 검증
- LocalDate.now() 대신 재현 가능한 고정 날짜 사용(e.g.
LocalDate.of(2099, 12, 1)) - (선택)
assertThatThrownBy(...) .extracting("errorCode") .isEqualTo(ErrorCode.AI_CHARGE_NOT_FOUND)
→ GssException에getErrorCode()가 Lombok@Getter로 생성되어 있어 검증 가능
🤖 Prompt for AI Agents
In
gss-api-app/src/test/java/com/devoops/repository/analysis/AiChargeRepositoryTest.java
around lines 36-42, the test uses LocalDate.now() making it non-deterministic;
replace LocalDate.now() with a fixed reproducible date such as
LocalDate.of(2099, 12, 1) so the test is stable, and optionally strengthen the
assertion by extracting/validating the exception's error code (e.g., assert that
the thrown GssException has errorCode equal to ErrorCode.AI_CHARGE_NOT_FOUND)
using the existing getErrorCode() getter.
| AI_CREATE_QUESTION_ERROR(500, "AI 질문 생성과정에 오류가 발생했습니다"), | ||
| AI_CHARGE_NOT_FOUND(500, "당월 AI 비용을 찾을 없습니다.") | ||
| ; |
There was a problem hiding this comment.
오류 메시지 오타 수정 및 상태코드 검토
- 사용자 노출 문구 “찾을 없습니다”는 비문입니다. “찾을 수 없습니다”로 수정하세요.
- 도메인 리소스 부재는 500보다는 404가 더 적합합니다(내부 예외라도 의미적으로 Not Found). 맵핑 정책에 따라 결정하시되, 최소한 문구는 즉시 수정 필요.
문구 수정안:
- AI_CHARGE_NOT_FOUND(500, "당월 AI 비용을 찾을 없습니다.")
+ AI_CHARGE_NOT_FOUND(500, "당월 AI 비용을 찾을 수 없습니다.")(선택) 상태코드 정합성 향상:
- AI_CHARGE_NOT_FOUND(500, "당월 AI 비용을 찾을 수 없습니다.")
+ AI_CHARGE_NOT_FOUND(404, "당월 AI 비용을 찾을 수 없습니다.")🤖 Prompt for AI Agents
In gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java
around lines 43 to 45, the enum entry AI_CHARGE_NOT_FOUND contains a typo in the
user-facing message ("찾을 없습니다") and an inappropriate 500 status for a missing
resource; update the message to "찾을 수 없습니다" and, depending on your error-mapping
policy, change the status code from 500 to 404 for AI_CHARGE_NOT_FOUND (or
document why 500 is retained) so the enum reflects correct wording and semantic
status.
| void addCharge(int year, int month, double charge); | ||
|
|
||
| AiCharge save(AiCharge charge); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
addCharge 결과값 반환 및 save 계약 명시화
- addCharge가 업데이트 성공 여부를 알려주지 않습니다. 0행 업데이트 시 NotFound 예외 매핑을 위해 업데이트 카운트를 반환하도록 변경을 권장합니다.
- save는 “신규 생성”인지 “업서트”인지 계약을 자바독으로 명확히 해주세요(동시성 중복 생성 시 동작 포함).
- void addCharge(int year, int month, double charge);
+ int addCharge(int year, int month, double charge);
- AiCharge save(AiCharge charge);
+ /**
+ * 지정 연/월의 AiCharge를 저장한다.
+ * 동시성으로 인한 중복키 충돌 발생 시의 처리(무시/조회 재시도/예외)를 명확히 보장해야 한다.
+ */
+ AiCharge save(AiCharge charge);검증 스크립트(리포지토리 구현이 업데이트 카운트를 반환하는지 확인):
🏁 Script executed:
#!/bin/bash
# updateChargeById 시그니처와 @Modifying 여부 확인
rg -nP -C3 'interface\s+AiChargeJpaRepository\b'
rg -nP -C2 '\bupdateChargeById\s*\('
rg -nP -C2 '@Modifying'Length of output: 4562
addCharge 반환값(int) 및 save 계약 자바독 명시화 필요
- AiChargeJpaRepository.updateChargeById를
int반환으로 변경하고, 도메인 레포지토리addCharge도int반환으로 수정하세요(업데이트 건수 반환). save메서드는 신규 생성(insert) vs. 기존 수정(upsert) 및 동시성 키 충돌 처리(무시/재시도/예외 등)를 JavaDoc으로 명시하세요.
// gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java
- void addCharge(int year, int month, double charge);
+ int addCharge(int year, int month, double charge);
- AiCharge save(AiCharge charge);
+ /**
+ * 지정 연/월의 AiCharge를 저장합니다.
+ * 신규 생성(insert) 또는 기존 수정(upsert) 동작을 보장하며,
+ * 동시성 중복 키 충돌 시(무시/조회 재시도/예외 처리) 동작을 문서화해야 합니다.
+ */
+ AiCharge save(AiCharge charge);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void addCharge(int year, int month, double charge); | |
| AiCharge save(AiCharge charge); | |
| } | |
| // gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java | |
| /** | |
| * 지정된 연/월의 요금을 업데이트하며, 영향을 받은 행 수를 반환합니다. | |
| * | |
| * @param year 연도 (예: 2025) | |
| * @param month 월 (1-12) | |
| * @param charge 갱신할 요금 값 | |
| * @return 업데이트된 레코드 수 | |
| */ | |
| int addCharge(int year, int month, double charge); | |
| /** | |
| * 지정 연/월의 AiCharge를 저장합니다. | |
| * 신규 생성(insert) 또는 기존 수정(upsert) 동작을 보장하며, | |
| * 동시성 중복 키 충돌 시 (무시/재조회 재시도/예외 처리) 동작을 문서화해야 합니다. | |
| * | |
| * @param charge 저장할 AiCharge 엔티티 | |
| * @return 저장된(또는 갱신된) AiCharge 엔티티 | |
| */ | |
| AiCharge save(AiCharge charge); | |
| } |
🤖 Prompt for AI Agents
In
gss-domain/src/main/java/com/devoops/domain/repository/analysis/AiChargeRepository.java
around lines 9-12, change the addCharge signature to return int (number of
updated rows) and update the underlying AiChargeJpaRepository.updateChargeById
to return int as well; additionally add a concise JavaDoc to the save(AiCharge)
method that specifies whether it performs an insert vs update (upsert) and
document the concurrency/key-conflict behavior (e.g., whether conflicts are
ignored, retried, or cause an exception) and any expected transactional
guarantees so callers know how to handle failures.
| @Override | ||
| @Transactional | ||
| public AiCharge save(AiCharge charge) { | ||
| return chargeJpaRepository.save(AiChargeEntity.from(charge)) | ||
| .toDomainEntity(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
save 동시성 중복키 대비 — 멱등 저장으로 보강
다중 노드/스케줄러 중복 실행 시 unique(year, month) 충돌 가능. DataIntegrityViolationException을 잡아 조회 재시도로 멱등화하세요.
import com.devoops.domain.entity.analysis.AiCharge;
import com.devoops.domain.repository.analysis.AiChargeRepository;
import com.devoops.exception.custom.GssException;
import com.devoops.exception.errorcode.ErrorCode;
import com.devoops.jpa.entity.analysis.AiChargeEntity;
import java.math.BigDecimal;
+import org.springframework.dao.DataIntegrityViolationException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@@
@Override
@Transactional
public AiCharge save(AiCharge charge) {
- return chargeJpaRepository.save(AiChargeEntity.from(charge))
- .toDomainEntity();
+ try {
+ return chargeJpaRepository.save(AiChargeEntity.from(charge))
+ .toDomainEntity();
+ } catch (DataIntegrityViolationException e) {
+ // 동시성으로 이미 생성된 경우: 조회로 멱등 보장
+ return chargeJpaRepository.findByYearAndMonth(charge.getYear(), charge.getMonth())
+ .orElseThrow(() -> new GssException(ErrorCode.AI_CHARGE_NOT_FOUND))
+ .toDomainEntity();
+ }
}(운영 제안) 스케줄러 단일 실행을 위해 ShedLock 등을 사용하면 DB 충돌 자체를 크게 줄일 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Override | |
| @Transactional | |
| public AiCharge save(AiCharge charge) { | |
| return chargeJpaRepository.save(AiChargeEntity.from(charge)) | |
| .toDomainEntity(); | |
| } | |
| ++ b/gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java | |
| @@ | |
| import com.devoops.jpa.entity.analysis.AiChargeEntity; | |
| import java.math.BigDecimal; | |
| import org.springframework.dao.DataIntegrityViolationException; | |
| import lombok.RequiredArgsConstructor; | |
| import org.springframework.stereotype.Repository; | |
| import org.springframework.transaction.annotation.Transactional; | |
| @@ | |
| @Override | |
| @Transactional | |
| public AiCharge save(AiCharge charge) { | |
| - return chargeJpaRepository.save(AiChargeEntity.from(charge)) | |
| try { | |
| return chargeJpaRepository.save(AiChargeEntity.from(charge)) | |
| .toDomainEntity(); | |
| } catch (DataIntegrityViolationException e) { | |
| // 동시성으로 이미 생성된 경우: 조회로 멱등 보장 | |
| return chargeJpaRepository.findByYearAndMonth(charge.getYear(), charge.getMonth()) | |
| .orElseThrow(() -> new GssException(ErrorCode.AI_CHARGE_NOT_FOUND)) | |
| .toDomainEntity(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
gss-domain/src/main/java/com/devoops/jpa/repository/analysis/AiChargeRepositoryImpl.java
around lines 32-37, the save method can throw a DataIntegrityViolationException
under concurrent inserts for the same unique(year, month); update the method to
catch DataIntegrityViolationException, and in the catch perform a lookup by year
and month (via the JPA repository) and return the existing entity’s domain
object to achieve idempotent save (optionally retry the lookup a couple times if
necessary); keep the method transactional and ensure any thrown exception types
are handled/translated appropriately.
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
There was a problem hiding this comment.
initializeNextMonth 멱등/동시성 안전성 부족 — 고유키 충돌 흡수 및 재조회
현재는 무조건 save하여 유니크(year, month) 충돌 시 예외가 납니다. 분산 스케줄/동시 트리거에 취약하므로 충돌은 정상 시나리오로 간주하고 재조회로 멱등화하세요.
-import org.springframework.stereotype.Service;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
@@
- public AiCharge initializeNextMonth() {
+ @Transactional
+ public AiCharge initializeNextMonth() {
LocalDate now = LocalDate.now(SEOUL_ZONE_ID);
YearMonth nextMonth = YearMonth.from(now).plusMonths(1);
AiCharge aiCharge = new AiCharge(nextMonth.getYear(), nextMonth.getMonthValue(), BigDecimal.ZERO);
- return chargeRepository.save(aiCharge);
+ try {
+ return chargeRepository.save(aiCharge);
+ } catch (DataIntegrityViolationException e) {
+ // 이미 다른 쓰레드/노드가 생성함: 재조회로 멱등 보장
+ return chargeRepository.getByYearAndMonth(nextMonth.getYear(), nextMonth.getMonthValue());
+ }
}Also applies to: 20-25
🤖 Prompt for AI Agents
In gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java
around lines 9-11 and 20-25, the initializeNextMonth method currently
unconditionally calls save and propagates unique (year,month) constraint
exceptions; change it to be idempotent and concurrency-safe by catching the
persistence exception thrown on unique-key conflict (e.g.,
DataIntegrityViolationException / DuplicateKeyException), then re-query the
repository for the record by year and month and return that existing entity
instead of failing; ensure the save call remains inside a transaction where
appropriate and prefer repository.findByYearAndMonth (or equivalent) for the
re-fetch so concurrent triggers simply return the already-created row.
| public AiCharge getMonthlyCharge() { | ||
| LocalDate today = LocalDate.now(SEOUL_ZONE_ID); | ||
| return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
미존재 월 예외 전파는 서비스 책임 회피 — 안전한 지연 생성으로 전환
운영 중 스케줄 누락/초기 데이터 부재 가능성을 고려해 서비스 레이어에서 “조회 또는 생성”을 보장하세요.
- public AiCharge getMonthlyCharge() {
- LocalDate today = LocalDate.now(SEOUL_ZONE_ID);
- return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue());
- }
+ public AiCharge getOrCreateMonthlyCharge() {
+ LocalDate today = LocalDate.now(SEOUL_ZONE_ID);
+ try {
+ return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue());
+ } catch (com.devoops.exception.GssException ex) {
+ if (ex.getErrorCode() != com.devoops.exception.errorcode.ErrorCode.AI_CHARGE_NOT_FOUND) throw ex;
+ AiCharge newCharge = new AiCharge(today.getYear(), today.getMonthValue(), BigDecimal.ZERO);
+ try {
+ return chargeRepository.save(newCharge);
+ } catch (org.springframework.dao.DataIntegrityViolationException e) {
+ return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue());
+ }
+ }
+ }사용처(PrAnalysisFacadeService)도 getOrCreateMonthlyCharge()로 교체해 주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public AiCharge getMonthlyCharge() { | |
| LocalDate today = LocalDate.now(SEOUL_ZONE_ID); | |
| return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); | |
| } | |
| // 기존 getMonthlyCharge() 대신 아래 메서드를 사용해 조회 또는 생성을 보장합니다. | |
| public AiCharge getOrCreateMonthlyCharge() { | |
| LocalDate today = LocalDate.now(SEOUL_ZONE_ID); | |
| try { | |
| // 이미 존재하면 바로 반환 | |
| return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); | |
| } catch (com.devoops.exception.GssException ex) { | |
| // 예상된 "월별 요금 없음" 예외가 아니면 그대로 재던짐 | |
| if (ex.getErrorCode() != com.devoops.exception.errorcode.ErrorCode.AI_CHARGE_NOT_FOUND) { | |
| throw ex; | |
| } | |
| // 없으면 지연 생성 | |
| AiCharge newCharge = new AiCharge( | |
| today.getYear(), | |
| today.getMonthValue(), | |
| BigDecimal.ZERO | |
| ); | |
| try { | |
| return chargeRepository.save(newCharge); | |
| } catch (org.springframework.dao.DataIntegrityViolationException e) { | |
| // 동시 생성된 경우, 다시 조회 | |
| return chargeRepository.getByYearAndMonth(today.getYear(), today.getMonthValue()); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In gss-domain/src/main/java/com/devoops/service/pranalysis/AiChargeService.java
around lines 27-30, replace the current getMonthlyCharge() with a
getOrCreateMonthlyCharge() that guarantees returning an AiCharge for the current
Seoul year/month by first attempting to fetch via
chargeRepository.getByYearAndMonth(...) and, if absent, creating and saving a
new AiCharge (populate year, month and any required defaults) in a safe/durable
way (wrap in a transaction and handle concurrent creation — e.g., attempt insert
and on unique constraint failure re-query or use repository.save with proper
locking), then update usages (PrAnalysisFacadeService) to call
getOrCreateMonthlyCharge(); ensure exceptions are handled and the
repository/save path is tested.
gss-mcp-app/src/main/java/com/devoops/service/pranalysis/AiChargeScheduler.java
Outdated
Show resolved
Hide resolved
gss-mcp-app/src/main/java/com/devoops/service/pranalysis/PrAnalysisFacadeService.java
Show resolved
Hide resolved
| ZoneId zoneId = ZoneId.of("Asia/Seoul"); | ||
| YearMonth yearMonth = YearMonth.from(LocalDate.now(zoneId)); | ||
| YearMonth nextMonth = yearMonth.plusMonths(1); | ||
|
|
||
| aiChargeScheduler.createNextMonthCharge(); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
경계 시각(자정/말일) 플래키 발생 가능 — Clock 주입으로 결정론 보장 필요
테스트/서비스가 각각 LocalDate.now(Asia/Seoul)을 호출하므로 자정 전후(특히 말일 23:59~00:01)에 월 계산이 어긋날 수 있습니다. AiChargeService에 Clock을 주입해 테스트에서는 고정 Clock을 주입하도록 리팩터링해 주세요.
원하시면 AiChargeService에 Clock 도입 및 테스트용 고정 Clock 설정까지 패치 드리겠습니다.
🏁 Script executed:
#!/bin/bash
set -e
# Locate the test file
TEST_FILE=$(find . -type f -name AiChargeSchedulerTest.java)
echo "=== Test file: $TEST_FILE ==="
sed -n '1,200p' "$TEST_FILE"
# Locate the service file defining AiChargeService
SERVICE_FILE=$(grep -R --include="*.java" "class AiChargeService" -l .)
echo "=== Service file(s): $SERVICE_FILE ==="
# Show the createNextMonthCharge method context
grep -R --include="*.java" -n "createNextMonthCharge" -C3 $SERVICE_FILE
sed -n '1,200p' $SERVICE_FILE | sed -n '/createNextMonthCharge/,/^}/p'
# Check for LocalDate.now usage in the service
echo "=== LocalDate.now usages in service ==="
grep -R --include="*.java" "LocalDate.now" -n $SERVICE_FILE
# Check if Clock is injected anywhere in the service or test
echo "=== Clock references in codebase ==="
grep -R --include="*.java" "\bClock\b" -n .Length of output: 2565
🏁 Script executed:
#!/bin/bash
set -e
# AiChargeScheduler 클래스 파일 위치 확인
SCHEDULER_FILE=$(find . -type f -name AiChargeScheduler.java)
echo "=== Scheduler file: $SCHEDULER_FILE ==="
# createNextMonthCharge 메서드 주변 코드 확인
grep -n -C3 "createNextMonthCharge" "$SCHEDULER_FILE"
# LocalDate.now 사용 여부 확인
grep -n "LocalDate.now" "$SCHEDULER_FILE"Length of output: 808
🏁 Script executed:
#!/bin/bash
set -e
# Locate AiChargeService file
SERVICE_FILE=$(find . -type f -path "*com/devoops/service/pranalysis/AiChargeService.java")
echo "=== Service file: $SERVICE_FILE ==="
# Show top of file
sed -n '1,200p' "$SERVICE_FILE"
# Show initializeNextMonth method context
grep -n -C3 "initializeNextMonth" "$SERVICE_FILE"
# Check for LocalDate.now usage in service
grep -n "LocalDate.now" "$SERVICE_FILE"Length of output: 2505
AiChargeService에 Clock 주입하고 테스트에서 고정 Clock 사용
서비스의 LocalDate.now(SEOUL_ZONE_ID) 호출을 생성자 주입된 Clock으로 교체하고, 테스트에서는 고정된 Clock 인스턴스를 주입해 경계 시각(자정/말일) 플래키를 방지하세요.
🤖 Prompt for AI Agents
In
gss-mcp-app/src/test/java/com/devoops/service/pranalysis/AiChargeSchedulerTest.java
around lines 29 to 33, replace direct calls to LocalDate.now(SEOUL_ZONE_ID) in
the AiChargeService with a constructor-injected Clock and update the test to
pass a fixed Clock for Asia/Seoul so boundary timing is deterministic: modify
the service to accept a Clock (use it with LocalDate.now(clock) and
ZoneId.of("Asia/Seoul") as needed), add a constructor or setter to inject the
Clock, and in this test create a fixed Clock at a known Instant with
ZoneId.of("Asia/Seoul") then construct/inject the AiChargeService (or
AiChargeScheduler) with that fixed Clock before calling createNextMonthCharge().
🚩 연관 이슈
close #84
🔂 변경 내역
Summary by CodeRabbit