TCC 패턴
- TCC(Try-Confirm-Cancel) 는 분산 시스템에서 데이터 정합성을 보장하기 위해 사용하는 분산 트랜잭션 처리 방식
- 전통적인 트랜잭션은 DB 의 Rollback, Commit 에 의존한다
- TCC는 트랜잭션을 애플리케이션 레이어에서 논리적으로 관리한다
- 세 단계로 나누어 트랜잭션을 관리한다
- Try : 필요한 리소스를 점유할 수 있는지 검사하고 임시로 예약한다
- Confirm : 실제 리소스를 확정 처리하여 DB에 반영한다
- Cancel : 문제가 생긴 경우, 예약 상태를 취소하여 원복한다
- Try, Confirm, Cancel 단계는 멱등하게 설계가 되어야 한다
주문 + 결제 요청 흐름시 OrderService 가 Coordinator 가 되는 상황

테스트
- 주문(
Order) / 상품(Product) / 포인트(Point) 가 존재한다 - 주문이 요청되면 주문을 생성한다
- 이후 클라이언트가 주문 + 결제 요청을 시도한다
OrderService는 재고를 차감하도록ProductService에 예약한다- 이후
OrderService는 포인트를 차감하도록PointService에 예약한다 - 동시성 처리
- Redis SETNX 를 통해 Lock 획득/해제
- Optimistic Lock 처리
각각의 도메인 Entity 는 예약에 대한 Entity 를 관리해야 한다
예시) 상품(Product)
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long quantity;
private Long price;
private Long reservedQuantity;
@Version
private Long version;
}
- 해당 도메인의 핵심 도메인 엔티티
version을 통해 낙관적 락 처리reservedQuantity컬럼을 가지고 있음- 1차 방어선 역할
- 실제 재고 수량과 예약된 재고 두 가지 수량을 가지고 있으며 이를 통해 수량 예외 방어로직처리
@Entity
@Table(name = "product_reservations")
public class ProductReservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String requestId;
private Long productId;
private Long reservedQuantity;
private Long reservedPrice;
@Enumerated(EnumType.STRING)
private ProductReservationStatus status;
}
public enum ProductReservationStatus {
RESERVED,
CONFIRMED,
CANCELLED
}
- 해당 도메인에 예약을 위해 존재하는 엔티티
- Try, Confirm, Cancel 각 단계는 예약 엔티티를 통해 관리 및 수정
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductReservationRepository productReservationRepository;
public ProductService(ProductRepository productRepository, ProductReservationRepository productReservationRepository) {
this.productRepository = productRepository;
this.productReservationRepository = productReservationRepository;
}
@Transactional
public ProductReserveResult tryReserve(ProductReserveCommand command) {
List<ProductReservation> exists = productReservationRepository.findAllByRequestId(command.requestId());
if (!exists.isEmpty()) {
long totalPrice = exists.stream().mapToLong(ProductReservation::getReservedPrice).sum();
return new ProductReserveResult(totalPrice);
}
Long totalPrice = 0L;
for (ProductReserveCommand.ReserveItem item : command.items()) {
Product product = productRepository.findById(item.productId()).orElseThrow();
Long price = product.reserve(item.reserveQuantity());
totalPrice += price;
productRepository.save(product);
productReservationRepository.save(
new ProductReservation(
command.requestId(),
item.productId(),
item.reserveQuantity(),
price
)
);
}
return new ProductReserveResult(totalPrice);
}
...
}
- 위와 같이 핵심 도메인(
Product) 생성 및 예약을 위한 엔티티(ProductReservation)을 생성한다
@Component
public class ProductFacadeService {
private final ProductService productService;
public ProductFacadeService(ProductService productService) {
this.productService = productService;
}
public ProductReserveResult tryReserve(ProductReserveCommand command) {
int tryCount = 0;
while (tryCount < 3) {
try {
return productService.tryReserve(command);
} catch (ObjectOptimisticLockingFailureException e) {
tryCount++;
}
}
throw new RuntimeException("예약에 실패하였습니다");
}
...
}
Service의 상위 레이어인Facade에서 낙관적 락에 대한 핸들링을 처리- 위 로직에서는 최대 3회 재시도
@RestController
public class ProductController {
private final ProductFacadeService productFacadeService;
private final RedisLockService redisLockService;
public ProductController(ProductFacadeService productFacadeService, RedisLockService redisLockService) {
this.productFacadeService = productFacadeService;
this.redisLockService = redisLockService;
}
@PostMapping("/product/reserve")
public ProductReserveResponse reserve(@RequestBody ProductReserveRequest request) {
String key = "product:" + request.requestId();
boolean acquiredLock = redisLockService.tryLock(key, request.requestId());
if (!acquiredLock) {
throw new RuntimeException("락 획득에 실패하였습니다.");
}
try {
ProductReserveResult result = productFacadeService.tryReserve(request.toCommand());
return new ProductReserveResponse(result.totalPrice());
} finally {
redisLockService.releaseLock(key);
}
}
...
}
requestId로 같은 요청에 대한 방어 로직 처리- 요청에 담긴
requestId를 통해 Lock 획득 및 해제
- 요청에 담긴
장점
- 확장성과 성능에 유리하다
- 2PC 에 비해 DB Lock 점유시간이 짧다
- 2PC 에 비해 Long Transaction 에 덜 취약하다
- 장애 복구와 재시도 처리에 유연하다
- 비즈니스 정책에 따라 전략을 정할 수 있다 → Confirm OR Cancel
단점
- 기존 시스템에 비해 설계와 구현이 복잡하다
- 모든 단계 (Try, Confirm, Cancel) 는 멱등적으로 설계되어야 한다
- 네트워크 오류, 재시도 시나리오를 고려한 복잡한 로직 구현이 필요하다
- Timeout 으로 인한 문제상황

테스트 코드
'학습일지 > MSA 분산 트랜잭션' 카테고리의 다른 글
| [분산트랜잭션 맛보기] - Saga (0) | 2025.10.19 |
|---|---|
| [분산트랜잭션 맛보기] - 2PC (0) | 2025.10.18 |