챕터 3-3 8주차 부하를 적절하게 축소하기 - 트랜잭션 범위
앞서 우리가 학습한 Transaction, Lock, Index, Cache 등은 모두 비즈니스 로직을 처리하는 데에 있어 중요한 Database의 부하를 줄이고, 속도 및 성능을 향상시키기 위함이었습니다. 그런데 Lock의 범위 뿐 아니라 무분별한 비즈니스 로직과 트랜잭션의 규모 또한 우리가 예측하지 못한 문제를 발생시킬 수 있습니다.
어떤 문제로 이어질 수 있을까?
- 다수의 SlowRead 작업으로 인해 요청 처리에 영향을 줄 수 있음
- Transaction 범위 내에서 Lock을 사용하고 있을 경우, 해당 자원에 접근하는 다른 요청의 대기 혹은 데드락 상황을 유발할 수 있음
- 긴 생명 주기의 Transaction의 경우, 오랜 시간은 소요되나 후속 작업에 의해 전체 트랜잭션이 실패할 수 있음
- DB 외적인 작업의 실패가 Transaction의 범위로 전파되어 전체 비즈니스 로직이 rollback되는 문제
- 만약, externalAPI가 실패하더라도 우리의 비즈니스는 정상적으로 성공시켜도 되는 요구사항이라면?
- externalAPI의 타임아웃으로 트랜잭션을 롤백시켰으나, external 서비스에서는 사실 정상적으로 처리 되었을 때 무결성을 잃게 되는 문제
위 문제들을 어떻게 해결할 수 있을까 고민해보세요.
Event 기반 흐름제어
- Event를 발행 및 구독하는 모델링을 통해 코드의 강항 결합을 분리함
- Event에 의해 본인의 관심사만 수행하도록 하여 비즈니스 로직간의 의존을 느슨하게 함
활용 방안
- 비대해진 트랜잭션 내의 각 작업을 작은 단위의 트랙잭션으로 분리할 수 있음
- 특정 작업이 완료되었을 떄, 후속 작업이 이벤트에 의해 triger되도록 구성함으로써 과도하게 많은 비즈니스 로직을 알고 있을 필요 없음
- 트랜잭션 내에서 외부 API 호출(e.g. DB 작업 등)의 실패나 수행이 주요 비즈니스 로직 (트랜잭션)에 영향을 주지 않도록 구성할 수 있음
동작 순서
1. service1 수행
2. service1 완료 이벤트 발행
3. service1 완료 이벤트에 대한 리스너가 본인의 비즈니스(service2_1 & service2_2) 수행
주의할 점
- 각 작업의 논리적 의존이나 관계를 잘 파악해야 함
- 만약 이벤트에 의해 파생된 작업이 실패하였을 때, 원본 작업 또한 실패 처리를 해야한다면 이를 위한 처리가 필요함 (keyword : 보상 트랜잭션, SAGA 패턴)
- 이벤트에 의해 각 작업이 영향을 주고 있는지, 혹은 이벤트가 발생하면 안되는 상황에서 이벤트가 발행되고 있지는 않은지 등
java 이벤트 예시
// 이벤트 객체
public class PaymentSuccessEvent {
private final String orderKey;
private final String paymentKey;
public PaymentSuccessEvent(String orderKey, String paymentKey) {
this.orderKey = orderKey;
this.paymentKey = paymentKey;
}
public String getOrderKey() {
return orderKey;
}
public String getPaymentKey() {
return paymentKey;
}
}
// 이벤트 발행서비스
@Component
public class PaymentEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public PaymentEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void success(PaymentSuccessEvent event) {
applicationEventPublisher.publishEvent(event);
}
}
// 이벤트 구독서비스
@Component
public class PaymentEventListener {
private final DataPlatformSendService sendService;
public PaymentEventListener(DataPlatformSendService sendService) {
this.sendService = sendService;
}
// 비동기로 이벤트 발행주체의 트랜잭션이 커밋된 후에 수행한다.
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void paymentSuccessHandler(PaymentSuccessEvent event) {
// (4) 주문 정보 전달
PaymentSuccessPayload payload = new PaymentSuccessPayload(event);
sendService.send(payload);
}
}
// 비즈니스 로직
@Service
public class PaymentService {
private final RequestValidator requestValidator;
private final UserService userService;
private final OrderService orderService;
private final PaymentRepository paymentRepository;
private final PaymentEventPublisher eventPublisher;
public PaymentService(RequestValidator requestValidator, UserService userService, OrderService orderService, PaymentRepository paymentRepository, PaymentEventPublisher eventPublisher) {
this.requestValidator = requestValidator;
this.userService = userService;
this.orderService = orderService;
this.paymentRepository = paymentRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public void pay(PaymentRequest request) {
// (1) 결제 요청 검증
requestValidator.validate(request);
// (2) 유저 포인트 차감
User user = userService.getWithLock(request.getUserId());
orderService.check(request.getOrderKey(), user.getId());
user.usePoint(request.getAmount());
// (3) 결제 정보 저장
Payment payment = paymentRepository.save(new Payment(request));
// 결제 성공 이벤트 발행
eventPublisher.success(new PaymentSuccessEvent(payment.getOrderKey(), payment.getKey()));
}
}