Conversation
- kis profile + env 주입 (KIS_* 환경변수) - Token/HashKey/현금주문 호출 플로우 - 설정/가짜브로커/프로퍼티 정리 - Demo 예약어 충돌 수정
WalkthroughWebFlux와 H2 의존성을 추가하고, KIS 관련 프로퍼티/빈 구성을 도입했다. 브로커 포트(BrokerGateway)와 로컬용 구현(FakeBrokerGateway), KIS 연동 구현(KisBrokerGateway, 토큰 서비스, DTO, 설정)을 추가했다. 주문 엔터티/리포지토리/서비스/컨트롤러와 DTO를 신설하고, Demo 엔터티의 컬럼 매핑을 수정했다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 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 unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (18)
src/main/java/trademill/apiserver/demo/domain/Demo.java (1)
12-24: 대안: 전역 식별자 인용 또는 명시적 DDL로 예약어 이슈의 재발 방지유사 이슈 재발을 막으려면:
- Hibernate:
hibernate.globally_quoted_identifiers=true설정 검토.- 마이그레이션 도구(Flyway/Liquibase)로 명시적 DDL 관리.
운영 DB와 H2 모두에서 동일하게 동작하는지 확인 바랍니다.
src/main/java/trademill/apiserver/order/OrderType.java (1)
1-3: enum 도입은 적절. 외부 입력(대소문자·별칭) 바인딩만 확인해 주세요.
- 클라이언트가 "market"/"limit" 등 소문자나 별칭을 보낼 가능성에 대비해 컨트롤러/DTO 레벨에서 대소문자 무시 또는 커스텀 디시리얼라이저 설정 여부를 점검해 주세요.
- 추후 STOP/STOP_LIMIT 등 확장 케이스가 있을지 로드맵만 명확히 해두면 좋습니다.
build.gradle (2)
30-30: spring-boot-starter-web + webflux 동시 사용: MVC 우선 구동됨. 의도 확인 및 명시 설정 권장
- 두 스타터 동시 포함 시 Spring Boot는 기본적으로 MVC(서블릿/Tomcat)로 기동됩니다. WebClient 사용 목적이라면 현재 구성은 동작하지만, 의도를 명확히 하려면 프로필에 다음을 지정하는 것을 권장합니다:
# application-kis.yml (예시) spring: main: web-application-type: servlet
- 리액티브 서버가 불필요한지(단지 WebClient만 쓰는지) 확인해 주세요. 필요 시 reactor-test 추가를 고려해 테스트에서 StepVerifier 등을 사용할 수 있습니다:
testImplementation 'io.projectreactor:reactor-test'
36-36: H2 runtimeOnly 추가 OK. 프로덕션 프로필에서의 누출만 차단 확인
- H2는 로컬/테스트 전용으로 보이며 runtimeOnly 스코프는 적절합니다. 다만 프로덕션 프로필에서 H2가 활성화되지 않도록 데이터소스/프로필 분리를 확인해 주세요(예: application-kis.yml만에서 H2 설정 유지).
- 필요 시 의존성도 테스트 전용으로 분리해 두는 선택지(testRuntimeOnly)도 있습니다.
src/main/java/trademill/apiserver/order/OrderStatus.java (1)
1-3: 상태 enum 구성이 직관적입니다. KIS 코드→내부 상태 매핑표만 고정화 권장
- KIS 응답 코드/메시지(접수/거부/체결/부분체결/취소 등)와 본 enum 간 일대일 매핑 규칙을 문서화/유닛테스트로 고정해 두면 회귀 방지에 유리합니다.
- 영속 계층에서 @Enumerated(EnumType.STRING) 사용 중이라면 현재 이름 변경이 스키마 호환성에 직접 영향 주므로 주의해 주세요.
src/main/java/trademill/apiserver/TrademillApiApplication.java (1)
5-10: @EnableConfigurationProperties 위치를 kis 프로필로 한정하세요비-kis 프로필에서도 KisProperties 빈이 생성될 수 있어(특히 @validated 제약 사용 시) 부트 실패 가능성이 있습니다. KisConfig(@Profile("kis"))로 이동을 권장합니다.
다음 diff로 본 파일에서 제거하고, KisConfig에 추가해 주세요.
-import org.springframework.boot.context.properties.EnableConfigurationProperties; -import trademill.apiserver.config.KisProperties; - -@EnableConfigurationProperties(KisProperties.class)KisConfig 예시(참고, 별도 파일 수정 필요):
@Configuration @Profile("kis") @EnableConfigurationProperties(KisProperties.class) public class KisConfig { /* 기존 구성 유지 */ }src/main/java/trademill/apiserver/order/OrderRepository.java (1)
6-8: 메서드 시그니처 적절 — 테이블/컬럼 예약어 충돌 및 인덱스 확인 권장
- Order는 SQL 예약어와 충돌합니다. 엔티티에
@Table(name = "orders")등으로 명시해주세요.userId조회가 빈번하므로 인덱스 추가를 권장합니다.엔티티 예시(참고, 별도 파일 수정 필요):
@Entity @Table(name = "orders", indexes = { @Index(name = "idx_orders_user_id", columnList = "user_id") }) class Order { /* ... */ }src/main/java/trademill/apiserver/broker/kis/dto/KisResponse.java (1)
3-11: JSON 호환성과 자바 관례 개선: snake_case 매핑 + 확장성 보강자바 관례(camelCase)로 필드명을 정리하고, JSON 필드와는 @JsonProperty로 매핑하면 유지보수가 좋아집니다. 알 수 없는 필드 무시 옵션도 추가하세요.
package trademill.apiserver.broker.kis.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data -public class KisResponse<T> { - private String rt_cd; // "0" 성공, "1" 실패 - private String msg_cd; - private String msg1; - private T output; -} +@JsonIgnoreProperties(ignoreUnknown = true) +public class KisResponse<T> { + @JsonProperty("rt_cd") + private String rtCd; // "0" 성공, "1" 실패 + @JsonProperty("msg_cd") + private String msgCd; + @JsonProperty("msg1") + private String msg1; + @JsonProperty("output") + private T output; + + @JsonIgnore + public boolean isOk() { return "0".equals(rtCd); } +}src/main/java/trademill/apiserver/config/KisProperties.java (2)
15-25: 계좌 파싱 로직 경계조건/공백 처리 보강좌측/우측 공백,
-가 끝/앞에 오는 이상치 등 경계조건을 보완해 주세요.- public String getCano() { - if (accountNo == null) return null; - int idx = accountNo.indexOf('-'); - return idx > 0 ? accountNo.substring(0, idx) : accountNo; - } + public String getCano() { + if (accountNo == null || accountNo.isBlank()) return null; + String acc = accountNo.trim(); + int idx = acc.indexOf('-'); + return (idx > 0) ? acc.substring(0, idx) : acc; + } - public String getAcntPrdtCd() { - if (accountNo == null) return null; - int idx = accountNo.indexOf('-'); - return idx > 0 ? accountNo.substring(idx + 1) : "01"; - } + public String getAcntPrdtCd() { + if (accountNo == null || accountNo.isBlank()) return null; + String acc = accountNo.trim(); + int idx = acc.indexOf('-'); + return (idx > 0 && idx < acc.length() - 1) ? acc.substring(idx + 1) : "01"; + }
27-30: 가상환경 판별 시 대소문자 무시
baseUrl의 대소문자 및 포맷 차이를 흡수하는 편이 안전합니다.- return baseUrl != null && baseUrl.contains("openapivts"); + return baseUrl != null && baseUrl.toLowerCase(java.util.Locale.ROOT).contains("openapivts");src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java (1)
13-13: 심볼 포맷 제약(선택): 국내 6자리 종목코드만 허용국내 주식만 대상으로 시작한다면 6자리 숫자 패턴을 권장합니다. 멀티마켓을 고려하면 이 코멘트는 무시해도 됩니다.
- @NotBlank private String symbol; + @NotBlank + @jakarta.validation.constraints.Pattern(regexp = "\\d{6}", message = "국내 종목코드는 6자리 숫자여야 합니다.") + private String symbol;src/main/java/trademill/apiserver/config/KisConfig.java (2)
20-21: 기본 Content-Type 헤더 전역 설정 지양GET 등 바디가 없는 요청에도 전역
Content-Type이 실릴 수 있습니다. 요청 단위로 설정하는 편이 안전합니다(토큰 요청처럼 JSON 바디가 있는 경우에만).- .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE + "; charset=UTF-8") .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
16-25: 네트워크 타임아웃/커넥터 설정 권장외부 API 연동 특성상 커넥트/응답 타임아웃과 압축/로깅 필터를 구성하는 편이 운영 안정성에 유리합니다.
예시(참고용, 별도 적용 파일 외 코드):
@Bean public WebClient kisWebClient(KisProperties props) { var httpClient = reactor.netty.http.client.HttpClient.create() .responseTimeout(java.time.Duration.ofSeconds(5)); return WebClient.builder() .baseUrl(props.getBaseUrl()) .clientConnector(new org.springframework.http.client.reactive.ReactorClientHttpConnector(httpClient)) .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE) .exchangeStrategies(ExchangeStrategies.builder() .codecs(c -> c.defaultCodecs().maxInMemorySize(4 * 1024 * 1024)) .build()) .build(); }src/main/java/trademill/apiserver/order/dto/OrderResponse.java (2)
3-11: API 응답 DTO는 불변으로 유지 권장 (@DaTa → @Value/@builder).외부로 노출되는 응답 객체는 setter 노출을 최소화하는 것이 안전합니다. 불변 DTO로 전환하고 생성은 빌더를 사용하세요.
-import lombok.Data; +import lombok.Value; +import lombok.Builder; -@Data -public class OrderResponse { +@Value +@Builder +public class OrderResponse { @@ - public static OrderResponse from(Order o) { - OrderResponse r = new OrderResponse(); - r.setId(o.getId()); - r.setUserId(o.getUserId()); - r.setSymbol(o.getSymbol()); - r.setSide(o.getSide()); - r.setOrderType(o.getOrderType()); - r.setQuantity(o.getQuantity()); - r.setPrice(o.getPrice()); - r.setStatus(o.getStatus()); - r.setBrokerOrderId(o.getBrokerOrderId()); - r.setCreatedAt(o.getCreatedAt()); - r.setUpdatedAt(o.getUpdatedAt()); - return r; - } + public static OrderResponse from(Order o) { + return OrderResponse.builder() + .id(o.getId()) + .userId(o.getUserId()) + .symbol(o.getSymbol()) + .side(o.getSide()) + .orderType(o.getOrderType()) + .quantity(o.getQuantity()) + .price(o.getPrice()) + .status(o.getStatus()) + .brokerOrderId(o.getBrokerOrderId()) + .createdAt(o.getCreatedAt()) + .updatedAt(o.getUpdatedAt()) + .build(); + }Also applies to: 23-37
6-10: null 필드 숨김으로 응답 간결화 (예: MARKET 주문 시 price=null).null 값을 응답에서 제외하면 클라이언트 해석이 단순해집니다.
+import com.fasterxml.jackson.annotation.JsonInclude; @@ +@JsonInclude(JsonInclude.Include.NON_NULL) public class OrderResponse {Also applies to: 20-21
src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java (1)
16-19: @OverRide 누락.인터페이스 구현 메서드에 @OverRide를 추가해 시그니처 변경 시 컴파일 타임 검증을 받으세요.
- 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)
41-43: 조회 트랜잭션에 readOnly 플래그 추가.읽기 전용 힌트로 불필요한 더티체킹을 방지합니다.
- public Page<Order> list(Long userId, Pageable pageable){ + @Transactional(readOnly = true) + public Page<Order> list(Long userId, Pageable pageable){ return orders.findByUserId(userId, pageable); }src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java (1)
70-89: WebClient 예외 처리/타임아웃 보강 고려.현재 retrieve().block() 사용은 4xx/5xx에서 WebClientResponseException을 던지지만, 타임아웃/재시도/에러 바디 로깅이 부족합니다. KisConfig의 WebClient에 타임아웃/재시도를 넣거나 여기서 onStatus로 상세 메시지를 포함하세요.
Also applies to: 101-118
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (21)
build.gradle(1 hunks)src/main/java/trademill/apiserver/TrademillApiApplication.java(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/broker/kis/KisBrokerGateway.java(1 hunks)src/main/java/trademill/apiserver/broker/kis/KisTokenService.java(1 hunks)src/main/java/trademill/apiserver/broker/kis/dto/KisResponse.java(1 hunks)src/main/java/trademill/apiserver/broker/kis/dto/OrderCashOutput.java(1 hunks)src/main/java/trademill/apiserver/config/KisConfig.java(1 hunks)src/main/java/trademill/apiserver/config/KisProperties.java(1 hunks)src/main/java/trademill/apiserver/demo/domain/Demo.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-kis.yml(1 hunks)
🔇 Additional comments (7)
src/main/java/trademill/apiserver/demo/domain/Demo.java (2)
21-22: 예약어 충돌 회피를 위한 컬럼명 변경 적절H2 등에서 VALUE는 예약어라 매핑을
val로 바꾼 결정은 합리적입니다. 로컬 부팅/DDL 생성 시 충돌을 줄여줍니다.
21-22:@Column(nullable=false)삭제로 null 허용 상태 변경 — 의도 확인 및 검증 필요
현재@Column(name="val")에nullable=false가 빠져 있어 JPA 자동 생성 시 해당 컬럼이 null 허용 상태가 됩니다. 마이그레이션 파일이나 자동 생성 스키마 설정에서 NOT NULL 제약이 유지되는지 확인하고, 필요 시 아래 수정안을 적용하세요.- @Column(name = "val") + @Column(name = "val", nullable = false)
- resources/db/migration 등에서
val컬럼 NOT NULL 제약 여부 확인- 서비스/컨트롤러 로직에서
Demo.getValue()null 처리 검증src/main/java/trademill/apiserver/order/OrderSide.java (1)
3-3: 심플한 enum 도입 적절 — LGTM도메인 모델 가독성 및 타입 안정성에 도움이 됩니다.
src/main/resources/application-kis.yml (1)
19-23: ExchangeFunctions DEBUG 레벨은 헤더 값을 기록하지 않습니다 — 헤더 세부 로깅은 TRACE + spring.codec.log-request-details 활성화 시에만 발생
DEBUG 로깅은 요청 시작 및 상태만 출력하며, 헤더 값은 노출되지 않습니다 (TRACE 레벨과spring.codec.log-request-details=true또는spring.http.log-request-details=true설정 시에만 헤더가 기록됨) (stackoverflow.com, runebook.dev)
운영 환경에서는 TRACE/DEBUG 로깅 비권장하며, 민감정보 로깅이 필요한 경우에만 헤더 마스킹 필터를 적용하세요.Likely an incorrect or invalid review comment.
src/main/java/trademill/apiserver/order/Order.java (1)
34-41: 감사/기본값 로직은 적절합니다.생성/수정 시각 설정과 기본 상태값(PENDING) 지정이 잘 되어 있습니다.
src/main/java/trademill/apiserver/broker/FakeBrokerGateway.java (1)
9-19: 로컬/기본 프로파일 바인딩 적절.kis 프로파일과 충돌 없이 기본/로컬에서만 활성화되도록 잘 구성되어 있습니다.
src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java (1)
52-55: 토큰/민감정보 로깅 없음 확인.민감 정보가 로그에 노출되지 않도록 잘 처리되었습니다. 유지하세요.
| // BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할 | ||
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
포트 인터페이스에 Bean Validation 추가(심볼 공백 금지, 수량/가격 양수)
- 구현체에 @validated를 붙이면 인터페이스 파라미터 제약이 런타임 검증됩니다. 조기 입력 검증으로 방어선을 올려주세요.
package trademill.apiserver.broker;
import java.math.BigDecimal;
-// BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
+
+// BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할
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);
+ String placeMarketBuy(@NotBlank String symbol, @Positive BigDecimal quantity);
+ String placeMarketSell(@NotBlank String symbol, @Positive BigDecimal quantity);
+ String placeLimitBuy(@NotBlank String symbol, @Positive BigDecimal quantity, @Positive BigDecimal price);
+ String placeLimitSell(@NotBlank String symbol, @Positive BigDecimal quantity, @Positive BigDecimal price);
}- 향후 확장성을 고려하면 반환값을
String대신 도메인 타입(예:BrokerOrderResult{orderId,status,raw...})으로 승격하는 것도 추천합니다.
📝 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.
| // BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할 | |
| 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); | |
| } | |
| package trademill.apiserver.broker; | |
| import java.math.BigDecimal; | |
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.Positive; | |
| // BrokerGateway : '증권사와 어떻게 대화할지' 계약서(포트) 역할 | |
| public interface BrokerGateway { | |
| String placeMarketBuy(@NotBlank String symbol, @Positive BigDecimal quantity); | |
| String placeMarketSell(@NotBlank String symbol, @Positive BigDecimal quantity); | |
| String placeLimitBuy(@NotBlank String symbol, @Positive BigDecimal quantity, @Positive BigDecimal price); | |
| String placeLimitSell(@NotBlank String symbol, @Positive BigDecimal quantity, @Positive BigDecimal price); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/broker/BrokerGateway.java around lines 6 to
12, the port interface lacks Bean Validation on parameters (symbol must not be
blank, quantity/price must be positive); annotate method parameters with
javax.validation constraints: use @NotBlank on symbol and @Positive on quantity
and price, and ensure any Spring implementation class is annotated with
@org.springframework.validation.annotation.Validated so the interface-level
constraints are enforced at runtime; optionally consider evolving the String
return type to a domain result object (e.g., BrokerOrderResult) in a follow-up.
| @Data | ||
| public class OrderCashOutput { | ||
| // KIS 주문응답의 주문번호 필드 (문서 기준) | ||
| private String ORD_NO; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Jackson 매핑 강화: 자바 관례 필드명 + @JsonProperty로 매핑, 알 수 없는 필드 무시
- 현재
ORD_NO처럼 대문자+언더스코어 필드명은 자바 관례와 달라 IDE/정적분석 경고가 발생할 수 있습니다. - JSON 키는 유지하되, 자바 필드는 관례대로 바꾸고 알 수 없는 필드 무시는 호환성에 유리합니다.
적용 diff:
package trademill.apiserver.broker.kis.dto;
-import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
@Data
-public class OrderCashOutput {
- // KIS 주문응답의 주문번호 필드 (문서 기준)
- private String ORD_NO;
-}
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OrderCashOutput {
+ // KIS 주문응답의 주문번호 필드 (문서 기준)
+ @JsonProperty("ORD_NO")
+ private String ordNo;
+}📝 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.
| @Data | |
| public class OrderCashOutput { | |
| // KIS 주문응답의 주문번호 필드 (문서 기준) | |
| private String ORD_NO; | |
| } | |
| package trademill.apiserver.broker.kis.dto; | |
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | |
| import com.fasterxml.jackson.annotation.JsonProperty; | |
| import lombok.Data; | |
| @Data | |
| @JsonIgnoreProperties(ignoreUnknown = true) | |
| public class OrderCashOutput { | |
| // KIS 주문응답의 주문번호 필드 (문서 기준) | |
| @JsonProperty("ORD_NO") | |
| private String ordNo; | |
| } |
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/broker/kis/dto/OrderCashOutput.java around
lines 5 to 9, rename the non‑conventional field ORD_NO to a Java‑style ordNo,
annotate the field with @JsonProperty("ORD_NO") to preserve JSON mapping, and
add @JsonIgnoreProperties(ignoreUnknown = true) to the class so unknown JSON
fields are ignored; keep the existing Lombok @Data and add the necessary Jackson
imports.
| @Override | ||
| public String placeMarketBuy(String symbol, BigDecimal quantity) { | ||
| return place(OrderSide.BUY, "MARKET", symbol, quantity, null); | ||
| } | ||
|
|
||
| @Override | ||
| public String placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price) { | ||
| return place(OrderSide.BUY, "LIMIT", symbol, quantity, price); | ||
| } | ||
|
|
||
| @Override | ||
| public String placeMarketSell(String symbol, BigDecimal quantity) { | ||
| return place(OrderSide.SELL, "MARKET", symbol, quantity, null); | ||
| } | ||
|
|
||
| @Override | ||
| public String placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price) { | ||
| return place(OrderSide.SELL, "LIMIT", symbol, quantity, price); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
String 기반 orderType 제거 → Enum 사용 및 수량/응답 유효성 강화.
- 내부 메서드에 String 상수를 넘기지 말고 OrderType enum을 사용하세요.
- ORD_QTY는 정수만 허용되므로(BigDecimal 소수 금지) toBigIntegerExact로 강제하십시오.
- 응답에 ORD_NO가 없으면 실패로 간주하고 예외를 던지세요(임의의 플레이스홀더 반환 금지).
import trademill.apiserver.config.KisProperties;
import trademill.apiserver.order.OrderSide;
+import trademill.apiserver.order.OrderType;
@@
@Override
public String placeMarketBuy(String symbol, BigDecimal quantity) {
- return place(OrderSide.BUY, "MARKET", symbol, quantity, null);
+ return place(OrderSide.BUY, OrderType.MARKET, symbol, quantity, null);
}
@@
@Override
public String placeLimitBuy(String symbol, BigDecimal quantity, BigDecimal price) {
- return place(OrderSide.BUY, "LIMIT", symbol, quantity, price);
+ return place(OrderSide.BUY, OrderType.LIMIT, symbol, quantity, price);
}
@@
@Override
public String placeMarketSell(String symbol, BigDecimal quantity) {
- return place(OrderSide.SELL, "MARKET", symbol, quantity, null);
+ return place(OrderSide.SELL, OrderType.MARKET, symbol, quantity, null);
}
@@
@Override
public String placeLimitSell(String symbol, BigDecimal quantity, BigDecimal price) {
- return place(OrderSide.SELL, "LIMIT", symbol, quantity, price);
+ return place(OrderSide.SELL, OrderType.LIMIT, symbol, quantity, price);
}
@@
- private String place(OrderSide side, String orderType, String symbol, BigDecimal qty, BigDecimal price) {
+ private String place(OrderSide side, OrderType orderType, String symbol, BigDecimal qty, BigDecimal price) {
@@
- // ORD_DVSN: 00=지정가, 01=시장가
- final String ordDvsn = "LIMIT".equals(orderType) ? "00" : "01";
- final String ordUnpr = "MARKET".equals(orderType) ? "0" : price.toPlainString();
+ // ORD_DVSN: 00=지정가, 01=시장가
+ final String ordDvsn = (orderType == OrderType.LIMIT) ? "00" : "01";
+ final String ordUnpr = (orderType == OrderType.MARKET) ? "0" : price.toPlainString();
@@
- body.put("ORD_QTY", qty.toPlainString());
+ // 주식 수량은 정수만 허용됨. 소수일 경우 예외 발생시켜 조기 실패.
+ final String ordQty = qty.toBigIntegerExact().toString();
+ body.put("ORD_QTY", ordQty);
@@
- String ordNo = resp.getOutput() != null ? resp.getOutput().getORD_NO() : null;
- log.info("[KIS] rt_cd=0, ORD_NO={}", ordNo);
- return ordNo != null ? ordNo : "KIS-NO-ORDER-NO";
+ String ordNo = resp.getOutput() != null ? resp.getOutput().getORD_NO() : null;
+ if (ordNo == null || ordNo.isBlank()) {
+ throw new IllegalStateException("KIS success response but missing ORD_NO");
+ }
+ log.info("[KIS] rt_cd=0, ORD_NO={}", ordNo);
+ return ordNo;검증 요청: KIS 현금주문 API 스펙에서 ORD_QTY가 정수만 허용되는지(소수 허용 여부)를 확인해 주세요.
Also applies to: 51-61, 67-69, 96-99
🌐 Web query:
KIS 현금주문 API ORD_QTY 형식이 정수만 허용되는지?
💡 Result:
간단히: ORD_QTY는 정수(주 단위, 전수)로 입력해야 합니다 — 한국투자증권 오픈API 문서와 샘플은 주문수량을 정수형(주 수)으로 취급하고 있으며, 서드파티 SDK도 ord_qty를 int로 정의합니다. [1][2]
참고자료
- 한국투자증권 오픈API 주문 샘플 / 필드 설명(ORD_QTY: 주문수량). [1]
- PyQQQ SDK 문서(국내주식 주문 파라미터 ord_qty: int). [2]
(원하시면 국내/해외 주식별 소수주(부분주) 지원 여부도 확인해 드리겠습니다.)
OrderType enum 사용·수량/응답 검증 강화 필요
- 내부 메서드에 String 상수 대신 OrderType enum 전달
- KIS API는 ORD_QTY를 정수(주 단위)로만 허용하므로 qty.toBigIntegerExact()로 강제 변환 (docs.pyqqq.net, occam-khs.github.io)
- getORD_NO() 결과가 null 또는 blank면 IllegalStateException 던지도록 예외 처리
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/broker/kis/KisBrokerGateway.java around
lines 31 to 49, replace the String literals "MARKET"/"LIMIT" with the
appropriate OrderType enum values when calling the internal place(...) method,
ensure quantities are converted to integer shares via
quantity.toBigIntegerExact() before passing to the API (since KIS expects
integer ORD_QTY), and after the place call validate the returned getORD_NO()
result: if it is null or blank throw an IllegalStateException with a clear
message; adjust method signatures/calls as needed to accept OrderType and
integer quantity and propagate these checks to both buy and sell market/limit
methods.
| public String getAccessToken() { | ||
| if (cachedToken != null && Instant.now().isBefore(expiresAt.minusSeconds(30))) { | ||
| return cachedToken; | ||
| } | ||
| Map<String, Object> resp = kisWebClient.post() | ||
| .uri("/oauth2/tokenP") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .bodyValue(Map.of( | ||
| "grant_type", "client_credentials", | ||
| "appkey", props.getAppKey(), | ||
| "appsecret", props.getAppSecret() | ||
| )) | ||
| .retrieve() | ||
| .bodyToMono(Map.class) | ||
| .block(); | ||
|
|
||
| String token = (String) resp.get("access_token"); | ||
| Number exp = (Number) resp.getOrDefault("expires_in", 0); | ||
| this.cachedToken = token; | ||
| this.expiresAt = Instant.now().plusSeconds(exp.longValue()); | ||
| log.info("[KIS] access_token issued, expires_in={}s", exp); | ||
| return token; | ||
| } |
There was a problem hiding this comment.
토큰 발급 동시성·에러처리·널가드 보강 필요
현재는 동시 다발 호출 시 중복 발급 가능성이 있고, HTTP 에러/비정상 응답 시 NPE가 발생할 수 있습니다. 동기화와 상태코드 처리, 빈 토큰 가드를 추가하세요.
- public String getAccessToken() {
+ public synchronized String getAccessToken() {
if (cachedToken != null && Instant.now().isBefore(expiresAt.minusSeconds(30))) {
return cachedToken;
}
- Map<String, Object> resp = kisWebClient.post()
+ Map<String, Object> resp = kisWebClient.post()
.uri("/oauth2/tokenP")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of(
"grant_type", "client_credentials",
"appkey", props.getAppKey(),
"appsecret", props.getAppSecret()
))
- .retrieve()
+ .retrieve()
+ .onStatus(org.springframework.http.HttpStatusCode::isError,
+ clientResponse -> clientResponse.createException())
.bodyToMono(Map.class)
.block();
- String token = (String) resp.get("access_token");
- Number exp = (Number) resp.getOrDefault("expires_in", 0);
+ if (resp == null) {
+ throw new IllegalStateException("[KIS] token response is null");
+ }
+ String token = (String) resp.get("access_token");
+ if (token == null || token.isBlank()) {
+ throw new IllegalStateException("[KIS] access_token missing in response: " + resp);
+ }
+ Number exp = (Number) resp.getOrDefault("expires_in", 0);
this.cachedToken = token;
this.expiresAt = Instant.now().plusSeconds(exp.longValue());
log.info("[KIS] access_token issued, expires_in={}s", exp);
return token;
}추가로, Map 대신 DTO 매핑(예: record TokenResp(String access_token, long expires_in) {})을 사용하면 캐스팅 오류를 줄일 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public String getAccessToken() { | |
| if (cachedToken != null && Instant.now().isBefore(expiresAt.minusSeconds(30))) { | |
| return cachedToken; | |
| } | |
| Map<String, Object> resp = kisWebClient.post() | |
| .uri("/oauth2/tokenP") | |
| .contentType(MediaType.APPLICATION_JSON) | |
| .bodyValue(Map.of( | |
| "grant_type", "client_credentials", | |
| "appkey", props.getAppKey(), | |
| "appsecret", props.getAppSecret() | |
| )) | |
| .retrieve() | |
| .bodyToMono(Map.class) | |
| .block(); | |
| String token = (String) resp.get("access_token"); | |
| Number exp = (Number) resp.getOrDefault("expires_in", 0); | |
| this.cachedToken = token; | |
| this.expiresAt = Instant.now().plusSeconds(exp.longValue()); | |
| log.info("[KIS] access_token issued, expires_in={}s", exp); | |
| return token; | |
| } | |
| public synchronized String getAccessToken() { | |
| if (cachedToken != null && Instant.now().isBefore(expiresAt.minusSeconds(30))) { | |
| return cachedToken; | |
| } | |
| Map<String, Object> resp = kisWebClient.post() | |
| .uri("/oauth2/tokenP") | |
| .contentType(MediaType.APPLICATION_JSON) | |
| .bodyValue(Map.of( | |
| "grant_type", "client_credentials", | |
| "appkey", props.getAppKey(), | |
| "appsecret", props.getAppSecret() | |
| )) | |
| .retrieve() | |
| .onStatus(org.springframework.http.HttpStatusCode::isError, | |
| clientResponse -> clientResponse.createException()) | |
| .bodyToMono(Map.class) | |
| .block(); | |
| if (resp == null) { | |
| throw new IllegalStateException("[KIS] token response is null"); | |
| } | |
| String token = (String) resp.get("access_token"); | |
| if (token == null || token.isBlank()) { | |
| throw new IllegalStateException("[KIS] access_token missing in response: " + resp); | |
| } | |
| Number exp = (Number) resp.getOrDefault("expires_in", 0); | |
| this.cachedToken = token; | |
| this.expiresAt = Instant.now().plusSeconds(exp.longValue()); | |
| log.info("[KIS] access_token issued, expires_in={}s", exp); | |
| return token; | |
| } |
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/broker/kis/KisTokenService.java around
lines 27 to 49, the method must be hardened for concurrent calls, HTTP errors,
and null responses: serialize token refresh (e.g. synchronize on a private lock
or use double-checked locking) so only one request issues a new token when
cachedToken is expired; validate HTTP response status and handle WebClient
errors (use onStatus/try-catch around block()) to log and propagate a clear
exception instead of letting NPEs surface; map the response to a typed
DTO/record (e.g. TokenResp) instead of raw Map to avoid casting issues; after
receiving the DTO, null-guard both access_token and expires_in (reject/throw if
missing or non-positive), set cachedToken and expiresAt only after validation,
and ensure callers receive a meaningful exception when token retrieval fails.
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @Getter @Setter | ||
| @ConfigurationProperties(prefix = "kis") | ||
| public class KisProperties { | ||
| private String baseUrl; | ||
| private String appKey; | ||
| private String appSecret; | ||
| private String accountNo; // 예: 50151652-01 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
환경설정 필수값 검증 추가 (@validated + 제약조건 권장)
런타임에 누락/오타를 조기에 발견하려면 @Validated와 필드 제약을 걸어두는 편이 안전합니다. 계좌번호 포맷도 명시적으로 제한해 주세요.
아래처럼 적용 제안드립니다(계좌번호 포맷은 서비스 정책에 맞춰 조정 가능).
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
-@Getter @Setter
-@ConfigurationProperties(prefix = "kis")
+@Getter @Setter
+@Validated
+@ConfigurationProperties(prefix = "kis")
public class KisProperties {
- private String baseUrl;
- private String appKey;
- private String appSecret;
- private String accountNo; // 예: 50151652-01
+ @NotBlank private String baseUrl;
+ @NotBlank private String appKey;
+ @NotBlank private String appSecret;
+ @NotBlank
+ @Pattern(regexp = "\\d{8}-\\d{2}", message = "계좌번호는 8자리-2자리 형식이어야 합니다. 예: 50151652-01")
+ private String accountNo; // 예: 50151652-01🏁 Script executed:
#!/usr/bin/env bash
# Spring Boot 버전 확인 (pom.xml 또는 build.gradle)
if [ -f pom.xml ]; then
echo "=== pom.xml 부모 정보 ==="
grep -n "<parent>" pom.xml -A3
echo
grep -n "spring-boot-starter-parent" pom.xml -A1
echo
grep -n "<spring-boot.version>" pom.xml
fi
if [ -f build.gradle ]; then
echo "=== build.gradle Spring Boot 플러그인/버전 정보 ==="
grep -n "org.springframework.boot" build.gradle -A1
echo
grep -n "springBootVersion" build.gradle
fi
# @ConfigurationProperties 빈 등록 확인
rg -n "@EnableConfigurationProperties" -t java || echo "@EnableConfigurationProperties 미발견"
rg -n "@ConfigurationPropertiesScan" -t java || echo "@ConfigurationPropertiesScan 미발견"Length of output: 1378
@validated 및 필드 제약조건 추가 적용
Spring Boot 3.5.4 환경에 이미 spring-boot-starter-validation가 포함되어 있으므로, @ConfigurationProperties 클래스에 @Validated와 JSR-380 제약조건을 적용해 런타임 초기화 시점에 필수값 누락·오타를 검증할 수 있습니다. 아래 제안대로 반영해 주세요.
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
-@Getter @Setter
-@ConfigurationProperties(prefix = "kis")
+@Getter @Setter
+@Validated
+@ConfigurationProperties(prefix = "kis")
public class KisProperties {
- private String baseUrl;
- private String appKey;
- private String appSecret;
- private String accountNo; // 예: 50151652-01
+ @NotBlank
+ private String baseUrl;
+
+ @NotBlank
+ private String appKey;
+
+ @NotBlank
+ private String appSecret;
+
+ @NotBlank
+ @Pattern(regexp = "\\d{8}-\\d{2}", message = "계좌번호는 8자리-2자리 형식이어야 합니다. 예: 50151652-01")
+ private String accountNo; // 예: 50151652-01📝 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.
| import lombok.Getter; | |
| import lombok.Setter; | |
| import org.springframework.boot.context.properties.ConfigurationProperties; | |
| @Getter @Setter | |
| @ConfigurationProperties(prefix = "kis") | |
| public class KisProperties { | |
| private String baseUrl; | |
| private String appKey; | |
| private String appSecret; | |
| private String accountNo; // 예: 50151652-01 | |
| import lombok.Getter; | |
| import lombok.Setter; | |
| import org.springframework.boot.context.properties.ConfigurationProperties; | |
| import org.springframework.validation.annotation.Validated; | |
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.Pattern; | |
| @Getter | |
| @Setter | |
| @Validated | |
| @ConfigurationProperties(prefix = "kis") | |
| public class KisProperties { | |
| @NotBlank | |
| private String baseUrl; | |
| @NotBlank | |
| private String appKey; | |
| @NotBlank | |
| private String appSecret; | |
| @NotBlank | |
| @Pattern( | |
| regexp = "\\d{8}-\\d{2}", | |
| message = "계좌번호는 8자리-2자리 형식이어야 합니다. 예: 50151652-01" | |
| ) | |
| private String accountNo; // 예: 50151652-01 | |
| } |
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/config/KisProperties.java around lines 3 to
13, add runtime validation by annotating the class with @Validated and applying
JSR-380 constraints (e.g. @NotBlank) to required fields (baseUrl, appKey,
appSecret, accountNo). Update imports accordingly (javax.validation.constraints
or jakarta.validation.constraints depending on project), and keep the class as a
@ConfigurationProperties so Spring's binder will validate values on startup.
| @NotNull @DecimalMin("0.00000001") private BigDecimal quantity; | ||
| @DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
수량은 정수, 가격은 양수(>0) 강제 권장
KIS 현물 주문 수량은 정수이며, 지정가 price는 0 초과가 타당합니다. 도메인 규칙에 맞게 제약을 강화해 주세요.
국내 주식만 지원한다면 아래처럼 권장합니다(미국/분할주 등 확장 계획이 있다면 별도 논의):
- @NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
- @DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요
+ @NotNull
+ @jakarta.validation.constraints.Digits(integer = 12, fraction = 0)
+ @DecimalMin(value = "1", inclusive = true)
+ private BigDecimal quantity; // 정수 수량
+
+ @DecimalMin(value = "0.0", inclusive = false)
+ private BigDecimal price; // LIMIT일 때만 필요🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the DTO file
FILE=$(find src -type f -name PlaceOrderRequest.java)
echo "Found file: $FILE"
# Show the first 50 lines to verify current annotations
sed -n '1,50p' "$FILE"
# Search for any references to supported markets or currencies
echo "Searching for market/currency references across codebase..."
rg -nP '\b(market|currency|exchange)\b' -C3 src || echo "No matches found"Length of output: 1144
수량은 정수(>=1), price는 0 초과로 엄격히 검증
아래처럼 제약을 강화해 주세요.
--- a/src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java
+++ b/src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java
@@ Lines 15-16
- @NotNull @DecimalMin("0.00000001") private BigDecimal quantity;
- @DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요
+ @NotNull
+ @jakarta.validation.constraints.Digits(integer = 12, fraction = 0)
+ @DecimalMin(value = "1", inclusive = true)
+ private BigDecimal quantity; // 정수 수량
+
+ @DecimalMin(value = "0.0", inclusive = false)
+ private BigDecimal price; // LIMIT일 때만 필요📝 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.
| @NotNull @DecimalMin("0.00000001") private BigDecimal quantity; | |
| @DecimalMin("0.0") private BigDecimal price; // LIMIT일 때만 필요 | |
| @NotNull | |
| @jakarta.validation.constraints.Digits(integer = 12, fraction = 0) | |
| @DecimalMin(value = "1", inclusive = true) | |
| private BigDecimal quantity; // 정수 수량 | |
| @DecimalMin(value = "0.0", inclusive = false) | |
| private BigDecimal price; // LIMIT일 때만 필요 |
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/dto/PlaceOrderRequest.java around
lines 15-16, strengthen validations: make quantity an integer >=1 by either
changing its type to Integer/Long with @NotNull and @Min(1) or, if keeping
BigDecimal, add @NotNull, @DecimalMin("1"), and enforce scale 0 (e.g., a custom
validator or @Digits(integer=..., fraction=0)) so fractional quantities are
rejected; for price require strictly greater than zero by replacing
@DecimalMin("0.0") with @DecimalMin(value = "0.0", inclusive = false) and keep
any existing note about LIMIT-only usage (conditional enforcement can be handled
in service/validator logic).
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
엔티티 제약 강화: NOT NULL/Index/낙관적 락 추가.
DB 일관성과 조회 성능, 동시성 안전성을 위해 최소한의 제약을 추가하세요.
import jakarta.persistence.*;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
@Entity
-@Table(name = "orders")
+@Table(name = "orders", indexes = {
+ @Index(name = "idx_orders_user_created", columnList = "userId,createdAt")
+})
@Getter @Setter @NoArgsConstructor
public class Order {
- @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- private Long userId;
- private String symbol;
+ @NotNull
+ @Column(nullable = false)
+ private Long userId;
+ @NotBlank
+ @Column(nullable = false, length = 16)
+ private String symbol;
- @Enumerated(EnumType.STRING) private OrderSide side;
- @Enumerated(EnumType.STRING) private OrderType orderType;
+ @Enumerated(EnumType.STRING) @NotNull @Column(nullable = false)
+ private OrderSide side;
+ @Enumerated(EnumType.STRING) @NotNull @Column(nullable = false)
+ private OrderType orderType;
- private BigDecimal quantity;
- private BigDecimal price; // MARKET이면 null 가능
+ @Positive @NotNull
+ @Column(nullable = false, precision = 19, scale = 0) // 주식 수량: 정수
+ private BigDecimal quantity;
+ @Column(precision = 19, scale = 2) // LIMIT일 때만 사용
+ private BigDecimal price; // MARKET이면 null 가능
- @Enumerated(EnumType.STRING) private OrderStatus status;
+ @Enumerated(EnumType.STRING) @NotNull @Column(nullable = false)
+ private OrderStatus status;
private String brokerOrderId;
- private OffsetDateTime createdAt;
- private OffsetDateTime updatedAt;
+ @Column(nullable = false)
+ private OffsetDateTime createdAt;
+ @Column(nullable = false)
+ private OffsetDateTime updatedAt;
+
+ @Version
+ private Long version;Also applies to: 11-14, 16-33
| @PostMapping("/buy") | ||
| public OrderResponse buy(@Valid @RequestBody PlaceOrderRequest req){ | ||
| return OrderResponse.from(service.placeBuy(req)); | ||
| } | ||
|
|
||
| @PostMapping("/sell") | ||
| public OrderResponse sell(@Valid @RequestBody PlaceOrderRequest req){ | ||
| return OrderResponse.from(service.placeSell(req)); | ||
| } |
There was a problem hiding this comment.
신원 검증 없이 userId를 바디로 받는 것은 수평 권한 상승 위험
클라이언트가 임의의 userId로 타인의 계정에 주문을 생성할 수 있습니다. 인증 컨텍스트에서 사용자 식별자를 추출하고, 바디의 userId는 제거/서버 측에서 오버라이드 하세요.
예시(보안 맥락에 맞게 변형 필요):
@PostMapping("/buy")
public OrderResponse buy(@Valid @RequestBody PlaceOrderRequest req/*, Authentication auth */){
// Long userId = ((CustomPrincipal) auth.getPrincipal()).getId();
// req.setUserId(userId);
return OrderResponse.from(service.placeBuy(req));
}원하시면 관련 보안 구성/테스트 케이스까지 PR 보완안 제시하겠습니다.
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderController.java around lines
16-24, the controller currently trusts a userId from the request body which
allows horizontal privilege escalation; instead extract the authenticated user's
id from the security context (e.g., Authentication/CustomPrincipal via
SecurityContextHolder), override or remove any userId in PlaceOrderRequest
before calling service.placeBuy/placeSell, and ensure the request cannot set a
different userId (server-side override and/or remove setter/ignore field in
mapping). Also add minimal validation/assertion that the authenticated id is
present and used for order creation.
| @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); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
리스트 조회도 동일한 취약점 + 페이지 사이즈 상한 설정 권장
userId 쿼리파라미터 신뢰 금지, 그리고 size에 상한(예: 100)과 page 음수 방지를 적용하세요.
- 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 Long userId,
+ @RequestParam(defaultValue="0") int page,
+ @RequestParam(defaultValue="20") int size){
+ page = Math.max(page, 0);
+ size = Math.max(1, Math.min(size, 100));
+ return service.list(userId, PageRequest.of(page, size, Sort.by("id").descending()))
.map(OrderResponse::from);
}보안 적용 시에는 userId 또한 인증 정보로 대체해 주세요.
인증된 사용자 ID 사용 및 페이지 파라미터 유효성 검증 적용
userId를 쿼리 파라미터가 아닌 인증 정보(Principal)에서 가져오도록 변경하고, page는 0 이상, size는 1~100 범위로 제한하세요.
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderController.java around lines 26
to 32, switch from reading userId as a @RequestParam to using the authenticated
principal (e.g., method parameter Principal or Authentication) and extract the
current user's ID from it; remove the userId request parameter. Add validation
for page and size: ensure page >= 0 and size between 1 and 100 (either via javax
validation annotations with @Validated on the controller or by checking manually
and throwing a ResponseStatusException(HttpStatus.BAD_REQUEST) with a clear
message). Keep the rest of the logic the same, passing the extracted userId and
a PageRequest.of(page, size, Sort.by("id").descending()) to the service and
mapping results to OrderResponse.
| @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); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
입력값 검증(수량/가격/심볼) 추가 필요.
Broker 호출 전 검증으로 예측 가능한 오류를 조기에 차단하세요. MARKET/LIMIT 규칙도 강제하세요.
@Transactional
public Order placeBuy(PlaceOrderRequest req){
+ validate(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 placeSell(PlaceOrderRequest req){
+ validate(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);
}
@@
+ private void validate(PlaceOrderRequest req) {
+ if (req.getUserId() == null) throw new IllegalArgumentException("userId is required");
+ if (req.getSymbol() == null || req.getSymbol().isBlank()) throw new IllegalArgumentException("symbol is required");
+ if (req.getQuantity() == null || req.getQuantity().compareTo(java.math.BigDecimal.ZERO) <= 0)
+ throw new IllegalArgumentException("quantity must be positive");
+ switch (req.getOrderType()) {
+ case MARKET -> {
+ if (req.getPrice() != null) throw new IllegalArgumentException("price must be null for MARKET");
+ }
+ case LIMIT -> {
+ if (req.getPrice() == null || req.getPrice().compareTo(java.math.BigDecimal.ZERO) <= 0)
+ throw new IllegalArgumentException("price must be positive for LIMIT");
+ }
+ }
+ }Also applies to: 30-39
🤖 Prompt for AI Agents
In src/main/java/trademill/apiserver/order/OrderService.java around lines 19-28
(also apply same changes for lines 30-39), the method places orders without
validating request fields; add pre-checks for req.getSymbol() (non-null, matches
allowed pattern/list), req.getQuantity() (positive, within min/max limits), and
for LIMIT orders validate req.getPrice() (non-null, >0 and within allowed
bounds); enforce that MARKET orders do not accept a price and LIMIT orders
require one; if validation fails throw a clear custom/appropriate exception
(e.g., InvalidOrderException or IllegalArgumentException) before calling broker,
and return consistent error/status handling so broker is only invoked with
validated inputs.
Summary
Changes
How to run
export KIS_BASE_URL="https://openapivts.koreainvestment.com:29443"
export KIS_APP_KEY="<APP_KEY>"
export KIS_APP_SECRET="<APP_SECRET>"
export KIS_ACCOUNT_NO="<계좌번호-상품코드 예: 50151652-01>"
SPRING_PROFILES_ACTIVE=kis ./gradlew bootRun
How to test (curl)
시장가 매수
curl -X POST "http://localhost:8080/api/v1/orders/buy"
-H "Content-Type: application/json"
-d '{"userId":1,"symbol":"005930","orderType":"MARKET","quantity":1}'
지정가 매도
curl -X POST "http://localhost:8080/api/v1/orders/sell"
-H "Content-Type: application/json"
-d '{"userId":1,"symbol":"005930","orderType":"LIMIT","quantity":1,"price":70000}'
40580000(장종료)응답 확인됨.Summary by CodeRabbit