해당 게시물은 이론 관련 게시글입니다
백엔드 개발자 여러분들은 주문 관리 로직, 어떻게 구현하시나요?
저는 이번에 주문 관리 시스템을 구현하면서 고민을 좀 했습니다.
CRUD를 만만하게 보지 맙시다.
나름의 고찰을 했습니다.
1. 주문 상태 수정
- 1. 주문 요청 시 주문 ROW를 하나 만들고, 주문 상태(신청 접수됨, 배달됨, 구매 확정됨 등) 업데이트마다 ROW를 수정한다.
해당 방식은 위험합니다. 기존에 존재하는 ROW를 수정하면 과거 이력 조회가 불가능해집니다.
- 2. 주문 상태 업데이트마다 동일 정보를 가진 ROW를 삽입한다.
해당 방식은 안전합니다. 하지만 주문 상태 업데이트마다 모든 주문 정보를 복사해야 하기 때문에 비효율적입니다.
- 3. 주문 상태를 다른 테이블로 빼고, 주문과 ManyToOne 연관관계를 설정한다.
해당 방식은 안전합니다. 주문 상태 업데이트 시 상태 + 외래키 크기의 DB만 소모하기 때문에 효율적입니다.
따라서 3번을 채택했습니다.
2. 주문 현황 조회
주문 현황 조회를 위해선 주문 상태가 가장 최신인 것을 가져와야 합니다.
해당 기능 구현을 위해 쿼리DSL을 사용했습니다.
SellOrderStateRepositoryImpl (snippet)
@Override
public SellOrderState findLastStateBySellOrderId(Long sellOrderId) {
// 판매 주문 아이디가 매치하고, 마지막으로 수정된(가장 최신의) 데이터를 반환
return jpaQueryFactory
.selectFrom(sellOrderState)
.where(sellOrderState.sellOrder.id.eq(sellOrderId))
.orderBy(sellOrderState.lastModifiedDate.desc())
.limit(1)
.fetchOne();
}
다음은 쿼리DSL의 code snippet입니다.
.orderBy를 통해 가장 최신의 state를 가져와 반환합니다.
그래서 어쩌라는거지?
핵심은 애플리케이션이 필터링을 수행하지 않는 것입니다.
SellOrderService (snippet)
/**
* <h1>판매 조회</h1>
* @param state (requested | canceled | delivered | published)
* @param token (accessToken)
* @return List<SellOrderResponse> (name, phoneNumber, bankName, accountNumber, bagQuantity, productQuantity, address, requestDetail, returnDate, sellState)
* @exception CustomException (PATH_NOT_RESOLVED)
* @author seochanhyeok
*/
public List<SellOrderResponse> getSellOrders(String state, String token) {
Member member = jwtProvider.getMemberByRawToken(token);
// String -> Enum SellState로 변환
SellState reqState;
switch (state) {
case "requested":
reqState = SellState.REQUESTED;
break;
case "canceled":
reqState = SellState.CANCELLED;
break;
case "delivered":
reqState = SellState.DELIVERED;
break;
case "published":
reqState = SellState.PUBLISHED;
break;
default:
throw new CustomException(PATH_NOT_RESOLVED);
}
List<SellOrderResponse> sellOrderResponses = new ArrayList<>();
// 멤버의 ID와 요청한 State에 해당하는 데이터 가져옴
sellOrderRepository.getSellOrdersByIdAndState(member.getId(), reqState).forEach(sellOrder -> {
// 그 중 최신의 데이터만 Response에 담음
if (sellOrderStateRepository.isLastBySellOrderId(sellOrder.getId(), reqState)) {
sellOrderResponses.add(
SellOrderResponse.builder()
.id(sellOrder.getId())
.orderNumber(sellOrder.getOrderNumber())
.name(sellOrder.getName())
.phoneNumber(sellOrder.getPhoneNumber())
.bank(sellOrder.getBank())
.bagQuantity(sellOrder.getBagQuantity())
.productQuantity(sellOrder.getProductQuantity())
.address(sellOrder.getAddress())
.requestDetail(sellOrder.getRequestDetail())
.returnDate(sellOrder.getReturnDate())
.sellState(reqState)
.build()
);
}
});
return sellOrderResponses;
}
판매 주문 조회 로직입니다.
서비스 로직 내부에서 .stream()이나 .filter()와 같은 기능은 사용하지 않았습니다.
여러분의 서비스를 이용하는 사람이 많아질수록, 데이터의 양도 많아질 것입니다.
리소스 낭비를 막기 위해선 최대한 SQL문으로 최소한의 정보를 fetch해야 하는데요,
개발 공부하실 때 이렇게 필요한 정보만 가져올 수 있도록 하는 것을 권장합니다.
또한, 페이지네이션, 만료기간을 적절이 선정하여 유저 편의성, 개발 속도와 서버 효율성의 tradeoff를 잘 맞추어야 합니다.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 로컬에서 application.yaml 에 .env 환경변수 주입하기 (1) | 2023.10.11 |
---|---|
[스프링부트] 배포환경에서 에러로그 Slack에 보내기 (0) | 2023.07.20 |
[스프링부트] 스마트 택배 API ( sweet-tracker ) 기능 구현하기 (0) | 2023.07.16 |
[스프링 부트] '주문번호' 서비스 구현하기 (Jpa사용) (1) | 2023.07.13 |