본문 바로가기

이커머스 프로젝트

상품의 수정 및 삭제에 대해 정책적으로 다루어보자!

ERD 구조


상품 수정

위 ERD를 보면 구매자의 주문 정보(Orders)가 상품(Item)이 아닌 Order_Item과 관계를 맺고 있는 것을 확인할 수 있습니다.

저는 처음에 Orders와 Item을 1:N 관계로 설계를 진행하였습니다. 그런데 이러한 설계는 어떠한 문제점을 가져올 수 있을지 같이 알아보겠습니다 :)

 

판매자가 주문 상품의 가격을 수정하게 되면 어떤 일이 발생할까요???

만약 Orders가 Item과 직접적인 관계를 맺고있다면, 주문했던 상품의 가격이 변경되는 순간, 주문 내역에서의 상품 가격도 변동이 생기게 됩니다.

 

이는 구매자들에게 혼란을 줄 수 있고, 기록의 일관성 또한 해칠 수 있습니다!

 

또한 주문 내역에는 각 상품에 대한 주문 수량이 정확히 기록되어야 합니다. 같은 상품을 여러 개 구매하는 경우도 흔하기 때문이죠. 또한 주문 취소 시, 해당 상품의 재고를 주문 수량만큼 다시 증가시켜야 하는 등의 로직 처리가 필요합니다.

 

이러한 문제점들을 고려하여, 구매 당시의 상품 정보를 스냅샷 형식으로 별도의 테이블에 저장하는 구조를 선택하게 되었습니다. Order_Item(주문 상품) 테이블은 주문 시점의 상품 정보들을 정확히 기록하고 관리할 수 있게 해 줍니다.


상품 삭제 (@Scheduled)

CRUD를 직접 구현하는 과정에서 판매자가 상품을 직접 삭제를 하는 api를 구현해야 할지에 대한 의문이 들었고, 삭제의 경우 정책을 어떻게 가져가야 할지 고민해 보았습니다.

 

저는 삭제 요청이 바로 DB에 반영되기보다는 두 가지 상황에 대해 생각해 보았습니다.

  1. 판매자가 상품을 삭제하려는 요청을 보낸 경우, 관리자의 승인을 받아야지만 최종적으로 DB에서 Item 정보 삭제
  2. 특정 조건을 만족하는 상품들을 주기적으로 제거

이번 포스팅에서는 2번째 상황에 대해 좀 더 자세히 다루어 보고자 합니다.

@Scheduled(cron = "0 0 3 * * ?") // 매일 새벽 3시마다
  public void deleteItemNotSoldForThreeYears() {
    List<ItemSellStatus> statuses = Arrays.asList(ItemSellStatus.SOLD_OUT,
        ItemSellStatus.SELL_STOPPED);
    LocalDateTime threeYearsAgo = LocalDateTime.now().minusYears(3);

    List<Item> items = itemRepository.findAllBySaleStatusInAndUpdatedAtBefore(
        statuses, threeYearsAgo);

    // 대량의 데이터를 한번에 삭제하려면 DB에 무리가 갈수 있으므로 나누어 진행
    int batchSize = 100;

    int totalSize = items.size();

    for(int i = 0; i < totalSize; i += batchSize){
      int end = Math.min(i + batchSize, totalSize);
      List<Item> batch = items.subList(i, end);
      itemRepository.deleteAllInBatch(batch);
    }
  }

저는 상품들 중 품절(SOLD_OUT) 또는 판매 중지(SELL_STOPPED)이고, 상품 정보 또는 상태가 수정된 지 3년 이상이 된 상품을 앞으로도 판매가 이어지지 않을 상품이라 판단하고 주기적으로 제거하는 정책을 세워보았습니다.

 

@Scheduled 어노테이션은 Spring Framework에서 제공하는 스케줄링 기능으로, 특정 작업을 정해진 주기나 시간에 자동으로 실행할 수 있게 해 줍니다. 이때 cron 표현식을 사용하여 정확한 실행 시간을 설정할 수 있는데, 저는 트래픽이 가장 적을 것으로 예상되는 새벽 3시에 해당 작업을 진행하게 하였습니다. 또한 너무 많은 데이터를 한 번에 삭제하려고 하면 DB에 성능 저하가 발생할 수 있을 것이라 생각하여, 적절한 크기의 배치로 나누어 처리를 진행하였습니다.

 

삭제 시 deleteAllInBatch 메서드를 사용해 보았는데 이에 대해 살펴보겠습니다.

기존에 저는 deleteAll 메서드를 사용하였는데, 이때는 아래와 같이 쿼리가 날아갑니다.

 

SELECT * FROM item WHERE id = ?;
DELETE FROM item WHERE id = ?;
SELECT * FROM item WHERE id = ?;
DELETE FROM item WHERE id = ?;

// 반복 ....

deletAll 방식의 경우 삭제할 객체 수만큼 SELECT와 DELETE 쿼리가 실행되므로, 많은 수의 쿼리가 발생하면서 DB의 성능이 저하될 수 있습니다.

 

반면 deleteAllInBatch의 경우 다음과 같이 동작합니다.

DELETE FROM item WHERE id IN (?, ?, ?, ....);

큰 규모의 데이터를 처리할 때, 쿼리의 수를 최소화하고 DB에 부하를 줄이기 위해 deleteAllInBatch 메서드를 사용하였습니다