티스토리 뷰

문제점

어느날부터 오전 11시만 되면 관리자 페이지 접속이 안 되는 것을 확인했습니다. 문제의 원인을 파악하기 위해 관리자 API 로그를 확인하던 중, 송장번호로 택배사 정보를 크롤링해서 배송완료된 주문의 상태를 배송완료로 변경하는 스케줄링 메서드 로그를 기점으로 서버가 다운되어 있음을 확인했습니다. 최근들어 급격하게 증가한 주문량으로 인해 서버가 다운이 된 것인가하는 의문점을 갖고 해당 소스코드를 들여다 보았습니다.



소스 코드의 내용은 이러하였습니다.

현재 배송중인 주문 데이터를 DB에서 가져와서 송장번호로 택배사의 정보를 크롤링한 뒤, 배송완료되었다면 주문 상태를 배송완료로 업데이트하고 있었습니다.

// 1. 상태가 배송중인 주문의 데이터를 DB에서 조회한다.
public void checkTraceResult() {
    deliveringOrders = orderService.getDeliveringOrders(lastId);

    List<List<OrderDeliveryDto.Request>> partitions
          = ListUtils.partition(deliveringOrders, Math.max(deliveringOrders.size() / 100, 1));

    partitions.forEach((dlvOrders) -> {
      List<OrderDeliveryDto.Response> deliveredOrderCodes = null;
      try {
      // 2. 송장번호로 크롤링하여 배송완료 여부를 확인한다.
        deliveredOrderCodes = deliveryService.checkDeliveredOrders(dlvOrders);

        // 3. 배송완료된 주문이면 주문상태를 변경한다.
        orderService.changeOrderDelivered(deliveredOrderCodes);
        Thread.sleep(500);
      } catch (IOException | InterruptedException e) {
        e.printStackTrace();
      }
      });
}

송장번호마다 택배사 페이지에 request해서 크롤링하는 부분도 문제지만, 무엇보다 큰 문제는 주문상태를 변경하는 메서드(changeOrderDelivered)에 숨어있었습니다.

// 주문상태를 변경하는 메서드
@Transactional
  public void changeOrderDelivered(List<OrderDeliveryDto.Response> orderCodes) {
    orderCodes.forEach((orderCode) -> orderRepository.findByCode(orderCode.getOrderCode())
      .ifPresent((order) -> order.delivered(orderCode.getDeliveredDateTime())));
  }

단순히 주문코드로 주문을 조회해서 상태와 배송완료시간을 업데이트하는 내용이지만, 메서드에 @transactional이 선언되어 있었습니다. 트랜잭션은 다음과 같이 작동합니다.

  1. 트랜잭션 프록시를 호출한다.
  2. 트랜잭션 프록시는 데이터소스를 찾아서 사용한다. 이때 커넥션 풀에서 커넥션을 획득한다.
  3. 커넥션을 모두 사용하고 나면 connection.close()를 호출한다. 이때 실제 커넥션이 커넥션 풀로 돌아간다.

만약 1,000번의 업데이트가 이루어지는 상황이라면, 커넥션을 획득하고 반복하는 작업도 1,000번 이루어져야 한다는 점입니다. 오전 11시에 수많은 주문 데이터의 상태를 변경하기 위해 계속 커넥션을 맺어야 하니 관리자 페이지의 접속이 안 되는 상황은 너무 당연한 것이었습니다.


개선

위 문제를 해결하기 위해 벌크 업데이트를 이용하려 했습니다. 하지만 주문마다 배송완료시간이 상이하기 때문에 벌크 업데이트로는 해결하기 어렵다는 생각이 들었습니다. 다른 방식으로 접근하기 위해 고민을 하던 중, 계속 커넥션을 맺지 않고, 한 번 맺어논 커넥션을 이용해서 업데이트를 처리하면 어떨까하는 생각으로 개선에 나섰습니다.

// 주문상태를 변경하는 메서드
@Transactional
public void deliveredOrderItem(List<DeliveredOrderItem> deliveredOrderItems) {

  // 1. 배송 완료된 주문 아이디를 리스트로 만든다.
  List<Long> orderItemIds = deliveredOrderItems.stream()
    .map(DeliveredOrderItem::getOrderItemId)
    .collect(Collectors.toList());

  // 2. 주문 아이디를 key, 배송완료된 시간을 value로 맵을 만든다.
  Map<Long, LocalDateTime> deliveredDateTimeMap = deliveredOrderItems.stream()
    .collect(toMap(DeliveredOrderItem::getOrderItemId, DeliveredOrderItem::getDeliveredDateTime));

  // 3. 주문 아이디들로 주문을 조회한 뒤, stream을 이용하여 상태와 배송완료 시간을 업데이트한다.
  orderItemRepository.findAllByIdIn(orderItemIds)
      .forEach(orderItem -> orderItem.delivered(deliveredDateTimeMap.get(orderItem.getId())));
}

JPA의 더티체킹을 이용해서 상태를 변경하고, 배송완료시간을 업데이트 하는 횟수는 이전과 동일하지만, 이전처럼 업데이트마다 커넥션 풀에서 새로운 커넥션을 획득하지 않고, 한 번만 커넥션을 맺어 업데이트하도록 수정하였습니다.


아쉬운 점과 느낀점

관리자 API에 코드를 반영하고나서 더이상 서버가 다운되지 않는 모습에 상당히 행복했던 기억이 납니다.
하지만 아직도 여전히 수많은 업데이트 쿼리가 데이터베이스로 전달되는 문제가 남아있습니다. 더 열심히 공부하고 부딪히며 좀 더 나은 방향으로 문제를 해결할 수 있는 개발자로 성장하도록 하겠습니다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함