Back to blog
Backend2 min read2023년 7월 13일

[스프링 부트] '주문번호' 서비스 구현하기 (Jpa사용)

구독·구매·판매마다 다른 주문번호를 쓰지 않도록, 포맷을 통일한 주문번호 발급 서비스를 JPA로 구현했습니다.

https://velog.io/@dochis/CS처리에-효율적인-주문번호-만들기-주문번호-알고리즘

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);

    }

}

다음과 같이 테스트를 진행해볼 수 있다.

짜잔

이상.

Topics

spring-bootjpa