-
Notifications
You must be signed in to change notification settings - Fork 2
3차: 결제 비동기 처리 및 결제 후속 작업 배치 처리 기반 DB 커넥션 감소
김현준 edited this page Sep 2, 2024
·
9 revisions
- 동기식 결제 처리
기존의 코드에서는 티켓을 구매할 때 결제 처리가 동기적으로 이루어지고 있었습니다. 이는 다음과 같은 문제를 일으켰습니다.
- 결제 처리 시간 동안 스레드가 블로킹되어 다른 요청을 처리하지 못한다.
- 동시에 많은 결제 요청이 들어올 경우 리소스 부족이 발생
- 개별적인 DB 연산
결제 건에 대해 개별적으로 DB 연산이 수행되고 있었습니다. 이는 다음과 같은 문제를 일으켰습니다.
- 많은 DB 커넥션 사용으로 인한 커넥션 풀 고갈
- 잦은 BD I/O로 인한 성능 저하
이러한 병목점들은 순간적인 고부하 상황에서 시스템의 성능을 저하시키고 안정성을 위협할 수 있습니다. 특히 결제 처리와 같은 중요한 비즈니스 로직에서 이러한 문제가 발생하면 사용자 경험에 직접적인 영향을 미칠 수 있습니다.
이러한 병목을 해결하기 위해 두 가지 전력을 채택했습니다. 결제 처리의 비동기화와 결제 후속 작업의 배치 처리입니다.
- 결제 처리의 비동기화
첫 번째 접근 방식은 결제 처리를 비동기적으로 변경하는 것이었습니다. 이를 위해 CompletableFuture를 활용했습니다.
public String processPurchase(PurchaseData purchaseData) {
validatePurchase(purchaseData);
String paymentId = UUID.randomUUID().toString();
paymentService.initiatePayment(paymentId, purchaseData.memberId(), purchaseData.ticketId())
.thenAcceptAsync(status -> handlePaymentResult(paymentId, status, purchaseData))
.exceptionally(e -> {
log.error("결제 처리 중 오류 발생", e);
compensationService.compensateFailedPurchase(paymentId, purchaseData.ticketId(),
purchaseData.ticketStockId());
return null;
});
return paymentId;
}
이 방식의 장점은 다음과 같습니다.
- 결제 요청을 즉시 반환하여 클라이언트의 대기 시간을 줄입니다.
- 서버 리소스를 효율적으로 사용할 수 있어, 동시에 더 많은 요청을 처리할 수 있습니다.
- 결제 처리 실패 시 보상 트랜잭션을 비동기적으로 처리할 수 있습니다.
- 결제 후속 작업의 배치 처리
두 번째 전략은 결제 후속 작업(구매 기록 생성, 재고 업데이트, 체크인 기록 생성 등)을 배치로 처리하는 것이었습니다. 이를 위해 우리는 인메모리 큐와 주기적인 배치 프로세싱을 도입했습니다.
@Scheduled(fixedRate = 5000) // 5초마다 실행
public void processPurchases() {
List<PurchaseData> batch = queue.pollBatch(calculateOptimalBatchSize());
if (!batch.isEmpty()) {
CompletableFuture.runAsync(() -> processBatch(batch), executor)
.exceptionally(e -> {
log.error("배치 처리 실패", e);
handleFailedBatch(batch);
return null;
});
}
}
@Transactional
protected void processBatch(List<PurchaseData> purchases) {
List<Purchase> successfulPurchases = new ArrayList<>();
for (PurchaseData purchase : purchases) {
try {
Purchase newPurchase = createPurchase(purchase);
successfulPurchases.add(newPurchase);
} catch (Exception e) {
log.error("구매 처리 실패", e);
}
}
if (!successfulPurchases.isEmpty()) {
batchInsertPurchases(successfulPurchases);
batchInsertCheckins(successfulPurchases);
synchronizeTicketStock();
}
}
이 접근 방식의 주요 이점은 다음과 같습니다.
- DB 커넥션 사용을 최소화하여 커넥션 풀 고갈 문제를 해결합니다.
- 배치 인서트를 통해 DB I/O 작업을 줄여 전체적인 처리 속도를 향상시킵니다.
- 실패한 작업을 재시도 할 수 있는 메커니즘을 제공합니다.
- 추가적인 최적화
위의 주요 전략 외에도 다음과 같은 추가적인 최적화를 수행했습니다.
- 캐시 활용: 자주 접근하는 데이터(예:티켓정보)를 캐싱하여 DB 부하를 줄였습니다.
- 커넥션 풀 최적화: HikariCP 설정을 튜닝하여 DB 커넥션 관리를 개선했습니다.
- 스레드 풀 조정: 비동기 작업을 위한 스레드 풀 크기를 최적화하여 리소스 사용을 효율화했습니다.
이러한 방안들을 통해 시스템의 처리량을 크게 향상시키고, 고부하 상황에서의 안정성을 개선할 수 있었습니다.
graph TD
A[Client] -->|HTTP Request| B[API Gateway]
B --> C[Purchase Service]
C --> D[Payment Service]
D -->|Async| E[Queue]
C -->|Async| E
E --> F[Batch Processor]
F --> G[Database]
F --> H[Redis Cache]
I[Scheduler] -->|Trigger| F
J[Error Handler] --> E
- API Gateway: 클라이언트의 요청을 적절한 서비스로 라우팅합니다.
- Purchase Service: 구매 요청을 처리하고 결제를 시작합니다.
- Payment Service: 외부 결제 시스템과 통신하여 실제 결제를 처리합니다.
- Queue: 결제 완료된 거래를 임시 저장합니다. 안정성을 위해 log를 비동기로 파일로 저장합니다.
- Batch Processor: 큐에서 거래를 가져와 일괄 처리합니다.
- Shceduler: 주기적으로 배치 프로세서를 트리거합니다.
- Error Handler: 실패한 거래를 재처리합니다.
- 인메모리 큐(Queue)
결제가 완료된 거래를 임시 저장하는 역할을 합니다. Java의 ConcurrentLinkedQueue를 사용하여 구현하였습니다.
public class InMemoryQueue<T> implements CustomQueue<T> {
private final ConcurrentLinkedQueue<T> queue;
private final AtomicInteger size;
// 구현 세부사항...
}
- 배치 프로세서
큐에서 데이터를 가저와 일괄적으로 처리합니다. 이는 DB 연산을 최적화하고 커넥션 사용을 줄이는데 중요한 역할을 합니다.
@Service
public class BatchProcessor {
@Transactional
public void processBatch(List<PurchaseData> purchases) {
List<Purchase> successfulPurchases = new ArrayList<>();
for (PurchaseData purchase : purchases) {
try {
Purchase newPurchase = createPurchase(purchase);
successfulPurchases.add(newPurchase);
} catch (Exception e) {
log.error("구매 처리 실패", e);
}
}
if (!successfulPurchases.isEmpty()) {
batchInsertPurchases(successfulPurchases);
batchInsertCheckins(successfulPurchases);
}
}
// 기타 메서드...
}
- 스케줄러
주기적으로 배치 프로세서를 실행하여 큐에 쌓이 데이터를 처리합니다. Spring의 @Scheduled 어노테이션을 활용했습니다.
@Service
public class QueueProcessor {
@Scheduled(fixedRate = 5000) // 5초마다 실행
public void processPurchases() {
List<PurchaseData> batch = queue.pollBatch(calculateOptimalBatchSize());
if (!batch.isEmpty()) {
batchProcessor.processBatch(batch);
}
}
}
- 에러 핸들러
실패한 거래를 관리하고 재처리를 시도합니다. 이를 통해 시스템의 안정성과 데이터 일관성을 향상시켰습니다.
@Service
public class ErrorHandler {
@Scheduled(fixedRate = 60000) // 1분마다 실행
public void processErrorQueue() {
List<PurchaseData> errorBatch = errorQueue.pollBatch(100);
for (PurchaseData data : errorBatch) {
try {
purchaseService.processSinglePurchase(data);
} catch (Exception e) {
handleRetry(data);
}
}
}
private void handleRetry(PurchaseData data) {
// 재시도 로직...
}
}
이러한 설계를 통해 시스템의 확장성과 성능을 개선할 수 있었습니다. 특히 비동기 처리와 배치 프로세싱의 도입으로 DB 커넥션 사용을 최적화하고, 동시에 더 많은 거래를 처리할 수 있게 되었습니다.
- 3000, 5000명 기준
- 상세 페이지 조회
- 티켓 목록 조회
- 티켓 결제 가능 여부 조회
- 결제 정보 미리보기 페이지 조회
- 티켓 결제