건강기능식품 소분 서비스의 결제 프로세스에서 여러 외부 API와 SQL 프로시저를 순차적으로 호출하는 과정에서 다음과 같은 문제가 발생했습니다
복잡한 순서처리
// confirmTossPayment 서버 액션의 처리 순서
1. 토스페이먼츠 결제 확인 (@repo/toss)
2. SQL 프로시저로 주문 정보 저장 (DB)
3. 이제너두 주문 생성 (@repo/exanadu)
4. 코스맥스 소분 주문 생성
트랜잭션 일관성 문제
에러 추적의 어려움
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);
}
}