티스토리 뷰
기록하는 이유
분산 트랜잭션을 처음으로 적용해보면서 어떤 논리로 SAGA 패턴을 적용했고 개발하면서 배우게 된 내용들을 기억하기 위해 기록해보려 한다.
시스템 요구사항
"계약 완료" 라는 기능 구현을 하게 되었는데 다음과 같은 시퀀스로 수행되어야 한다.
1. 계약 검증 (DB A: MySQL)
현재 요청한 계약서가 검토과정을 거쳐서 계약 완료 상태로 전환이 가능한지 확인. (이 상태 정보는 DB A (mysql)에 들어있음)
2. 계약 확정 (DB B: Oracle)
전환이 가능한 상태면 페이지에서 기입한 계약 사항을 확정하고 이걸 DB B (oracle)에 저장
3. 외부 인증 및 이력 기록 (DB C: Oracle)
확정된 계약 정보를 외부 인증 API에 HTTP 요청으로 전송하여, 제3 인증 파티에서 계약 유효성을 검증
요청 및 응답 이력은 DB C(Oracle)에 기록하는 별도 서비스(외부 인증/이력 기록 서비스)에서 관리
4. 계약 완료 상태 업데이트 (DB A: MySQL)
2단계가 성공적이면 내부 DB A 에 계약완료 상태를 write
이렇게 여러 단계를 거쳐 최종 완료 상태까지 트랜잭셔널하게 처리가 보장되어야 하는 상황이다.
서비스 오픈 당시에는 아주 적은 트래픽이 예상되어 분산 트랜잭션을 고려하지 않고 그냥 개발하였다.
사실 초기 상태로도 크게 문제가 없었지만, 한 두 개 씩 외부 API가 실패나면서 DB B에 대한 시퀀스를 수기로 롤백하는 케이스가 생기기 시작하자 분산 트랜잭션을 구현하기로 마음을 먹었다.
설계한 내용
kafka 없이 http 동기 방식으로 Orchestration SAGA로 분산 트랜잭션을 구현했다.

[웹 UI]
│
▼
[Saga Orchestrator (Spring Boot)]
│
├─ Step1: HTTP 호출 → [계약 검증 서비스] → DB A (MySQL)
│
├─ Step2: HTTP 호출 → [계약 확정 서비스] → DB B (Oracle)
│
├─ Step3: HTTP 호출 → [외부 인증/이력 기록 서비스] → 외부 인증 API & DB C (Oracle)
│
└─ Step4: HTTP 호출 → [계약 완료 상태 업데이트 서비스] → DB A (MySQL)
왜 kafka 없이 http 동기?
1. 메세지 브로커가 필요할 만큼 트래픽이 들어오지 않는다. (하루 최대 20건)
2. 새로운 인프라에 대한 공수 부담
3. 외부 API 등 순서 보장이 필요함
왜 Orchestration?
1. 각 상태 별 트래킹이 필요함 (로깅 및 디버깅)
2. 서비스 간 의존성이 높아도 상관없음 (확장성 X)
3. 조정자가 실패 시 보상 트랜잭션을 명확하게 실행
4. 순서 보장이 필요함
컨셉 샘플 코드
@RestController
@RequestMapping("/saga")
public class ContractSagaController {
private final RestTemplate restTemplate;
public ContractSagaController(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
@PostMapping("/completeContract")
public ResponseEntity<String> completeContract(@RequestBody ContractRequest request) {
String transactionId = UUID.randomUUID().toString();
try {
// Step 1: 계약 검증 (DB A: MySQL)
String reviewUrl = "http://contract-review-service/api/review";
ReviewRequest reviewRequest = new ReviewRequest(transactionId, request.getContractId());
ResponseEntity<ReviewResponse> reviewResponse = restTemplate.postForEntity(reviewUrl, reviewRequest, ReviewResponse.class);
if (reviewResponse.getBody() == null || !reviewResponse.getBody().isEligible()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("계약 검토 실패: 계약 완료로 전환 불가");
}
// Step 2: 계약 확정 (DB B: Oracle)
String finalizeUrl = "http://contract-finalization-service/api/finalize";
FinalizeRequest finalizeRequest = new FinalizeRequest(transactionId, request);
ResponseEntity<FinalizeResponse> finalizeResponse = restTemplate.postForEntity(finalizeUrl, finalizeRequest, FinalizeResponse.class);
if (finalizeResponse.getStatusCode() != HttpStatus.CREATED) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("계약 확정 실패");
}
// Step 3: 외부 인증 및 이력 기록 (외부 API, DB C: Oracle)
String externalCertUrl = "http://external-certification-service/api/certify";
CertificationRequest certRequest = new CertificationRequest(transactionId, request);
ResponseEntity<CertificationResponse> certResponse = restTemplate.postForEntity(externalCertUrl, certRequest, CertificationResponse.class);
if (certResponse.getBody() == null || !certResponse.getBody().isCertified()) {
// 보상 트랜잭션: 계약 확정 취소 (DB B 데이터 삭제 또는 롤백)
String compensationUrl = "http://contract-finalization-service/api/finalize/" + finalizeResponse.getBody().getFinalizedContractId();
restTemplate.delete(compensationUrl);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("외부 인증 실패 - 계약 확정 롤백");
}
// Step 4: 계약 완료 상태 업데이트 (DB A: MySQL)
String completeUrl = "http://contract-review-service/api/complete";
CompletionRequest completionRequest = new CompletionRequest(transactionId, request.getContractId());
ResponseEntity<CompletionResponse> completeResponse = restTemplate.postForEntity(completeUrl, completionRequest, CompletionResponse.class);
if (completeResponse.getStatusCode() != HttpStatus.OK) {
// 보상: 필요한 경우 추가 보상 로직 실행
String compensationUrl = "http://contract-finalization-service/api/finalize/" + finalizeResponse.getBody().getFinalizedContractId();
restTemplate.delete(compensationUrl);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("계약 완료 상태 업데이트 실패 - 롤백");
}
return ResponseEntity.ok("계약 완료 처리 성공");
} catch (Exception e) {
// 전체 예외 처리 및 보상 로직 (추가 구현 필요)
// 예: 이미 진행된 단계에 대한 보상 API 호출
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("처리 중 예외 발생: " + e.getMessage());
}
}
}
'실무 개발' 카테고리의 다른 글
2PC (2-Phase-Commit)은 무엇인가? (2) | 2024.09.26 |
---|---|
커스텀 어노테이션으로 중복 코드 제거하기 (0) | 2023.10.31 |
NGINX는 왜 Apache보다 좋은가? (0) | 2022.06.17 |
- Total
- Today
- Yesterday
- okhttp3
- 카카오
- 카카오코테
- 신규 아이디 추천
- 코테
- IOC
- Kakao Blind
- PatternSyntaxException
- zipkin
- 프로그래밍 모델
- jvm
- 2019 Kakao Blind
- spring cloud sleuth
- 카카오 코테
- nginx 내부
- Java
- KAKAO 2021
- 스프링
- 2021
- 카카오 인턴
- WORE
- Java #GC #가비지콜렉터 #Garbage Collector
- 2020 KAKAO
- behavior parameterization
- Spring
- WORA
- 디자인패턴
- decorator
- Java #JIT #JVM
- 모던 자바 인 액션
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |