반응형

자바를 공부하다 보면 한 번쯤 마주하게 되는 키워드가 있습니다. 바로 자바 Enum 클래스입니다.


많은 초보 개발자들이 enum을 단순히 “상수를 나열하는 문법” 정도로만 알고 있지만, 사실 그 이상으로 강력한 기능을 제공합니다.


이번 글에서는 자바 Enum 클래스의 개념부터 실무 활용 예시까지 정리해보겠습니다.

 

(저도 아직 초보에요..)

 

Enum이란 무엇일까?

Enum(열거형)은 서로 관련된 상수들의 집합을 표현하기 위한 클래스입니다.
예를 들어, 요일을 나타내는 상수를 MONDAY, TUESDAY, WEDNESDAY로 정의한다고 할 때, 기존에는 다음처럼 작성했죠.

 

public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;

 

하지만 이런 방식은 숫자 값이 헷갈리기 쉽고, 타입 안전성(type safety)이 없습니다.
그래서 등장한 것이 바로 자바 Enum 클래스입니다.

 

(그리고 코딩하면서 static 변수가 많아지면 유지보수 하기도 힘들더라구요..)

 


기본 문법과 사용 예시

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

 

이제 위와 같이 정의해두면,
Day.MONDAY처럼 직관적이고 안전한 코드로 사용할 수 있습니다.

 

Day today = Day.MONDAY;

if (today == Day.MONDAY) {
    System.out.println("월요일입니다!");
}

 

 

Enum은 내부적으로 클래스이기 때문에, 생성자, 변수, 메서드도 정의할 수 있습니다.


 

Enum의 확장 기능

 

public enum Season {
    SPRING("봄"), SUMMER("여름"), AUTUMN("가을"), WINTER("겨울");

    private final String koreanName;

    Season(String koreanName) {
        this.koreanName = koreanName;
    }

    public String getKoreanName() {
        return koreanName;
    }
}

 

이제 아래처럼 사용할 수 있습니다 👇

System.out.println(Season.SPRING.getKoreanName()); // 봄

 

Enum은 단순한 값 나열이 아니라, 상수를 객체처럼 관리할 수 있는 구조라는 점이 핵심입니다.

 

Enum의 장점 정리

 

 

  • 가독성 향상 — 의미 있는 이름으로 상수를 표현 (개인적으로 이게 제일 큰 장점인거 같아요)
  • 타입 안전성 확보 — 잘못된 값 대입 방지
  • switch문과 궁합 — Enum은 switch문에서 바로 사용 가능
  • 객체처럼 활용 가능 — 변수, 생성자, 메서드 포함 가능
switch (today) {
    case MONDAY -> System.out.println("한 주의 시작!");
    case FRIDAY -> System.out.println("불금!");
    default -> System.out.println("평일 혹은 주말!");
}

 

 


실무에서의 활용 예시

 

Enum은 특히 API 응답 코드, 주문 상태, 결제 방식 등에서 자주 사용됩니다.
예를 들어, 주문 상태를 Enum으로 관리하면 코드 유지보수가 쉬워집니다.

 

public enum OrderStatus {
    READY, PAID, SHIPPED, COMPLETED, CANCELLED
}

 

이처럼 Enum은 비즈니스 로직의 안정성과 가독성 높이는 핵심 도구입니다.

 

 

자바 Enum 클래스는 단순한 상수 집합이 아닙니다.
객체 지향적인 지향하는 클래스 구조로, 유지보수성과 안정성을 높여줍니다.
실무에서는 상수 관리, 상태 정의, 코드 일관성 확보를 위해 필수적으로 사용됩니다.

 

핵심 키워드 자바 Enum 클래스 이해하고 나면,
여러분의 코드 품질이 단계 업그레이드될 거예요.

 

반응형
반응형

(스터디 목적으로 작성한 글입니다.)

https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-6

 

가상화폐 매매 체결 엔진 만들기 (6)

(스터디 목적으로 작성한 글입니다.) https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-5 가상화폐 매매 체결 엔진 만들기 (5) (

gitsul.tistory.com

 

Service 클래스에 대해서 정리하겠습니다. 

 

Service 클래스는 Controller에서 받은 주문 정보를 Model 객체로 변환하고

DB에 저장하는 로직이 포함되어 있습니다. 

 

package org.example.service;

import org.example.Side;
import org.example.dto.OrderRequestDto;
import org.example.dto.OrderResponseDto;
import org.example.model.Order;
import org.example.model.OrderBook;
import org.example.model.OrderMatchInfo;
import org.example.model.Trade;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Service
public class OrderService {

    private final OrderSave orderSave;

    private final OrderBook orderBook;

    public OrderService(OrderSave orderSave, OrderBook orderBook) {
        this.orderSave = orderSave;
        this.orderBook = orderBook;
    }

    /**
     * 주문을 추가하고 처리하는 함수
     * @param orderRequestDto 주문 요청 정보를 포함한 DTO
     * @return 주문과 관련된 응답 DTO
     */
    public OrderResponseDto addOrder(OrderRequestDto orderRequestDto){
        // 주문 생성
        Order order = Order.of(orderRequestDto);

        // 주문 저장
        orderSave.orderSave(order);

        // 주문 처리 및 체결 정보 얻어옴
        List<Trade> trades = orderBook.process(order);

        // 체결 정보를 사용하여 주문 상태 및 연관된 주문 업데이트
        for (Trade trade : trades){
            orderSave.orderUpdate(trade.getTakeOrderId());
            orderSave.orderUpdate(trade.getMakerOrderId());
            orderSave.orderMatchingSave(trade.getMakerOrderId(), trade.getTakeOrderId());
        }

        // 주문 및 체결 정보를 포함한 응답 DTO 변환
        return OrderResponseDto.ofOrder(order, trades);
    }

    /**
     * 매수 및 매도 주문 목록을 가져옴
     * @return 매수 및 매도 주문 목록
     */
    public Map<Side, List<Order>> getOrderBook(){
        // 매수 및 매도 주문 목록을 가져옴
        List<Order> buyOrderBook = orderBook.getOrderBook(5, Side.ask);
        List<Order> sellOrderBook = orderBook.getOrderBook(5, Side.bid);

        // 결과를 Map에 저장하여 변환
        Map<Side, List<Order>> orderBooks = new HashMap<>();
        orderBooks.put(Side.bid, buyOrderBook);
        orderBooks.put(Side.ask, sellOrderBook);

        return orderBooks;
    }

    /**
     * 주문 ID를 사용하여 주문 정보를 찾음
     * @param uuid 주문 ID
     * @return 주문 정보를 포함한 응답 DTO
     */
    public OrderResponseDto findOrder(UUID uuid) {
        // 주문 ID를 사용하여 주문 저보를 조회
        Order order = orderBook.findOrder(uuid);

        // 주문 정보를 포함한 응답 DTO 반환
        return OrderResponseDto.ofFindOrder(order);
    }

    /**
     * 주문 ID를 사용하여 주문을 취소하고 취소된 주문 정보를 반환
     *
     * @param uuid 주문 ID
     * @return 취소된 주문 정보를 포함한 응답 DTO
     */
    public OrderResponseDto deleteOrder(UUID uuid) {
        Order order = orderBook.cancelOrder(uuid);

        return OrderResponseDto.ofDeleteOrder(order);
    }
}

 

다음은 OrderBook.java 입니다. 

 

지정가 매수 / 매도 주문을 처리하는 함수입니다. 

/**
     * 지정가 매수 주문을 처리
     * @param order 처리할 주문
     * @return 처리된 체결 목록
     */
    private synchronized List<Trade> processLimitBuy(Order order) {
        final ArrayList<Trade> trades = new ArrayList<>();

        // 매도 리스트 체크 및 가장 저렴한 매도 주문의 가격이 현재 주문의 가격보다 작거나 같을 경우
        if (!sellOrders.isEmpty() && sellOrders.peek().getPrice().compareTo(order.getPrice()) <= 0) {

            // 현재 주문과 가장 저렴한 매도 주문을 비교하여 처리
            while (!sellOrders.isEmpty()) {
                Order sellOrder = sellOrders.peek();

                // 현재 매도 주문의 가격이 주문의 가격보다 높으면 루프 종류
                if (sellOrder.getPrice().compareTo(order.getPrice()) > 0) {
                    break;
                }

                BigDecimal maxQuantityToTrade = sellOrder.getQuantity().min(order.getQuantity());

                sellOrder.executeTrade(maxQuantityToTrade);
                order.executeTrade(maxQuantityToTrade);

                trades.add(Trade.of(order,
                        sellOrder,
                        maxQuantityToTrade,
                        sellOrder.getPrice()));

                // 매도 주문이 완전히 체결되면 주문서에서 제거
                if (sellOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                    sellOrders.poll();
                }
                this.lastPrice = sellOrder.getPrice();

                if (order.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                    return trades;
                }
            }
        }

        // 남은 주문을 buyOrders에 추가
        buyOrders.add(order);

        return trades;
    }


    /**
     * 지정가 매도 주문을 처리
     * @param order 처리할 주문
     * @return 처리된 체결 목록
     */
    private synchronized List<Trade> processLimitSell(Order order) {
        final ArrayList<Trade> trades = new ArrayList<>();

        // 매수 주문 리스트 체크 및 가장 높은 주문의 가격이 현재 주문의 가격보다 크거나 같을 경우
        if (!buyOrders.isEmpty() && buyOrders.peek().getPrice().compareTo(order.getPrice()) >= 0) {

            // 현재 주문과 가장 높은 매수 주문을 비교하며 처리
            while (!buyOrders.isEmpty()) {
                final Order buyOrder = buyOrders.peek();

                // 현재 매수 주문이 현재 주문의 가격보다 낮으면 종료
                if (buyOrder.getPrice().compareTo(order.getPrice()) < 0) {
                    break;
                }

                BigDecimal maxQuantityToTrade = buyOrder.getQuantity().min(order.getQuantity());

                // 주문 실행 및 체결 정보 생성
                buyOrder.executeTrade(maxQuantityToTrade);
                order.executeTrade(maxQuantityToTrade);

                trades.add(Trade.of(order,
                        buyOrder,
                        maxQuantityToTrade,
                        buyOrder.getPrice()));

                if (buyOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                    buyOrders.poll();
                }
                this.lastPrice = buyOrder.getPrice();

                if (order.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                    return trades;
                }
            }
        }

        // 남은 주문을 sellOrders 추가합니다.
        sellOrders.add(order);

        return trades;
    }

 

 

다음은 Order.java 입니다.

주문의 수량이 0일 경우에 EXECUTED상태로 변하고 그렇지 않으면 PARTIALLY_FILLED 상태로 변경합니다. 

체결된 수량과 수수료 계산의 대한 정보도 변경해줍니다.

 public void executeTrade(BigDecimal executedQuantity){

        this.quantity = this.quantity.subtract(executedQuantity);

        if(this.quantity.compareTo(BigDecimal.ZERO) == 0){
            orderStatus = OrderStatus.EXECUTED;
        }else{
            orderStatus = OrderStatus.PARTIALLY_FILLED;
        }
        this.executedQuantity = this.executedQuantity.add(executedQuantity);


        this.tradingFee = calculateTradingFee(this.price.multiply(executedQuantity));
    }

 

JPA 소스 및 나머지 소스는 기본적인거라 여기서 이만 정리를 마치겠습니다. 

 

다음 시간에는 유저 자산에 대하여 프로그램 개발을 진행하겠습니다. 

 

 

반응형
반응형

(스터디 목적으로 작성한 글입니다.)

https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-5

 

가상화폐 매매 체결 엔진 만들기 (5)

(스터디 목적으로 작성한 글입니다.) https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-4 가상화폐 매매 체결 엔진 만들기 (4) (

gitsul.tistory.com

 

지난번 시간까지 전반적인 로직을 작성하는 시간을 가졌습니다. 

 

order-matching-engine 프로젝트는 market-symbol 만 지원을 하는 방식입니다.
다양한 코인들에 거래를 지원하기 위해서는 order-matching-engine 서비스를 추가로 실행을 해야됩니다. 

 

그다음에 거래에 따른 자산을 위한 서비스를 개발을 해야합니다.

 

그전에 지금까지 개발한 부분을 정리를 해보겠습니다. 

 

오늘 시간은 컨트롤러 입니다.

 

public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/order")
    public ResponseEntity<OrderResponseDto> order(@RequestParam UUID uuid){
        try{
            OrderResponseDto orderResponseDto = orderService.findOrder(uuid);

            return ResponseEntity.ok(orderResponseDto);
        }catch(Exception e){
 	        // 예외 처리 로직
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @PostMapping("/orders")
    public ResponseEntity<OrderResponseDto> orders(@RequestBody OrderRequestDto orderRequestDto){
        try {
            OrderResponseDto orderResponseDto = orderService.addOrder(orderRequestDto);

            return ResponseEntity.ok(orderResponseDto);
        } catch (Exception e) {
            // 예외 처리 로직
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @DeleteMapping("/order")
    public ResponseEntity<OrderResponseDto> delete(@RequestParam UUID uuid){
        try {
            OrderResponseDto orderResponseDto = orderService.deleteOrder(uuid);

            return ResponseEntity.ok(orderResponseDto);
        }catch (Exception e){
            // 예외 처리 로직
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @GetMapping("/order-book")
    public ResponseEntity<Map<Side, List<Order>>> orderbook(){
        try{
            Map<Side, List<Order>> orderBooks = orderService.getOrderBook();
            return ResponseEntity.ok(orderBooks);
        }catch (Exception e){
        	// 예외 처리 로직
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

 

Get, Post, Delete HTTP 메서드를 이용하여 주문을 조회하고, 생성하고 삭제하는 컨트롤러 입니다.

RequestBody 혹은 Param 값으로 데이터를 받아 서비스 함수를 호출합니다. 

 

  • @GetMapping("/order") : 주문 UUID로 현재 호가 리스트에 있는 정보를 조회합니다.
  • @PostMapping("/orders") : 주문 메시지를 받아 주문을 생성하고 조건에 맞으면 체결이 됩니다. 
  • @DeleteMapping("order") : 주문 UUID로 주문를 취소합니다. 
  • @GetMapping("/order-book") : 해당 symbol에 대한 호가 정보를 리턴합니다. 

간단하게 컨트롤러에 대한 기능을 구현하였습니다.

 

다음 시간에는 서비스에서 필요한 model 클래스에 대해 정리하도록 하겠습니다. 

 

 

반응형
반응형

(스터디 목적으로 작성한 글입니다.)

https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-4

 

가상화폐 매매 체결 엔진 만들기 (4)

(스터디 목적으로 작성한 글입니다.) https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-3 가상화폐 매매 체결 엔진 만들기 (3) (

gitsul.tistory.com

 

이번시간은 주문 취소, 주문 조회에 대하여 개발하도록 하겠습니다.

 

지난 시간까지 주문 데이터는 List 자료구조를 통해 저장하였습니다.  매도 주문이 들어오면 매도 주문 리스트에 저장, 매수 주문이 들어오면 매수 주문 리스트에 저장 구조로 되어있습니다.

 

여기서 주문을 취소하거나, 조회하려면 리스트 전체를 조회하는 방법으로 사용을 해야되는데, 이는 매우 비 효율적이라고 생각이 듭니다.

 

그래서 고안한 방법이 HashMap의 자료구조를 활용하자 입니다.

 

주문이 들어오면 매수 혹은 매수 리스트에 저장을 하고 주문 HashMap에 저장을 합니다. 

 

주문 매칭은 리스트를 이용하고 단순 조회나 삭제같은 경우 HashMap을 활용하여 처리를 좀더 빠르다고 생각이 들었습니다. 

 

여기서 HashMap을 활용하면 왜 빠른가??  (자세한건 포스팅으로 따로 정리를 하겠습니다..)

 

Key-Value구조로 HashMap<UUID, Order> 객체를 사용하면 UUID로 빠르게 Order 정보를 조회할수 있습니다.

 

기존 리스트 조회

 public synchronized Order findOrder(String id,
                                        Side side) {
        PriorityQueue<Order> toSearch;
        if (side == Side.bid) {
            toSearch = this.buyOrders;
        } else {
            toSearch = this.sellOrders;
        }
        return toSearch.stream().filter(order -> order.getOrderId().equals(id))
                .findFirst().orElse(null);
    }

 

HashMap 활용하여 조회

 public synchronized Order findOrder(UUID id) {
        return orderMap.get(id);
    }

 

소스만 봐도 간단하게 처리가 되는거 같아 보이죠?? 맞습니다. 

 

다만, 기존 리스트 2개만 사용하여 처리하던게 HashMap이라는 새로운 객체를 사용하기 때문에 메모리의 사용량이 늘어날수 있다는 단점이 있지만, 요즘 메모리 사양이 좋고, 가상 화폐거래는 빠르게 처리하는게 가장 중요하다고 생각을 하기 때문에,, 위에 방식을 사용하겠습니다. 

 

 

기존 리스트 삭제

 private synchronized boolean cancel(String orderId,
                                        PriorityQueue<Order> orderBook) {
        // 요소를 저장할 임시 큐를 생성
        PriorityQueue<Order> tempQueue = new PriorityQueue<>(orderBook);

        // 임시 큐에서 요소를 하나씩 꺼내면서 orderId와 비교하여 삭제 여부 결정
        while (!tempQueue.isEmpty()) {
            Order currentOrder = tempQueue.poll();
            if (currentOrder.getOrderId().equals(orderId)) {
                // 원본 큐에서도 해당 요소를 삭제
                orderBook.remove(currentOrder);
                return true;
            }
        }

        return false;
    }

 

HashMap 활용하여 삭제

 private synchronized Order cancel(Order canceledOrder,
                                        PriorityQueue<Order> orderBook) {
        boolean removed = orderBook.remove(canceledOrder);

        if(removed){
            orderMap.remove(canceledOrder.getOrderId());
            return canceledOrder;
        }else{
            return null;
        }
    }

 

 

주문이 들어왔을때 리스트에만 저장 뿐 아니라 새롭게 생긴 HashMap에도 저장을 합시다!

 public synchronized List<Trade> process(Order order){
        // Add the order to the orderMap
        orderMap.put(order.getOrderId(), order);

        if(order.getOrdType() == OrderType.limit){
            if(order.getSide() == Side.bid){
                return this.processLimitBuy(order);
            }else{
                return this.processLimitSell(order);
            }
        }else{
            if(order.getSide() == Side.bid){
                return this.processMarketBuy(order);
            }else{
                return this.processMarketSell(order);
            }
        }
    }

 

 

다음은 주문 취소에 대한 요청 내용을 캡쳐한 사진입니다.

 

 

 

 

 

이로써 주문, 주문 조회, 주문 취소 등 우선적으로 거래를 위한 API는 만든거 같습니다. 

 

더 추가할게 많습니다. 

 

차근차근 업데이트 해가면서 완벽한 거래엔진을 만들어보고 추후에 프론트엔드까지 개발하면서 거래를 할 수있게 1차적인 목표입니다. 

 

반응형
반응형

(스터디 목적으로 작성한 글입니다.)

https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-3

 

가상화폐 매매 체결 엔진 만들기 (3)

(스터디 목적으로 작성한 글입니다.) https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-2 가상화폐 매매 체결 엔진 만들기 (2) (

gitsul.tistory.com

 

이번시간에는 시장가 체결에 대해 개발을 하겠습니다.

 

간단히 시장가 / 지정가에 대해 정리를 먼저 정리를 하면 다음과 같습니다.

 

시장가 체결

  • 가격 : 주문이 시장에서 즉시 체결되도록 하기 위한 주문 유형
  • 체결 보장 : 시장가 주문은 보통 높은 확률로 체결되지만 , 정확한 체결 가격은 주문을 실행할 때까지 알 수 없습니다. 

 

지정가 체결

  • 가격 : 특정 가격에서 주문자가 원하는 가격에 거래를 원할 때 사용됩니다. 
  • 체결 보장 : 주문 가격 이상에서만 체결되므로 주문이 즉시 체결 되지 않을 수 있고 가격이 시장 가격에 도달할때까지 기다릴수 있습니다. 

 

- 시장가 매수 함수

private synchronized List<Trade> processMarketBuy(Order order) {
        final ArrayList<Trade> trades = new ArrayList<>();

        while (!sellOrders.isEmpty() && order.getQuantity().compareTo(BigDecimal.ZERO) > 0) {
            Order sellOrder = sellOrders.peek();

            BigDecimal maxQuantityToTrade = sellOrder.getQuantity().min(order.getQuantity());

            sellOrder.executeTrade(maxQuantityToTrade);
            order.executeTrade(maxQuantityToTrade);

            trades.add(Trade.of(order,
                    sellOrder,
                    maxQuantityToTrade,
                    sellOrder.getPrice()));

            if (sellOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                sellOrders.poll(); // Remove the order if it's completely filled.
            }
            this.lastPrice = sellOrder.getPrice();
        }

        if (order.getQuantity().compareTo(BigDecimal.ZERO) > 0) {
            buyOrders.add(order);
        }

        return trades;
    }
  • 시장가 주문에서는 while 루프를 사용하여 판매 주문 큐(sellOrders)에서 가장 높은 가격의 주문을 찾아내고, 주문된 수량과 판매 주문의 수량 중 더 적은 양을 거래합니다.
  • 주문된 수량(order.getQuantity())을 모두 처리하거나, 판매 주문 큐가 비거나, 주문된 수량이 0이 될 때까지 계속해서 판매 주문을 처리합니다.
  • 주문이 완전히 처리된 경우, 판매 주문 큐에서 해당 주문을 제거하고, 마지막 거래 가격(lastPrice)을 업데이트합니다.
  • 주문이 완전히 처리되지 않은 경우, 남은 주문을 구매 주문 큐(buyOrders)에 추가합니다.

 

위에 로직을 바탕으로 시장가 매수 / 매도 에대한 로직을 구현하였습니다. 

 

오늘은 회사 업무가 많은 이유로 짧게 작성했습니다 ㅠㅠ 

 

 

반응형
반응형

(스터디 목적으로 작성한 글입니다.)

https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-2

 

가상화폐 매매 체결 엔진 만들기 (2)

(스터디 목적으로 작성한 글입니다.) https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-1 가상화폐 매매 체결 엔진 만들기 (1)

gitsul.tistory.com

 

이전에 개발한 내용을 전반적으로 수정을 했습니다. 

 

수정한 내용은 밑에서 설명을 하고 먼저 이유부터 말씀드리겠습니다. 

 

수정한 이유는??

 

첫번째 : 체결이 되는 메서드가 서비스 클래스에 있는 것보다는  Component 객체에서 체결 메서드를 선언하는게 SOLID 방식으로 적합하다고 생각을 했고 비지니스적으로 분리를 함으로써 유지보수나 추후 개발에 있어 효율적일거 같아서...

 

두번째 : 하나의 매칭엔진에서 여러가지 코인 쌍을 거래하려고 했는데 프로젝트 확장성에서 불리할거 같았고 MSA 방식을 추구하기 때문에 Symbol에 대한 내용을 제외하고 하나의 매칭엔진은 하나의 코인쌍으로 처리하는게 추후 확장성에도 효율적일거 같아서...

 

 

OrderBook.java 

 

process 메서드

 public synchronized List<Trade> process(Order order){
        if(order.getSide() == Side.bid){
            return this.processLimitBuy(order);
        }else{
            return this.processLimitSell(order);
        }
    }
  • API 통해 주문 메시지가 요청이 오면 호출하는 메서드 
  • 주문 종류를 보고 매도/매수 메서드 호출

 

매수 메서드

private synchronized List<Trade> processLimitBuy(Order order) {
        final ArrayList<Trade> trades = new ArrayList<>();

        if (!sellOrders.isEmpty() && sellOrders.peek().getPrice().compareTo(order.getPrice()) <= 0) {

            while (!sellOrders.isEmpty()) {
                Order sellOrder = sellOrders.peek();
                if (sellOrder.getPrice().compareTo(order.getPrice()) > 0) {
                    break;
                }
                
				if (sellOrder.getQuantity().compareTo(order.getQuantity()) >= 0) {
                    sellOrder.executeTrade(order.getQuantity());
                    order.executeTrade(order.getQuantity());
                    trades.add(Trade.of(order,
                            sellOrder,
                            order.getQuantity(),
                            sellOrder.getPrice())
                    );

                    if (sellOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                        sellOrders.poll(); 
                    }
                    this.lastPrice = sellOrder.getPrice();

                    return trades;
                }

                if (sellOrder.getQuantity().compareTo(order.getQuantity()) < 0) {
                    sellOrder.executeTrade(sellOrder.getQuantity());
                    order.executeTrade(sellOrder.getQuantity());

                    trades.add(Trade.of(order,
                            sellOrder,
                            sellOrder.getQuantity(),
                            sellOrder.getPrice())
                    );

                    sellOrders.poll(); 

                    this.lastPrice = sellOrder.getPrice();

                    continue;
                }
            }
        }

        buyOrders.add(order);

        return trades;
    }
  • 리턴 값은 체결된 객체를 리스트 형태로 리턴합니다.
  • 주문 큐에 저장되어 있는 주문과 상대방의 주문일 비교하여 수량을 업데이트합니다.

 

체결 객체 클래스

public class Trade {

    /**
     * Take's order ID. 상대방 주문의 주문 ID (체결되는 상대방의 주문)
     */
    private Order takeOrderId;

    /**
     * Maker's order ID. (체결 주문의 주문 ID (주문을 발생시킨 주문)
     */
    private Order makerOrderId;

    /**
     * Trade amount.
     */
    private BigDecimal amount;

    /**
     * Trade price.
     */
    private BigDecimal price;

    /**
     * Trade Time.
     */
    private LocalDateTime time;


    /**
     * Create an instance of Trade.
     * @param takeOrderId
     * @param makerOrderId
     * @param amount
     * @param price
     * @return
     */
    public static Trade of (Order takeOrderId,
                            Order makerOrderId,
                            BigDecimal amount,
                            BigDecimal price){
        return Trade.builder()
                .takeOrderId(takeOrderId)
                .makerOrderId(makerOrderId)
                .amount(amount)
                .price(price)
                .time(LocalDateTime.now())
                .build();
    }
}

 

컨트롤러에서 요청할 메서드 

 public void addOrder(OrderRequestDto orderRequestDto){
        Order order = Order.of(orderRequestDto);
        orderSave.orderSave(order);

        List<Trade> trades = orderBook.process(order);

        for (Trade trade : trades){
            orderSave.orderUpdate(trade.getTakeOrderId());

            orderSave.orderMatchingSave(trade.getMakerOrderId(), trade.getTakeOrderId());

        }

        orderSave.orderUpdate(order);
    }
  • 주문 객체를 먼저 저장합니다.
  • OrderBook 클래스에 process 메서드를 호출하여 체결된 객체를 리턴받습니다. 
  • 체결된 정보가 있으면 taker 주문을 업데이트 합니다.
  • maker 주문과 taker 주문을 추적하기 위해 매칭 정보를 저장합니다.
  • maker 주문을 업데이트 합니다. 
반응형
반응형

(스터디 목적으로 작성한 글입니다.)

 https://gitsul.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94%ED%8F%90-%EB%A7%A4%EB%A7%A4-%EC%B2%B4%EA%B2%B0-%EC%97%94%EC%A7%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-1

 

가상화폐 매매 체결 엔진 만들기 (1)

가상화폐 거래소 구축을 위해서 매매 체결 엔진을 개발 시작해 보도록 해보겠습니다. 레디스나 카프카를 활용해서 구축을 해볼수 있겠지만 우선, 정상적인 거래를 동작하기 위해 하나의 솔루션

gitsul.tistory.com

 

이번에는 체결이 되었을 경우, 체결정보를 DB에 저장하는 기능을 추가해보도록 하겠습니다.

(업비트의 API 문서를 참고하면서 개발을 진행했음)

 

- 업비트 주문 조회 Response 내용

{
  "uuid": "9ca023a5-851b-4fec-9f0a-48cd83c2eaae",
  "side": "ask",
  "ord_type": "limit",
  "price": "4280000.0",
  "state": "done",
  "market": "KRW-BTC",
  "created_at": "2019-01-04T13:48:09+09:00",
  "volume": "1.0",
  "remaining_volume": "0.0",
  "reserved_fee": "0.0",
  "remaining_fee": "0.0",
  "paid_fee": "2140.0",
  "locked": "0.0",
  "executed_volume": "1.0",
  "trades_count": 1,
  "trades": [
    {
      "market": "KRW-BTC",
      "uuid": "9e8f8eba-7050-4837-8969-cfc272cbe083",
      "price": "4280000.0",
      "volume": "1.0",
      "funds": "4280000.0",
      "side": "ask"
    }
  ]
}

 

Response 을 보고 추가로 유추한 내용

  • 주문 ID는 UUID 클래스로 선언했음을 알 수 있음
  • trades_count랑 trades에 대한 내용이 있음
  • 주문이 체결되었을 경우 체결된 주문에 대한 정보도 포함되어 있음
  • 주문 테이블 뿐 아니라 체결된 정보를 저장하는 테이블도 필요하겠구나라고 생각이 듬

 

체결 정보를 추적하기 위해서 다음과 같은 테이블을 구성해보았습니다. 

주문 데이터는 order 테이블에 저장되고, 매칭된 주문 정보는 match_order 테이블에 저장됩니다.

match_order 테이블에서는 매수 주문과 매도 주문의 ID를 외래키로 가지고 있으므로 어떤 주문과 매칭되었는지 추적할 수 있습니다.

 

 

OrderMatchingEntity.java

buy_order_id, sell_order_id가 외래키로 선언되어 있어 @ManyToOne 어노테이션을 선언합니다. 

public class OrderMatchingEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) // UUID 생성 전략 수정
    @Column(name = "match_id")
    private UUID matchId;

    @ManyToOne
    @JoinColumn(name = "buy_order_id")
    private OrderEntity buyOrder; // 매칭된 매수 주문

    @ManyToOne
    @JoinColumn(name = "sell_order_id")
    private OrderEntity sellOrder; // 매칭된 매도 주문

    @Column(name = "match_price")
    private BigDecimal matchPrice; // 체결 가격

    @Column(name = "match_quantity")
    private BigDecimal matchQuantity; // 체결 수량

    @Column(name = "match_time")
    private LocalDateTime matchTime; // 체결 시간

    public OrderMatchingEntity() {

    }

    public static OrderMatchingEntity toEntity(OrderEntity buyOrder, OrderEntity sellOrder){
        return OrderMatchingEntity.builder()
                .buyOrder(buyOrder)
                .sellOrder(sellOrder)
                .matchPrice(buyOrder.getPrice())
                .matchQuantity(buyOrder.getExecutedQuantity())
                .matchTime(LocalDateTime.now()) // 현재 시간 사용
                .build();
    }
}

 

OrderSave.java

레파지토리 클래스에 match 테이블 저장 메서드 생성합니다.

public void orderMatchingSave(Order buyOrder, Order sellOrder){
	orderMatchingRepository.save(OrderMatchingEntity.toEntity(OrderEntity.toEntity(buyOrder), OrderEntity.toEntity(sellOrder)));
}

 

OrderService.java

주문이 체결이 되고 주문을 업데이트 후에 매칭 정보를 저장합니다.

public Order createOrder(OrderNewRequestDto orderNewRequestDto) {
        Order order = Order.of(orderNewRequestDto);
        orderSave.orderSave(order);

        // 주문 체결 엔진
        OrderBook orderBook = orderMatcher.submitOrder(order);

        // 매칭 주문 리스트
        List<OrderBook.OrderMatch> matchedOrders = orderBook.getMatchedOrders();

        // 주문 정보 저장
        for (OrderBook.OrderMatch orderMatch : matchedOrders) {
            Order buyOrder = orderMatch.getBuyOrder();
            Order sellOrder = orderMatch.getSellOrder();

            // 체결된 주문 정보 저장
            orderSave.orderUpdate(buyOrder);
            orderSave.orderUpdate(sellOrder);

            // 매칭 정보 저장
            orderSave.orderMatchingSave(buyOrder, sellOrder);
        }

        return order;
    }

 

이렇게 저장이 되면 주문에 대한 체결 정보들이 match_orders  테이블에 저장이 되고 다음 아래와 같은 쿼리로 체결된 정보를 조회할수 있습니다. 

 

SELECT m.*
FROM orders o
         LEFT JOIN match_orders m ON o.order_id = m.buy_order_id OR o.order_id = m.sell_order_id
WHERE o.order_id = :order_id

 

테스트

 

1. 주문 데이터 요청 (주문 UUID : c288442a-4bda-434e-ae5c-acf8fe8d4409)

{
    "market": "KRW-BTC",
    "side": "bid",
    "user_id": 1,
    "volume": 10,
    "price": 100.0,
    "ord_type": "limit"
}

 

2. 체결을 위한 주문 데이터 요청 

{
    "market": "KRW-BTC",
    "side": "ask",
    "user_id": 1,
    "volume": 2,
    "price": 100.0,
    "ord_type": "limit"
}

{
    "market": "KRW-BTC",
    "side": "ask",
    "user_id": 1,
    "volume": 3,
    "price": 100.0,
    "ord_type": "limit"
}

 

3. 주문 UUID 조회

SELECT m.*
FROM orders o
         LEFT JOIN match_orders m ON o.order_id = m.buy_order_id OR o.order_id = m.sell_order_id
WHERE o.order_id = 'c288442a-4bda-434e-ae5c-acf8fe8d4409';

#|match_id|buy_order_id|sell_order_id|match_price|match_quantity|match_time
1|d0d3add5-5511-4b3a-8399-2d3fddebf12e|c288442a-4bda-434e-ae5c-acf8fe8d4409|8656a794-cff0-43e5-b25c-bf396bcfb37d|100.00000000|2.00000000|2023-08-18 23:13:58.012105
2|cbb96413-21e6-46dc-98f5-f7a27694cf96|c288442a-4bda-434e-ae5c-acf8fe8d4409|ad626f1f-127b-4fb8-986e-6f0c3a678747|100.00000000|5.00000000|2023-08-18 23:14:31.603982

 

정상적으로 2건에 대한 이력이 조회되는것을 확인할 수 있습니다. 

 

반응형
반응형

가상화폐 거래소 구축을 위해서 매매 체결 엔진을 개발 시작해 보도록 해보겠습니다. 

레디스나 카프카를 활용해서 구축을 해볼수 있겠지만 우선, 정상적인 거래를 동작하기 위해 하나의 솔루션에서 개발을 진행할 예정입니다. 

 

데이터 베이스 모델링 

매매 체결 엔진 ERD 구조

 

 

주문 플로우

1. 주문 입력 단계:
    - 사용자가 주문을 입력합니다. 주문은 주문 체결 엔진 프로젝트에서 처리됩니다.
    - 주문 정보는 orders 테이블에 저장되며, 주문 체결 엔진은 주문을 오더북(주문 창구)에 매수 또는 매도 주문으로 저장합니다.

2. 주문 체결 단계:
    - 주문 체결 엔진은 주문 큐를 조회하여 매수와 매도 주문을 매칭시킵니다.
    - 가격이 매칭되는 경우, 체결된 주문 정보를 저장 및 업데이트를 합니다.

3. 사용자 자산 업데이트 단계:
    - 주문 체결에 따라 사용자의 자산이 변경될 수 있습니다.
    - 주문 체결 엔진은 사용자들의 자산 정보를 업데이트합니다.

 

Spring boot 프레임워크를 사용해서 Rest API 방식으로 주문 데이터를 저장하고 조회하는 기능을 만들었습니다.  

JPA 방식을 이용해 데이터를 저장 및 조회를 하고 있습니다. 

Post 방식으로 사용자로부터 데이터를 받으면 OrderMatcher 클래스에서 큐에 저장하고 체결 메서드를 호출합니다. 초기 개발이고 우선 전체적인 거래 시스템의 동작이 우선이기 때문에 싱글 스레드 방식으로 개발을 하고

추후 MSA 방식으로 분산 처리 시스템으로 업데이트 할 예정입니다. ^^ 

 

주문 컨트롤러 - OrderController.java

@RestController
@RequestMapping("/api/v1")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    @PostMapping("/orders")
    public ResponseEntity<OrderNewResponseDto> orders(@RequestBody OrderNewRequestDto orderNewRequestDto){
        try {
            Order order = orderService.createOrder(orderNewRequestDto);

            return ResponseEntity.ok(OrderNewResponseDto.of(order));
        } catch (Exception e) {
            // 예외 처리 로직
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

 

주문 관련 서비스 - OrderService.java

@Service
public class OrderService {

    private final OrderMatcher orderMatcher;
    private final OrderSave orderSave;

    public OrderService(OrderMatcher orderMatcher, OrderSave orderSave) {
        this.orderMatcher = orderMatcher;
        this.orderSave = orderSave;
    }

    public Order createOrder(OrderNewRequestDto orderNewRequestDto) {
        Order order = Order.of(orderNewRequestDto);

        // 주문 체결 엔진
        orderMatcher.submitOrder(order);

        // 주문 정보 저장
        orderSave.orderUpdate(order);

        return order;
    }
}

주문 체결 소스 - OrderMatcher.java

@Service
public class OrderMatcher {
    private final PriorityQueue<Order> buyOrders;
    private final PriorityQueue<Order> sellOrders;

    public OrderMatcher() {
        buyOrders = new PriorityQueue<>(Comparator.comparing(Order::getPrice).reversed());
        sellOrders = new PriorityQueue<>(Comparator.comparing(Order::getPrice));
    }

    // 주문 체결 엔진에 주문을 제출하는 메서드
    public void submitOrder(Order order) {
        if (order.getSideType() == SideType.bid) {
            buyOrders.add(order);
        } else {
            sellOrders.add(order);
        }
        matchOrders(); // 주문 체결 실행
    }

    private void updateOrdersAfterTrade(Order buyOrder, Order sellOrder, BigDecimal quantity) {
        buyOrder.updateOrderAfterTrade(quantity);
        sellOrder.updateOrderAfterTrade(quantity);

        if (buyOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
            buyOrders.remove(buyOrder);
        }

        if (sellOrder.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
            sellOrders.remove(sellOrder);
        }
    }

    private void matchOrders() {
        while (!buyOrders.isEmpty() && !sellOrders.isEmpty()) {
            Order buyOrder = buyOrders.peek();
            Order sellOrder = sellOrders.peek();

            if (buyOrder == null || sellOrder == null) {
                break;
            }

            if (buyOrder.getOrdType() == OrderType.market || sellOrder.getOrdType() == OrderType.market) {
                // 시장가 주문 체결
                BigDecimal quantity = buyOrder.getQuantity().min(sellOrder.getQuantity());
                BigDecimal price = sellOrder.getPrice();

                executeTrade(buyOrder, sellOrder, price, quantity);

                updateOrdersAfterTrade(buyOrder, sellOrder, quantity);

            } else if (buyOrder.getOrdType() == OrderType.limit && sellOrder.getOrdType() == OrderType.limit) {
                // 지정가 주문 체결
                if (buyOrder.getPrice().compareTo(sellOrder.getPrice()) >= 0) {
                    BigDecimal quantity = buyOrder.getQuantity().min(sellOrder.getQuantity());
                    BigDecimal price = sellOrder.getPrice();

                    executeTrade(buyOrder, sellOrder, price, quantity);

                    updateOrdersAfterTrade(buyOrder, sellOrder, quantity);
                } else {
                    break; // 가격이 맞지 않으면 더 이상 체결하지 않음
                }
            }
        }
    }

    private void executeTrade(Order buyOrder, Order sellOrder, BigDecimal price, BigDecimal quantity) {
        BigDecimal totalAmount = price.multiply(quantity);
        BigDecimal tradingFee = calculateTradingFee(totalAmount);

        buyOrder.executeTrade(quantity, tradingFee);

        sellOrder.executeTrade(quantity, tradingFee);
    }

    // 거래 수수료 계산 메서드
    private BigDecimal calculateTradingFee(BigDecimal totalAmount) {
        BigDecimal feePercentage = new BigDecimal("0.05");
        BigDecimal fee = totalAmount.multiply(feePercentage).divide(new BigDecimal("100"));
        return fee;
    }

}

 

지정가/시장가 방식으로 처리를 되게 로직을 구현했습니다. 주문 데이터 전량을 메모리에 가지고 있기 때문에 데이터양이 많아지면 시스템 문제가 발생할 가능성이 많아 보입니다. 

 

이러한 문제도 차차 해결해 가면서 프로그램을 업그레이드를 해봐야겠습니다. 

반응형

+ Recent posts