ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 다중 데이터소스 설정 및 트랜잭션 동기화
    spring/JPA 2021. 5. 23. 17:53

    배경

    서비스 규모가 작다면 한 애플리케이션에서 보통 한 DB리소스만 사용한다. 어떤 경우는 둘 이상의 DB접근이 필요한 경우도 있고 서로 다른 두 DB서비스를 논리적인 트랜잭션 단위로 묶어 작업을 원자적으로 처리할 필요가 있다. 심지어 DB가 아니더라도 JMS와 같은 서비스 또한 한 트랜잭션 단위로 묶일 필요가 있다. 이번 글에서 위 2가지 경우를 예제로 작성해보고 정리해보자.

     

    1. 한 애플리케이션에서 두개의 DB 연결

    2. 두 DB의 작업을 트랜잭션으로 묶어서 처리하기

     

     

    배경지삭 쌓고 가기

    - 로컬 트랜잭션 : 하나의 DB커넥션 안에서 말들어지는 트랜잭션

    - 트랜잭션 경계설정 : autoCommit를 false로 설정한 뒤 commit 또는 rollback을 원하는 시점에 호출

    - PlatformTransactionManager : 각 DB의 transactionManager를 추상화하여 DI를 이용하면 상황에 맞게 적절한 트랜잭션 전략을 구사할 수 있도록 스프링이 제공하는 서비스

    한 애플리케이션에서 두개의 DB연결

    - API : JPA + HIbernate

    - DB : h2, mysql

     

     

    - @PropertySource에 설정파일을 지정함으로써 Environment에 설정파일 값이 들어간다

    - @EnableJpaRepository 

    1) basePackages : 패키지 기준으로 설정 레파지토리 범위를 정한다.

    2) entityManagerFactoryRef : 레파지토리에서 사용할 엔티티메니저를 지정한다.

    3) transactionManagerRef : 레파지토리에서 사용할 트랜잭션매니저를 지정한다.

    - @Primary : 동일한 타입의 빈에 대해서 주입 우선권을 부여한다.

     

    H2를 설정할 때와 똑같이 mysql로 데이터소스와 트랜잭션매니저, 엔티티메니저 빈을 등록했다. 이제 각각 엔티티와 레포지토리를 만들고 서비스로직에서 실행한 뒤 잘 저장되었는지 확인해보자.

     

    - Member 엔티티와 레포지토리는 각 DB별로 다른 패키지에 생성했다. Member는 빈 등록 대상이 아니어서 중복이름을 고려하지 않아도 되지만 MemberRepository는 빈 등록대상이어서 빈의 이름을 DB별로 따로 설정해주었다.

     

    눈으로 직접 확인해보기 위해 테스트에 롤백을 하지 않고 실제 DB에 작업하고 확인해보았다. 잘 저장된 모습을 확인할 수 있다. 

     

    한 애플리케이션에서 DB를 여러개 사용하더라도 여러개의 데이터소스를 등록하고 API스팩에 맞게 원하는 데이터소스 및 트랜잭션매니저를 설정해주면 잘 사용할 수 있는 걸 확인할 수 있다.

     

     

    두 DB의 작업을 한 트랜잭션으로 묶어서 처리하기

    우선 스프링의 마법과 같은 힘으로 그냥 선언적 트랜잭션만으로도 두 작업이 원자적으로 묶이는지 확인해보자.

     

    Member는 name필드가 Unique로 설정되어 yw란 이름의 Member를 저장한 상태에서 다시 저장하려한다면 예외가 발생할 것이다. 다만, h2의 예외가 발생하기 전 mysql의 save는 이미 호출이 된 상태고 이 두 작업이 트랜잭션으로 묶여 있다면 mysql의 save도 롤백되어 있을 것이다.

     

    save()처리 중에 예외가 발생한 걸 확인할 수 있고 H2에는 처음에 들어간 데이터만 있고 추가 데이터는 롤백된 걸 확인할 수 있다. 다만, mysql은 롤백되지 않고 데이터가 저장된 걸 확인할 수 있는데 이는 두 DB간의 트랜잭션이 원자적으로 묶이지 않았다는 걸 알 수 있다. 이제 두 트랜잭션이 묶어보자.(되겠지..?)

     

     

    ChainedTransactionManager

    우선 트랜잭션매니저를 만들 때 데이터소스를 등록한 엔티티매니저를 등록하는 걸 확인할 수 있다. 즉, 트랜잭션 속성도 동일한 데이터소스에서 가져온 트랜잭션끼리만 통한다는 걸 추측할 수 있고 위와 같이 완전히 다른 DB의 트랜잭션은 따로 뭔가를 설정해줘야 함을 알 수 있다. 우선 가장 간편하지만 deprecated된 ChainedTransactionManager를 사용해보자.

     

    원하는 대로 한 쪽에서 롤백이 발생한 트랜잭션에서 다른 쪽 또한 롤백이 발생되었다. 다만, 이 방식은 체인 방식으로 두 트랜잭션의 완전한 원자성을 보장하지 못하고 에러발생 가능성을 확인해 적절히 체이닝의 순서를 정해야 한다.

     

    T1 시작 -> T2 시작 -> 서비스 -> T2 커밋 -> T1 롤백

    이런 경우 T2를 롤백할 방법이 없다. deprecated된 이유도 이렇게 나온다.

     

    AtomikosJta를 이용한 글로벌 트랜잭션 설정

    Hibernate 시스템에서 DataSource를 등록할 때 각각 DataSource에 의존한 EntityManager를 이용해 TransactionManager를 등록했다. 이제 각 DataSource에 의존한 TransactionManager가 아닌 JTA인터페이스를 이용한 추상화한 방법으로 트랜잭션을 관리해보자. 인터넷 자료가 많지 않고 설정이 제각각이어서 정확한 설명을 하기 어려운 부분이 있다. 우선 된다 정도만 참고하고 나중에 자세히 공부하자.

     

    JTA 구현체 중 하나인 atomikos 의존성을 추가하자

     

    우선 DataSource는 각 DB별로 JTA를 위한 전용 DataSource를 사용해야 한다. H2인 경우 jdbcDataSource를, Mysql인 경우 MysqlXADataSource를 사용한다. 

     

    이제 전역으로 사용할 트랜잭션 매니저를 등록한다. JTA전용 DataSource를 사용한다면 이 트랜잭션 매니저를 사용하게 되어 있다. 글로벌 트랜잭션으로 모든 트랜잭션을 동기화한다. (자세한 코드 설명은 본인도 긴가민가하므로 일단 생략)

     

    마치며

    아키텍처를 어떻게 추상화했으며 글로벌 트랜잭션을 위해 DataSource를 어떻게 처리할지 아주 대략적으로 그림이 그려진다. 다만, 하이버네이트의 데이터소스 및 엔티티매니저 관리 등 모르는 게 많고 글로벌 트랜잭션 관련해서는 좀 더 정복해야할 산인 것 같다. 부족한 점을 알았다는 것과 원하는 결과를 얻었다는 점에서 일단은 만족하자.

    참고

    https://www.fabiomaffioletti.me/blog/2014/04/15/distributed-transactions-multiple-databases-spring-boot-spring-data-jpa-atomikos/

     

     

     

     

     

     

    댓글

Designed by Tistory.