-
Notifications
You must be signed in to change notification settings - Fork 2
feat(order): 매수/매도/주문내역 API(MVP) + local(H2) 실행 설정 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| services: | ||
| db: | ||
| image: postgres:16 | ||
| environment: | ||
| POSTGRES_DB: trademill | ||
| POSTGRES_USER: tm | ||
| POSTGRES_PASSWORD: tm | ||
| ports: | ||
| - "5432:5432" | ||
| volumes: | ||
| - pgdata:/var/lib/postgresql/data | ||
| volumes: | ||
| pgdata: {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package trademill.apiserver.broker; | ||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
|
|
||
| // 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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package trademill.apiserver.broker; | ||
|
|
||
| import org.springframework.context.annotation.Profile; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.util.UUID; | ||
|
|
||
| @Profile({"local","dev","default"}) | ||
| @Component | ||
|
|
||
| // FakeBrokerGateway : 실제 증권사 호출 대신 로컬 개발/테스트용으로 응답을 꾸며주는 구현체(어댑터) | ||
| public class FakeBrokerGateway implements BrokerGateway { | ||
| private String id(){ return "SIM-" + UUID.randomUUID(); } | ||
|
|
||
| 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(); } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.OffsetDateTime; | ||
|
|
||
| @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; | ||
|
|
||
| @PrePersist | ||
| void onCreate() { | ||
| createdAt = OffsetDateTime.now(); | ||
| updatedAt = createdAt; | ||
| if (status == null) status = OrderStatus.PENDING; | ||
| } | ||
| @PreUpdate | ||
| void onUpdate() { updatedAt = OffsetDateTime.now(); } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import trademill.apiserver.order.dto.*; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/orders") | ||
| public class OrderController { | ||
| private final OrderService service; | ||
| public OrderController(OrderService service){ this.service = service; } | ||
|
|
||
| @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)); | ||
| } | ||
|
|
||
| @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); | ||
|
Comment on lines
+26
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 페이지 파라미터 검증 추가 + 정렬 컬럼을 생성시각으로 변경 권장
적용 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 |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| import org.springframework.data.domain.*; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface OrderRepository extends JpaRepository<Order, Long> { | ||
| Page<Order> findByUserId(Long userId, Pageable pageable); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package trademill.apiserver.order; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.domain.*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import trademill.apiserver.broker.BrokerGateway; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import trademill.apiserver.order.dto.PlaceOrderRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class OrderService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final OrderRepository orders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final BrokerGateway broker; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public OrderService(OrderRepository orders, BrokerGateway broker) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.orders = orders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.broker = broker; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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);
}권장 구조(개요):
📝 Committable suggestion
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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Page<Order> list(Long userId, Pageable pageable){ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return orders.findByUserId(userId, pageable); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| public enum OrderSide {BUY, SELL} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| public enum OrderStatus {PENDING, ACCEPTED, PARTIALLY_FILLED, FILLED, REJECTED, CANCELED} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package trademill.apiserver.order; | ||
|
|
||
| public enum OrderType {MARKET, LIMIT} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package trademill.apiserver.order.dto; | ||
|
|
||
| import lombok.Data; | ||
| import trademill.apiserver.order.*; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.OffsetDateTime; | ||
|
|
||
| @Data | ||
| public class OrderResponse { | ||
| private Long id; | ||
| private Long userId; | ||
| private String symbol; | ||
| private OrderSide side; | ||
| private OrderType orderType; | ||
| private BigDecimal quantity; | ||
| private BigDecimal price; | ||
| private OrderStatus status; | ||
| private String brokerOrderId; | ||
| private OffsetDateTime createdAt; | ||
| private OffsetDateTime updatedAt; | ||
|
|
||
| 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package trademill.apiserver.order.dto; | ||
|
|
||
| import jakarta.validation.constraints.*; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import trademill.apiserver.order.OrderType; | ||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
| @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일 때만 필요 | ||
| } | ||
|
Comment on lines
+10
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 금지 유효성 검증 추가 필요 서비스 계층에서는
되므로, DTO 단계에서 코드상 주요 위치
제안된 변경사항(클래스 레벨 커스텀 제약 추가 + 검증 로직) 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;
}
}
}테스트 케이스
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 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 | ||
| server: | ||
| port: 8080 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
엔티티 제약 조건(Nullable/Precision/Index/Version) 보강으로 데이터 무결성과 조회 성능 개선
적용 diff:
추가 import 필요:
🤖 Prompt for AI Agents