Skip to content

Conversation

@discipline24
Copy link
Contributor

@discipline24 discipline24 commented Oct 29, 2025

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 백테스팅 기능 추가: 사용자 정의 매매 전략 생성 및 실행 지원
    • 기술 지표 기반 거래 규칙 설정 가능 (SMA, EMA, RSI, MACD 등)
    • 백테스트 상태 추적 및 성과 지표 분석
    • 비동기 실행으로 향상된 시스템 응답성
  • Chores

    • 멀티 데이터소스 설정 개선
    • 인증 체계 최적화

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

워크스루

Ta4j 라이브러리를 추가하고 백테스팅 기능을 구현했습니다. 새로운 DTO와 엔티티로 백테스트 요청/응답 구조를 재설계하고, 비동기 실행 엔진(BacktestingEngine)과 Ta4j 통합 서비스(Ta4jHelperService)를 도입했습니다. 또한 다중 데이터소스 설정과 JWT 인증 개선사항을 추가했습니다.

변경사항

코호트 / 파일 변경 요약
의존성 추가
backend/build.gradle
ta4j-core 0.15 라이브러리 의존성 추가
백테스트 요청/응답 DTO
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java, BacktestResponse.java
BacktestRequest: paramsJson 필드 제거, strategy 필드 추가, Swagger 스키마 주석 추가 / BacktestResponse: 구조 재설계로 id, template, title, status, paramsJson, startDate, endDate 필드 추가
새로운 전략 DTO
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java, StrategyCondition.java, StrategyOperand.java
BacktestRunRequest, StrategyCondition, StrategyOperand 신규 DTO 클래스 작성 (전략 조건 및 피연산자 정의)
백테스트 실행 엔티티
backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRun.java, BacktestStatus.java
BacktestRun: status, errorMessage 필드 추가, @JdbcTypeCode 주석 추가 / BacktestStatus: 새로운 상태 열거형 (PENDING, RUNNING, COMPLETED, FAILED)
백테스트 서비스
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java, BacktestingEngine.java
BacktestService: 응답 필드 세분화, 비동기 처리 구현 / BacktestingEngine: 새로운 서비스로 @async 메서드를 통한 백테스트 실행 엔진 구현
Ta4j 통합
backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java
BarSeries 생성, 거래 규칙 생성, 기술 지표 빌드 기능이 포함된 새로운 Ta4j 헬퍼 서비스
인증 개선
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java, CustomUserDetailsService.java, JwtAuthenticationFilter.java
JwtParser: CustomUserDetailsService 의존성 주입 추가 / CustomUserDetailsService: UUID 기반 조회로 변경 / JwtAuthenticationFilter: /user/signup 패턴 제외 추가
비동기 및 다중 데이터소스 설정
backend/src/main/java/org/sejongisc/backend/common/config/AsyncConfig.java, PrimaryDataSourceConfig.java, StockDataSourceConfig.java
AsyncConfig: @EnableAsync 추가 / PrimaryDataSourceConfig, StockDataSourceConfig: 다중 데이터소스 설정 (EntityManagerFactory, TransactionManager 포함)
에러 코드
backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java
PRICE_DATA_NOT_FOUND, INVALID_BACKTEST_JSON_PARAMS 두 가지 새로운 에러 코드 추가
가격 데이터 엔티티 및 리포지토리
backend/src/main/java/org/sejongisc/backend/stock/entity/PriceData.java, PriceDataId.java, backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java
PriceData: 새로운 JPA 엔티티 (ticker, date 복합키) / PriceDataId: 복합키 클래스 / PriceDataRepository: 날짜 범위 기반 가격 데이터 조회 메서드

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant Controller as BacktestController
    participant Service as BacktestService
    participant Engine as BacktestingEngine
    participant DB as Database
    participant Ta4j as Ta4jHelperService
    
    User->>Controller: runBacktest(BacktestRequest)
    Controller->>Service: runBacktest(request)
    Service->>DB: resolve User & Template
    Service->>DB: create BacktestRun<br/>(status=PENDING)
    Service->>Engine: execute(backtestRunId) @Async
    Service-->>Controller: return BacktestResponse
    Controller-->>User: 202 Accepted
    
    par Async Execution
        Engine->>DB: load BacktestRun
        Engine->>Engine: parse paramsJson to BacktestRunRequest
        Engine->>DB: fetch PriceData (date range)
        Engine->>Ta4j: createBarSeries(priceData)
        Ta4j-->>Engine: BarSeries
        Engine->>Ta4j: buildCombinedRule(conditions)
        Ta4j-->>Engine: buy/sell Rules
        Engine->>Engine: simulate trading<br/>(iterate bars)
        Engine->>Engine: calculate metrics<br/>(return, drawdown)
        Engine->>DB: save BacktestRunMetrics
        Engine->>DB: update BacktestRun<br/>(status=COMPLETED)
    end
    
    Note over Engine: On Error
    Engine->>DB: update BacktestRun<br/>(status=FAILED,<br/>errorMessage)
Loading

코드 리뷰 예상 소요시간

🎯 4 (복잡함) | ⏱️ ~60분

주의가 필요한 영역:

  • Ta4jHelperService: 기술 지표 빌드 로직의 정확성, 캐싱 메커니즘, 복합 규칙 구성 (AND/OR 조합)
  • BacktestingEngine: 트랜잭션 처리, 비동기 오류 처리, 포트폴리오 계산 정확성 (수익률, 최대낙폭)
  • 다중 데이터소스 설정: 두 개의 독립적인 EntityManagerFactory와 TransactionManager 구성이 올바르게 분리되었는지 확인
  • BacktestRequest/Response DTO 변경: 기존 API 호환성 및 직렬화/역직렬화 검증
  • UUID 기반 조회 변경: CustomUserDetailsService의 UUID 파싱 오류 가능성

🐰 백테스트 엔진이 달려가네,
전략 조건 펄럭이며 규칙을 세우고,
Ta4j의 막대들 춤을 추니,
수익과 손실이 노래 부르고,
비동기 마법으로 결과가 피어난다! 🚀📈

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR title "[BE] SISC1-56 [FEAT] 백테스팅 실행 api 구현"은 changeset의 주요 목적을 명확히 반영하고 있습니다. Raw summary를 분석하면 BacktestService의 runBacktest 메서드 구현, BacktestingEngine을 통한 비동기 백테스트 실행, 그리고 관련 DTO/Entity/Service 추가가 핵심 변경사항이며, 이는 title에서 명시한 "백테스팅 실행 api 구현"과 정확히 일치합니다. Title은 간결하고 구체적이며 개발자 관점에서 중요한 기능 추가를 명확하게 전달하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SISC1-56-BE-백테스팅-실행-API-구현

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@discipline24 discipline24 merged commit 2cca9b0 into main Oct 29, 2025
1 check failed
@discipline24 discipline24 deleted the SISC1-56-BE-백테스팅-실행-API-구현 branch October 29, 2025 11:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java (1)

82-87: JwtException만 캐치하면 500이 날 수 있습니다 — 캐치 범위를 확장하세요.

CustomUserDetailsService/JwtParser에서 발생 가능한 UsernameNotFoundException·CustomException·IllegalArgumentException이 여기서 캐치되지 않으면 500으로 전파됩니다. 아래처럼 401로 정상화하세요.

-        } catch (JwtException e) {
+        } catch (JwtException
+                 | org.springframework.security.core.userdetails.UsernameNotFoundException
+                 | org.sejongisc.backend.common.exception.CustomException
+                 | IllegalArgumentException e) {
+            log.debug("JWT authentication failure: {}", e.getMessage());
             ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_ACCESS_TOKEN);
             response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
             response.setContentType("application/json;charset=UTF-8");
             response.getWriter().write(toJson(errorResponse));
         }
backend/build.gradle (1)

30-34: spring-boot-starter-webflux 불필요 - WebClient만 사용 중

검증 결과, 코드베이스에서 WebClient를 OAuth2 제공자 API 호출(GoogleServiceImpl, GithubServiceImpl, KakaoServiceImpl)에만 사용하고 있습니다. RouterFunction, ServerResponse, @EnableWebFlux 등 서버 측 WebFlux 컴포넌트가 없으므로 spring-boot-starter-webflux 전체를 포함할 필요가 없습니다.

권장사항: spring-boot-starter-webflux를 제거하고, WebClient만 필요하다면 spring-webflux 의존성을 직접 추가하거나 WebClient 전용 의존성(org.springframework:spring-webflux + io.projectreactor:reactor-core)을 사용하세요. 이렇게 하면 불필요한 서버 측 자동 구성을 제거할 수 있습니다.

🧹 Nitpick comments (25)
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java (1)

20-21: 미사용 import 제거 제안

org.springframework.security.core.userdetails.User가 더 이상 사용되지 않습니다. 정리해주세요.

backend/build.gradle (1)

31-31: 중복 의존성 정리: spring-boot-starter-validation

동일 의존성이 두 번 선언되어 있습니다. 하나만 남기고 제거해 주세요. 빌드에는 영향 없지만 유지보수성에 불필요한 노이즈입니다.

적용 예:

-implementation 'org.springframework.boot:spring-boot-starter-validation'

Also applies to: 59-59

backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1)

13-20: 새 오류 코드 추가는 적절합니다

상태값/메시지 매핑이 합리적입니다. 사소한 문구 다듬기 제안:

  • "유효하지 않은 paramsJson 요청값 입니다." → "유효하지 않은 paramsJson 요청 값입니다." (띄어쓰기/맞춤법)
backend/src/main/java/org/sejongisc/backend/stock/entity/PriceDataId.java (1)

8-36: IdClass 직렬화 안정성 보강: serialVersionUID 추가 권장

JPA 요건은 충족하지만, 장기적으로 직렬화 경계(캐시/메시징)에서 경고를 줄이려면 명시적 serialVersionUID를 두는 편이 안전합니다.

적용 예:

 public class PriceDataId implements Serializable {
+  private static final long serialVersionUID = 1L;
   private String ticker;
   private LocalDate date;

필요 시 디버깅 편의를 위해 toString 추가도 고려해 주세요.

backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java (1)

13-13: Between 포함 범위(양끝 포함) 의도 확인 및 범위 조회 성능 점검 필요

  • Spring Data JPA의 Between는 시작/끝 날짜를 포함합니다. 요구사항이 [start, end]가 맞는지 확인해주세요.
  • 이 쿼리는 (ticker, date) 범위 인덱스가 있어야 선형 스캔을 피합니다. 현재 PK가 해당 순서로 구성되어 있지 않다면 보조 인덱스를 고려해주세요. 대규모 구간 조회가 잦다면 페이징/스트리밍 여부도 검토 바랍니다.
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java (1)

19-21: 연산자 문자열을 enum으로 타입 안전화

허용 값이 고정되어 있으므로 String operator 대신 StrategyOperator enum 사용을 권장합니다. 직렬화/문서화도 명확해집니다.

적용 예시(diff):

-    // 연산자 (예: "GT", "LT", "CROSSES_ABOVE")
-    private String operator;
+    // 연산자 (예: GT, LT, CROSSES_ABOVE)
+    private StrategyOperator operator;

추가 파일(예시):

public enum StrategyOperator {
  GT, GTE, LT, LTE, EQ, CROSSES_ABOVE, CROSSES_BELOW
}
backend/src/main/java/org/sejongisc/backend/stock/entity/PriceData.java (2)

12-14: 범위 조회를 위한 인덱스 명시 고려

ticker + date로 조회가 빈번합니다. DB에 PK가 정확히 (ticker, date) 순서로 설정되지 않았다면 보조 인덱스를 권장합니다. 스키마를 JPA가 생성한다면 아래와 같이 명시 가능합니다.

적용 예시(diff):

-import jakarta.persistence.*;
+import jakarta.persistence.*;
+import jakarta.persistence.Index;
-@Table(name = "price_data")
+@Table(name = "price_data",
+       indexes = { @Index(name = "idx_price_ticker_date", columnList = "ticker,date") })

(이미 동일한 PK 인덱스가 있다면 생략)


22-29: 금액/가격 필드 precision/scale 명시 권장

BigDecimal 컬럼에 precision/scale을 지정하면 DB별 기본값 차이로 인한 반올림 문제를 예방합니다.

적용 예시(diff):

-  private BigDecimal open;
-  private BigDecimal high;
-  private BigDecimal low;
+  @Column(precision = 19, scale = 6) private BigDecimal open;
+  @Column(precision = 19, scale = 6) private BigDecimal high;
+  @Column(precision = 19, scale = 6) private BigDecimal low;
   @Column(name = "close")
-  private BigDecimal closePrice; // 'close'는 예약어일 수 있어 필드명 변경
+  @Column(name = "close", precision = 19, scale = 6)
+  private BigDecimal closePrice; // 'close'는 예약어일 수 있어 필드명 변경
-  private Long volume;
-  private BigDecimal adjustedClose;
+  private Long volume;
+  @Column(precision = 19, scale = 6) private BigDecimal adjustedClose;
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java (1)

20-33: 필수 필드 Bean Validation 추가로 조기 검증

런타임 NPE를 방지하려면 DTO 수준에서 최소한의 검증을 권장합니다(컨트롤러에서 @Valid 사용 전제).

적용 예시(diff):

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
@@
-    @Schema(description = "초기 자본금")
-    private BigDecimal initialCapital;
+    @Schema(description = "초기 자본금")
+    @NotNull @Positive
+    private BigDecimal initialCapital;
@@
-    @Schema(description = "대상 종목 티커")
-    private String ticker;
+    @Schema(description = "대상 종목 티커")
+    @NotBlank
+    private String ticker;
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java (1)

100-106: 0 또는 비정상 가격 방어 코드 권장

데이터 결함으로 가격이 0이면 divide에서 ArithmeticException이 납니다. 스킵/로그 처리 등을 권장합니다.

적용 예시(diff):

-                if (shares.compareTo(BigDecimal.ZERO) == 0 && shouldBuy) {
-                    shares = cash.divide(currentClosePrice, 8, RoundingMode.HALF_UP);
+                if (shares.compareTo(BigDecimal.ZERO) == 0 && shouldBuy) {
+                    if (currentClosePrice.compareTo(BigDecimal.ZERO) <= 0) {
+                        log.warn("비정상 가격으로 매수 스킵: {}", currentClosePrice);
+                    } else {
+                        shares = cash.divide(currentClosePrice, 8, RoundingMode.HALF_UP);
+                        cash = BigDecimal.ZERO;
+                        tradesCount++;
+                        log.debug("[{}] BUY at {}", series.getBar(i).getEndTime().toLocalDate(), currentClosePrice);
+                    }
-                    cash = BigDecimal.ZERO;
-                    tradesCount++;
-                    log.debug("[{}] BUY at {}", series.getBar(i).getEndTime().toLocalDate(), currentClosePrice);
                 }
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java (2)

19-29: 자유 문자열 대신 enum 사용으로 오입력 방지

type(indicator/price/const), priceField(Open/High/Low/Close/Volume)는 열거형으로 고정 가능합니다. 클라이언트/서버 모두 안전해집니다.

적용 예시(diff):

-    private String type;
+    private OperandType type;
@@
-    private String priceField;
+    private PriceField priceField;

예시:

public enum OperandType { indicator, price, const }
public enum PriceField { Open, High, Low, Close, Volume }

34-39: 지표 파라미터 검증 가이드

params.get("length") 등 캐스팅 실패/누락 시 NPE가 발생합니다. DTO 수준 기본값/검증 추가 또는 서비스에서 명확한 예외 메시지를 권장합니다.

backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java (1)

83-97: 실행 전 요청 파라미터 사전 검증으로 실패 전이 최소화

strategy, initialCapital, ticker, 날짜 범위 등을 저장 전에 검증하면 실행 중 실패를 줄일 수 있습니다.

적용 예시(diff):

   public BacktestResponse runBacktest(BacktestRequest request) {
+    if (request.getStrategy() == null) {
+      throw new CustomException(ErrorCode.INVALID_BACKTEST_JSON_PARAMS);
+    }
+    if (request.getStartDate() != null && request.getEndDate() != null &&
+        request.getStartDate().isAfter(request.getEndDate())) {
+      throw new CustomException(ErrorCode.INVALID_BACKTEST_JSON_PARAMS);
+    }
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java (3)

3-15: 불필요한 import 정리 제안

이 DTO에서는 JPA/Hibernate 관련 import(jakarta.persistence.*, JdbcTypeCode, SqlTypes)와 BacktestRun, User가 사용되지 않습니다. 불필요한 의존성 노출과 경고를 줄이기 위해 정리해 주세요.

-import jakarta.persistence.*;
-import org.hibernate.annotations.JdbcTypeCode;
-import org.hibernate.type.SqlTypes;
-import org.sejongisc.backend.backtest.entity.BacktestRun;
-import org.sejongisc.backend.user.entity.User;

24-30: API DTO에서 엔티티 타입(Template, BacktestRunMetrics) 노출 자제

DTO에 JPA 엔티티를 직접 포함하면

  • Lazy 로딩 시 직렬화 예외(LazyInitializationException) 위험
  • 양방향 연관에 따른 순환 직렬화/과도한 페이로드
  • 외부 계약에 내부 스키마가 새어나감

대신 식별자/요약 DTO로 치환을 권장합니다: templateId(UUID) 또는 TemplateResponse, metrics 전용 DTO. 단기적으로는 @JsonIgnoreProperties 등으로 회피 가능하나 권장하지 않습니다.

직렬화 경로(컨트롤러 응답)에서 N+1/LAZY 예외가 없는지 한번 점검 부탁드립니다.

Also applies to: 32-32


28-28: paramsJson 문자열 대신 구조화 타입 고려

응답에서 paramsJson을 String으로 내보내면 클라이언트 파싱 비용과 스키마 가독성이 떨어집니다. BacktestRunRequest 형태(또는 Map<String,Object>)로 직렬화하는 방안을 검토해 주세요. 서비스/리포지토리 레벨에서는 JSONB 보관을 유지하되, DTO에는 구조화 타입을 권장합니다.

backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (2)

20-37: 요청 유효성 검증 추가 권장 (@NotNull, 범위 검증)

실행 필수 필드에 Bean Validation을 붙여 런타임 NPE/검증 누락을 사전에 차단해 주세요: templateId, title, startDate, endDate, strategy 등.

+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import jakarta.validation.constraints.AssertTrue;
@@
-  private UUID templateId;
+  @NotNull
+  private UUID templateId;
@@
-  private String title;
+  @NotNull @Size(min = 1, max = 100)
+  private String title;
@@
-  private LocalDate startDate;
+  @NotNull
+  private LocalDate startDate;
@@
-  private LocalDate endDate;
+  @NotNull
+  private LocalDate endDate;
@@
-  private BacktestRunRequest strategy;
+  @NotNull
+  private BacktestRunRequest strategy;
+
+  @AssertTrue(message = "startDate는 endDate보다 이후일 수 없습니다.")
+  public boolean isValidDateRange() {
+    return startDate == null || endDate == null || !startDate.isAfter(endDate);
+  }

38-40: 단일 DTO에 삭제용 필드 혼재 — 역할 분리 권장

동일 DTO에 실행용 필드와 삭제 리스트(backtestRunIds)가 함께 있으면 스키마/검증이 모호해집니다. Create/Execute 요청 DTO와 BulkDelete DTO를 분리해 컨트롤러 엔드포인트별로 명확한 계약을 권장합니다.

backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRun.java (3)

17-17: 엔티티 전역 @Setter 제한 권장

엔티티 전체에 @Setter를 허용하면 불변성이 약해지고 상태 전이가 통제되지 않습니다. 비즈니스적으로 필요한 필드에만 명시적 setter 또는 전이 메서드(ex. start(), complete(), fail(msg))를 두는 방향을 권장합니다.

-@Setter
 public class BacktestRun {

36-38: status 기본값 지정/전이 메서드 추가 제안

새 Run 생성 시 기본값 PENDING이 필요하다면 @PrePersist로 기본값 설정 또는 생성/전이 메서드로만 변경되도록 하여 일관성을 보장해 주세요.

+  @PrePersist
+  void prePersist() {
+    if (status == null) status = BacktestStatus.PENDING;
+  }

41-43: JSON 매핑 이중 지정 확인

@JdbcTypeCode(SqlTypes.JSON)과 columnDefinition="jsonb"를 함께 사용 중입니다. PostgreSQL Dialect라면 둘 중 하나만으로도 동작합니다. 팀 표준에 맞춰 일관되게 선택해 주세요(성능/DDL 관리 측면에서 columnDefinition 유지 권장).

backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java (1)

22-29: 기본 JPA 리포지토리 스캔 범위 축소 권장

basePackages="org.sejongisc.backend" 후 exclude로 stock.repository를 배제하는 방식은 과도한 스캔과 오배치 위험이 있습니다. 비‑stock 리포지토리 패키지들만 명시적으로 지정하는 편이 안전합니다.

backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java (2)

244-264: 캐시 키 생성 NPE/충돌 가능성 개선

  • operand.getParams()가 null이면 NPE
  • 키를 값만 이어붙이면 서로 다른 파라미터 조합이 충돌할 수 있음

키에 name=value 쌍을 포함하고 null‑safe 처리하세요.

-        case "indicator":
-                String params = operand.getParams().entrySet().stream()
-                    .sorted(Map.Entry.comparingByKey())
-                    .map(e -> e.getValue().toString())
-                    .collect(Collectors.joining(","));
-                String key = String.format("%s(%s)", operand.getIndicatorCode(), params);
+        case "indicator":
+                Map<String, Object> p = operand.getParams() == null ? Map.of() : operand.getParams();
+                String params = p.entrySet().stream()
+                    .sorted(Map.Entry.comparingByKey())
+                    .map(e -> e.getKey() + "=" + String.valueOf(e.getValue()))
+                    .collect(Collectors.joining(","));
+                String key = String.format("%s(%s)", operand.getIndicatorCode(), params);

203-207: Close 기준 기본 지표 생성 방식 확인

new StrategyOperand("price", null, null, null, "Close", null) 생성자가 실제로 존재하지 않으면 컴파일 실패합니다. DTO에 빌더/정적 팩토리를 두고 사용하는 방식을 권장합니다. 임시로는 ClosePriceIndicator를 직접 생성해 캐시하세요.

-        Indicator<Num> baseIndicator = resolveOperand(
-            new StrategyOperand("price", null, null, null, "Close", null),
-            series, cache
-        );
+        Indicator<Num> baseIndicator = cache.computeIfAbsent(
+            "Close",
+            k -> new ClosePriceIndicator(series)
+        );
backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java (1)

19-21: 불필요한 import 정리

HashMap/Map은 사용되지 않습니다. 정리해 주세요.

-import java.util.HashMap;
-import java.util.Map;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 759255b and 050c320.

📒 Files selected for processing (21)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRun.java (4 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestStatus.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java (3 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java (3 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/config/AsyncConfig.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/stock/entity/PriceData.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/stock/entity/PriceDataId.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (2)
backend/src/main/java/org/sejongisc/backend/common/exception/CustomException.java (2)
  • Getter (5-14)
  • CustomException (10-13)
backend/src/main/java/org/sejongisc/backend/common/exception/ErrorResponse.java (1)
  • Getter (7-21)
backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java (1)
backend/src/main/java/org/sejongisc/backend/stock/entity/PriceDataId.java (1)
  • PriceDataId (8-36)
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java (1)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (1)
  • RequiredArgsConstructor (16-32)
backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java (1)
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java (1)
  • Service (32-149)
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java (1)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (1)
  • RequiredArgsConstructor (16-32)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java (3)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (1)
  • Getter (13-41)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java (1)
  • Getter (16-39)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java (1)
  • Getter (11-31)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java (2)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java (1)
  • Getter (16-39)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java (1)
  • Getter (14-39)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java (3)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (1)
  • Getter (13-41)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java (1)
  • Getter (11-31)
backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java (1)
  • Getter (14-39)
backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java (1)
backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java (1)
  • Configuration (19-74)
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java (2)
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java (1)
  • Service (27-170)
backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java (1)
  • Service (28-287)
backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java (1)
backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java (1)
  • Configuration (22-60)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java (2)
backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java (1)
  • Builder (10-15)
backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (1)
  • Getter (13-41)
🔇 Additional comments (7)
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java (1)

25-28: 생성자 주입 도입은 적절합니다.

@requiredargsconstructor + final 필드로 전환은 테스트 용이성과 불변성 측면에서 바람직합니다.

backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java (1)

37-47: 회원가입 경로 예외 처리 추가, 좋습니다.

/user/signup를 인증 제외로 추가한 것은 합리적입니다. 스웨거/문서 제외 규칙과도 일관됩니다.

backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestStatus.java (1)

3-8: 상태 모델 추가 좋습니다

백테스트 라이프사이클에 필요한 최소 상태가 잘 정의되어 있습니다. DB 매핑은 EnumType.STRING 사용을 유지해 주세요(이미 그렇게 사용 중이라면 OK).

실행/실패 전이 시점과 예외 매핑(예: INVALID_BACKTEST_JSON_PARAMS → FAILED)이 일관되게 처리되는지 한 번만 점검 부탁드립니다.

backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java (1)

122-122: JDK 설정이 21로 구성되어 있으므로 List#getLast() 사용은 문제 없음

build.gradle에서 languageVersion = JavaLanguageVersion.of(21)로 명시적으로 설정되어 있습니다. getLast()는 JDK 21 SequencedCollection API이며, 프로젝트의 실제 대상 버전과 일치하므로 컴파일 에러가 발생하지 않습니다. 현재 코드는 올바릅니다.

Likely an incorrect or invalid review comment.

backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java (1)

16-18: userId 숨김 처리 LGTM

@Schema(hidden) + @JsonIgnore 구성 적절합니다.

backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java (1)

270-277: CachedIndicator 생성자 호환성 확인(ta4j 버전 의존)

super(macd) 호출은 ta4j 버전에 따라 시그니처가 다릅니다(Indicator/BarSeries). 현재 의존 버전에서 컴파일 되는지 확인해 주세요. 필요 시 super(macd.getBarSeries()) 로 변경합니다.

backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java (1)

25-28: 구성 전반 LGTM

리포지토리/EMF/트랜잭션매니저 분리와 바인딩 경로가 명확합니다.

Also applies to: 43-59

Comment on lines +60 to +62

// backtesting library
implementation 'org.ta4j:ta4j-core:0.15'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

ta4j-core 버전 최신성 확인 및 BOM 연동 고려

백테스팅 라이브러리 추가는 👍. 다만 0.15가 현재 기준 최신 안정판인지 확인해 주세요. Spring BOM에 의해 관리되지 않는 아티팩트라 주기적 점검이 필요합니다. 필요 시 버전 카탈로그(gradle/libs.versions.toml)나 dependencyManagement에 명시적으로 등록을 권장합니다.


🌐 Web query:

What is the latest stable version of org.ta4j:ta4j-core as of today?

💡 Result:

The latest stable version is 0.18. [1][2]

Sources:
[1] GitHub ta4j README (shows dependency version 0.18).
[2] MVNRepository listing for org.ta4j:ta4j-core — 0.18 (May 15, 2025).


org.ta4j:ta4j-core 버전 업그레이드 필요

현재 지정된 0.15 버전은 더 이상 최신 안정판이 아닙니다. 최신 안정 버전은 0.18입니다(2025년 5월 출시). 0.15에서 0.18로 업그레이드하여 호환성 개선과 잠재적 보안 업데이트를 적용해 주세요. 이 아티팩트는 Spring BOM의 관리 대상이 아니므로, 버전 카탈로그(gradle/libs.versions.toml)나 dependencyManagement 블록에 명시적으로 등록하여 장기 유지보수성을 높이는 것을 권장합니다.

🤖 Prompt for AI Agents
In backend/build.gradle around lines 60 to 62, the ta4j dependency is pinned to
0.15 and needs to be upgraded; update the implementation declaration to use
version 0.18 instead of 0.15, and add a corresponding entry in the version
catalog (gradle/libs.versions.toml) or the project's dependencyManagement block
so the artifact version is managed centrally for future maintenance; ensure the
build.gradle uses the catalog alias (or dependencyManagement BOM) rather than a
hardcoded literal version to keep consistency.

Comment on lines +43 to +45
@Async
@Transactional
public void execute(Long backtestRunId) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

@async + @transactional 장기 트랜잭션: 상태(RUNNING) 가시성 문제

메서드 전체가 하나의 트랜잭션이면 RUNNING 업데이트가 커밋 전까지 외부에서 보이지 않습니다. 상태 전이 용도에는 짧은 트랜잭션이 적합합니다.

권장:

  • execute에서 메서드 수준 @Transactional 제거.
  • 상태 업데이트/메트릭 저장 지점에 REQUIRES_NEW 트랜잭션을 가진 보조 메서드 사용.
  • 또는 상태 변경 직후 커밋되는 전용 서비스 호출로 분리.

Also applies to: 50-53

🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java
around lines 43-45 and 50-53, the current method-level @Transactional combined
with @Async causes the RUNNING state update to remain uncommitted and invisible
to external callers; remove the @Transactional from execute(), keep @Async, and
refactor state/metric persistence into small helper methods annotated with
@Transactional(propagation = REQUIRES_NEW) (or delegate to a separate service)
so each state transition (e.g., set RUNNING, save metrics) runs in its own short
transaction and commits immediately.

Comment on lines +79 to +83
BigDecimal initialCapital = strategyDto.getInitialCapital();
BigDecimal cash = initialCapital;
BigDecimal shares = BigDecimal.ZERO;
int tradesCount = 0;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

초기 자본 검증 누락으로 NPE 위험

initialCapital이 null 또는 0/음수면 이후 계산에서 예외가 납니다. 즉시 검증하고 사용자 친화적 에러로 전환하세요.

적용 예시(diff):

-            BigDecimal initialCapital = strategyDto.getInitialCapital();
+            BigDecimal initialCapital = strategyDto.getInitialCapital();
+            if (initialCapital == null || initialCapital.compareTo(BigDecimal.ZERO) <= 0) {
+                throw new CustomException(ErrorCode.INVALID_BACKTEST_JSON_PARAMS);
+            }
📝 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.

Suggested change
BigDecimal initialCapital = strategyDto.getInitialCapital();
BigDecimal cash = initialCapital;
BigDecimal shares = BigDecimal.ZERO;
int tradesCount = 0;
BigDecimal initialCapital = strategyDto.getInitialCapital();
if (initialCapital == null || initialCapital.compareTo(BigDecimal.ZERO) <= 0) {
throw new CustomException(ErrorCode.INVALID_BACKTEST_JSON_PARAMS);
}
BigDecimal cash = initialCapital;
BigDecimal shares = BigDecimal.ZERO;
int tradesCount = 0;
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java
around lines 79-83, the code assigns initialCapital directly which can be null
or non-positive and lead to NPEs or invalid calculations; validate
initialCapital immediately (check for null and that
initialCapital.compareTo(BigDecimal.ZERO) > 0) and if the check fails throw a
clear IllegalArgumentException (or a domain-specific exception) with a
user-friendly message; after validation safely assign cash = initialCapital and
proceed, ensuring all downstream math assumes a non-null, positive BigDecimal.

BigDecimal totalReturnPct = finalPortfolioValue.divide(initialCapital, 4, RoundingMode.HALF_UP)
.subtract(BigDecimal.ONE);
// MDD (백분율로 변환)
BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(-100));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

MDD 부호 오류로 음수 값 저장

drawdown은 [0,1] 값입니다. -100을 곱하면 음수가 됩니다. 백분율 양수 값으로 저장해야 합니다.

적용 예시(diff):

-            BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(-100));
+            BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(100));
📝 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.

Suggested change
BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(-100));
BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(100));
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java
around line 127, maxDrawdownPct is computed by multiplying by -100 which
produces a negative percentage; change the calculation to produce a positive
percentage by multiplying maxDrawdown by 100 (or by taking the absolute value
and then multiplying by 100 if you want to be explicit), and ensure any
necessary BigDecimal scale/rounding is applied after the multiplication.

Comment on lines +35 to +45
public BarSeries createBarSeries(List<PriceData> priceDataList) {
// ⭐️ (수정) BarSeries 이름에 Ticker 추가
BarSeries series = new BaseBarSeries(priceDataList.get(0).getTicker());
for (PriceData p : priceDataList) {
series.addBar(
p.getDate().atStartOfDay(ZoneId.systemDefault()),
p.getOpen(), p.getHigh(), p.getLow(), p.getClosePrice(), p.getVolume()
);
}
return series;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

빈 가격 목록 대비 방어 코드 추가

priceDataList가 빈 경우 get(0)에서 예외가 납니다. 초기에 명시적으로 검사하고 의미 있는 메시지를 던지세요.

-    public BarSeries createBarSeries(List<PriceData> priceDataList) {
-        // ⭐️ (수정) BarSeries 이름에 Ticker 추가
-        BarSeries series = new BaseBarSeries(priceDataList.get(0).getTicker());
+    public BarSeries createBarSeries(List<PriceData> priceDataList) {
+        if (priceDataList == null || priceDataList.isEmpty()) {
+            throw new IllegalArgumentException("priceDataList is empty");
+        }
+        BarSeries series = new BaseBarSeries(priceDataList.get(0).getTicker());
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java
around lines 35 to 45, add defensive checks for an empty or null priceDataList
before calling get(0); if priceDataList is null or empty, throw an
IllegalArgumentException (or a custom RuntimeException) with a clear message
like "priceDataList is null or empty in createBarSeries" (or include expected
ticker info if available), otherwise proceed to build the BaseBarSeries; ensure
the check occurs at the very start of the method so no IndexOutOfBoundsException
is possible.

Comment on lines +190 to +194
return new VolumeIndicator(series, 0);
case "Close":
default:
return new ClosePriceIndicator(series);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

컴파일 오류: VolumeIndicator 생성자 인자

ta4j VolumeIndicator는 보통 (BarSeries) 단일 인자를 받습니다. new VolumeIndicator(series, 0) 는 컴파일되지 않습니다. 0 인자를 제거해 주세요.

-                return new VolumeIndicator(series, 0);
+                return new VolumeIndicator(series);
📝 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.

Suggested change
return new VolumeIndicator(series, 0);
case "Close":
default:
return new ClosePriceIndicator(series);
}
return new VolumeIndicator(series);
case "Close":
default:
return new ClosePriceIndicator(series);
}
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java
around lines 190 to 194, the call new VolumeIndicator(series, 0) is invalid
because Ta4j's VolumeIndicator constructor accepts only the BarSeries; remove
the extraneous 0 argument and instantiate it as new VolumeIndicator(series) so
the code compiles.

Comment on lines +94 to 95
UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

UserDetails 조회 예외를 JwtException으로 래핑하세요 (필터에서 401로 처리되도록).

현재 loadUserByUsername에서 발생하는 UsernameNotFoundException/CustomException/IllegalArgumentException이 필터에서 캐치되지 않아 500으로 전파될 수 있습니다. 아래처럼 래핑을 권장합니다.

@@
-        UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());
-        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
+        try {
+            UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());
+            return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
+        } catch (org.springframework.security.core.userdetails.UsernameNotFoundException
+                 | org.sejongisc.backend.common.exception.CustomException
+                 | IllegalArgumentException e) {
+            throw new JwtException("유효하지 않은 사용자 식별자입니다.", e);
+        }

추가 권장(선택):

  • 권한은 토큰의 role 클레임만 신뢰하지 말고, userDetails.getAuthorities()와 교차하거나 그것을 그대로 사용하도록 리팩터링을 고려하세요. 토큰 발급 이후 DB에서 권한이 바뀐 경우를 방지할 수 있습니다.
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java
around lines 94-95, wrap any exceptions thrown by
customUserDetailsService.loadUserByUsername (e.g., UsernameNotFoundException,
CustomException, IllegalArgumentException) in a JwtException and rethrow so the
authentication filter treats them as 401 instead of propagating 500;
specifically, catch those exceptions around the loadUserByUsername call and
throw new JwtException(...) with the original exception as cause. Also consider
replacing or cross-checking the authorities derived from the token with
userDetails.getAuthorities() (or merging them) to avoid trusting token role
claims alone when DB roles may have changed.

Comment on lines 23 to 26
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User findUser = userRepository.findUserByEmail(email).orElseThrow(
User findUser = userRepository.findById(UUID.fromString(email)).orElseThrow(
() -> new CustomException(ErrorCode.USER_NOT_FOUND)
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

JWT 경로에서 예외 미래핑으로 500 전파 위험 + UUID 파싱 예외 미처리

  • UUID.fromString 파싱 실패(IllegalArgumentException)와 USER_NOT_FOUND(CustomException)가 JwtAuthenticationFilter에서 캐치되지 않아 500이 될 수 있습니다. UserDetailsService 계약(override 메서드)에도 맞게 UsernameNotFoundException으로 변환하고, JwtParser에서 JwtException으로 재래핑하도록 하는 것이 안전합니다.
  • 또한 파라미터명이 email로 남아 있어 오해 소지가 큽니다.

권장 수정안 A(서비스에서 UsernameNotFoundException으로 표준화):

-    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
-        User findUser = userRepository.findById(UUID.fromString(email)).orElseThrow(
-                () -> new CustomException(ErrorCode.USER_NOT_FOUND)
-        );
-        return new CustomUserDetails(findUser);
-    }
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        try {
+            UUID userId = UUID.fromString(username);
+            User findUser = userRepository.findById(userId)
+                    .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+            return new CustomUserDetails(findUser);
+        } catch (IllegalArgumentException e) { // not a UUID
+            throw new UsernameNotFoundException("User not found");
+        }
+    }

이와 함께 JwtParser.getAuthentication에서 UsernameNotFoundException 등을 JwtException으로 래핑해 필터가 401을 반환하도록 하세요(별도 코멘트 참고).

추가 확인 요청:

  • 이 변경으로 로그인 흐름에서 이메일 기반 조회가 사라졌습니다. 로그인 시점이 이메일/아이디 문자열을 전달한다면 인증이 바로 깨질 수 있습니다. 사용처를 확인해주세요.

다음 스크립트로 저장소 내 사용처를 점검할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# loadUserByUsername/이메일 기반 인증 사용 흔적 검색
rg -nP -C2 --type=java '(loadUserByUsername\s*\()|find(ByEmail|UserByEmail)|usernameParameter\s*\(|"username"|\"email\"'

Length of output: 8547



---

🏁 Script executed:

```shell
#!/bin/bash
# JwtParser.getAuthentication 전체 메서드 구현 확인 (예외 처리 포함)
ast-grep --pattern $'public Authentication getAuthentication($_) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# JwtAuthenticationFilter 에서 어떤 예외를 캐치하는지 확인
rg -nP -A10 --type=java 'class JwtAuthenticationFilter|catch.*Exception|catch.*JwtException' backend/src/main/java/org/sejongisc/backend/common/auth/jwt/

Length of output: 4747



---

🏁 Script executed:

```shell
#!/bin/bash
# JwtAuthenticationFilter 클래스 전체 검색
rg -nP -A30 --type=java 'class JwtAuthenticationFilter' backend/src/main/java/org/sejongisc/backend/common/auth/jwt/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# CustomUserDetailsService 파일 전체 내용 확인
cat backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java

Length of output: 1093



---

🏁 Script executed:

```shell
#!/bin/bash
# JwtAuthenticationFilter 다른 경로에서 검색
fd -e java JwtAuthenticationFilter backend/

Length of output: 99


🏁 Script executed:

#!/bin/bash
# 전체 파일 내용: JwtParser
cat backend/src/main/java/org/sejongisc/backend/common/auth/jwt/JwtParser.java

Length of output: 3728



---

🏁 Script executed:

```shell
#!/bin/bash
# JwtAuthenticationFilter 전체 내용
cat backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/JwtAuthenticationFilter.java

Length of output: 4279


예외 처리 미흡으로 인한 500 에러 전파 위험 (로그인 흐름 우려는 부정확함)

핵심 문제: JwtAuthenticationFilter는 JwtException만 캐치합니다. CustomUserDetailsService.loadUserByUsername()에서 발생하는 CustomExceptionIllegalArgumentException(UUID.fromString 실패 시)이 필터에 캐치되지 않아 500 에러로 전파됩니다.

  • UUID.fromString(email) 파싱 실패 → IllegalArgumentException 미처리
  • orElseThrow로 CustomException 발생 → UsernameNotFoundException 계약 위반
  • JwtParser.getAuthentication() 94번 줄에서 예외 처리 없음 → 필터로 전파

수정안:

 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-    User findUser = userRepository.findById(UUID.fromString(username)).orElseThrow(
-            () -> new CustomException(ErrorCode.USER_NOT_FOUND)
-    );
-    return new CustomUserDetails(findUser);
+    try {
+        UUID userId = UUID.fromString(username);
+        User findUser = userRepository.findById(userId)
+                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+        return new CustomUserDetails(findUser);
+    } catch (IllegalArgumentException e) {
+        throw new UsernameNotFoundException("Invalid user ID format");
+    }
 }

참고: 로그인 흐름(LoginServiceImpl)은 findUserByEmail 메서드를 사용하므로 이 변경의 영향을 받지 않습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java
around lines 23-26, loadUserByUsername currently calls UUID.fromString(email)
and rethrows repository miss as CustomException which leaks unchecked exceptions
into the Jwt filter; catch IllegalArgumentException from UUID.fromString and any
CustomException thrown by the repository and convert them into a
UsernameNotFoundException (include the original exception as the cause and a
clear message), so the method adheres to the UserDetailsService contract and
prevents uncaught 500s in the JWT filter.

Comment on lines +6 to +9
@Configuration
@EnableAsync
public class AsyncConfig {
} No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

@EnableAsync만 켜진 상태는 운영 리스크가 큽니다 — 풀/큐/예외 처리 설정 필요

기본 SimpleAsyncTaskExecutor는 스레드를 재사용하지 않아 부하 시 스레드 폭증 우려가 있습니다. 제한 있는 ThreadPoolTaskExecutor와 예외 처리기를 명시해 주세요.

예시:

-@Configuration
-@EnableAsync
-public class AsyncConfig {
-}
+@Configuration
+@EnableAsync
+public class AsyncConfig implements org.springframework.scheduling.annotation.AsyncConfigurer {
+
+  @Override
+  public java.util.concurrent.Executor getAsyncExecutor() {
+    var exec = new org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor();
+    exec.setThreadNamePrefix("async-");
+    exec.setCorePoolSize(4);
+    exec.setMaxPoolSize(16);
+    exec.setQueueCapacity(1000);
+    exec.setKeepAliveSeconds(60);
+    exec.setWaitForTasksToCompleteOnShutdown(true);
+    exec.setAwaitTerminationSeconds(30);
+    exec.initialize();
+    return exec;
+  }
+
+  @Override
+  public java.lang.Thread.UncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+    return (t, ex) -> {
+      // TODO: 로거/알림 시스템 연동
+      System.err.println("[async-uncaught] thread=" + t.getName() + ", ex=" + ex.getMessage());
+    };
+  }
+}

추가로, 백테스트 실행처럼 장시간 작업은 큐 적재/타임아웃/취소 전략을 함께 검토해 주세요.

📝 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.

Suggested change
@Configuration
@EnableAsync
public class AsyncConfig {
}
@Configuration
@EnableAsync
public class AsyncConfig implements org.springframework.scheduling.annotation.AsyncConfigurer {
@Override
public java.util.concurrent.Executor getAsyncExecutor() {
var exec = new org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor();
exec.setThreadNamePrefix("async-");
exec.setCorePoolSize(4);
exec.setMaxPoolSize(16);
exec.setQueueCapacity(1000);
exec.setKeepAliveSeconds(60);
exec.setWaitForTasksToCompleteOnShutdown(true);
exec.setAwaitTerminationSeconds(30);
exec.initialize();
return exec;
}
@Override
public java.lang.Thread.UncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (t, ex) -> {
// TODO: 로거/알림 시스템 연동
System.err.println("[async-uncaught] thread=" + t.getName() + ", ex=" + ex.getMessage());
};
}
}
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/common/config/AsyncConfig.java
around lines 6 to 9, @EnableAsync is enabled but you rely on the default
SimpleAsyncTaskExecutor which can spawn unbounded threads; replace it by
declaring a ThreadPoolTaskExecutor bean with sensible corePoolSize, maxPoolSize,
queueCapacity, keepAliveSeconds and threadNamePrefix and set a
rejectedExecutionHandler (e.g., CallerRunsPolicy or custom handler) to avoid
thread explosions under load; also implement and register an
AsyncUncaughtExceptionHandler bean that logs method, params and exception for
void async methods; finally, for long-running tasks consider wiring
timeouts/cancellation (e.g., Future/CompletableFuture with timeouts or wrapping
tasks with timeout logic) or routing them through a queued executor to avoid
blocking worker threads.

Comment on lines +53 to +63
.packages(
"org.sejongisc.backend.attendance.entity",
"org.sejongisc.backend.auth.entity",
"org.sejongisc.backend.backtest.entity",
"org.sejongisc.backend.betting.entity",
"org.sejongisc.backend.common.entity.postgres",
"org.sejongisc.backend.point.entity",
"org.sejongisc.backend.stock.entity",
"org.sejongisc.backend.template.entity",
"org.sejongisc.backend.user.entity"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

중복 엔티티 스캔으로 인한 이중 매핑/DDL 충돌 위험 (stock.entity 포함됨)

primary EMF의 packages()에 org.sejongisc.backend.stock.entity가 포함되어 있어, StockDataSourceConfig의 stock EMF와 동일 엔티티를 두 개의 영속성 유닛이 관리하게 됩니다. 이는

  • 이중 매핑/DDL 적용 충돌
  • 잘못된 데이터소스로의 엔티티 접근
  • 런타임 부트 실패 가능성
    을 유발합니다. primary 쪽에서 stock.entity를 제거해 주세요.
             .packages(
@@
-                "org.sejongisc.backend.stock.entity",
                 "org.sejongisc.backend.template.entity",
                 "org.sejongisc.backend.user.entity"
             )

다음 스크립트로 현재 설정을 확인할 수 있습니다(파일 내용 점검):


🏁 Script executed:

#!/bin/bash
rg -nC2 'packages\\(' backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java
rg -n 'packages\\(' backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java

Length of output: 145


정규식 문법 오류를 수정하여 재검증하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Fix: use simpler pattern without regex group syntax
rg -nC5 'packages' backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java
rg -nC5 'packages' backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java

Length of output: 1029



---

🏁 Script executed:

```shell
#!/bin/bash
# Get exact lines 53-63 from PrimaryDataSourceConfig
sed -n '53,63p' backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java

Length of output: 542


기존 리뷰 코멘트 내용 확인됨 - stock.entity 중복 스캔 이중 매핑 위험 실제 존재

검증 결과, 리뷰 코멘트의 우려가 정확합니다:

  • PrimaryDataSourceConfig (라인 53-63): org.sejongisc.backend.stock.entity 포함됨 ✓
  • StockDataSourceConfig (라인 50): org.sejongisc.backend.stock.entity 포함됨 ✓

동일 엔티티가 두 개의 서로 다른 데이터소스 구성에서 스캔되어 다중 영속성 유닛에 의한 이중 매핑, DDL 충돌, 런타임 부트 실패 등의 실질적 위험이 존재합니다. 제안된 수정(primary 구성에서 stock.entity 제거)이 필요합니다.

🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java
around lines 53 to 63, the package list currently includes
org.sejongisc.backend.stock.entity which causes duplicate scanning of the same
entities across PrimaryDataSourceConfig and StockDataSourceConfig; remove
org.sejongisc.backend.stock.entity from the .packages(...) call in this file so
those entities are only scanned by StockDataSourceConfig, then recompile to
confirm no duplicate mapping errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant