From 50e93e5b79498c000fb2fa6a3a2f4494fd5e9ff3 Mon Sep 17 00:00:00 2001 From: Junh-b Date: Wed, 2 Jul 2025 17:53:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=96=B4=EC=A0=9C=20=EC=A2=85?= =?UTF-8?q?=EA=B0=80=20=EC=A1=B0=ED=9A=8C=20queryrepository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit querydsl로 구현. h2, mariadb 두가지 환경에서의 테스트 포함. --- .../service/TradeQueryService.java | 22 +++++ .../coin/orderbook/dto/ClosingPriceDto.java | 10 ++ .../orderbook/infra/TradeQueryRepository.java | 39 ++++++++ .../infra/TradeQueryRepositoryH2Test.java | 92 +++++++++++++++++++ .../TradeQueryRepositoryMariadbTest.java | 78 ++++++++++++++++ .../infra/TradeQueryRepository/clearTrade.sql | 1 + .../insertDuplicateTimeYesterdayTrade.sql | 9 ++ .../insertStartOfTodayTrade.sql | 4 + .../insertYesterdayTrade.sql | 9 ++ 9 files changed, 264 insertions(+) create mode 100644 src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java create mode 100644 src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java create mode 100644 src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java create mode 100644 src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java create mode 100644 src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java create mode 100644 src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql create mode 100644 src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql create mode 100644 src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql create mode 100644 src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql diff --git a/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java b/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java new file mode 100644 index 00000000..d3f830ba --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java @@ -0,0 +1,22 @@ +package com.cleanengine.coin.orderbook.application.service; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import com.cleanengine.coin.orderbook.infra.TradeQueryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class TradeQueryService { + private final TradeQueryRepository tradeQueryRepository; + + @Transactional(isolation = Isolation.READ_COMMITTED) + public ClosingPriceDto getYesterdayClosingPrice(String ticker, LocalDate yesterdayDate) { + return tradeQueryRepository.getYesterdayClosingPrice(ticker, yesterdayDate); + } +} diff --git a/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java b/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java new file mode 100644 index 00000000..c05b3c8b --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java @@ -0,0 +1,10 @@ +package com.cleanengine.coin.orderbook.dto; + +import com.querydsl.core.annotations.QueryProjection; + +import java.time.LocalDate; + +public record ClosingPriceDto(String ticker, LocalDate baseDate, Double closingPrice) { + @QueryProjection + public ClosingPriceDto {} +} diff --git a/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java b/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java new file mode 100644 index 00000000..f55c4c75 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java @@ -0,0 +1,39 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import com.cleanengine.coin.orderbook.dto.QClosingPriceDto; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static com.cleanengine.coin.trade.entity.QTrade.trade; + +@Component +public class TradeQueryRepository { + private final JPAQueryFactory queryFactory; + + public TradeQueryRepository(EntityManager entityManager){ + this.queryFactory = new JPAQueryFactory(entityManager); + } + + public ClosingPriceDto getYesterdayClosingPrice(String ticker, LocalDate yesterdayDate) { + LocalDateTime yesterday = yesterdayDate.atStartOfDay(); + + return queryFactory + .select(new QClosingPriceDto( + trade.ticker, + Expressions.asDate(yesterdayDate).as("baseDate"), + trade.price)) + .from(trade) + .where( + trade.ticker.eq(ticker) + .and(trade.tradeTime.goe(yesterday)) + .and(trade.tradeTime.lt(yesterday.plusDays(1)))) + .orderBy(trade.tradeTime.desc(), trade.id.desc()) + .fetchFirst(); + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java new file mode 100644 index 00000000..82f597d9 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java @@ -0,0 +1,92 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +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.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("dev, it, h2-mem") +@DataJpaTest +@Import({TradeQueryRepository.class}) +public class TradeQueryRepositoryH2Test { + @Autowired + private TradeQueryRepository tradeQueryRepository; + + @DisplayName("어제 trade가 있었을 경우, 정상적으로 yesterdayClosingPrice를 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithTradeExecutedYesterday_shouldReturnSuccessfully() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(400.0, closingPriceDto.closingPrice()); + } + + @DisplayName("어제 trade가 없었을 경우, null을 조회한다.") + @Test + @Transactional + public void queryYesterdayClosingPriceWithoutYesterdayTrade_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 trade가 없고, 오늘 00시 00분 00초의 trade가 있었을 때, null을 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithTradeExecutedToday_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 같은 시간에 여러건의 trade가 있었을 때, id가 가장 큰 ClosingPrice를 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithDuplicateTimeTrades_shouldReturnBiggestIdDto() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(600.0, closingPriceDto.closingPrice()); + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java new file mode 100644 index 00000000..7893c2a3 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java @@ -0,0 +1,78 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.base.MariaDBAdapterTest; +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@Import({ + TradeQueryRepository.class +}) +public class TradeQueryRepositoryMariadbTest extends MariaDBAdapterTest { + @Autowired + private TradeQueryRepository tradeQueryRepository; + + @DisplayName("어제 trade가 있었을 경우, 정상적으로 yesterdayClosingPrice를 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithTradeExecutedYesterday_shouldReturnSuccessfully() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(400.0, closingPriceDto.closingPrice()); + } + + @DisplayName("어제 trade가 없었을 경우, null을 조회한다.") + @Test + @Transactional + public void queryYesterdayClosingPriceWithoutYesterdayTrade_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 trade가 없고, 오늘 00시 00분 00초의 trade가 있었을 때, null을 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithTradeExecutedToday_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 같은 시간에 여러건의 trade가 있었을 때, id가 가장 큰 ClosingPrice를 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithDuplicateTimeTrades_shouldReturnBiggestIdDto() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(600.0, closingPriceDto.closingPrice()); + } +} diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql new file mode 100644 index 00000000..0d04bda7 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql @@ -0,0 +1 @@ +DELETE FROM trade; \ No newline at end of file diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql new file mode 100644 index 00000000..086aa653 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql @@ -0,0 +1,9 @@ +DELETE FROM trade; + +INSERT INTO trade + (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) +VALUES + (1, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 300, 30), + (2, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 400, 30), + (4, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 600, 30), + (3, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 500, 30); diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql new file mode 100644 index 00000000..d772a767 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql @@ -0,0 +1,4 @@ +DELETE FROM trade; + +INSERT INTO trade (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES + (1, 'BTC', '2025-07-02 00:00:00.000000',1, 2, 300, 30); \ No newline at end of file diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql new file mode 100644 index 00000000..670d9667 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql @@ -0,0 +1,9 @@ +DELETE FROM trade; + +INSERT INTO trade + (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) +VALUES + (1, 'BTC', '2025-07-01 19:00:00.000000',1, 2, 300, 30), + (2, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 400, 30), + (3, 'BTC', '2025-07-01 17:00:00.000000',1, 2, 500, 30), + (4, 'BTC', '2025-07-01 18:00:00.000000',1, 2, 600, 30); \ No newline at end of file From 47b74c1367c88bf054be8d2badbb360aa6e01103 Mon Sep 17 00:00:00 2001 From: Junh-b Date: Wed, 2 Jul 2025 18:00:56 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20orderbook=EC=9D=B4=20=EC=A0=84?= =?UTF-8?q?=EB=82=A0=EC=A2=85=EA=B0=80=EC=99=80=EC=9D=98=20=EB=B3=80?= =?UTF-8?q?=EB=8F=99=EB=A5=A0=EB=A5=BC=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전날 데이터가 없을 경우, 변동률은 0.0이 됩니다. --- .../application/service/OrderBookService.java | 26 ++++- .../coin/orderbook/dto/OrderBookUnitInfo.java | 24 ++++- .../dto/OrderBookInfoSerializationTest.java | 10 +- .../orderbook/dto/OrderBookUnitInfoTest.java | 94 +++++++++++++++++++ .../OrderBookUpdatedNotifierAdapterTest.java | 4 +- 5 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java diff --git a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java index 51ee275e..acdbe58d 100644 --- a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java +++ b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java @@ -6,11 +6,14 @@ import com.cleanengine.coin.order.domain.spi.ActiveOrders; import com.cleanengine.coin.order.domain.spi.ActiveOrdersManager; import com.cleanengine.coin.orderbook.domain.OrderBookDomainService; +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; import com.cleanengine.coin.orderbook.dto.OrderBookInfo; import com.cleanengine.coin.orderbook.dto.OrderBookUnitInfo; +import com.cleanengine.coin.orderbook.infra.TradeQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -22,6 +25,7 @@ public class OrderBookService implements UpdateOrderBookUsecase, ReadOrderBookUs private final ActiveOrdersManager activeOrdersManager; private final OrderBookDomainService orderBookDomainService; private final OrderBookUpdatedNotifierPort orderBookUpdatedNotifierPort; + private final TradeQueryService tradeQueryService; @Override public void updateOrderBookOnNewOrder(Order order) { @@ -67,15 +71,33 @@ private void updateOrderBookOnTradeExecuted(String ticker, Long orderId, boolean } private OrderBookInfo extractOrderBookInfo(String ticker){ + ClosingPriceDto finalClosingPriceDto = getYesterdayClosingPrice(ticker); + List buyOrderBookUnitInfos = orderBookDomainService.getBuyOrderBookList(ticker, 10) - .stream().map(OrderBookUnitInfo::new).toList(); + .stream() + .map(orderBookUnit -> new OrderBookUnitInfo(orderBookUnit, finalClosingPriceDto.closingPrice())) + .toList(); List sellOrderBookUnitInfos = orderBookDomainService.getSellOrderBookList(ticker, 10) - .stream().map(OrderBookUnitInfo::new).toList(); + .stream() + .map(orderBookUnit -> new OrderBookUnitInfo(orderBookUnit, finalClosingPriceDto.closingPrice())) + .toList(); + return new OrderBookInfo(ticker, buyOrderBookUnitInfos, sellOrderBookUnitInfos); } + private ClosingPriceDto getYesterdayClosingPrice(String ticker){ + LocalDate yesterday = LocalDate.now().minusDays(1); + ClosingPriceDto closingPriceDto = tradeQueryService.getYesterdayClosingPrice(ticker, yesterday); + + if(closingPriceDto == null) { + closingPriceDto = new ClosingPriceDto(ticker, yesterday, 0.0); + } + + return closingPriceDto; + } + private void sendOrderBookUpdated(String ticker){ orderBookUpdatedNotifierPort.sendOrderBooks(extractOrderBookInfo(ticker)); } diff --git a/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java b/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java index 1a893d4c..1a4d870d 100644 --- a/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java +++ b/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java @@ -4,9 +4,27 @@ public record OrderBookUnitInfo( Double price, - Double size + Double size, + Double priceChangePercent ){ - public OrderBookUnitInfo(OrderBookUnit orderBookUnit) { - this(orderBookUnit.getPrice(), orderBookUnit.getSize()); + public OrderBookUnitInfo{ + if(price == null || size == null || priceChangePercent == null){ + throw new IllegalArgumentException("price, size, priceChangePercent cannot be null."); + } + } + + public OrderBookUnitInfo(OrderBookUnit orderBookUnit, Double yesterdayClosingPrice) { + this(orderBookUnit.getPrice(), + orderBookUnit.getSize(), + calculateChangePercent(orderBookUnit.getPrice(), yesterdayClosingPrice)); + } + + private static Double calculateChangePercent(Double price, Double yesterdayClosingPrice) { + if(price == null || yesterdayClosingPrice == null){ + throw new IllegalArgumentException("price, yesterdayClosingPrice cannot be null."); + } + + return (yesterdayClosingPrice <= 0) ? + 0.0 : (price - yesterdayClosingPrice) / yesterdayClosingPrice * 100; } } diff --git a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java index 4ddf061e..78acaefe 100644 --- a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java +++ b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java @@ -21,22 +21,22 @@ static void initStatic(){ @Test void eachOrderBookHasOnePrice_serializeIt_resultEqualsAsExpected() throws JsonProcessingException { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), - List.of(new OrderBookUnitInfo( 2.0, 2.0))); + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), + List.of(new OrderBookUnitInfo( 2.0, 2.0, 0.0))); String json = objectMapper.writeValueAsString(orderBookInfo); System.out.println(json); - assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0}],\"sellOrderBookUnits\":[{\"price\":2.0,\"size\":2.0}]}", json); + assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0,\"priceChangePercent\":0.0}],\"sellOrderBookUnits\":[{\"price\":2.0,\"size\":2.0,\"priceChangePercent\":0.0}]}", json); } @Test void oneOfOrderBookIsEmpty_serializeIt_resultEqualsAsExpected() throws JsonProcessingException { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), List.of()); String json = objectMapper.writeValueAsString(orderBookInfo); System.out.println(json); - assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0}],\"sellOrderBookUnits\":[]}", json); + assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0,\"priceChangePercent\":0.0}],\"sellOrderBookUnits\":[]}", json); } } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java new file mode 100644 index 00000000..a814c755 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java @@ -0,0 +1,94 @@ +package com.cleanengine.coin.orderbook.dto; + +import com.cleanengine.coin.orderbook.domain.BuyOrderBookUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OrderBookUnitInfoTest { + + @Nested + @DisplayName("기본 생성 유효성 테스트") + class CreateOrderBookUnitInfoTest { + @DisplayName("price가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullPrice_throwsIllegalArgumentException() { + Double nullPrice = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(nullPrice, 1.0, 1.0)); + } + + @DisplayName("size가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullSize_throwsIllegalArgumentException() { + Double nullSize = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(1.0, nullSize, 1.0)); + } + + @DisplayName("priceChangePercent가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullPriceChangePercent_throwsIllegalArgumentException() { + Double nullPriceChangePercent = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(1.0, 1.0, nullPriceChangePercent)); + } + } + + @Nested + @DisplayName("priceChangePercent 반영 테스트") + class ChangePercentTest { + @DisplayName("closingPrice가 0이하일 경우 percentChange도 0이다.") + @Test + public void createOrderBookUnitInfoWithZeroClosingPrice_percentChangeIsZero() { + Double closingPrice = 0.0; + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(1.0, 1.0, closingPrice); + + assertEquals(0.0, orderBookUnitInfo.priceChangePercent()); + } + + @DisplayName("closingPrice가 null이라면 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullClosingPrice_throwsIllegalArgumentException() { + Double nullClosingPrice = null; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(1.0, 1.0); + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(buyOrderBookUnit, nullClosingPrice)); + } + + @DisplayName("price가 null이라면 IllegalArgumentException을 반환다.") + @Test + public void createOrderBookUnitInfoWithNullPrice_throwsIllegalArgumentException() { + Double nullPrice = null; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(nullPrice, 1.0); + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(buyOrderBookUnit, 1.0)); + } + + @DisplayName("closingPrice가 비교대상 price의 절반이라면, percentChange는 +100.0이다.") + @Test + public void createOrderBookUnitInfoWithHalfClosingPrice_percentChangeIs100() { + Double closingPrice = 1.0; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(closingPrice * 2.0, 1.0); + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(buyOrderBookUnit, closingPrice); + + assertEquals(100.0, orderBookUnitInfo.priceChangePercent()); + } + + @DisplayName("closingPrice가 비교대상 price의 두배라면, percentChange는 -50.0이다.") + @Test + public void createOrderBookUnitInfoWithDoubleClosingPrice_percentChangeIsMinus50() { + Double closingPrice = 1.0; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(closingPrice / 2.0, 1.0); + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(buyOrderBookUnit, closingPrice); + + assertEquals(-50.0, orderBookUnitInfo.priceChangePercent()); + } + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java b/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java index 8059137a..c8f0a468 100644 --- a/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java @@ -22,8 +22,8 @@ public class OrderBookUpdatedNotifierAdapterTest extends WebSocketTest { @Test public void getOrderBooks() throws Exception { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), - List.of(new OrderBookUnitInfo( 2.0, 2.0))); + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), + List.of(new OrderBookUnitInfo( 2.0, 2.0, 0.0))); session.subscribe("/topic/orderbook/BTC", new GenericStompFrameHandler<>(OrderBookInfo.class, responseQueue)); From 2eadf074d7ed450b3ea0c27fc716be385a2d2d23 Mon Sep 17 00:00:00 2001 From: Junh-b Date: Wed, 2 Jul 2025 18:01:34 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=B6=80=ED=95=98=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9A=A9=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전체 test workflow에 포함되던 현상 제거 --- src/test/java/com/cleanengine/coin/tool/TokenGenerator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java index a89b282e..1c6d650a 100644 --- a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java +++ b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java @@ -2,6 +2,7 @@ import com.cleanengine.coin.user.login.application.JWTUtil; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +@Disabled @SpringBootTest @Profile("dev, it") public class TokenGenerator {