문제 상황

건강기능식품 소분 서비스의 결제 프로세스에서 여러 외부 API와 SQL 프로시저를 순차적으로 호출하는 과정에서 다음과 같은 문제가 발생했습니다

  1. 복잡한 순서처리

    // confirmTossPayment 서버 액션의 처리 순서
    1. 토스페이먼츠 결제 확인 (@repo/toss)
    2. SQL 프로시저로 주문 정보 저장 (DB)
    3. 이제너두 주문 생성 (@repo/exanadu)
    4. 코스맥스 소분 주문 생성
    
  2. 트랜잭션 일관성 문제

  3. 에러 추적의 어려움

해결 방법

1. 단계별 예외 처리 구조화 및 실패 복구 전략

try {
  // 1. 토스페이먼츠 결제 확인
  const paymentData = await tossPaymentsService.confirmPayment({
    paymentKey: params.paymentKey,
    orderId: params.orderId,
    amount: params.amount,
  });

  if (!paymentData || paymentData.status !== 'DONE') {
    throw new Error(`토스페이먼츠 결제 실패: ${paymentData?.status}`);
  }

  // 2. 이제너두 주문 생성 시도
  const exanaduOrder = await exanaduService.createOrder(exanaduOrderInfo);
  
  // 이제너두 API 실패 시 성공으로 처리하고 관리자에게 알림
  if (exanaduOrder.TYPE === 'ERR') {
    await sendErrorToSwit([
      '[이제너두 주문 생성 실패]',
      `주문 번호: ${params.orderId}`,
      `실패 사유: ${exanaduOrder.RESULT}`,
      '관리자 페이지에서 수동 처리가 필요합니다.',
    ].join('\\n'));

    // 결제는 성공으로 처리하고 관리자 수동 처리를 위한 상태 저장
    await makeServerAction(
      {
        pOrderId: params.orderId,
        pStatus: 'EXANADU_FAILED',
        pErrorMessage: exanaduOrder.RESULT,
      },
      OrderStatus.updateV1.inputParams,
      async (params) => {
        await OrderStatusRepository.updateV1(params);
      },
    );

    // 성공 응답 반환
    return {
      status: 'SUCCESS',
      message: '결제가 완료되었습니다.',
      data: {
        paymentKey: params.paymentKey,
        orderId: params.orderId,
        amount: params.amount,
      },
    };
  }
} catch (error) {
  return errorHandler(error);
}

2. 에러 모니터링 시스템 개선

async function sendErrorToSwit(errorMessage: string) {
  try {
    // 1. Swit 메시지 발송
    await switService.sendMessage(errorMessage);
    
    // 2. DB에 에러 로그 저장
    await makeServerAction(
      {
        pErrorMessage: errorMessage,
        pCreatedAt: new Date().toISOString(),
      },
      ErrorLogs.createV1.inputParams,
      async (params) => {
        await ErrorLogsRepository.createV1(params);
      },
    );
  } catch (switError) {
    // 모니터링 시스템 자체의 실패도 기록
    console.error('Error monitoring system failure:', switError);
  }
}

3. 관리자 페이지를 통한 수동 처리 지원

// 관리자 페이지에서 실패한 주문 조회
export async function fetchFailedOrders() {
  return makeServerAction(
    { pStatus: 'EXANADU_FAILED' },
    FailedOrders.findAllV1.inputParams,
    async (params) => {
      const data = await FailedOrdersRepository.findAllV1(params);
      return {
        status: 'SUCCESS',
        data,
      };
    },
  );
}

// 이제너두 주문 수동 재시도
export async function retryExanaduOrder(params: { orderId: string }) {
  try {
    // 1. 주문 정보 조회
    const orderData = await ExanaduOrderRequestsRepository.findOneV1({
      pOrderId: params.orderId,
    });

    // 2. 이제너두 주문 재시도
    const exanaduOrder = await exanaduService.createOrder(
      mapExanaduOrderRequestToOrderInfo(orderData[0]),
    );

    // 3. 성공 시 주문 상태 업데이트
    if (exanaduOrder.TYPE !== 'ERR') {
      await makeServerAction(
        {
          pOrderId: params.orderId,
          pStatus: 'COMPLETED',
          pExanaduOrderNo: exanaduOrder.EXAN_ORDER,
        },
        OrderStatus.updateV1.inputParams,
        async (params) => {
          await OrderStatusRepository.updateV1(params);
        },
      );
    }

    return {
      status: 'SUCCESS',
      message: '주문 처리가 완료되었습니다.',
    };
  } catch (error) {
    return errorHandler(error);
  }
}

개선 효과

  1. 안정성 향상
  2. 유지보수성 개선
  3. 운영 효율성 향상

배운 점