Skip to content

feat(order): 매수/매도/주문내역 API(MVP) + local(H2) 실행 설정#6

Open
yooondaeun wants to merge 1 commit intotrade-mill:developfrom
yooondaeun:feat/order
Open

feat(order): 매수/매도/주문내역 API(MVP) + local(H2) 실행 설정#6
yooondaeun wants to merge 1 commit intotrade-mill:developfrom
yooondaeun:feat/order

Conversation

@yooondaeun
Copy link

@yooondaeun yooondaeun commented Aug 17, 2025

작업 내용 (MVP)

  • 엔드포인트
    • POST /api/v1/orders/buy (시장가 매수)
    • POST /api/v1/orders/sell (지정가/시장가 매도)
    • GET /api/v1/orders (주문 내역 페이지네이션)
  • 도메인: Order, Repository, Service, Controller 구성
  • 로컬 실행: local 프로필(H2)로 구동 확인
  • 임시 브로커: FakeBrokerGateway (brokerOrderId = SIM-****)

로컬 실행

./gradlew bootRun --args='--spring.profiles.active=local'

확인 예시

다음 단계

  • 내일 KIS(AppKey/Secret, CANO, 상품코드) 수령 후 KisBrokerGateway로 실주문 연동
  • Docker(Postgres) 전환
  • demo 엔티티(H2 예약어 충돌) 정리

Summary by CodeRabbit

  • 새 기능

    • 주문 관리 REST API 추가: 매수/매도 주문 접수(MARKET/LIMIT), 요청 유효성 검사, 주문 상세 응답(상태/브로커 주문 ID/생성·수정 시각), 사용자별 주문 목록 페이징·정렬 조회
  • 작업

    • 로컬 프로필에 인메모리 DB(H2) 기본 설정 추가
    • Docker Compose에 Postgres 16 데이터베이스 서비스와 영속 볼륨 도입
    • 런타임 데이터베이스 드라이버(PostgreSQL, H2) 의존성 추가

@yooondaeun yooondaeun requested a review from k-kbk August 17, 2025 14:38
@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

데이터베이스 런타임 의존성(PostgreSQL/H2) 추가 및 로컬 설정/도커 컴포즈 도입. 브로커 연동 포트(BrokerGateway)와 로컬용 가짜 구현 추가. 주문 도메인(Entity/Enum/Repository), 서비스, DTO, REST 컨트롤러(매수/매도, 페이징 조회) 신규 구현.

Changes

Cohort / File(s) Change summary
환경/런타임 구성
build.gradle, docker-compose.yml, src/main/resources/application-local.yml
Gradle에 DB 드라이버(runtimeOnly) 추가 및 devtools 위치 조정. Postgres 16 db 서비스(포트/볼륨/환경변수) 도커 컴포즈 추가. 로컬 프로파일용 H2(PostgreSQL 모드) 데이터소스와 JPA ddl-auto=update 설정 추가.
브로커 게이트웨이
src/main/java/trademill/apiserver/broker/BrokerGateway.java, src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java
브로커 연동 인터페이스(BUY/SELL, MARKET/LIMIT) 신설. 로컬/개발 프로파일에서 활성화되는 가짜 구현 추가(UUID 기반 주문 ID 반환).
주문 모듈(API/도메인)
src/main/java/trademill/apiserver/order/Order.java, src/main/java/trademill/apiserver/order/OrderRepository.java, src/main/java/trademill/apiserver/order/OrderService.java, src/main/java/trademill/apiserver/order/OrderController.java, src/main/java/trademill/apiserver/order/OrderSide.java, src/main/java/trademill/apiserver/order/OrderStatus.java, src/main/java/trademill/apiserver/order/OrderType.java, src/main/java/trademill/apiserver/order/dto/*.java
JPA 엔티티(Order)와 Enum(OrderSide/Status/Type) 추가, 리포지토리(findByUserId) 정의. 서비스에서 매수/매도 처리 및 브로커 호출/저장 구현, 페이징 조회 제공. 컨트롤러에 /api/v1/orders 엔드포인트(POST /buy, POST /sell, GET 목록) 추가. DTO(PlaceOrderRequest 검증 포함, OrderResponse 매핑) 추가.

Estimated code review effort

🎯 3 (보통) | ⏱️ ~25 minutes

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.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the feat label Aug 17, 2025
@k-kbk
Copy link
Contributor

k-kbk commented Aug 26, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 6

🧹 Nitpick comments (16)
src/main/java/trademill/apiserver/order/OrderType.java (1)

1-3: 간단한 Javadoc/주석 및 EOF 개행 제안

외부/클라이언트와의 용어 불일치 방지를 위해 의미를 짧게 명시해 두는 것이 좋습니다. 동시에 파일 끝 개행도 추가해 두세요(툴링/포매터 일관성).

 package trademill.apiserver.order;
 
-public enum OrderType {MARKET, LIMIT}
+/**
+ * 주문 유형
+ * MARKET: 시장가
+ * LIMIT : 지정가
+ */
+public enum OrderType { MARKET, LIMIT }
src/main/java/trademill/apiserver/order/OrderSide.java (1)

1-3: 의미 부연 주석 추가 권장

후속으로 공매도/환매수 등의 개념을 확장할 여지가 있다면 파일 상단에 현재 범위를 명시해 두면 유지보수에 도움이 됩니다.

 package trademill.apiserver.order;
 
-public enum OrderSide {BUY, SELL}
+/**
+ * 주문 방향
+ * BUY  : 매수
+ * SELL : 매도
+ */
+public enum OrderSide { BUY, SELL }
src/main/java/trademill/apiserver/order/OrderStatus.java (1)

3-3: 헬퍼 메서드(isTerminal 등)와 Javadoc 추가를 제안

상태 전이를 다루는 곳에서 동일한 분기를 반복하지 않도록 enum 내부에 헬퍼를 두면 서비스/리포지토리 쪽 가독성이 좋아집니다.

-package trademill.apiserver.order;
-
-public enum OrderStatus {PENDING, ACCEPTED, PARTIALLY_FILLED, FILLED, REJECTED, CANCELED}
+package trademill.apiserver.order;
+
+/**
+ * 주문 상태
+ * - PENDING          : 브로커 전송 전/처리 중
+ * - ACCEPTED         : 브로커 접수 완료
+ * - PARTIALLY_FILLED : 일부 체결
+ * - FILLED           : 전량 체결(종결)
+ * - REJECTED         : 거부(종결)
+ * - CANCELED         : 취소(종결)
+ */
+public enum OrderStatus {
+  PENDING, ACCEPTED, PARTIALLY_FILLED, FILLED, REJECTED, CANCELED;
+
+  public boolean isTerminal() {
+    return this == FILLED || this == REJECTED || this == CANCELED;
+  }
+
+  public boolean isActive() {
+    return !isTerminal();
+  }
+}
src/main/resources/application-local.yml (1)

1-11: 개발 편의·안전성 옵션 몇 가지 추가를 제안

  • 영속성 컨텍스트를 웹 계층으로 누수하지 않도록 open-in-view: false.
  • 타임존 일관성을 위해 Hibernate JDBC 타임존 고정.
  • SQL 디버깅 옵션(로컬에서만).
  • 필요 시 H2 콘솔 활성화.
 spring:
   datasource:
     url: jdbc:h2:mem:tm;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1
     username: sa
     password:
     driver-class-name: org.h2.Driver
   jpa:
     hibernate:
       ddl-auto: update
+    open-in-view: false
+    properties:
+      hibernate:
+        jdbc:
+          time_zone: UTC
+  h2:
+    console:
+      enabled: true
 server:
   port: 8080
build.gradle (1)

26-41: 개발 생산성·테스트 보강을 위한 의존성 제안

  • 구성 메타데이터(IDE 지원)를 위해 configuration-processor.
  • 통합 테스트 용이성을 위해 Testcontainers(PostgreSQL).
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-validation'
     implementation 'org.springframework.boot:spring-boot-starter-web'
 
     compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
+    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
 
     developmentOnly 'org.springframework.boot:spring-boot-devtools'
 
     runtimeOnly 'org.postgresql:postgresql'
     runtimeOnly 'com.h2database:h2'
 
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testImplementation 'org.testcontainers:junit-jupiter'
+    testImplementation 'org.testcontainers:postgresql'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 }
docker-compose.yml (2)

5-9: 보안/구성 강화: DB 비밀번호 하드코딩과 포트 전체 공개는 최소화하세요

로컬 용도라도 기본값을 유지하되, .env 혹은 환경변수로 덮어쓸 수 있도록 하고, 호스트 바인딩을 localhost로 제한하는 편이 안전합니다.

아래와 같이 변수 확장과 로컬 바인딩을 적용하는 것을 제안합니다.

     environment:
-      POSTGRES_DB: trademill
-      POSTGRES_USER: tm
-      POSTGRES_PASSWORD: tm
+      POSTGRES_DB: ${POSTGRES_DB:-trademill}
+      POSTGRES_USER: ${POSTGRES_USER:-tm}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tm}
     ports:
-      - "5432:5432"
+      - "127.0.0.1:5432:5432"

3-11: 가용성/부트 순서 안정화: healthcheck 및 restart 정책 추가 권장

애플리케이션이 DB 준비 전 연결을 시도하면 실패할 수 있습니다. healthcheck와 restart 정책을 추가하면 로컬/CI에서 더 안정적으로 동작합니다.

   db:
     image: postgres:16
     environment:
       POSTGRES_DB: ${POSTGRES_DB:-trademill}
       POSTGRES_USER: ${POSTGRES_USER:-tm}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tm}
     ports:
       - "127.0.0.1:5432:5432"
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
+      interval: 5s
+      timeout: 3s
+      retries: 10
+    restart: unless-stopped
     volumes:
       - pgdata:/var/lib/postgresql/data
src/main/java/trademill/apiserver/broker/BrokerGateway.java (1)

7-11: 브로커 응답 결과 반환 타입 확장 권장

현재 BrokerGateway의 placeXXX 메서드가 단순 String(브로커 주문 ID)만 반환하여,
성공/실패 여부, 이유 코드, 체결 상태(접수·대기), 서버 타임스탬프 등 중요한 메타데이터가 누락됩니다.

검증 결과

  • FakeBrokerGateway 구현체(src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java:16–19):
    모든 placeMarketBuy, placeMarketSell, placeLimitBuy, placeLimitSell 메서드에서 랜덤 UUID 문자열만 반환
  • OrderService(src/main/java/trademill/apiserver/order/OrderService.java:23–27, 33–38):
    브로커 응답을 받더라도 항상 OrderStatus.ACCEPTED로만 매핑하며, 오류 또는 대기 상태 분기 로직 부재

권장 사항

  • String 대신 다음과 같은 BrokerOrderResult VO 도입
  • VO에 brokerOrderId, status(ACCEPTED, REJECTED, PENDING 등), reasonCode, message, brokerTime 등을 포함
  • OrderService에서 반환된 VO의 상태를 기반으로 성공·실패·대기 분기 및 예외 처리 로직 추가

예시 변경안

 public interface BrokerGateway {
-    String placeMarketBuy(String symbol, BigDecimal quantity);
-    String placeMarketSell(String symbol, BigDecimal quantity);
-    String placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price);
-    String placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price);
+    BrokerOrderResult placeMarketBuy(String symbol, BigDecimal quantity);
+    BrokerOrderResult placeMarketSell(String symbol, BigDecimal quantity);
+    BrokerOrderResult placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price);
+    BrokerOrderResult placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price);
 }
// src/main/java/trademill/apiserver/broker/BrokerOrderResult.java
package trademill.apiserver.broker;

import java.time.OffsetDateTime;

public record BrokerOrderResult(
    String brokerOrderId,
    OrderExecutionStatus status,   // ACCEPTED, REJECTED, PENDING 등
    String reasonCode,             // 선택적
    String message,                // 선택적
    OffsetDateTime brokerTime      // 선택적
) {}

검증 포인트

rg -n -C3 'place(Market|Limit)(Buy|Sell)' --glob '!**/build/**'
src/main/java/trademill/apiserver/order/OrderRepository.java (1)

6-8: 페이지 정렬 보장과 인덱스 고려

현재 메서드는 정렬 보장이 없습니다. API 응답 일관성을 위해 기본 정렬(예: createdAt DESC)을 고정하거나 컨트롤러에서 Pageable 기본 sort를 강제하세요. 또한 userId, createdAt 조합에 인덱스가 없으면 페이징 성능이 급격히 저하될 수 있습니다.

선택지:

  • 컨트롤러에서 Pageable 기본값 설정 또는
  • 레포지토리에 정렬 포함 메서드 추가:
 public interface OrderRepository extends JpaRepository<Order, Long> {
     Page<Order> findByUserId(Long userId, Pageable pageable);
+    Page<Order> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
 }

추가 권장: Order 엔티티에 인덱스 부여(예: @table(indexes = @Index(name="idx_order_user_created", columnList="userId,createdAt DESC"))).

src/main/java/trademill/apiserver/order/dto/OrderResponse.java (3)

23-37: NPE 방지: 매퍼 파라미터에 NonNull 계약 명시

from(Order o) 호출자가 null을 넘기면 즉시 NPE가 발생합니다. 계약을 명확히 해두면 디버깅이 쉬워집니다.

 package trademill.apiserver.order.dto;
 
 import lombok.Data;
+import lombok.NonNull;
 import trademill.apiserver.order.*;
 
 ...
-    public static OrderResponse from(Order o) {
+    public static OrderResponse from(@NonNull Order o) {
         OrderResponse r = new OrderResponse();
         r.setId(o.getId());
         r.setUserId(o.getUserId());

9-11: 응답 JSON 잡음 최소화: null 필드 제외

MARKET 주문의 price 등 null 필드가 잦을 수 있습니다. 전역 정책이 없다면 DTO 차원에서 null 제외를 권장합니다.

 package trademill.apiserver.order.dto;
 
 import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import trademill.apiserver.order.*;
 
 ...
-@Data
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class OrderResponse {

4-4: 와일드카드 임포트 축소(선택)

별칭 충돌과 예측 가능성을 줄이려면 필요한 타입만 명시 임포트하는 편이 안전합니다.

-import trademill.apiserver.order.*;
+import trademill.apiserver.order.Order;
+import trademill.apiserver.order.OrderSide;
+import trademill.apiserver.order.OrderStatus;
+import trademill.apiserver.order.OrderType;
src/main/java/trademill/apiserver/order/OrderController.java (1)

26-29: 쿼리 파라미터 userId 신뢰는 IDOR 위험 — 인증 컨텍스트에서 유저 식별자 주입 권장

현재 userId를 클라이언트가 임의 지정할 수 있어 다른 사용자의 주문내역을 조회할 수 있는 취약점(IDOR)으로 이어질 수 있습니다. Spring Security 도입 후 Authentication/Principal에서 userId를 해석하거나, 컨트롤러에서 주입받아 repository 쿼리에 사용하세요.

향후 보안 적용 시 검증 포인트:

  • GET /api/v1/orders 요청에서 userId 쿼리 파라미터 제거
  • SecurityContext에서 인증된 사용자 ID 매핑
  • 통합 테스트 추가: 타 사용자 데이터 접근 차단 확인
src/main/java/trademill/apiserver/order/Order.java (1)

34-41: 시간대 일관성 확보(UTC 권장) 및 감사 필드 자동화

서버 로컬 타임존에 의존하지 않도록 UTC로 고정하여 저장하세요.

적용 diff:

 @PrePersist
 void onCreate() {
-    createdAt = OffsetDateTime.now();
-    updatedAt = createdAt;
+    createdAt = OffsetDateTime.now(ZoneOffset.UTC);
+    updatedAt = createdAt;
     if (status == null) status = OrderStatus.PENDING;
 }
 @PreUpdate
-void onUpdate() { updatedAt = OffsetDateTime.now(); }
+void onUpdate() { updatedAt = OffsetDateTime.now(ZoneOffset.UTC); }

대안: Hibernate 사용 시 @CreationTimestamp/@UpdateTimestamp 사용으로 콜백 제거 가능.

src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java (1)

16-19: 구현체 메서드에 @OverRide 누락

인터페이스 시그니처 변경 시 컴파일 타임 검증을 위해 @OverRide를 붙이세요.

적용 diff:

-    public String placeMarketBuy(String s, BigDecimal q){ return id(); }
-    public String placeMarketSell(String s, BigDecimal q){ return id(); }
-    public String placeLimitBuy(String s, BigDecimal q, BigDecimal p){ return id(); }
-    public String placeLimitSell(String s, BigDecimal q, BigDecimal p){ return id(); }
+    @Override public String placeMarketBuy(String s, BigDecimal q){ return id(); }
+    @Override public String placeMarketSell(String s, BigDecimal q){ return id(); }
+    @Override public String placeLimitBuy(String s, BigDecimal q, BigDecimal p){ return id(); }
+    @Override public String placeLimitSell(String s, BigDecimal q, BigDecimal p){ return id(); }
src/main/java/trademill/apiserver/order/OrderService.java (1)

45-55: 중복 설정(Nit): PENDING 상태 이중 설정

@PrePersist에서 status가 null일 때 PENDING을 설정하므로, baseOf에서의 PENDING 지정은 기능상 중복입니다. 유지보수 관점에서 한 곳으로 일원화하는 편이 좋습니다.

가능한 정리:

  • baseOf에서의 o.setStatus(OrderStatus.PENDING) 제거, 혹은
  • @PrePersist의 기본값 로직 제거(택1)
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 19d64d8 and 290af05.

📒 Files selected for processing (14)
  • build.gradle (1 hunks)
  • docker-compose.yml (1 hunks)
  • src/main/java/trademill/apiserver/broker/BrokerGateway.java (1 hunks)
  • src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java (1 hunks)
  • src/main/java/trademill/apiserver/order/Order.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderController.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderRepository.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderService.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderSide.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderStatus.java (1 hunks)
  • src/main/java/trademill/apiserver/order/OrderType.java (1 hunks)
  • src/main/java/trademill/apiserver/order/dto/OrderResponse.java (1 hunks)
  • src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java (1 hunks)
  • src/main/resources/application-local.yml (1 hunks)
🔇 Additional comments (9)
src/main/java/trademill/apiserver/order/OrderType.java (1)

3-3: enum 정의 적절 — MVP 범위 충족

MARKET/LIMIT 두 가지로 시작하는 선택이 깔끔합니다. 다른 파일 요약에 따르면 Order 엔티티에서 EnumType.STRING으로 저장된다고 하니(요약 근거), 이 형태면 향후 상수 추가/정렬 변경에도 안전합니다.

src/main/java/trademill/apiserver/order/OrderSide.java (1)

3-3: enum 정의 무난

BUY/SELL 분리로 서비스 계층 분기 로직이 명확해집니다. 엔드포인트(/buy, /sell) 설계와도 자연스럽게 맞물립니다.

src/main/java/trademill/apiserver/order/OrderStatus.java (2)

3-3: 상태 세트 적절

MVP 단계에서 필요한 핵심 상태가 모두 포함되어 있습니다. 요약대로 서비스에서 ACCEPTED 설정 흐름도 자연스럽습니다.


3-3: 미국식 ‘CANCELED’ 표기 일관 확인됨

리포지토리 전체에서 ‘CANCELED’만 사용되고 ‘CANCELLED’는 발견되지 않았습니다. 요청하신 일관성 검토가 완료되었습니다.

src/main/resources/application-local.yml (2)

1-11: 로컬 H2 설정 합리적

  • PostgreSQL 호환 모드 + DATABASE_TO_LOWER 조합으로 로컬/도커(Postgres) 전환 시 스키마 괴리를 줄일 수 있습니다.
  • DB_CLOSE_DELAY=-1로 애플리케이션 라이프사이클 동안 커넥션 유지도 OK.

1-11: ✔ 비-로컬 프로파일에 ddl-auto: update 및 H2 설정 누출 없음 확인
제공해주신 스크립트 실행 결과, application-local.yml 외의 모든 프로파일에서 ddl-auto: update와 H2 관련 설정이 발견되지 않았습니다. 추가 조치 불필요합니다.

build.gradle (2)

36-38: DB 드라이버 runtimeOnly 추가 OK

로컬(H2)/도커(Postgres) 전환 시 필요한 드라이버를 런타임 범위로 추가한 선택이 타당합니다. Spring Boot BOM에 의해 호환 버전이 정렬되는 것도 장점입니다.


1-5: Spring Boot 플러그인 버전 호환성 및 최신 릴리스 확인 필요

build.gradle 1–5행의 org.springframework.boot 플러그인(3.5.4)이 Java 21과 완벽하게 호환되는지, 그리고 현재 사용 가능한 최신 안정 릴리스 버전이 무엇인지 공식 Spring Boot 문서를 통해 반드시 확인해 주세요. 특히 스프링 부트 마이너 버전 업그레이드 시 전이(dependency BOM) 의존성 버전이 크게 변경될 수 있으므로, 아래 사항을 검증해 주시기 바랍니다.

• 대상 파일: build.gradle
• 대상 라인: 1–5행 (id 'org.springframework.boot' version '3.5.4')

src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java (1)

9-13: 추가 검증을 위해 다음 스크립트를 실행했습니다:

#!/bin/bash
# BrokerGateway 인터페이스 정의 확인
rg -n 'interface\s\+BrokerGateway' -C3 --type java

# BrokerGateway를 구현하는 클래스 확인
rg -n 'implements\s\+BrokerGateway' -C3 --type java

# BrokerGateway 타입을 반환하는 @Bean 메서드 확인
rg -n '@Bean.*BrokerGateway' -C3 --type java

위 스크립트 결과를 기다리는 중입니다. 결과를 기반으로 FakeBrokerGateway의 @Profile("default") 제거가 안전한지, 또는 프로덕션용 구현체가 별도로 존재하는지를 판단한 뒤 최종 리뷰 코멘트를 업데이트하겠습니다.

Comment on lines +10 to +17
@Getter @Setter
public class PlaceOrderRequest {
@NotNull private Long userId;
@NotBlank private String symbol;
@NotNull private OrderType orderType; // MARKET or LIMIT
@NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
@DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

치명적 입력 검증 누락: LIMIT일 때 price 필수, MARKET일 때 price 금지

현재 Bean Validation만으로는 orderType에 따른 조건부 검증이 되지 않습니다. 서비스에서 수동 체크하지 않으면 잘못된 주문이 저장/전송되거나 NPE가 발생할 수 있습니다. 클래스 레벨 제약으로 조건부 검증을 추가하세요.

권장 변경(클래스에 커스텀 제약 추가 + 금액/수량 스케일 기본 제한 예시):

 package trademill.apiserver.order.dto;
 
 import jakarta.validation.constraints.*;
 import lombok.Getter;
 import lombok.Setter;
 import trademill.apiserver.order.OrderType;
+import trademill.apiserver.order.dto.validation.ValidPlaceOrder;
 
 import java.math.BigDecimal;
 
-@Getter @Setter
+@Getter @Setter
+@ValidPlaceOrder
 public class PlaceOrderRequest {
     @NotNull private Long userId;
     @NotBlank private String symbol;
     @NotNull private OrderType orderType;            // MARKET or LIMIT
-    @NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
-    @DecimalMin("0.0") private BigDecimal price;     // LIMIT일 때만 필요
+    @NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
+    @DecimalMin("0.0") private BigDecimal price;     // LIMIT일 때만 필요
 }

새 제약 어노테이션:

// src/main/java/trademill/apiserver/order/dto/validation/ValidPlaceOrder.java
package trademill.apiserver.order.dto.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PlaceOrderRequestValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPlaceOrder {
    String message() default "LIMIT는 price가 필수이고, MARKET은 price를 허용하지 않습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

검증기:

// src/main/java/trademill/apiserver/order/dto/validation/PlaceOrderRequestValidator.java
package trademill.apiserver.order.dto.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import trademill.apiserver.order.OrderType;
import trademill.apiserver.order.dto.PlaceOrderRequest;

import java.math.BigDecimal;

public class PlaceOrderRequestValidator implements ConstraintValidator<ValidPlaceOrder, PlaceOrderRequest> {
    @Override
    public boolean isValid(PlaceOrderRequest req, ConstraintValidatorContext ctx) {
        if (req == null || req.getOrderType() == null) return true; // @NotNull이 따로 처리
        BigDecimal price = req.getPrice();

        if (req.getOrderType() == OrderType.LIMIT) {
            return price != null && price.compareTo(BigDecimal.ZERO) > 0;
        } else { // MARKET
            return price == null;
        }
    }
}

검증 스크립트(서비스/컨트롤러에서 중복 체크 있는지 탐색):

원하시면 위 변경으로 인한 단위/통합 테스트 케이스도 함께 만들어 드리겠습니다.


🏁 Script executed:

#!/bin/bash
# LIMIT 시 price 필수, MARKET 시 금지 로직이 서비스/컨트롤러에 수동 구현돼 있는지 확인
rg -n -C3 'OrderType\.LIMIT|OrderType\.MARKET|\.getPrice\(\)' src/main/java --glob '!**/build/**'

Length of output: 3125


LIMIT 요청 시 price 필수, MARKET 요청 시 price 금지 유효성 검증 추가 필요

서비스 계층에서는 PlaceOrderRequestprice 값을 무조건 Order 엔티티에 세팅하고(o.setPrice(req.getPrice())), 브로커 호출 시에도 LIMIT/MARKET 분기만 할 뿐 price 유무를 검증하지 않습니다. 이대로 두면

  • LIMIT 주문에 price 누락 시 NPE 발생
  • MARKET 주문에 잘못된 price 제공 시 불필요한 금액 정보가 DB에 저장

되므로, DTO 단계에서 orderType에 따른 조건부 검증을 추가해야 합니다.

코드상 주요 위치

  • src/main/java/trademill/apiserver/order/OrderService.java
    • baseOf(...) 메서드에서 o.setPrice(req.getPrice())가 무조건 호출됨
    • switch (req.getOrderType()) 분기에서 LIMIT 시 broker.placeLimit*(..., req.getPrice()), MARKET 시 price 인자를 사용하지 않음

제안된 변경사항(클래스 레벨 커스텀 제약 추가 + 검증 로직)

 package trademill.apiserver.order.dto;
 
 import jakarta.validation.constraints.*;
 import lombok.Getter;
 import lombok.Setter;
 import trademill.apiserver.order.OrderType;
+import trademill.apiserver.order.dto.validation.ValidPlaceOrder;
 
 @Getter @Setter
+@ValidPlaceOrder
 public class PlaceOrderRequest {
     @NotNull private Long userId;
     @NotBlank private String symbol;
     @NotNull private OrderType orderType;            // MARKET or LIMIT
     @NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
     @DecimalMin("0.0") private BigDecimal price;     // LIMIT일 때만 필요
 }

추가할 커스텀 제약 어노테이션 & 검증기

// src/main/java/trademill/apiserver/order/dto/validation/ValidPlaceOrder.java
@Documented
@Constraint(validatedBy = PlaceOrderRequestValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPlaceOrder {
    String message() default "LIMIT는 price가 필수이고, MARKET은 price를 입력할 수 없습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// src/main/java/trademill/apiserver/order/dto/validation/PlaceOrderRequestValidator.java
public class PlaceOrderRequestValidator
    implements ConstraintValidator<ValidPlaceOrder, PlaceOrderRequest> {
    @Override
    public boolean isValid(PlaceOrderRequest req, ConstraintValidatorContext ctx) {
        if (req == null || req.getOrderType() == null) return true; // @NotNull 처리
        BigDecimal price = req.getPrice();

        if (req.getOrderType() == OrderType.LIMIT) {
            return price != null && price.compareTo(BigDecimal.ZERO) > 0;
        } else { // MARKET
            return price == null;
        }
    }
}

테스트 케이스

  • LIMIT 주문에 price=null → 검증 실패
  • LIMIT 주문에 price≤0 → 검증 실패
  • MARKET 주문에 price!=null → 검증 실패
  • MARKET 주문에 price=null → 검증 성공
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java around
lines 10-17, add class-level conditional validation so LIMIT orders require a
positive non-null price and MARKET orders forbid any price; implement a custom
@ValidPlaceOrder annotation and a ConstraintValidator that checks if
req.getOrderType()==LIMIT then price!=null and price>0, else if MARKET then
price==null, annotate PlaceOrderRequest with @ValidPlaceOrder, and ensure
controller/validation pipeline triggers bean validation so invalid requests are
rejected before OrderService runs.

Comment on lines +11 to +33
@Entity
@Table(name = "orders")
@Getter @Setter @NoArgsConstructor
public class Order {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long userId;
private String symbol;

@Enumerated(EnumType.STRING) private OrderSide side;
@Enumerated(EnumType.STRING) private OrderType orderType;

private BigDecimal quantity;
private BigDecimal price; // MARKET이면 null 가능

@Enumerated(EnumType.STRING) private OrderStatus status;
private String brokerOrderId;

private OffsetDateTime createdAt;
private OffsetDateTime updatedAt;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

엔티티 제약 조건(Nullable/Precision/Index/Version) 보강으로 데이터 무결성과 조회 성능 개선

  • 비null 필드에 @column(nullable=false) 누락.
  • 수량/가격 BigDecimal precision/scale 미지정 → 금액/수량 반올림/절삭 이슈 가능.
  • 조회 패턴상 userId, createdAt 조합 인덱스, brokerOrderId 유니크 제약 권장.
  • 낙관적 락(@Version) 추가로 동시 수정 방지.

적용 diff:

-@Table(name = "orders")
+@Table(name = "orders", indexes = {
+        @Index(name = "idx_orders_user_created", columnList = "userId,createdAt"),
+        @Index(name = "uk_orders_broker_order_id", columnList = "brokerOrderId", unique = true)
+})
 @Getter @Setter @NoArgsConstructor
 public class Order {
@@
-    private Long userId;
-    private String symbol;
+    @Column(nullable = false)
+    private Long userId;
+    @Column(length = 20, nullable = false)
+    private String symbol;
@@
-    @Enumerated(EnumType.STRING) private OrderSide side;
-    @Enumerated(EnumType.STRING) private OrderType orderType;
+    @Enumerated(EnumType.STRING) @Column(nullable = false)
+    private OrderSide side;
+    @Enumerated(EnumType.STRING) @Column(nullable = false)
+    private OrderType orderType;
@@
-    private BigDecimal quantity;
-    private BigDecimal price; // MARKET이면 null 가능
+    @Column(precision = 19, scale = 4, nullable = false)
+    private BigDecimal quantity;
+    @Column(precision = 19, scale = 4) // MARKET이면 null 가능
+    private BigDecimal price;
@@
-    @Enumerated(EnumType.STRING) private OrderStatus status;
-    private String brokerOrderId;
+    @Enumerated(EnumType.STRING) @Column(nullable = false)
+    private OrderStatus status;
+    @Column(length = 64)
+    private String brokerOrderId;
@@
-    private OffsetDateTime createdAt;
-    private OffsetDateTime updatedAt;
+    @Column(nullable = false, updatable = false)
+    private OffsetDateTime createdAt;
+    @Column(nullable = false)
+    private OffsetDateTime updatedAt;
+
+    @Version
+    private Long version;

추가 import 필요:

import java.time.ZoneOffset;
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/Order.java around lines 11-33,
several JPA constraints and indexes are missing: mark non-nullable fields with
@Column(nullable = false) (at least userId, symbol, side, orderType, quantity,
status, createdAt), specify BigDecimal precision/scale for quantity and price
via @Column(precision=18, scale=8) (adjust numbers to your domain), add
table-level indexes/constraints with @Table(indexes =
{@Index(name="idx_user_created", columnList="userId,createdAt")},
uniqueConstraints = {@UniqueConstraint(columnNames = "brokerOrderId")}), and add
an optimistic lock field annotated @Version (e.g., private Long version) plus
the required imports; update getters/setters/constructors accordingly.

Comment on lines +26 to +31
@GetMapping
public Page<OrderResponse> list(@RequestParam Long userId,
@RequestParam(defaultValue="0") int page,
@RequestParam(defaultValue="20") int size){
return service.list(userId, PageRequest.of(page, size, Sort.by("id").descending()))
.map(OrderResponse::from);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

페이지 파라미터 검증 추가 + 정렬 컬럼을 생성시각으로 변경 권장

  • page/size에 대한 최소/최대값 검증이 없어 오남용(대용량 조회) 가능성이 있습니다.
  • 정렬 컬럼으로 id 대신 createdAt 사용을 권장합니다. 실제 시간 역순이 의도에 더 부합하며, RDBMS별 ID 시퀀싱 정책/샤딩에 덜 의존적입니다.

적용 diff:

-    public Page<OrderResponse> list(@RequestParam Long userId,
-                                    @RequestParam(defaultValue="0") int page,
-                                    @RequestParam(defaultValue="20") int size){
-        return service.list(userId, PageRequest.of(page, size, Sort.by("id").descending()))
+    public Page<OrderResponse> list(@RequestParam @NotNull Long userId,
+                                    @RequestParam(defaultValue="0") @PositiveOrZero int page,
+                                    @RequestParam(defaultValue="20") @Positive @Max(200) int size){
+        return service.list(userId, PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")))
                 .map(OrderResponse::from);

추가 필요(파일 상단 import 및 클래스 애노테이션):

// import
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

// 클래스 상단(예: @RestController 아래 줄)
@Validated
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderController.java around lines
26-31, the controller method lacks validation for page/size and uses id for
sorting; add parameter validation annotations (@NotNull for userId,
@PositiveOrZero for page, @Positive and @Max(100) for size) on the method
signature, change the PageRequest sort from Sort.by("id").descending() to
Sort.by("createdAt").descending(), and add the required imports
(jakarta.validation.constraints.*) plus the @Validated annotation on the
controller class to enable method-level validation. Ensure the defaultValue
attributes remain but use the annotations to enforce limits and return
appropriate validation responses from Spring.

Comment on lines +19 to +28
@Transactional
public Order placeBuy(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.BUY);
String id = switch (req.getOrderType()){
case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
return orders.save(o);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

외부 브로커 호출을 트랜잭션 내부에서 수행 — 롱 락/롤백 전파로 인한 리스크

외부 I/O(브로커 호출)를 @transactional 경계 안에서 처리하면 연결 지연/예외 시 DB 트랜잭션이 장시간 유지되며 롤백 시 전체 작업이 무효화됩니다. 최소한 예외를 포착하여 주문 레코드가 남도록 처리하고, 가능하면 외부 호출은 트랜잭션 밖에서 수행하거나 2단계로 분리(REQUIRES_NEW)하세요.

안전망 추가를 위한 최소 변경 diff:

 @Transactional
 public Order placeBuy(PlaceOrderRequest req){
     Order o = baseOf(req, OrderSide.BUY);
-    String id = switch (req.getOrderType()){
-        case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity());
-        case LIMIT  -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice());
-    };
-    o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
-    return orders.save(o);
+    try {
+        String id = switch (req.getOrderType()){
+            case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity());
+            case LIMIT  -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice());
+        };
+        o.setBrokerOrderId(id);
+        o.setStatus(OrderStatus.ACCEPTED);
+    } catch (Exception e) {
+        // TODO: 실패 상태 도입 시 교체(e.g. FAILED/REJECTED)
+        o.setStatus(OrderStatus.PENDING);
+        // TODO: 로깅 추가
+    }
+    return orders.save(o);
 }

권장 구조(개요):

  • Tx1: 주문 PENDING 생성/저장
  • Tx 바깥: 브로커 호출
  • Tx2(REQUIRES_NEW): brokerOrderId/상태 업데이트
  • 실패 시 재시도/보상 로직 또는 outbox/event 기반 비동기화
📝 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
@Transactional
public Order placeBuy(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.BUY);
String id = switch (req.getOrderType()){
case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
return orders.save(o);
}
@Transactional
public Order placeBuy(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.BUY);
try {
String id = switch (req.getOrderType()) {
case MARKET -> broker.placeMarketBuy(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitBuy(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id);
o.setStatus(OrderStatus.ACCEPTED);
} catch (Exception e) {
// TODO: 실패 상태 도입 시 교체(e.g. FAILED/REJECTED)
o.setStatus(OrderStatus.PENDING);
// TODO: 로깅 추가
}
return orders.save(o);
}

Comment on lines +30 to +39
@Transactional
public Order placeSell(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.SELL);
String id = switch (req.getOrderType()){
case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
return orders.save(o);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

매도 로직도 동일한 예외 처리 및 트랜잭션 리스크

위 매수와 동일하게 try/catch 및 저장 경로 보장 필요.

적용 diff:

 @Transactional
 public Order placeSell(PlaceOrderRequest req){
     Order o = baseOf(req, OrderSide.SELL);
-    String id = switch (req.getOrderType()){
-        case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity());
-        case LIMIT  -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice());
-    };
-    o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
-    return orders.save(o);
+    try {
+        String id = switch (req.getOrderType()){
+            case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity());
+            case LIMIT  -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice());
+        };
+        o.setBrokerOrderId(id);
+        o.setStatus(OrderStatus.ACCEPTED);
+    } catch (Exception e) {
+        // TODO: 실패 상태 도입 시 교체(e.g. FAILED/REJECTED)
+        o.setStatus(OrderStatus.PENDING);
+        // TODO: 로깅 추가
+    }
+    return orders.save(o);
 }
📝 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
@Transactional
public Order placeSell(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.SELL);
String id = switch (req.getOrderType()){
case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id); o.setStatus(OrderStatus.ACCEPTED);
return orders.save(o);
}
@Transactional
public Order placeSell(PlaceOrderRequest req){
Order o = baseOf(req, OrderSide.SELL);
try {
String id = switch (req.getOrderType()){
case MARKET -> broker.placeMarketSell(req.getSymbol(), req.getQuantity());
case LIMIT -> broker.placeLimitSell(req.getSymbol(), req.getQuantity(), req.getPrice());
};
o.setBrokerOrderId(id);
o.setStatus(OrderStatus.ACCEPTED);
} catch (Exception e) {
// TODO: 실패 상태 도입 시 교체(e.g. FAILED/REJECTED)
o.setStatus(OrderStatus.PENDING);
// TODO: 로깅 추가
}
return orders.save(o);
}
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderService.java around lines 30 to
39, the sell flow lacks the same try/catch and guaranteed persistence path as
the buy flow, so wrap the broker calls and subsequent state updates in a
try/catch that mirrors the buy logic: create and persist the initial Order
before external broker call if your buy flow does that, call broker inside try,
on success set brokerOrderId and ACCEPTED and save, on broker exception set
order status to REJECTED (or appropriate failure state), record the exception
details, save the order in the catch to guarantee persistence, and rethrow or
handle transaction boundaries as the buy path does to avoid leaving the system
in an inconsistent state.

Comment on lines +45 to +55
private Order baseOf(PlaceOrderRequest req, OrderSide side){
Order o = new Order();
o.setUserId(req.getUserId());
o.setSymbol(req.getSymbol());
o.setSide(side);
o.setOrderType(req.getOrderType());
o.setQuantity(req.getQuantity());
o.setPrice(req.getPrice());
o.setStatus(OrderStatus.PENDING);
return o;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

비즈니스 규칙 검증(수량/가격) 강화 — LIMIT/​MARKET별 가격 유효성

서비스 레벨에서 최소 검증을 추가하여 잘못된 입력이 브로커까지 전달되지 않도록 하세요. 특히 LIMIT는 price 필수/양수, MARKET은 price 무시(null).

적용 diff:

 private Order baseOf(PlaceOrderRequest req, OrderSide side){
     Order o = new Order();
     o.setUserId(req.getUserId());
     o.setSymbol(req.getSymbol());
     o.setSide(side);
     o.setOrderType(req.getOrderType());
-    o.setQuantity(req.getQuantity());
-    o.setPrice(req.getPrice());
+    if (req.getQuantity() == null || req.getQuantity().signum() <= 0) {
+        throw new IllegalArgumentException("quantity must be positive");
+    }
+    o.setQuantity(req.getQuantity());
+    switch (req.getOrderType()) {
+        case MARKET -> o.setPrice(null);
+        case LIMIT -> {
+            if (req.getPrice() == null || req.getPrice().signum() <= 0) {
+                throw new IllegalArgumentException("price must be positive for LIMIT order");
+            }
+            o.setPrice(req.getPrice());
+        }
+    }
     o.setStatus(OrderStatus.PENDING);
     return o;
 }

참고: DTO(PlaceOrderRequest)에도 Bean Validation(@NotNull, @positive 등)으로 1차 방어를 추가하면 더 견고해집니다.

📝 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
private Order baseOf(PlaceOrderRequest req, OrderSide side){
Order o = new Order();
o.setUserId(req.getUserId());
o.setSymbol(req.getSymbol());
o.setSide(side);
o.setOrderType(req.getOrderType());
o.setQuantity(req.getQuantity());
o.setPrice(req.getPrice());
o.setStatus(OrderStatus.PENDING);
return o;
}
private Order baseOf(PlaceOrderRequest req, OrderSide side){
Order o = new Order();
o.setUserId(req.getUserId());
o.setSymbol(req.getSymbol());
o.setSide(side);
o.setOrderType(req.getOrderType());
- o.setQuantity(req.getQuantity());
if (req.getQuantity() == null || req.getQuantity().signum() <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
o.setQuantity(req.getQuantity());
switch (req.getOrderType()) {
case MARKET -> o.setPrice(null);
case LIMIT -> {
if (req.getPrice() == null || req.getPrice().signum() <= 0) {
throw new IllegalArgumentException("price must be positive for LIMIT order");
}
o.setPrice(req.getPrice());
}
}
o.setStatus(OrderStatus.PENDING);
return o;
}
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderService.java around lines 45 to
55, strengthen business-rule validation in baseOf: for LIMIT orders ensure
req.getPrice() is non-null and > 0 (throw IllegalArgumentException or a
domain-specific ValidationException if violated); for MARKET orders ignore any
supplied price and set Order.price to null; also validate req.getQuantity() is >
0 and fail fast on invalid values; keep setting status to PENDING. Optionally
add or complement DTO Bean Validation annotations (@NotNull, @Positive) on
PlaceOrderRequest as a first-line defense.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants