| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- java
- 백엔드
- 성능 개선
- 자바
- 트러블슈팅
- 토스
- Til
- 프로그래머스
- 코드카타
- 스프링 배치
- Effective Java
- 배치
- 추상클래스
- 이펙티브 자바
- Spring
- 빌더 패턴
- lv1
- 로드밸런서
- 스케줄러
- 디자인 패턴
- 김영한
- Spring Batch
- DB
- redis
- 스프링
- 프록시 패턴
- spring boot
- 템플릿 메서드 패턴
- GoF 23
- 계산기
- Today
- Total
김코딩
결제 시스템 성능 개선: 동기식에서 비동기식 처리로의 전환 본문
개선 목적
프론트엔드에서 토스 결제 완료 후, 서버에서 수행하는 포인트 충전 로직의 성능 개선을 통해 전체 결제 프로세스의 응답시간 단축과 안정성 향상을 달성한다.
참고 : https://ddokyun.tistory.com/66
토스 결제 승인부터 포인트 충전까지의 전체 플로우
도입 배경기존에는 사용자가 원하는 포인트, 결제 수단을 Mock 방식을 이용해서 요청 body에 넣어주는 방식으로 포인트 충전을 진행하였다. 하지만 이번에 토스페이먼츠를 도입하면서 결제 요청
ddokyun.tistory.com
성능상 문제점
1. 긴 응답시간
현재 결제 API는 동기 방식으로 모든 작업을 순차 처리하여 긴 응답시간이 발생합니다.
처리 플로우:
유저 조회 → 토스 결제 승인 → Payment 성공 처리 → 포인트 충전 → 응답
(동기) (동기) (동기) (동기)
각 단계별 타임아웃:
- 유저 조회 API: 5초 (빠른 실패 응답)
- 토스 결제 승인 API: 30초 (토스 페이에서 권장하는 타임아웃이 30초 ~ 60초인데, 빠른 응답을 위해 30초로 설정)
- 포인트 충전 API: 5초 (빠른 실패 응답)
최악의 경우: 총 40초 소요 가능
2. 장애 전파 문제
포인트 서비스 장애 시 전체 결제가 실패하는 구조적 문제가 있습니다.
문제 시나리오:
- 토스페이 결제 진행 → 사용자 카드에서 돈 차감 완료
- 포인트 충전 시도 → 포인트 서버 오류로 실패
- 전체 API 실패 응답 → 사용자는 "결제 실패"로 인식
상세 장애 시나리오 분석
1. 토스 결제 승인 실패 (토스 서버 문제, 네트워크 응답문제, 타임아웃 문제 등)
→ 토스 결제 승인에 실패하면 사용자의 카드에서 돈이 빠지지 않으므로 별도의 취소 처리가 불필요하다. 사용자에게 "결제 실패" 메시지를 보여주면 된다.
2. 토스 결제 승인 성공 후 후속 처리(Payment객체 성공 처리, 포인트 충전) 실패
→ 토스 결제 승인에 성공하면 이미 사용자 카드에서 돈이 빠진 상태이다. 이후 단계에서 실패할 경우:
2-1. 금액 검증 실패
- 프론트에서 요청한 금액과 토스에서 실제 결제된 금액이 불일치
- 이미 결제된 상태이므로 토스 취소 API 호출 필요
- 취소 실패 시 수동 처리 필요
2-2. Payment 객체 성공 처리 실패
- DB 연결 오류, 제약조건 위반 등으로 Payment 성공 처리 실패
- 이미 결제된 상태이므로 토스 취소 API 호출 필요
- 취소 실패 시 수동 처리 필요
2-3. 포인트 충전 실패
- 포인트 서비스 장애, 네트워크 지연, 타임아웃 등으로 포인트 충전 실패
- 핵심 문제: 결제는 완벽하게 성공했는데 포인트 때문에 전체를 실패 처리
- 토스 취소 API 호출하여 성공한 결제를 억지로 취소
- 사용자는 "결제 실패" 경험 → 이탈률 증가
3. 토스 취소 처리 실패 (극한 상황)
→ 토스 결제는 성공했지만 후속 처리 실패로 취소 시도 시:
- 토스 취소 API도 동시에 장애 발생
- 사용자 카드에서는 돈이 빠진 상태로 남음
- 시스템에서는 "결제 실패"로 처리
- 결과: 고객은 돈을 잃고 상품/포인트도 받지 못함
- 수동 환불 처리 및 고객 불만 증가
2-1. 토스페이에서 결제를 진행

2-2. 토스페이 결제 완료 (이때, 사용자의 계좌에서 돈이 빠져나감)

2-3. Point 충전 중 오류가 발생하였을 때, 사용자에게 실패 응답

핵심 문제:
- 결제는 이미 완료된 상태인데 부가 기능(포인트) 때문에 전체 실패 처리
- 사용자 혼란: "돈은 빠졌는데 왜 실패?"
- 부분 장애가 전체 시스템을 마비시키는 구조
- 결과적으로 사용자 이탈률 증가 및 고객 불만 야기
3. 동기식에서 포인트 충전에 재시도 로직 도입 시, 사용자 대기시간이 더욱 증가
- 포인트 충전의 안정성을 위해 재시도 로직을 도입할 경우, 동기 방식에서는 사용자 대기시간이 기하급수적으로 증가합니다.
- 1차 시도: 포인트 충전 실패 (5초 타임아웃)
- 1초 대기 후 2차 시도: 포인트 충전 실패 (5초 타임아웃)
- 1초 대기 후 3차 시도: 포인트 충전 실패 (5초 타임아웃)
- 재시도 로직 없음 → 포인트 유실 위험
- 재시도 로직 있음 → 사용자 경험 악화
- 이는 포인트 충전만으로도 18초가 걸리며, 전체 결제 프로세스는 최악의 경우 50초 이상 소요될 수 있습니다.
해결 방안: 비동기 도입
핵심 아이디어
결제 승인(핵심 기능)과 포인트 충전(부가 기능)을 분리하여, 사용자는 결제 완료를 즉시 확인하고 포인트 충전은 백그라운드에서 안정적으로 처리하도록 개선했습니다.
Before(동기 처리)
public PaymentConfirmResult confirmAndChargePoint(Long userId, PaymentConfirmCommand command) {
// 유저 검증
userClient.getUserById(userId);
Payment payment = null;
TossConfirmResult tossConfirmResult = null;
TossCancelResult tossCancelResult = null;
try {
// 토스 페이먼츠의 /payments/confirm API 호출 (결제 승인)
tossConfirmResult = tossClient.confirmPayment(
command.getPaymentKey(),
command.getOrderId(),
command.getAmount()
);
// 프론트에서 결제 요청한 금액과, 토스에서 실제로 결제한 금액이 일치하는지 확인
// 일치하지 않는다면 예외를 터트려서 토스 결제 취소
paymentService.validatePaymentAmount(tossConfirmResult.getTotalAmount(), command.getAmount());
// 토스 결제를 완료하고, 금액 검증을 통과하였으면 토스에서 응답받은 값을 이용하여 payment 객체 생성
payment = paymentService.createPaymentFromConfirm(userId, tossConfirmResult);
// 포인트 충전(동기식 처리)
pointClient.chargePoint(
userId,
command.getAmount(),
"CHARGE",
tossConfirmResult.getOrderId()
);
return PaymentApplicationMapper.toPaymentConfirmResult(payment);
} catch (RestClientResponseException e) {
...
}
}
After(비동기 처리)
/**
* 결제 승인 및 포인트 충전 요청 처리
* 1. 사용자 검증 -> 2. 토스 결제 승인 -> 3. Payment 객체를 DONE 으로 변경 -> 4. 포인트 충전, 알림 이벤트 발행
*/
public PaymentConfirmResult confirmAndChargePoint(Long userId, PaymentConfirmCommand command) {
boolean userVerified = false;
boolean tossPaymentSucceeded = false;
TossConfirmResult tossConfirmResult = null;
Payment payment;
try {
// 유저 검증
userClient.getUserById(userId);
userVerified = true;
// 토스 페이먼츠의 /payments/confirm API 호출 (결제 승인)
tossConfirmResult = tossClient.confirmPayment(
command.getPaymentKey(),
command.getOrderId(),
command.getAmount()
);
tossPaymentSucceeded = true;
// 프론트에서 요청한 결제 금액과 토스에서 실제로 결제한 금액이 일치하는지 확인
// 일치하지 않는다면 예외를 터트린다.
// 일치한다면, 토스에서 응답받은 값을 이용하여 payment 객체 생성
payment = paymentService.markAsSuccess(
tossConfirmResult,
command.getAmount()
);
// 포인트 충전 및 알림 전송 이벤트 발행
publishPaymentCompletedEvent(payment);
return PaymentApplicationMapper.toPaymentConfirmResult(payment);
} catch (RestClientResponseException e) {
// RestClient 에서 오류가 발생하였을 때,
// 1. 토스 결제 요청 하기 전 실행된 prepare API 에서 저장된 Payment 객체를 fail 상태로 바꾼다.
paymentService.markAsFailed(
command.getOrderId(),
command.getPaymentKey()
);
// 2. userClient 오류인지, tossClient 오류인지 확인 후에, 적절한 오류를 던져준다.
handleRestClientError(e, userVerified, tossPaymentSucceeded);
// 실행되지 않음, 컴파일 에러 방지용
return null;
} catch (Exception e) {
// 어쩔 수 없이 발생하는 모든 예외 상황
// 1. Payment 객체를 fail 상태로 수정
paymentService.markAsFailed(
command.getOrderId(),
command.getPaymentKey()
);
// 2. 토스 결제 승인을 한 상태로 예외가 발생하면, 토스 결제를 취소해줘야한다.
if (tossConfirmResult != null) {
cancelToss(tossConfirmResult);
}
throw new PaymentException(PaymentErrorCode.PAYMENT_PROCESSING_FAILED, e);
}
}
이벤트 핸들러
@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCompletedEventHandler {
private final PointClient pointClient;
@EventListener
@Async
@Retryable(
retryFor = {Exception.class}, // 모든 예외에 대하여 재시도
noRetryFor = {IllegalStateException.class}, // 파라미터 오류는 재시도 해도 똑같이 오류가 나니까 재시도에서 제외
maxAttempts = 3,
backoff = @Backoff(delay = 1000) // 1초 기다리고 재시도
)
public void handlePointCharge(PaymentCompletedEvent event) {
log.info("포인트 충전 시도: orderId = {}", event.getOrderId());
pointClient.chargePoint(
event.getUserId(),
event.getAmount(),
event.getReason(),
event.getOrderId()
);
log.info("포인트 충전 성공: orderId = {}", event.getOrderId());
}
@Recover
public void recover(Exception e, PaymentCompletedEvent event) {
log.error("포인트 충전 최종 실패 - 수동 처리 필요: orderId = {}", event.getOrderId(), e);
}
}
개선 결과
1. 성능 개선 (실측 데이터)
| 동기식 | ||
| 항목 | 값 | 비고 |
| 총 샘플 수 | 30개 | |
| 평균 응답시간 | 738.3ms | |
| 최소 응답시간 | 590ms | 최고 성능 |
| 최대 응답시간 | 1,008ms | 유일한 1초 초과 사례 |
| 표준편차 | 약 90ms | 비교적 안정적 |

Before: https://ddokyun.tistory.com/62
동기식 결제 승인 API의 응답속도 테스트 및 분석
테스트 대상 API 경로 및 메서드: POST /payments/confirm설명:프론트엔드에서 토스 결제가 완료된 후, 결제 승인과 동시에 사용자 포인트를 충전하는 API내부적으로 paymentFacade.confirmAndChargePoint를 호출해
ddokyun.tistory.com
| 비동기식 | ||
| 항목 | 값 | 비고 |
| 총 샘플 수 | 30개 | |
| 평균 응답시간 | 621.6ms | |
| 최소 응답시간 | 541ms | 최고 성능 |
| 최대 응답시간 | 786ms | 상대적으로 느린 구간 |
| 표준편차 | 약 70.7ms | 비교적 안정적 |

After: https://ddokyun.tistory.com/64
비동기식 결제 승인 API의 응답속도 테스트 및 분석
테스트 대상 API@PostMapping("/payments/confirm")public ResponseEntity> confirmAndChargePoint( @RequestBody PaymentConfirmRequest request, @AuthenticationPrincipal Auth auth) { long startTime = System.currentTimeMillis(); PaymentConfirmResult paymentCo
ddokyun.tistory.com

| 구분 | Before (동기식) | After (비동기식) | 개선 효과 |
| 평균 응답시간 | 738.3ms | 621.6ms | 116.7ms 단축 (15.8% 개선) |
| 최소 응답시간 | 590ms | 541ms | 49ms 단축 |
| 최대 응답시간 | 1,008ms | 786ms | 222ms 단축 (22% 개선) |
| 표준편차 | 약 90ms | 약 70.7ms | 안정성 21% 향상 |
2. 장애 격리 달성
https://ddokyun.tistory.com/65
동기식 처리에서 비동기식 처리로 장애 격리 달성 테스트
결제 시스템에서 동기식 처리를 비동기식으로 전환했을 때 정말로 장애 격리가 달성되는지 실제 테스트해보려고 합니다.포인트 서비스에 의도적으로 장애를 발생시켜서 직접 확인해보겠습니
ddokyun.tistory.com
- 결제 서비스 독립성: 포인트 서비스 장애가 결제에 영향 주지 않음
- 부분 장애 대응: 결제는 성공하고 포인트만 나중에 처리하는 구조
동기식 vs 비동기식 비교
| 구분 | 동기식 | 비동기식 | 개선 효과 |
| 사용자 대기시간 | 20284ms | 665ms | 19,619ms 단축(약 29배 개선) |
| 포인트 실패 시 결제 | 전체 실패 | 결제 성공 | 매출 보호 |
| 사용자 경험 | 최악 | 최고 | 천지차이 |
| 재시도 처리 | 사용자 대기 | 백그라운드 처리 | 격리 달성 |
동기식 (장애 전파)
토스 결제 성공 → 포인트 실패 → 전체 실패 → 사용자 "20초 대기 후 실패"
비동기식 (장애 격리)
토스 결제 성공 → 즉시 성공 응답 → 백그라운드 포인트 처리 → 사용자 "1초 만에 성공"
동기식 vs 비동기식 전환 효과 종합
| 측면 | 동기식 (Before) | 비동기식 (After) | 개선 효과 |
| 성능 | 평균 738.3ms | 평균 621.6ms | 15.8% 개선 |
| 장애 대응 | 포인트 실패 → 전체 실패 | 포인트 실패 → 결제 성공 | 장애 격리 |
| 사용자 경험 | 20초 대기 후 실패 | 1초 만에 성공 | 29배 개선 |
| 재시도 처리 | 사용자 대기 필요 | 백그라운드 처리 | 투명한 처리 |
| 매출 보호 | 포인트 오류로 매출 손실 | 결제 성공으로 매출 보호 | 비즈니스 가치 |
결론
Spring Event를 활용한 비동기 처리 전환을 통해 결제 시스템의 성능과 안정성을 동시에 향상시켰습니다.
핵심 성과:
- 성능: 평균 응답시간 15.8% 개선 (738.3ms → 621.6ms)
- 안정성: 장애 격리로 포인트 서비스 장애가 결제에 미치는 영향 차단
- 사용자 경험: 결제 완료 즉시 확인 가능 (20초 → 1초)
- 비즈니스 가치: 부분 장애로 인한 매출 손실 방지
'개발팁' 카테고리의 다른 글
| 인덱스(Index)란 무엇인가? (0) | 2025.11.25 |
|---|---|
| [ 면접의 神 ] 동시 요청 환경에서 DB 병목 해결을 위한 캐싱 전략 수립 (0) | 2025.11.23 |
| Redis의 분산락을 활용한 동시성 제어 테스트 (0) | 2025.07.14 |
| 같은 자원에 동시에 요청이 발생하였을 때, 어떻게 대처할까? (lock을 이용한 동시성 제어) (0) | 2025.07.13 |
| 코딩에도 밀키트가 있더라(docker) - 1 (2) | 2025.06.20 |