Skip to content

Commit

Permalink
refactor: 경매 생성 시 설정 가능한 경매 시간 및 적용할 수 있는 할인 주기를 다양하게 설정할 수 있게 검증을 변경…
Browse files Browse the repository at this point in the history
…합니다. (#312)

* test: 경매 입찰에 대한 동시성 테스트 추가

- 한정된 재고에서 구매할 수 있는 최대 수량까지만 구매 가능하고, 나머지는 구매하지 못하는 테스트 추가
- 포인트가 부족한 사람과 포인트가 넉넉한 사람이 구매할때, 순서가 섞여있다면, 포인트가 넉넉한 사람의 구매처리가 원자적으로 동작하는지 테스트 추가

* refactor: 기존 1, 5, 10분 경매 시간에서 경매 검증 방식에서 경매시간을 할인 주기로 나누었을때 나누어 떨어질때만 생성할 수 있게 검증을 변경한다.

- 분, 초 상관없이 나누어 떨어지기만 하면 됨

* refactor: 경매 주기 시간에 대한 기준을 분 단위이고, 1시간을 넘지 않도록 검증하게 변경한다.

- 기존에는 10분단위로 최대 60분이었음
- 변경한 부분은 분 단위면 가능하고, 1시간만 넘지 않도록 변경함
- 이에 경매 주기 시간을 더해 할인 주기 시간으로 나누었을때 나누어떨어지는 시간이어야 함

* chore: 테스트 스크립트 실패 확인을 위한 --info 옵션을 추가합니다

* feat: 임베디드 레디스 시작, 종료 시 로깅 추가

* feat: 테스트 마다 레디스 초기화 하도록 변경

* fix(test): 시간 정밀도 차이를 마이크로초까지 제한한 공통 LocalDateTime 필드를 사용하도록 테스트를 수정합니다.

* feat: Entity에서 Enum값 `@Enumerated` 매핑 추가

* fix: 거래내역 ID 변경으로 인한 컴파일 에러 수정

- #310이 반영되면서 컴파일이 깨지는 오류를 수정합니다.

---------

Co-authored-by: 유동근 <[email protected]>
Co-authored-by: yudonggeun <[email protected]>
  • Loading branch information
3 people authored Aug 26, 2024
1 parent 6367dcb commit f33753c
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 93 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ jobs:
cache: gradle

- name: Run tests
run: ./gradlew clean test
run: ./gradlew clean test --info

Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
public class Auction {

private static final int MINIMUM_STOCK_COUNT = 1;
private static final int DURATION_ONE_MINUTE = 1;
private static final int DURATION_FIVE_MINUTE = 5;
private static final int DURATION_TEN_MINUTE = 10;
private static final long NANOS_IN_MINUTE = 60_000_000_000L; // 1분의 나노초
private static final long MAX_AUCTION_DURATION_NANOS = 60 * NANOS_IN_MINUTE; // 60분의 나노초

private Long id;
private final Long sellerId;
Expand Down Expand Up @@ -65,24 +64,26 @@ public Auction(
}

private void validateAuctionTime(LocalDateTime startedAt, LocalDateTime finishedAt) {
Duration diff = Duration.between(startedAt, finishedAt);
long diffNanos = diff.toMillis();
long tenMinutesInNanos = 10L * 60 * 1_000; // 10분을 나노초로 변환
Duration duration = Duration.between(startedAt, finishedAt);
long durationNanos = duration.toNanos();

if (!(diffNanos % tenMinutesInNanos == 0 && diffNanos / tenMinutesInNanos <= 6)) {
String message = String.format("경매 지속 시간은 10분 단위여야하고, 최대 60분까지만 가능합니다. 현재: %.9f분",
diffNanos / (60.0 * 1_000));
if (durationNanos > MAX_AUCTION_DURATION_NANOS) {
long leftNanoSeconds = durationNanos % NANOS_IN_MINUTE;
String message = String.format("경매 지속 시간은 최대 60분까지만 가능합니다. 현재 초과되는 나노초: %d초", leftNanoSeconds);
throw new BadRequestException(message, ErrorCode.A007);
}

if (durationNanos % NANOS_IN_MINUTE != 0) {
long leftNanoSeconds = durationNanos % NANOS_IN_MINUTE;
String message = String.format("경매 지속 시간은 정확히 분 단위여야 합니다. 현재 남는 나노초: %d초", leftNanoSeconds);
throw new BadRequestException(message, ErrorCode.A030);
}
}

private void validateVariationDuration(Duration variationDuration, Duration auctionDuration) {
if (!isAllowedDuration(variationDuration)) {
String message = String.format("경매 할인 주기 시간은 %d, %d, %d만 선택할 수 있습니다.",
DURATION_ONE_MINUTE,
DURATION_FIVE_MINUTE,
DURATION_TEN_MINUTE
);
if (!isAllowedDuration(variationDuration, auctionDuration)) {
String message = String.format("경매 할인 주기는 경매 지속 시간에서 나누었을때 나누어 떨어져야 합니다. 할인 주기 시간(초): %d, 경매 주기 시간(초): %d",
variationDuration.getSeconds(), auctionDuration.getSeconds());
throw new BadRequestException(message, ErrorCode.A028);
}

Expand All @@ -92,10 +93,14 @@ private void validateVariationDuration(Duration variationDuration, Duration auct
}
}

private boolean isAllowedDuration(Duration duration) {
return duration.equals(Duration.ofMinutes(DURATION_ONE_MINUTE))
|| duration.equals(Duration.ofMinutes(DURATION_FIVE_MINUTE))
|| duration.equals(Duration.ofMinutes(DURATION_TEN_MINUTE));
private boolean isAllowedDuration(Duration duration, Duration auctionDuration) {
if (duration.isZero() || auctionDuration.isZero()) {
return false;
}
long durationSeconds = duration.getSeconds();
long auctionDurationSeconds = auctionDuration.getSeconds();

return auctionDurationSeconds % durationSeconds == 0;
}

private void validateMinimumPrice(LocalDateTime startedAt, LocalDateTime finishedAt, Duration variationDuration,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.wootecam.luckyvickyauction.core.member.entity;

import com.wootecam.luckyvickyauction.core.member.domain.Role;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -20,11 +23,12 @@ public class MemberEntity {
private Long id;
private String signInId;
private String password;
private String role;
@Enumerated(value = EnumType.STRING)
private Role role;
private Long point;

@Builder
private MemberEntity(Long id, String signInId, String password, String role, Long point) {
private MemberEntity(Long id, String signInId, String password, Role role, Long point) {
this.id = id;
this.signInId = signInId;
this.password = password;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptStatus;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -24,6 +26,7 @@ public class ReceiptEntity {
private String productName;
private long price;
private long quantity;
@Enumerated(value = EnumType.STRING)
private ReceiptStatus receiptStatus;
private long auctionId;
private Long sellerId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public enum ErrorCode {
A004("가격 변동폭은 0과 같거나 작을 수 없습니다."),
A005("변동 시간 단위는 0과 같거나 작을 수 없습니다."),
A006("경매의 시작 시간은 종료 시간보다 클 수 없습니다."),
A007("경매 생성 시, 경매 지속시간은 10, 20, 30, 40, 50, 60분이 아닌 경우 예외가 발생합니다."),
A007("경매 생성 시, 경매 지속시간이 60분을 넘기는 경우 예외가 발생합니다."),
A008("경매 가격 변동폭은 경매 가격보다 낮아야 합니다."),
A009("경매 생성 시, 할인율이 0퍼센트 미만이거나 50 퍼센트를 초과한 경우 예외가 발생합니다."),
A010("경매ID를 기준으로 경매를 찾으려고 했지만 찾을 수 없습니다."),
Expand All @@ -30,8 +30,9 @@ public enum ErrorCode {
A025("JSON을 가격 정책 객체로 변환 시, 변환이 불가능할 경우 예외가 발생합니다."),
A026("경매 입찰 요청 시, 요청 가격이 0보다 작으면 예외가 발생합니다."),
A027("경매 입찰 요청 시, 요청 수량이 1 미만일 경우 예외가 발생합니다."),
A028("경매 생성 시, 경매 할인 주기 시간이 1, 5, 10분이 아닌 경우 예외가 발생합니다."),
A028("경매 생성 시, 경매 지속 시간에서 경매 할인 주기 시간을 나누었을때, 나누어 떨어지지 않는 경우 예외가 발생합니다."),
A029("경매 생성 시, 경매 할인 주기 시간이 경매 지속 시간보다 크거나 같은 경우 예외가 발생합니다."),
A030("경매 생성 시, 경매 지속 시간이 정확히 분 단위가 아닌 경우 예외가 발생합니다."),

// Receipt 관련 예외 코드
R000("거래 내역 조회 시, 거래 내역을 찾을 수 없을 경우 예외가 발생합니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.wootecam.luckyvickyauction.core.auction.entity.AuctionEntity;
import com.wootecam.luckyvickyauction.core.member.domain.Member;
import com.wootecam.luckyvickyauction.core.member.domain.Point;
import com.wootecam.luckyvickyauction.core.member.domain.Role;
import com.wootecam.luckyvickyauction.core.member.entity.MemberEntity;
import com.wootecam.luckyvickyauction.core.payment.domain.Receipt;
import com.wootecam.luckyvickyauction.core.payment.dto.BuyerReceiptSimpleInfo;
Expand Down Expand Up @@ -198,7 +197,7 @@ public static Member convertToMember(MemberEntity entity) {
.id(entity.getId())
.signInId(entity.getSignInId())
.password(entity.getPassword())
.role(Role.find(entity.getRole()))
.role(entity.getRole())
.point(new Point(entity.getPoint()))
.build();
}
Expand All @@ -208,7 +207,7 @@ public static MemberEntity convertToMemberEntity(Member member) {
.id(member.getId())
.signInId(member.getSignInId())
.password(member.getPassword())
.role(member.getRole().name())
.role(member.getRole())
.point(member.getPoint().getAmount())
.build();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ spring:
database-platform: org.hibernate.dialect.MySQLDialect
open-in-view: off
hibernate:
ddl-auto: none
ddl-auto: create

data:
redis:
Expand Down
46 changes: 25 additions & 21 deletions src/main/resources/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -835,14 +835,14 @@ <h4 id="_경매_목록_조회_http_response"><a class="link" href="#_경매_목
"id" : 1,
"title" : "쓸만한 경매품 1",
"price" : 2000,
"startedAt" : "2024-08-26T23:46:45.093313",
"finishedAt" : "2024-08-27T00:16:45.093319"
"startedAt" : "2024-08-26T21:18:34.51341",
"finishedAt" : "2024-08-26T21:48:34.513414"
}, {
"id" : 2,
"title" : "쓸만한 경매품 2",
"price" : 4000,
"startedAt" : "2024-08-26T23:46:45.09334",
"finishedAt" : "2024-08-27T00:16:45.093343"
"startedAt" : "2024-08-26T21:18:34.513429",
"finishedAt" : "2024-08-26T21:48:34.513431"
} ]</code></pre>
</div>
</div>
Expand Down Expand Up @@ -1336,8 +1336,8 @@ <h4 id="_경매_등록고정_할인_정책_http_request"><a class="link" href="#
"variationWidth" : 100
},
"variationDuration" : "PT10M",
"startedAt" : "2024-08-27T00:46:45.428466",
"finishedAt" : "2024-08-27T01:46:45.428466",
"startedAt" : "2024-08-26T22:18:34.772453",
"finishedAt" : "2024-08-26T23:18:34.772453",
"isShowStock" : true
}</code></pre>
</div>
Expand Down Expand Up @@ -1505,8 +1505,8 @@ <h4 id="_경매_등록퍼센트_할인_정책_http_request"><a class="link" href
"discountRate" : 10.0
},
"variationDuration" : "PT10M",
"startedAt" : "2024-08-27T00:46:45.403574",
"finishedAt" : "2024-08-27T01:46:45.403574",
"startedAt" : "2024-08-26T22:18:34.753005",
"finishedAt" : "2024-08-26T23:18:34.753005",
"isShowStock" : true
}</code></pre>
</div>
Expand Down Expand Up @@ -1741,7 +1741,7 @@ <h4 id="_경매_목록_조회_2_http_response"><a class="link" href="#_경매_
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 501
Content-Length: 500

[ {
"id" : 1,
Expand All @@ -1750,17 +1750,17 @@ <h4 id="_경매_목록_조회_2_http_response"><a class="link" href="#_경매_
"currentPrice" : 1500,
"totalStock" : 100,
"currentStock" : 30,
"startedAt" : "2024-08-26T23:46:45.35705",
"finishedAt" : "2024-08-27T00:16:45.357055"
"startedAt" : "2024-08-26T21:18:34.716375",
"finishedAt" : "2024-08-26T21:48:34.716379"
}, {
"id" : 2,
"title" : "내가 판매하는 경매품 2",
"originPrice" : 4000,
"currentPrice" : 3500,
"totalStock" : 200,
"currentStock" : 60,
"startedAt" : "2024-08-26T23:46:45.357074",
"finishedAt" : "2024-08-27T00:16:45.357076"
"startedAt" : "2024-08-26T21:18:34.716398",
"finishedAt" : "2024-08-26T21:48:34.7164"
} ]</code></pre>
</div>
</div>
Expand Down Expand Up @@ -1928,8 +1928,8 @@ <h4 id="_경매_상세_조회고정_할인_정책_조회_2_http_response"><a cla
"variationWidth" : 10
},
"variationDuration" : "PT10M",
"startedAt" : "2024-08-26T23:46:45.309182",
"finishedAt" : "2024-08-27T00:46:45.309188",
"startedAt" : "2024-08-26T21:18:34.677127",
"finishedAt" : "2024-08-26T22:18:34.677132",
"isShowStock" : true
}</code></pre>
</div>
Expand Down Expand Up @@ -2130,8 +2130,8 @@ <h4 id="_경매_상세_조회퍼센트_할인_정책_조회_2_http_response"><a
"discountRate" : 10.0
},
"variationDuration" : "PT10M",
"startedAt" : "2024-08-26T23:46:45.332454",
"finishedAt" : "2024-08-27T00:46:45.33246",
"startedAt" : "2024-08-26T21:18:34.696537",
"finishedAt" : "2024-08-26T22:18:34.696543",
"isShowStock" : true
}</code></pre>
</div>
Expand Down Expand Up @@ -2876,8 +2876,8 @@ <h4 id="_거래_상세_조회판매자_구매자_권한_필요_http_response"><a
"auctionId" : 1,
"sellerId" : 1,
"buyerId" : 2,
"createdAt" : "2024-08-26T23:46:45.230124",
"updatedAt" : "2024-08-27T00:46:45.23013"
"createdAt" : "2024-08-26T21:18:34.618016",
"updatedAt" : "2024-08-26T22:18:34.618021"
}</code></pre>
</div>
</div>
Expand Down Expand Up @@ -3068,7 +3068,7 @@ <h3 id="error-code_error_code"><a class="link" href="#error-code_error_code">Err
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>A007</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 지속시간은 10, 20, 30, 40, 50, 60분이 아닌 경우 예외가 발생합니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 지속시간이 60분을 넘기는 경우 예외가 발생합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>A008</code></p></td>
Expand Down Expand Up @@ -3152,13 +3152,17 @@ <h3 id="error-code_error_code"><a class="link" href="#error-code_error_code">Err
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>A028</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 할인 주기 시간이 1, 5, 10분이 아닌 경우 예외가 발생합니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 지속 시간에서 경매 할인 주기 시간을 나누었을때, 나누어 떨어지지 않는 경우 예외가 발생합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>A029</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 할인 주기 시간이 경매 지속 시간보다 크거나 같은 경우 예외가 발생합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>A030</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">경매 생성 시, 경매 지속 시간이 정확히 분 단위가 아닌 경우 예외가 발생합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>R000</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">거래 내역 조회 시, 거래 내역을 찾을 수 없을 경우 예외가 발생합니다.</p></td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import redis.embedded.RedisServer;

@Configuration
public class EmbeddedRedisConfig {

private static final Logger log = LoggerFactory.getLogger(EmbeddedRedisConfig.class);

@Value("${spring.data.redis.port}")
private int redisPort;

Expand All @@ -26,12 +30,14 @@ public void redisServer() throws IOException {
.build();

redisServer.start();
log.info("Embedded Redis Server start!!!");
}

@PreDestroy
public void stopRedis() throws IOException {
if (redisServer != null) {
redisServer.stop();
log.info("Embedded Redis Server stop!!!");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public abstract class ServiceTest {
Expand Down Expand Up @@ -47,8 +48,12 @@ public abstract class ServiceTest {

protected LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS);

@Autowired
protected RedisTemplate<String, Long> redisTemplate;

@AfterEach
void tearDown() {
databaseCleaner.clear();
redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll();
}
}
Loading

0 comments on commit f33753c

Please sign in to comment.