diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index 1337a0b1..46e09705 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -34,7 +34,7 @@ services: image: mariadb:latest container_name: mariadb2 ports: - - "3306:3306" + - "3307:3306" env_file: - ../docker/local.properties environment: @@ -66,6 +66,25 @@ services: - monitoring-net restart: unless-stopped + # --- 추가된 InfluxDB 서비스 --- + influxdb: + image: influxdb:2.7 + container_name: influxdb + ports: + - "8086:8086" + volumes: + - influxdb_data:/var/lib/influxdb2 + environment: + - DOCKER_INFLUXDB_INIT_MODE=setup + - DOCKER_INFLUXDB_INIT_USERNAME=k6-user # 임의의 사용자/비밀번호 + - DOCKER_INFLUXDB_INIT_PASSWORD=k6-password + - DOCKER_INFLUXDB_INIT_ORG=k6-org + - DOCKER_INFLUXDB_INIT_BUCKET=k6-bucket + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=vJvTx5b8vjgH7mu-M-BsEvdy5_COovexAjyniVhMF1yPzvsp2g8kp62opqXVMq6ICAq2tLJxxD6ifXPBZ9YVmA== + networks: + - monitoring-net + restart: unless-stopped + grafana: image: grafana/grafana:11.0.0 container_name: grafana @@ -77,9 +96,10 @@ services: networks: - monitoring-net restart: unless-stopped - depends_on: + depends_on: # --- 수정된 부분 --- - prometheus - jaeger + - influxdb jaeger: image: jaegertracing/all-in-one:latest @@ -96,6 +116,7 @@ volumes: grafana_data: {} mariadb_data: driver: local + influxdb_data: {} # --- 추가된 부분 --- networks: app-network: @@ -104,3 +125,8 @@ networks: monitoring-net: name: monitoring-net driver: bridge + + + # export K6_INFLUXDB_ORGANIZATION=k6-org + # - export K6_INFLUXDB_BUCKET=k6-bucket + # ./k6 run --out xk6-influxdb=http://localhost:8086 /Users/jangbongjun/coin/src/test/resources/k6/chart-stomp-test.js diff --git a/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java index db294c80..656829be 100644 --- a/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java +++ b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java @@ -23,6 +23,7 @@ public class TradeEventHandler { private final WebsocketSendService websocketSendService; private final ChartSubscriptionService chartSubscriptionService; // 주입 private final RealTimeDataPrevRateService realTimeDataPrevRateService; + //event로 이벤틀 처리해야한다. //eventListener는 void로 처리를 해야한다 @TransactionalEventListener @@ -37,8 +38,29 @@ public void handleTradeEvent(TradeExecutedEvent event) { TradeEventDto tradeEventDto = getTradeEventDto(trade); log.debug("TradeEventHandler 수신 : {}", trade); - // 실시간 데이터 체결내역 - // 해당 종목에 대한 구독자가 있는지 확인 + // 실시간 데이터 전송 + processRealTimeTradeRate(ticker, tradeEventDto); + + //전날 종가 변동률 전송 + processPrevRateData(ticker, tradeEventDto); + } + + public void processPrevRateData(String ticker, TradeEventDto tradeEventDto) { + if (chartSubscriptionService.isSubscribedToPrevRate(ticker)) { + log.debug("종목 {} 전일 대비 변동률 구독자 확인됨. 데이터 전송 시작.", ticker); + try { + PrevRateDto dto = realTimeDataPrevRateService.generatePrevRateData(tradeEventDto); + websocketSendService.sendPrevRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일 + log.debug("종목 {} 전일 대비 변동률 전송 완료 : {}", ticker, dto); + } catch (Exception e) { + log.error("종목 {} 전일 대비 변동률 전송 중 오류: {}", ticker, e.getMessage(), e); + } + } else { + log.debug("종목 {} 전일 대비 변동률 구독자 없음. 데이터 전송 생략.", ticker); + } + } + + public void processRealTimeTradeRate(String ticker, TradeEventDto tradeEventDto) { if (chartSubscriptionService.isSubscribedToRealTimeTradeRate(ticker)) { log.debug("종목 {} 실시간 체결 정보 구독자 확인됨. 데이터 처리 및 전송 시작.", ticker); @@ -54,21 +76,6 @@ public void handleTradeEvent(TradeExecutedEvent event) { } else { log.debug("종목 {} 실시간 체결 정보 구독자 없음. 데이터 전송 생략.", ticker); } - - //전날 종가 변동률 전송 - if(chartSubscriptionService.isSubscribedToPrevRate(ticker)) { - log.debug("종목 {} 전일 대비 변동률 구독자 확인됨. 데이터 전송 시작.", ticker); - try { - PrevRateDto dto = realTimeDataPrevRateService.generatePrevRateData(tradeEventDto); - websocketSendService.sendPrevRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일 - log.debug("종목 {} 전일 대비 변동률 전송 완료 : {}", ticker, dto); - } catch (Exception e) { - log.error("종목 {} 전일 대비 변동률 전송 중 오류: {}", ticker, e.getMessage(), e); - } - }else { - log.debug("종목 {} 전일 대비 변동률 구독자 없음. 데이터 전송 생략.", ticker); - } - } @NotNull diff --git a/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java deleted file mode 100644 index bc393874..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.cleanengine.coin.chart.repository; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.data.repository.query.Param; - -public interface ChartDataRepository { - /** - * Projection interface for minute-candle aggregation results. - */ - interface MinuteCandleProjection { - String getTicker(); - LocalDateTime getBucketStart(); - Double getLowPrice(); - Double getHighPrice(); - Double getOpenPrice(); - Double getClosePrice(); - Double getVolume(); - } - - /** - 시작시간과 끝시간 (1분)의 ohlc 데이터를 반환 - */ - List findMinuteCandles( - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); - - /** - * 특정 티커의 캔들 데이터만 조회 - */ - List findMinuteCandlesByTicker( - @Param("ticker") String ticker, - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); -} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java deleted file mode 100644 index d7b63cb7..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.cleanengine.coin.chart.repository; - -import com.cleanengine.coin.trade.entity.Trade; -import org.springframework.context.annotation.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.util.List; - -@Repository -@Profile({"h2", "default"}) // 기본 프로필과 h2 프로필에서 사용 -public interface H2ChartDataRepository extends ChartDataRepository, JpaRepository { - - @Override - @Query(value = """ - SELECT - t.ticker AS ticker, - PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') AS bucket_start, - MIN(t.price) AS low_price, - MAX(t.price) AS high_price, - AVG(t.price) AS open_price, -- 간소화: 개/종가 대신 평균가 사용 - AVG(t.price) AS close_price, -- 간소화: 개/종가 대신 평균가 사용 - SUM(t.size) AS volume - FROM trade t - WHERE t.trade_time >= :start AND t.trade_time < :end - GROUP BY t.ticker, PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') - ORDER BY t.ticker, bucket_start -""", nativeQuery = true) - List findMinuteCandles( - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); - - @Override - @Query(value = """ - SELECT - t.ticker AS ticker, - PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') AS bucket_start, - MIN(t.price) AS low_price, - MAX(t.price) AS high_price, - AVG(t.price) AS open_price, -- 간소화: 개/종가 대신 평균가 사용 - AVG(t.price) AS close_price, -- 간소화: 개/종가 대신 평균가 사용 - SUM(t.size) AS volume - FROM trade t - WHERE UPPER(t.ticker) = UPPER(:ticker) AND t.trade_time >= :start AND t.trade_time < :end - GROUP BY t.ticker, PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') - ORDER BY bucket_start -""", nativeQuery = true) - List findMinuteCandlesByTicker( - @Param("ticker") String ticker, - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); -} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java deleted file mode 100644 index 5bd0271d..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java +++ /dev/null @@ -1,94 +0,0 @@ -//package com.cleanengine.coin.chart.repository; -// -//import com.cleanengine.coin.trade.entity.Trade; -//import org.springframework.context.annotation.Profile; -//import org.springframework.data.jpa.repository.JpaRepository; -//import org.springframework.data.jpa.repository.Query; -//import org.springframework.data.repository.query.Param; -//import org.springframework.stereotype.Repository; -// -//import java.time.LocalDateTime; -//import java.util.List; -// -//@Repository -//@Profile("mariadb") // mariadb 프로필에서 사용 -//public interface MariaDbChartDataRepository extends ChartDataRepository, JpaRepository { -// -// @Override -// @Query(value = """ -// WITH cte AS ( -// SELECT -// t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) AS bucket_start, -// t.price, -// t.size, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time ASC -// ) AS rn_open, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time DESC -// ) AS rn_close -// FROM trade t -// WHERE t.trade_time >= :start -// AND t.trade_time < :end -// ) -// SELECT -// ticker, -// bucket_start, -// MAX(CASE WHEN rn_open = 1 THEN price END) AS open_price, -// MAX(price) AS high_price, -// MIN(price) AS low_price, -// MAX(CASE WHEN rn_close = 1 THEN price END) AS close_price, -// SUM(size) AS volume -// FROM cte -// GROUP BY ticker, bucket_start -// ORDER BY ticker, bucket_start -// """, nativeQuery = true) -// List findMinuteCandles( -// @Param("start") LocalDateTime start, -// @Param("end") LocalDateTime end); -// -// @Override -// @Query(value = """ -// WITH cte AS ( -// SELECT -// t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) AS bucket_start, -// t.price, -// t.size, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time ASC -// ) AS rn_open, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time DESC -// ) AS rn_close -// FROM trade t -// WHERE UPPER(t.ticker) = UPPER(:ticker) -// AND t.trade_time >= :start -// AND t.trade_time < :end -// ) -// SELECT -// ticker, -// bucket_start, -// MAX(CASE WHEN rn_open = 1 THEN price END) AS open_price, -// MAX(price) AS high_price, -// MIN(price) AS low_price, -// MAX(CASE WHEN rn_close = 1 THEN price END) AS close_price, -// SUM(size) AS volume -// FROM cte -// GROUP BY ticker, bucket_start -// ORDER BY bucket_start -// """, nativeQuery = true) -// List findMinuteCandlesByTicker( -// @Param("ticker") String ticker, -// @Param("start") LocalDateTime start, -// @Param("end") LocalDateTime end); -//} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java index 26d3f177..58ca786a 100644 --- a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java +++ b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java @@ -45,9 +45,7 @@ public Set getAllRealTimeTradeRateSubscribedTickers() { //종목에 대한 구독 여부 public boolean isSubscribedToRealTimeTradeRate(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return realTimeTradeRateSubscribedTickers.contains(ticker); } @@ -76,9 +74,7 @@ public Set getAllRealTimeOhlcSubscribedTickers() { } public boolean isSubscribedToRealTimeOhlc(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return realTimeOhlcSubscribedTickers.contains(ticker); } @@ -103,9 +99,7 @@ public Set getAllPrevRateSubscribedTickers(String ticker) { } public boolean isSubscribedToPrevRate(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return PrevRateSubscribedTickers.contains(ticker); } diff --git a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java index 5d1d20e6..26d36bfb 100644 --- a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java +++ b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java @@ -94,8 +94,8 @@ void validateTradeList(List trades) { // OHLC 계산 로직 @NotNull static OhlcData calculateOhlcData(List trades) { - double open = trades.get(0).getPrice(); - double close = trades.get(trades.size() - 1).getPrice(); + double open = trades.getFirst().getPrice(); + double close = trades.getLast().getPrice(); double high = trades.stream() .mapToDouble(Trade::getPrice) diff --git a/src/main/java/com/cleanengine/coin/trade/entity/Trade.java b/src/main/java/com/cleanengine/coin/trade/entity/Trade.java index 9f6e11de..bac87d50 100644 --- a/src/main/java/com/cleanengine/coin/trade/entity/Trade.java +++ b/src/main/java/com/cleanengine/coin/trade/entity/Trade.java @@ -23,7 +23,6 @@ public class Trade { @Column(name = "trade_time", nullable = false) @CreationTimestamp - //생성 타임은 추후 논의 예정 private LocalDateTime tradeTime; @Column(name = "buy_user_id", nullable = false) diff --git a/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java b/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java new file mode 100644 index 00000000..056e72bb --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java @@ -0,0 +1,321 @@ +package com.cleanengine.coin.chart.handler; + +import com.cleanengine.coin.chart.dto.PrevRateDto; +import com.cleanengine.coin.chart.dto.RealTimeDataDto; +import com.cleanengine.coin.chart.dto.TradeEventDto; +import com.cleanengine.coin.chart.service.ChartSubscriptionService; +import com.cleanengine.coin.chart.service.RealTimeDataPrevRateService; +import com.cleanengine.coin.chart.service.RealTimeTradeService; +import com.cleanengine.coin.chart.service.WebsocketSendService; +import com.cleanengine.coin.trade.application.TradeExecutedEvent; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TradeEventHandler 단위 테스트") +class TradeEventHandlerTest { + + @Mock + private RealTimeTradeService realTimeTradeService; + + @Mock + private WebsocketSendService websocketSendService; + + @Mock + private ChartSubscriptionService chartSubscriptionService; + + @Mock + private RealTimeDataPrevRateService realTimeDataPrevRateService; + + @InjectMocks + private TradeEventHandler tradeEventHandler; + + private Trade validTrade; + private TradeExecutedEvent validEvent; + private LocalDateTime testTime; + + @BeforeEach + void setUp() { + testTime = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + validTrade = createTrade("BTC", 50000.0, 1.5, testTime); validEvent = TradeExecutedEvent.of(validTrade, 1L, 2L); + validEvent = TradeExecutedEvent.of(validTrade, 1L, 2L); + } + + // ===== handleTradeEvent 테스트 ===== + + @Test + @DisplayName("정상적인 거래 이벤트 처리 - 모든 구독자 있음") + void handleTradeEvent_ValidEvent_AllSubscribersPresent() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + PrevRateDto prevRateDto = createPrevRateDto(); + + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))).thenReturn(realTimeDto); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))).thenReturn(prevRateDto); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("정상적인 거래 이벤트 처리 - 구독자 없음") + void handleTradeEvent_ValidEvent_NoSubscribers() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(false); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("null Trade 이벤트 처리") + void handleTradeEvent_NullTrade_HandlesGracefully() { + // given + TradeExecutedEvent nullTradeEvent = TradeExecutedEvent.of(null, null, null); + // when + tradeEventHandler.handleTradeEvent(nullTradeEvent); + + // then + verifyNoInteractions(chartSubscriptionService); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("실시간 트레이드 처리 중 예외 발생") + void handleTradeEvent_RealTimeTradeException_ContinuesProcessing() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))) + .thenThrow(new RuntimeException("실시간 데이터 생성 실패")); + + PrevRateDto prevRateDto = createPrevRateDto(); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))).thenReturn(prevRateDto); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService, never()).sendChangeRate(any(), any()); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("전일 대비 변동률 처리 중 예외 발생") + void handleTradeEvent_PrevRateException_ContinuesProcessing() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))).thenReturn(realTimeDto); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))) + .thenThrow(new RuntimeException("전일 대비 변동률 생성 실패")); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + verify(websocketSendService, never()).sendPrevRate(any(), any()); + } + + // ===== processRealTimeTradeRate 테스트 ===== + + @Test + @DisplayName("실시간 트레이드 처리 - 구독자 있음") + void processRealTimeTradeRate_WithSubscribers_SendsData() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(tradeEventDto)).thenReturn(realTimeDto); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(realTimeTradeService).generateRealTimeData(tradeEventDto); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + } + + @Test + @DisplayName("실시간 트레이드 처리 - 구독자 없음") + void processRealTimeTradeRate_NoSubscribers_SkipsProcessing() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("실시간 트레이드 처리 중 예외 발생 시 로그 출력") + void processRealTimeTradeRate_Exception_LogsError() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(tradeEventDto)) + .thenThrow(new RuntimeException("데이터 생성 실패")); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(realTimeTradeService).generateRealTimeData(tradeEventDto); + verify(websocketSendService, never()).sendChangeRate(any(), any()); + } + + // ===== processPrevRateData 테스트 ===== + + @Test + @DisplayName("전일 대비 변동률 처리 - 구독자 있음") + void processPrevRateData_WithSubscribers_SendsData() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + PrevRateDto prevRateDto = createPrevRateDto(); + + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeDataPrevRateService.generatePrevRateData(tradeEventDto)).thenReturn(prevRateDto); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verify(realTimeDataPrevRateService).generatePrevRateData(tradeEventDto); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("전일 대비 변동률 처리 - 구독자 없음") + void processPrevRateData_NoSubscribers_SkipsProcessing() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("전일 대비 변동률 처리 중 예외 발생 시 로그 출력") + void processPrevRateData_Exception_LogsError() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeDataPrevRateService.generatePrevRateData(tradeEventDto)) + .thenThrow(new RuntimeException("변동률 계산 실패")); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(realTimeDataPrevRateService).generatePrevRateData(tradeEventDto); + verify(websocketSendService, never()).sendPrevRate(any(), any()); + } + + // ===== getTradeEventDto 정적 메서드 테스트 ===== + + @Test + @DisplayName("Trade에서 TradeEventDto 생성") + void getTradeEventDto_ValidTrade_CreatesDto() { + // when + TradeEventDto result = TradeEventHandler.getTradeEventDto(validTrade); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTicker()).isEqualTo("BTC"); + assertThat(result.getSize()).isEqualTo(1.5); + assertThat(result.getPrice()).isEqualTo(50000.0); + assertThat(result.getTimestamp()).isEqualTo(testTime); + } + // ===== 헬퍼 메서드 ===== + + private Trade createTrade(String ticker, Double price, Double size, LocalDateTime tradeTime) { + return new Trade( + 1, // id + ticker, // ticker + tradeTime, // tradeTime + 1, // buyUserId + 2, // sellUserId + price, // price + size // size + ); + } + + private TradeEventDto createTradeEventDto() { + return new TradeEventDto("BTC", 1.5, 50000.0, testTime); + } + + private RealTimeDataDto createRealTimeDataDto() { + return new RealTimeDataDto( + "BTC", + 1.5, + 50000.0, + 2.5, + testTime, + "test-transaction-id" + ); + } + + private PrevRateDto createPrevRateDto() { + return new PrevRateDto( + "BTC", + 50000.0, + 48000.0, + 4.17, + testTime + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java b/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java new file mode 100644 index 00000000..635a45c3 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java @@ -0,0 +1,67 @@ +package com.cleanengine.coin.chart.repository; + +import com.cleanengine.coin.trade.entity.Trade; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + + +@DataJpaTest +class MinuteOhlcDataRepositoryTest { + + @Autowired + private MinuteOhlcDataRepository repository; + + @DisplayName("db를 ticker로 모든 트레이드를 시간 순으로 조회") + @Test + public void findByTickerOrderByTradeTimeAsc() throws Exception { + //given + Trade trade1 = new Trade(); + trade1.setTicker("BTC"); + trade1.setSize(1.0); + trade1.setPrice(10000.0); + trade1.setTradeTime(LocalDateTime.now()); + trade1.setBuyUserId(Integer.valueOf("1")); + trade1.setSellUserId(Integer.valueOf("2")); + + Trade trade2 = new Trade(); + trade2.setTicker("BTC"); + trade2.setSize(2.0); + trade2.setPrice(20000.0); + trade2.setTradeTime(LocalDateTime.now()); + trade2.setBuyUserId(3); + trade2.setSellUserId(4); + + + Trade trade3 = new Trade(); + trade3.setTicker("BTC"); + trade3.setSize(3.0); + trade3.setPrice(30000.0); + trade3.setTradeTime(LocalDateTime.now()); + trade3.setBuyUserId(5); + trade3.setSellUserId(6); + + repository.saveAll(List.of(trade1, trade2, trade3)); + + + //when + List result = repository.findByTickerOrderByTradeTimeAsc("BTC"); + + //then + assertThat(result).hasSize(3) + .extracting("ticker", "size", "price", "TradeTime") + .containsExactlyInAnyOrder( + tuple("BTC", 1.0, 10000.0, trade1.getTradeTime()), + tuple("BTC", 2.0, 20000.0, trade2.getTradeTime()), + tuple("BTC", 3.0, 30000.0, trade3.getTradeTime()) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java b/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java new file mode 100644 index 00000000..3015ebcc --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java @@ -0,0 +1,77 @@ +package com.cleanengine.coin.chart.repository; + +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +//jpa의 시간 테스트를 할때 + + +@DataJpaTest +class RealTimeTradeRepositoryTest { + + private LocalDateTime today; + private LocalDateTime yesterdayStart; + private LocalDateTime yesterdayEnd; + + @BeforeEach + void setUp() { + + today = LocalDateTime.now(); + yesterdayStart = today.minusDays(1).withHour(0).withMinute(0).withSecond(0); + yesterdayEnd = today.minusDays(1).withHour(23).withMinute(59).withSecond(59); + } + + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private RealTimeTradeRepository repository; + + @DisplayName("특정 기간 내 가장 마지막 거래를 올바르게 조회") + @Test + public void findFirstByTickerAndTradeTimeBetweenOrderByTradeTimeDesc() throws Exception { + // given - 네이티브 쿼리로 정확한 시간 설정 + LocalDateTime time1 = yesterdayStart.plusHours(10); + LocalDateTime time2 = yesterdayStart.plusHours(15); + LocalDateTime time3 = yesterdayStart.plusHours(20); + + // 직접 SQL로 데이터 삽입 + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time1).setParameter(3, 1).setParameter(4, 2).setParameter(5, 50000.0).setParameter(6, 1.0) + .executeUpdate(); + + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time2).setParameter(3, 1).setParameter(4, 2).setParameter(5, 52000.0).setParameter(6, 2.0) + .executeUpdate(); + + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time3).setParameter(3, 1).setParameter(4, 2).setParameter(5, 51500.0).setParameter(6, 3.0) + .executeUpdate(); + + entityManager.flush(); + + // when + Trade result = repository.findFirstByTickerAndTradeTimeBetweenOrderByTradeTimeDesc( + "BTC", yesterdayStart, yesterdayEnd); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTicker()).isEqualTo("BTC"); + assertThat(result.getPrice()).isEqualTo(51500.0); // 가장 마지막 시간의 거래 + } + + + +} \ No newline at end of file