https://velog.io/@dochis/CS처리에-효율적인-주문번호-만들기-주문번호-알고리즘
해당 링크를 참고하였습니다. 좋은 정보 감사합니다.
쇼핑몰 서비스를 제작중 고객 문의 관리를 위해 통합 주문 번호 시스템이 필요해졌다.
통합 주문 번호 서비스
- 현재 구독, 구매, 판매 서비스가 있다
- 구독, 구매, 판매 요청 기록별로 각자 다른 양식의 주문 번호를 사용하는건 번거로울 것이다.
- 따라서 주문번호를 생성하는 서비스를 따로 만들어 줄 것이다.
포맷
- 포맷을 선정하는 것은 본인의 자유이다.
- 나는 'TYYMMDDXXXXX' 형태의 포맷을 채택했다.
/*
* 주문 번호 생성 로직
* 포맷은 TYYMMDDXXXXXX 이다.
* T : Type (S : Subscribe, O : 구매, R : 판매)
* YY : 년도
* MM : 월
* DD : 일
* XXXXX : 5자리 영문자 + 숫자
*/
OrderNumber
주문번호 모델이다.
package ?.?.domain.ordernumber.domain;
import lombok.*;
import ?.?.domain.model.BaseTimeEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Getter
@Entity @Builder @NoArgsConstructor @AllArgsConstructor
public class OrderNumber extends BaseTimeEntity {
@Id @GeneratedValue
private Long id;
@Column(unique = true)
private String orderNumber;
}
아주 간단하게, id와 orderNumber만을 담고있다.
BaseTimeEntity를 사용한다. 이는 createdDate와 lastModifiedDate를 가지고 있다.
OrderType (ENUM)
주문 타입을 결정하는 Enum 이다.
package ?.?.domain.ordernumber.domain;
public enum OrderType {
SUBSCRIBE, ORDER, SELL_ORDER // S : 구독, O : 구매 주문, R : 판매 주문
}
주문을 생성할 때 (서비스에서 generateOrderNumber 메서드를 호출할 때) 매개변수로 Enum을 입력받는데, 그 때 쓰일 Enum 타입이다.
OrderNumberRepository
package ?.?.domain.ordernumber.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import ?.?.domain.ordernumber.domain.OrderNumber;
public interface OrderNumberReository extends JpaRepository<OrderNumber, Long> {
boolean existsByOrderNumber(String orderNumber);
}
JpaRepository를 사용한다. 중복 체크만 하면 되므로, existsByOrderNumber만 두면 되겠다.
OrderNumberService
package ?.?.domain.ordernumber.application;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ?.?.domain.ordernumber.domain.OrderType;
import ?.?.domain.ordernumber.dao.OrderNumberReository;
import ?.?.domain.ordernumber.domain.OrderNumber;
import ?.?.global.error.exception.CustomException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import static ?.?.global.error.exception.ErrorCode.INVALID_ORDER_TYPE;
@Service
@RequiredArgsConstructor
public class OrderNumberService {
private final OrderNumberReository orderNumberReository;
/*
* 햇갈릴 수 있는 문자 제외 : 숫자 2, 5 영문자 E, I, L, O 제외 -> 총 30개
* 왜 제외했나요 :
* 2 : E와 발음이 동일
* 5 : O와 발음이 동일
* E : 2와 발음이 동일
* I : L과 모양이 비슷
* L : I와 모양이 비슷
* O : 5와 발음이 동일
*/
private static final String CHARACTERS = "01346789ABCDFGHJKMNPQRSTUVWXYZ";
private static final int LENGTH = 5;
public String generateOrderNumber(OrderType orderType) {
/*
* 주문 번호 생성 로직
* 포맷은 TYYMMDDXXXXXX 이다.
* T : Type (S : Subscribe, O : Order, R : SellOrder)
* YY : 년도
* MM : 월
* DD : 일
* XXXXX : 5자리 영문자 + 숫자
*/
// 코드 타입 변환
String orderTypeCode = "";
switch (orderType) {
case SUBSCRIBE:
orderTypeCode = "S";
break;
case ORDER:
orderTypeCode = "O";
break;
case SELL_ORDER:
orderTypeCode = "R";
break;
default:
throw new CustomException("오더 타입이 올바르지 않습니다.", INVALID_ORDER_TYPE);
}
// 년도와 날짜 정보를 담는다: yyMMdd 형식
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
String formattedDate = currentDate.format(formatter);
/*
* 5자리 정수 생성 후, 해당 주문번호가 이미 존재하는지 확인을 반복한다.
* 매일, 타입마다 약 30^5 개의 주문번호를 생성할 수 있다. ( 정확히는 30^5 - 30^4 = 23,490,000개 )
* 현재 추정되는 하루 주문건수가 매우 낮으므로 서버에 치명적 문제가 발생할 확률이 매우 매우 매우 낮다.
* (하루에 2천만개 거래되면 난 이미 부자일 것이므로 서버고 뭐고 ...헉)
*/
String randomStr = "";
Random random = new Random();
do {
// 숫자, 알파벳 대소문자로 이루어진 5자리 랜덤 문자열 생성
StringBuilder sb = new StringBuilder(LENGTH);
for (int i = 0; i < LENGTH; i++) {
int randomIndex = random.nextInt(CHARACTERS.length());
char randomChar = CHARACTERS.charAt(randomIndex);
sb.append(randomChar);
randomStr = sb.toString();
}
} while (orderNumberReository.existsByOrderNumber(orderTypeCode + formattedDate + randomStr));
OrderNumber orderNumber = OrderNumber.builder()
.orderNumber(orderTypeCode + formattedDate + randomStr).build();
// 주문번호 저장
orderNumberReository.save(orderNumber);
return orderNumber.getOrderNumber();
}
}
대망의 서비스이다. 내용이 길다고 생각할 수 있는데, 별 거 없으니 잘 따라오기 바란다.
( 이 밑에 나오는 코드 snippet들은 모두 위의 OrderNumberService에 있는거 잘라온거니까 추가할 필요 없다. )
private static final String CHARACTERS = "01346789ABCDFGHJKMNPQRSTUVWXYZ";
CHARACTERS는 주문번호 포맷에서 'XXXXX' 부분에 들어갈 문자+숫자 조합들을 나타낸다.
소문자를 넣고 싶다면 여기에 추가만 해주면 된다.
나는 숫자 2, 5와 영문자 E, I, L, O를 제외시켰다.
서비스 특성상 전화 문의가 올 수 있는데, 이 때 말로 주문번호를 전달해야 하는 상황이 생길 수 있다.
- 2 : E와 발음이
- 5 : O와 발음이 동일
- E : 2와 발음이 동일
- I : L과 모양이 비슷
- L : I와 모양이 비슷
- O : 5와 발음이 동일
다음과 같은 이유로.. 쟤네들은 뺐다. 기호에 따라 넣던 말던 본인 자유다.
private static final int LENGTH = 5;
'XXXXX' 부분의 길이에 해당한다. 예를 들어 여섯자리로 하고싶다면 6으로 바꾸면 되겠다.
switch (orderType) {
case SUBSCRIBE:
orderTypeCode = "S";
break;
case ORDER:
orderTypeCode = "O";
break;
case SELL_ORDER:
orderTypeCode = "R";
break;
default:
throw new CustomException("오더 타입이 올바르지 않습니다.", INVALID_ORDER_TYPE);
}
매개변수로 OrderType orderType을 받고 이에 따라 문자로 변환한다.
default 부분 CustomException은 제거하셔도 무방하다.
// 년도와 날짜 정보를 담는다: yyMMdd 형식
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
String formattedDate = currentDate.format(formatter);
'YYMMDD' 부분을 생성한다. DateTimeFormatter로 하면 된다. 굿
String randomStr = "";
/*
* 5자리 정수 생성 후, 해당 주문번호가 이미 존재하는지 확인을 반복한다.
* 매일, 타입마다 약 30^5 개의 주문번호를 생성할 수 있다. ( 정확히는 30^5 - 30^4 = 23,490,000개 )
* 현재 추정되는 하루 주문건수가 매우 낮으므로 서버에 치명적 문제가 발생할 확률이 매우 매우 매우 낮다.
* (하루에 2천만개 거래되면 난 이미 부자일 것이므로 서버고 뭐고 ...헉)
*/
Random random = new Random();
do {
// 숫자, 알파벳 대소문자로 이루어진 5자리 랜덤 문자열 생성
StringBuilder sb = new StringBuilder(LENGTH);
for (int i = 0; i < LENGTH; i++) {
int randomIndex = random.nextInt(CHARACTERS.length());
char randomChar = CHARACTERS.charAt(randomIndex);
sb.append(randomChar);
randomStr = sb.toString();
}
} while (orderNumberReository.existsByOrderNumber(orderTypeCode + formattedDate + randomStr));
위에 정의해두었던 CHARACTERS 에서 문자를 LENGTH 길이만큼 randomStr에 저장한다.
그 후, orderNumberRepository에 해당 번호가 이미 존재하는지 check한다.
중복이 없을때까지 반복한다.
중복에 관하여 :
5자리 정수 생성 후, 해당 주문번호가 이미 존재하는지 확인을 반복한다.
매일, 타입마다 약 30^5 개의 주문번호를 생성할 수 있다. ( 정확히는 30^5 - 30^4 = 23,490,000개 )
현재 추정되는 하루 주문건수가 매우 낮으므로 서버에 치명적 문제가 발생할 확률이 매우 매우 매우 낮다.
OrderNumber orderNumber = OrderNumber.builder()
.orderNumber(orderTypeCode + formattedDate + randomStr).build();
// 주문번호 저장
orderNumberReository.save(orderNumber);
return orderNumber.getOrderNumber();
}
그 후 OrderNumber를 빌드하고 Repository에 저장한다.
구현 끝! 수고했다.
하지만 아쉬우니 테스트도 해볼까?
OrderNumberServiceTest
package ?.?;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import ?.?.domain.ordernumber.application.OrderNumberService;
import ?.?.domain.ordernumber.domain.OrderType;
@SpringBootTest
public class OrderNumberServiceTest {
@Autowired
OrderNumberService orderNumberService;
/**
* 주문번호 생성 테스트
* 테스트 주의사항 : 로컬 DB 사용할것
*/
@DisplayName("주문번호 생성 테스트")
@Test
void createOrderNumberTest() {
String orderNumber1 = orderNumberService.generateOrderNumber(OrderType.SUBSCRIBE);
String orderNumber2 = orderNumberService.generateOrderNumber(OrderType.SUBSCRIBE);
String orderNumber3 = orderNumberService.generateOrderNumber(OrderType.SUBSCRIBE);
String orderNumber4 = orderNumberService.generateOrderNumber(OrderType.ORDER);
String orderNumber5 = orderNumberService.generateOrderNumber(OrderType.SELL_ORDER);
System.out.println("orderNumber1 = " + orderNumber1);
System.out.println("orderNumber2 = " + orderNumber2);
System.out.println("orderNumber3 = " + orderNumber3);
System.out.println("orderNumber4 = " + orderNumber4);
System.out.println("orderNumber5 = " + orderNumber5);
}
}
다음과 같이 테스트를 진행해볼 수 있다.
짜잔
이상.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 로컬에서 application.yaml 에 .env 환경변수 주입하기 (1) | 2023.10.11 |
---|---|
[스프링부트] 배포환경에서 에러로그 Slack에 보내기 (0) | 2023.07.20 |
[스프링부트] 스마트 택배 API ( sweet-tracker ) 기능 구현하기 (0) | 2023.07.16 |
[스프링 부트] 효율적 주문 관리 시스템 (0) | 2023.07.14 |