From c2a05a02a843caed34086f65774e226cc8b62cd3 Mon Sep 17 00:00:00 2001 From: caniro Date: Sat, 7 Jun 2025 22:29:15 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=B2=B4=EA=B2=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TradeExecutedNotificationHandler.java | 51 +++++++ .../application/TradeExecutedNotifyDto.java | 36 +++++ .../TradeExecutedNotificationHandlerTest.java | 133 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java create mode 100644 src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java create mode 100644 src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java new file mode 100644 index 00000000..036ead0c --- /dev/null +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java @@ -0,0 +1,51 @@ +package com.cleanengine.coin.trade.application; + +import com.cleanengine.coin.trade.entity.Trade; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; +import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; + +@Slf4j +@Component +public class TradeExecutedNotificationHandler { + + private final SimpMessagingTemplate messagingTemplate; + + private static final String ASK = "ask"; // 매도 + private static final String BID = "bid"; // 매수 + + public TradeExecutedNotificationHandler(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + + @TransactionalEventListener + public void notifyAfterTradeExecuted(TradeExecutedEvent tradeExecutedEvent) { + Trade trade = tradeExecutedEvent.getTrade(); + if (trade == null) { + log.error("체결 알림 실패! trade == null"); + return ; + } + + Integer sellUserId = trade.getSellUserId(); + Integer buyUserId = trade.getBuyUserId(); + if (sellUserId == null || buyUserId == null) { + log.error("체결 알림 실패! sellUserId: {}, buyUserId: {}", sellUserId, buyUserId); + return ; + } + if (sellUserId == SELL_ORDER_BOT_ID && buyUserId == BUY_ORDER_BOT_ID) { + return ; + } + + log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId ); + + TradeExecutedNotifyDto soldDto = TradeExecutedNotifyDto.of(trade, ASK); + TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID); + messagingTemplate.convertAndSend("/topic/tradeNotification/" + sellUserId, soldDto); + messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto); + } + +} diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java new file mode 100644 index 00000000..d26a39ba --- /dev/null +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java @@ -0,0 +1,36 @@ +package com.cleanengine.coin.trade.application; + +import com.cleanengine.coin.trade.entity.Trade; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Builder; + +import java.time.LocalDateTime; + +@JsonPropertyOrder({ "ticker", "price", "size", "type", "tradedTime"}) +public class TradeExecutedNotifyDto { + public String ticker; + public Double price; + public Double size; + public String type; + public LocalDateTime tradedTime; + + @Builder + private TradeExecutedNotifyDto(String ticker, Double price, Double size, String type, LocalDateTime tradedTime) { + this.ticker = ticker; + this.price = price; + this.size = size; + this.type = type; + this.tradedTime = tradedTime; + } + + public static TradeExecutedNotifyDto of(Trade trade, String type) { + return TradeExecutedNotifyDto.builder() + .ticker(trade.getTicker()) + .price(trade.getPrice()) + .size(trade.getSize()) + .type(type) + .tradedTime(trade.getTradeTime()) + .build(); + } + +} diff --git a/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java b/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java new file mode 100644 index 00000000..5f7db59e --- /dev/null +++ b/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java @@ -0,0 +1,133 @@ +package com.cleanengine.coin.trade.application; + +import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; +import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import com.cleanengine.coin.trade.entity.Trade; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import java.time.LocalDateTime; + +@DisplayName("체결 알림 단위테스트") +@ExtendWith(MockitoExtension.class) +class TradeExecutedNotificationHandlerTest { + + @Mock + private SimpMessagingTemplate messagingTemplate; + + private TradeExecutedNotificationHandler handler; + + @BeforeEach + void setUp() { + handler = new TradeExecutedNotificationHandler(messagingTemplate); + } + + @DisplayName("정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") + @Test + void shouldSendNotificationsForValidTrade() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), 3, SELL_ORDER_BOT_ID, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/1"), any(TradeExecutedNotifyDto.class)); + verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); + } + + @DisplayName("정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") + @Test + void shouldSendNotificationsForValidTrade2() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, 3, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/2"), any(TradeExecutedNotifyDto.class)); + verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); + } + + @DisplayName("매수인과 매도인의 userId가 null이면 메시지를 전송하지 않는다.") + @Test + void shouldNotSendNotificationForNullUserIds() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), null, null, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verifyNoInteractions(messagingTemplate); + } + + @DisplayName("매수인의 userId가 null이면 메시지를 전송하지 않는다.") + @Test + void shouldNotSendNotificationForNullBuyUserId() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), null, SELL_ORDER_BOT_ID, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verifyNoInteractions(messagingTemplate); + } + + @DisplayName("매도인의 userId가 null이면 메시지를 전송하지 않는다.") + @Test + void shouldNotSendNotificationForNullSellUserId() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, null, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verifyNoInteractions(messagingTemplate); + } + + @DisplayName("봇끼리의 체결은 메시지를 전송하지 않는다.") + @Test + void shouldNotSendNotificationForBotTrade() { + // given + Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, SELL_ORDER_BOT_ID, 50000.0, 1.0); + TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verifyNoInteractions(messagingTemplate); + } + + @DisplayName("체결이 null이면 메시지를 전송하지 않는다.") + @Test + void shouldNotSendNotificationForNullTrade() { + // given + TradeExecutedEvent event = TradeExecutedEvent.of(null, null, null); + + // when + handler.notifyAfterTradeExecuted(event); + + // then + verifyNoInteractions(messagingTemplate); + } + +} \ No newline at end of file From 224a004d02815e0faef58443b7ab8459828404d5 Mon Sep 17 00:00:00 2001 From: caniro Date: Sat, 7 Jun 2025 22:36:36 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=EC=9E=90=20private=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/application/TradeExecutedNotifyDto.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java index d26a39ba..e0087dad 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java @@ -8,11 +8,16 @@ @JsonPropertyOrder({ "ticker", "price", "size", "type", "tradedTime"}) public class TradeExecutedNotifyDto { - public String ticker; - public Double price; - public Double size; - public String type; - public LocalDateTime tradedTime; + + private String ticker; + + private Double price; + + private Double size; + + private String type; + + private LocalDateTime tradedTime; @Builder private TradeExecutedNotifyDto(String ticker, Double price, Double size, String type, LocalDateTime tradedTime) { From ea6dd2b22466493869da83c77ac930c1aee777b4 Mon Sep 17 00:00:00 2001 From: caniro Date: Sun, 8 Jun 2025 01:02:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EB=B4=87=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=B2=B4=EA=B2=B0=EC=95=8C=EB=A6=BC=EC=9D=80=20?= =?UTF-8?q?=EC=88=98=ED=96=89=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TradeExecutedNotificationHandler.java | 15 ++++++++------- .../TradeExecutedNotificationHandlerTest.java | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java index 036ead0c..f1bea0ce 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java @@ -36,16 +36,17 @@ public void notifyAfterTradeExecuted(TradeExecutedEvent tradeExecutedEvent) { log.error("체결 알림 실패! sellUserId: {}, buyUserId: {}", sellUserId, buyUserId); return ; } - if (sellUserId == SELL_ORDER_BOT_ID && buyUserId == BUY_ORDER_BOT_ID) { - return ; + + if (sellUserId != SELL_ORDER_BOT_ID) { + TradeExecutedNotifyDto soldDto = TradeExecutedNotifyDto.of(trade, ASK); + messagingTemplate.convertAndSend("/topic/tradeNotification/" + sellUserId, soldDto); + } + if (buyUserId != BUY_ORDER_BOT_ID) { + TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID); + messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto); } log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId ); - - TradeExecutedNotifyDto soldDto = TradeExecutedNotifyDto.of(trade, ASK); - TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID); - messagingTemplate.convertAndSend("/topic/tradeNotification/" + sellUserId, soldDto); - messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto); } } diff --git a/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java b/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java index 5f7db59e..d3793153 100644 --- a/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java +++ b/src/test/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandlerTest.java @@ -31,7 +31,7 @@ void setUp() { handler = new TradeExecutedNotificationHandler(messagingTemplate); } - @DisplayName("정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") + @DisplayName("매도인은 봇인 정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") @Test void shouldSendNotificationsForValidTrade() { // given @@ -42,11 +42,11 @@ void shouldSendNotificationsForValidTrade() { handler.notifyAfterTradeExecuted(event); // then - verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/1"), any(TradeExecutedNotifyDto.class)); + verify(messagingTemplate, times(1)).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); } - @DisplayName("정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") + @DisplayName("매수인은 봇인 정상 체결내역을 리스닝하면 웹소켓으로 전송한다.") @Test void shouldSendNotificationsForValidTrade2() { // given @@ -57,7 +57,7 @@ void shouldSendNotificationsForValidTrade2() { handler.notifyAfterTradeExecuted(event); // then - verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/2"), any(TradeExecutedNotifyDto.class)); + verify(messagingTemplate, times(1)).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class)); } From d864f9ed9ad70a0323562c22165fcf42a9eac925 Mon Sep 17 00:00:00 2001 From: caniro Date: Sun, 8 Jun 2025 01:35:08 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20private=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=EC=9E=90=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EA=B3=B5=EB=B0=B1=EC=9C=BC=EB=A1=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/trade/application/TradeExecutedNotifyDto.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java index e0087dad..10585a79 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java @@ -3,10 +3,12 @@ import com.cleanengine.coin.trade.entity.Trade; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.Builder; +import lombok.Getter; import java.time.LocalDateTime; -@JsonPropertyOrder({ "ticker", "price", "size", "type", "tradedTime"}) +@Getter +@JsonPropertyOrder({"ticker", "price", "size", "type", "tradedTime"}) public class TradeExecutedNotifyDto { private String ticker; From 1314884bdb2c177903779f0b556e5a8d9c65e776 Mon Sep 17 00:00:00 2001 From: caniro Date: Sun, 8 Jun 2025 01:35:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=EB=B4=87=EB=81=BC=EB=A6=AC?= =?UTF-8?q?=EC=9D=98=20=EC=B2=B4=EA=B2=B0=EC=9D=80=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/application/TradeExecutedNotificationHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java index f1bea0ce..8d94a666 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java @@ -45,8 +45,9 @@ public void notifyAfterTradeExecuted(TradeExecutedEvent tradeExecutedEvent) { TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID); messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto); } - - log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId ); + if (sellUserId != SELL_ORDER_BOT_ID || buyUserId != BUY_ORDER_BOT_ID) { + log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId ); + } } }