자바 백엔드 실무
들어가며
지난 시간에 우리는 Java(JCo)를 이용해 SAP ERP와 통신하는 방법을 배웠습니다. 이제 기술적으로 연결은 되었습니다. 하지만 운영 환경에서는 항상 “실패할 가능성”을 염두에 두어야 합니다.
시나리오: 자산 구매 신청 버튼을 클릭했습니다.
- 내 시스템(Oracle)의
T_ASSET테이블에INSERT성공. - SAP ERP에 RFC로 전표 생성 요청 -> 네트워크 타임아웃 발생!
이때 아무 조치도 취하지 않으면, 내 시스템에는 ‘구매 완료’된 자산이 있는데 회계 장부(ERP)에는 없는 데이터 불일치(Inconsistency) 상태가 됩니다. 이는 자산 관리 업무에서 치명적인 결함입니다.
오늘은 이러한 이기종 시스템 간의 트랜잭션 문제를 해결하기 위한 Two-Phase Commit(2PC)과 보상 트랜잭션(Compensation Transaction) 전략을 비교해 봅니다.
전략 1. 고전적이지만 확실한 방법: JTA와 XA 트랜잭션 (2PC)
전통적인 엔터프라이즈 환경에서는 JTA(Java Transaction API)를 사용한 분산 트랜잭션(Global Transaction)을 사용했습니다.
- 개념: 트랜잭션 관리자(Transaction Manager)가 두 개 이상의 리소스(예: Oracle A, Oracle B)를 묶어서, “둘 다 성공하지 않으면 둘 다 롤백”시키는 방식입니다.
- 제약사항: 하지만 이 방식은 참여하는 모든 시스템(DB, ERP 등)이 XA 프로토콜을 지원해야 합니다.
- 한계:
- SAP JCo는 기본적으로 XA 트랜잭션을 완벽하게 지원하기 어렵거나 설정이 매우 복잡합니다.
- 마이크로서비스(MSA)나 REST API 환경에서는 2PC를 사용할 수 없습니다.
- 성능 저하(Locking)가 심합니다.
따라서 현대적인 시스템 연계, 특히 레거시와 신규 시스템이 혼재된 환경에서는 2PC보다는 애플리케이션 레벨의 보정 처리를 더 선호합니다.
전략 2. 현실적인 해결책: 보상 트랜잭션 (Saga Pattern의 응용)
“일단 내 거 먼저 저르고, 실패하면 되돌린다.” 이것이 보상 트랜잭션의 핵심 아이디어입니다.
구현 시나리오 (Java 코드 레벨)
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
@Service
public class AssetPurchaseService {
@Transactional
public void purchaseAsset(AssetDto assetDto) {
// 1. [로컬 트랜잭션] 자산 정보 생성 (Status: 'TEMP')
assetRepository.insertAsset(assetDto);
try {
// 2. [원격 호출] ERP 전표 생성 (별도 트랜잭션 또는 외부 통신)
String docNo = sapService.createDoc(assetDto);
// 3. [로컬 업데이트] 전표 번호 업데이트 및 상태 확정 (Status: 'CONFIRMED')
assetRepository.updateAssetStatus(assetDto.getId(), "CONFIRMED", docNo);
} catch (Exception e) {
// ★ 중요: ERP 호출 실패 시 보상 트랜잭션 수행
log.error("ERP 연동 실패, 자산 정보 롤백 수행", e);
// RuntimeException을 던져서 1번 과정(insertAsset)을
// Spring @Transactional이 자동 롤백하게 하거나,
// 별도의 상태 업데이트 로직을 수행함
throw new RuntimeException("ERP 연동 실패로 인한 롤백");
}
}
}
이 방식의 맹점과 해결책
위 코드는 insertAsset이 아직 커밋되지 않은 상태에서 ERP를 호출하므로, DB 락(Lock)을 오래 잡고 있을 수 있습니다. 더 나은 방법은 상태 기반의 비동기 처리입니다.
- 자산 데이터를 ‘전송 대기(READY)’ 상태로 DB에 저장하고 트랜잭션 종료.
- 사용자에게는 “신청되었습니다” 응답.
- 백그라운드 스케줄러(또는 배치)가 ‘READY’ 상태 데이터를 조회하여 ERP 전송.
- 성공 시 ‘완료(DONE)’, 실패 시 ‘오류(ERROR)’로 업데이트.
이 방식(Eventual Consistency, 결과적 일관성)이 사용자 반응 속도도 빠르고 시스템 간 결합도(Coupling)를 낮추는 운영의 지혜입니다.
전략 3. 멱등성(Idempotency) 보장
네트워크 오류로 TimeOut이 났는데, 사실 ERP 쪽에서는 성공했을 수도 있습니다. 이때 재전송(Retry)을 하면 전표가 중복 생성될 위험이 있습니다.
이를 방지하기 위해 반드시 유니크한 Transaction ID(요청 번호)를 생성해서 보내야 합니다.
- 나(자산): 요청 ID
REQ-20251115-001을 담아 보냄. - 상대(ERP):
REQ-20251115-001로 이미 처리된 내역이 있다면, 신규 생성 대신 기존 결과를 리턴.
인터페이스 설계 시 이 멱등성 개념을 포함하느냐 아니냐가 시니어와 주니어의 차이를 만듭니다.
마무리하며
분산 트랜잭션은 정답이 없는 문제입니다. 데이터 무결성이 최우선이라면 성능을 희생하더라도 엄격한 검증 로직을 넣어야 하고, 가용성이 중요하다면 사후 보정 방식을 택해야 합니다.
PL(Project Leader)이나 아키텍트 역할을 수행한다면, 개발자들에게 “그냥 @Transactional 붙이면 돼요”가 아니라, “외부 연동 실패 시 우리 데이터는 어떻게 되는가?”를 끊임없이 질문하고 가이드해야 합니다.
기술적인 고민은 여기까지. 이제 시선을 조금 돌려보겠습니다. 이 모든 기술도 결국은 고객의 요구사항을 만족시키기 위한 도구일 뿐입니다. 다음 시간에는 개발자의 영원한 숙제, [고객의 모호한 요구사항을 명확한 기술 명세로 바꾸는 커뮤니케이션 기술]에 대해 이야기해 보겠습니다.