트랜잭션·데이터 정합성 · Transaction

DB Link 제거 후 복수 DataSource를 한 트랜잭션으로 관리하기

DB Link 대신 각 DB에 직접 연결하고 매퍼를 분리한 뒤, 조건에 따라 참여하는 데이터소스를 하나의 Spring 트랜잭션 선언으로 관리하도록 Atomikos 기반 JTA를 구성했습니다.

Spring JTAAtomikosMyBatisXA DataSource

DB Link를 없애자 트랜잭션 책임이 이동했습니다

기존 다운로드 시스템은 한 DB에서 다른 DB의 일부 데이터를 DB Link로 조회했습니다. 코드에서는 하나의 접근 경로처럼 보였지만, 실제 실행은 로컬 DB와 원격 DB, 둘 사이의 네트워크에 모두 기대고 있었습니다.

DB Link를 걷어내고 두 DB에 직접 연결하자 접근 대상은 분명해졌습니다. 대신 어떤 요청이 어느 데이터소스를 쓰는지, 두 DB가 함께 참여할 때 트랜잭션을 어디까지 묶을지를 애플리케이션에서 책임져야 했습니다. 요청마다 커넥션 풀과 SessionFactory를 만들던 문제도 이때 함께 정리했습니다.

조건에 따라 서로 다른 매퍼를 사용하는 서비스의 트랜잭션을 어떻게 관리해야 할까요?

DataSource와 Mapper를 먼저 분리했습니다

각 DB 연결은 별도의 DataSource, SessionFactory와 Mapper 영역을 갖도록 구성했습니다. 아래 이름은 공개 설명을 위한 가명입니다.

DataSource와 Mapper를 먼저 분리했습니다 데이터 표
주 데이터 영역보조 데이터 영역
PrimaryDataSourceConfigSecondaryDataSourceConfig
primaryDataSourcesecondaryDataSource
primarySessionFactorysecondarySessionFactory
primaryMappersecondaryMapper

아래 코드는 실제 동작이 드러나도록 클래스·테이블·설정 이름을 바꿔 줄인 예시입니다.

@MapperScan(
    basePackages = "example.primary",
    sqlSessionTemplateRef = "primarySessionTemplate"
)
class PrimaryDataSourceConfig {
    // DataSource와 SessionFactory를 애플리케이션 단위로 구성한다.
}

SessionFactory마다 접속 대상을 고정하니 설정 실수와 Mapper 오접근을 찾기 쉬워졌습니다.

같은 서비스에서도 참여하는 리소스가 달라졌습니다

DB 선택은 URL 하나로 고정되지 않았습니다. 주문 체계, 업무 유형과 법인 조건에 따라 같은 서비스가 서로 다른 매퍼를 선택했습니다. 대부분의 처리는 한 번에 하나의 데이터소스만 사용했고, 모든 요청이 두 DB를 동시에 갱신한 것은 아닙니다.

boolean useLegacySource = routingPolicy.useLegacySource(orderKey);

List<OrderItem> items = useLegacySource
    ? primaryMapper.findItems(orderKey)
    : secondaryMapper.findItems(orderKey);

if (useLegacySource) {
    primaryMapper.markDownloaded(orderKey);
} else {
    secondaryMapper.markDownloaded(orderKey);
}

어느 DB가 트랜잭션에 참여할지는 메서드가 실행된 뒤 업무 조건에 따라 정해졌습니다. 데이터소스별 로컬 트랜잭션 매니저를 애너테이션에 고정하면 이 분기를 공통 서비스 선언으로 표현하기 어려웠습니다.

검토한 대안과 선택 기준

검토한 대안과 선택 기준 데이터 표
대안장점한계
DataSource별 로컬 트랜잭션단일 DB 처리에 단순함실행 중 매퍼 분기와 선언한 매니저가 어긋날 수 있음
서비스를 DB별로 완전 분리대상이 명확함공통 업무 규칙과 법인 분기가 중복될 수 있음
여러 로컬 트랜잭션 조합구성은 단순해 보임표준 XA의 원자적 완료·복구 모델과 다름
Atomikos 기반 JTA실제 참여한 XA 리소스를 하나의 Spring 트랜잭션으로 관리XA 설정과 운영 복잡도가 증가

데이터소스가 여러 개라고 무조건 JTA가 필요한 것은 아닙니다. 이 시스템에서는 같은 서비스가 실행 중인 업무 조건에 따라 다른 매퍼를 선택하면서도 공통 @Transactional 선언을 유지해야 했습니다. 단일 DB만 쓰는 처리에도 같은 관리 방식이 적용된다는 부담은 있지만, 두 XA 리소스가 함께 쓰이는 경우까지 하나의 선언으로 다룰 수 있다는 쪽을 택했습니다.

Atomikos와 XA DataSource의 역할

두 데이터소스는 Atomikos가 관리하는 XA 리소스로 구성하고 서로 다른 이름을 부여했습니다. 아래 이름은 공개 설명을 위한 가명입니다.

AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("primaryResource");
dataSource.setXaDataSourceClassName(driverClassName);
dataSource.setXaProperties(connectionProperties);
  • XA 데이터소스는 DB가 XA 리소스로 참여할 수 있는 연결을 제공합니다.
  • Atomikos는 XA 연결을 풀로 관리하고 리소스를 트랜잭션 매니저에 등록합니다.
  • Spring JTA는 @Transactional의 시작·완료와 참여 리소스 등록을 연결합니다.
  • MyBatis SessionTemplate은 각 매퍼가 지정된 데이터소스의 세션을 사용하게 합니다.

실제 실행에서 하나의 매퍼만 사용하면 해당 리소스만 참여합니다. 두 매퍼를 모두 사용하는 처리에서만 두 XA 리소스가 같은 JTA 트랜잭션에 함께 참여할 수 있습니다.

운영 설정과 점검 기준

내부 설정 키, 리소스 이름, 로그 경로와 세부 타임아웃·풀 설정값은 운영 정보이므로 공개하지 않습니다.

운영에서는 다음 항목을 확인해야 합니다.

  • 리소스 이름이 환경별로 충돌하지 않습니까?
  • 사용하지 않는 데이터소스가 실행에 참여하지 않습니까?
  • 연결 검증 방식이 DB 종류와 맞습니까?
  • 여러 인스턴스가 같은 복구 로그를 동시에 소유하지 않도록 설정했습니까?
  • 법인별 분기가 비활성 리소스를 호출하지 않습니까?

JTA가 되돌리지 못하는 작업

서비스에는 DB 처리뿐 아니라 ZIP 생성, 파일 삭제와 외부 전송도 있었습니다. 파일 시스템과 일반 파일 전송은 XA 리소스가 아니므로 JTA가 자동으로 되돌리지 못합니다.

JTA가 롤백할 수 있는 범위
  → 참여한 XA DB 변경

별도 보상 설계가 필요한 범위
  → 생성한 파일
  → 삭제한 파일
  → 외부 시스템으로 전송한 파일

따라서 파일 작업은 실행 순서와 임시 파일 관리, 재시도·보상·멱등성을 따로 설계해야 했습니다.

검증과 남은 트랜잭션 한계

DNA2 전체 기능 테스트는 20개 시나리오·557개 케이스로 진행했고, 처음 실패한 9건을 수정한 뒤 모두 통과했습니다. 이 557개가 모두 JTA만을 위한 테스트였던 것은 아닙니다. 전체 회귀 테스트 안에서 다중 데이터소스 설정과 조건별 매퍼 선택, 반복 요청 뒤 풀 추가 생성 여부, 기존 조회 결과가 유지되는지를 함께 확인했습니다.

두 DB를 함께 갱신하다 한쪽을 일부러 실패시켜 전체 롤백을 확인한 전용 테스트 기록은 남아 있지 않습니다. 확인한 것은 조건별 매퍼 선택, 반복 요청 뒤 풀 재생성 방지와 기존 조회 결과 유지까지입니다.

정리

DB Link를 없애면 숨어 있던 DB 선택과 트랜잭션 책임이 애플리케이션으로 이동합니다. 이 사례에서는 업무 조건에 따라 참여 리소스가 달라진다는 점을 기준으로 Atomikos를 선택하고, 데이터소스·매퍼·트랜잭션의 관계를 함께 정리했습니다.

TECHNICAL SERIES

DNA2 레거시 현대화

DB 연결 생명주기부터 기존 요청 형식, 다국어 처리, 입력 검증과 런타임 충돌까지 이어지는 현대화 기록입니다.

시리즈 전체 보기

현재 2/6

  1. 1요청마다 Connection Pool과 SessionFactory를 만들면 생기는 문제
  2. 2DB Link 제거 후 복수 DataSource를 한 트랜잭션으로 관리하기
  3. 3레거시 URL의 언어 규칙을 Spring Locale로 연결하기
  4. 4축약 파라미터는 유지하고 DTO 필드명은 명확하게 바꾸기
  5. 5같은 DTO에서 API마다 다른 필수값을 검증하기
  6. 6JVM 클래스 로딩 로그로 추적한 Oracle JDBC 드라이버 충돌