조금 평범한 개발 이야기

JPA 다중 데이타소스 관리 (2) 본문

개발/JPA 다중 데이타소스 관리

JPA 다중 데이타소스 관리 (2)

jogeum 2018. 9. 6. 14:51

트랜잭션

데이타를 사용하고 관리할때 가장 중요하게 고려되어야 하는 것은 데이타의 무결성을 어떻게 보장 할 것인가 입니다.

이를 위해 데이타베이스에서는 트랜잭션 이라는 작업 단위를 제공 하는데 트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 영속성(Durability) 등의 속성을 통해 데이타의 무결성을 보장해 줍니다.

  • 원자성 : 트랜잭션 작업 단위는 전부 실행이 되던지 아니면 전부 실패되어야 합니다.
  • 일관성 : 특정한 조건(데이타 길이, 데이타 타입, Null 여부 등)이 지정된 데이타는 도중에 데이타가 변조되지 않아야 합니다.
  • 격리성 : 하나의 트랜잭션에서 다루고 있는 데이타는 동시에 다른 트랜잭션에서 사용할 수 없습니다.
  • 영속성 : 트랜잭션이 성공적으로 완료된 데이타는 안정적으로 데이타베이스에 저장/관리 됩니다.

 

Spring 에서는 데이타소스에 지정된 트랜잭션 매니저를 이용해 트랜잭션을 사용할 수 있는데 @Transactional 이라는 어노테이션이 지정된 함수 범위 안에서 예기치 못한, 혹은 의도된 문제가 발생 되었을때 트랜잭션 매니저가 알아서 데이타를 롤백 처리를 진행해 줍니다.

이와 반대로 @Transactional 작업 단위가 정상적으로 끝난다면 트랜잭션 매니저가 commit 을 통해 데이타를 영속화 시킵니다.

@Transactional(rollbackFor = Exception.class)
public void execute() {
    throw new RuntimeException("강제로 에러를 발생시켜 데이타를 롤백 !!");
}

 

다중 데이타소스에서 트랜잭션 처리의 어려움

하지만 다중 데이타 소스를 사용할때는 보다 복잡한 트랜잭션 처리를 해야 하는데 이것은 Spring 이 어떤 데이타 소스에서 트랜잭션을 처리해야 되는지를 알 수 없기 때문이죠. 만약 데이타소스가 2개이고 트랜잭션 매니저가 2개인데 @Transactional 을 지정했다면 @Primary 로 우선권을 가지는 트랜잭션 매니저가 실행이 됩니다. 결국 하나의 데이타는 정상적인 트랜잭션 범위안에 포함 될 수 없다는 이야기 입니다.

그렇기 때문에 다중 데이타 소스에서 트랜잭션 관리를 해야 한다면 데이타 소스에서 직접 Connnection 을 가지고 와서 수동으로 트랜잭션 처리를 해주는 방법이 있습니다만 이는 생각보다 소스 코드의 양도 많아지고 시점 관리가 어려워 지는 문제점이 있습니다.

public void execute() throws Exception {
	Connection con1 = null, con2 = null;
	try {
		con1 = dataSource1.getConnection();
		con2 = dataSource2.getConnection();

        //뭔가를 실행
        
		con1.commit();
		con2.commit();

	} catch (Exception e) {
		
        con1.rollback();
		con2.rollback();
        
	} finally {
		if (con1 != null) con1.close();
		if (con2 != null) con2.close();
	}
}

 

ChainedTransactionManager

이런 부분 때문에 다중 트랜잭션 처리를 위해 Spring 에서는 ChainedTransactionManager 라는 구현체를 제공해 주고 있는데요. 데이타 소스마다 수동으로 트랜잭션을 관리 해야 되는 어려움과 복잡함을 간단히 처리할 수 있게 기능을 제공하고 있습니다.

실제 구현되어 있는 내용을 분석해 보면 수동으로 트랜잭션을 처리하는 것과 크게 다르지 않습니다. 결국 내부적으로 사용되는 방식은 JDBC API 를 통해 데이타를 관리 되기 때문입니다.

좀더 자세히 ChainedTransactionManager 를 살펴 보면 객체 생성시 주입 받은 각각의 트랜잭션 매니저들을 List 에 담아 두고 있다가 Rollback 혹은 Commit 상황이 발생될때 트랜잭션 매니저를 담아두고 있는 List 를 반복 하면서 Rollback, Commit 처리를 일괄 진행해 줍니다.

public class ChainedTransactionManager implements PlatformTransactionManager {
    ...
 
    public ChainedTransactionManager(PlatformTransactionManager... transactionManagers) {
        this(SpringTransactionSynchronizationManager.INSTANCE, transactionManagers);
    }
 
    ChainedTransactionManager(SynchronizationManager synchronizationManager, PlatformTransactionManager... transactionManagers) {
        ...
        this.transactionManagers = Arrays.asList(transactionManagers);
    }
 
    public void commit(TransactionStatus status) throws TransactionException {
        ...
        Iterator var6 = this.reverse(this.transactionManagers).iterator();
 
        while(var6.hasNext()) {
            PlatformTransactionManager transactionManager = (PlatformTransactionManager)var6.next();
            if (commit) {
                try {
                    multiTransactionStatus.commit(transactionManager);
                } catch (Exception var9) {
                    commit = false;
                    commitException = var9;
                    commitExceptionTransactionManager = transactionManager;
                }
            } else {
                try {
                    multiTransactionStatus.rollback(transactionManager);
                } catch (Exception var10) {
                    LOGGER.warn("Rollback exception (after commit) (" + transactionManager + ") " + var10.getMessage(), var10);
                }
            }
        }
        ...
    }
 
    public void rollback(TransactionStatus status) throws TransactionException {
        ...
        Iterator var5 = this.reverse(this.transactionManagers).iterator();
 
        while(var5.hasNext()) {
            PlatformTransactionManager transactionManager = (PlatformTransactionManager)var5.next();
 
            try {
                multiTransactionStatus.rollback(transactionManager);
            } catch (Exception var8) {
                if (rollbackException == null) {
                    rollbackException = var8;
                    rollbackExceptionTransactionManager = transactionManager;
                } else {
                    LOGGER.warn("Rollback exception (" + transactionManager + ") " + var8.getMessage(), var8);
                }
            }
        }
        ...
    }
 
    ...
}

 

ChainedTransactionManager 설정

실질적으로 프로젝트에서 사용하기 위해 앞선 글에서 다중 데이타 소스를 생성할때 같이 생성한 트랜잭션 매니저들을 주입 받아서 ChainedTransactionManager 객체를 생성합니다.

이때 ChainedTransactionManager@Primary 어노테이션을 지정하여 프로젝트에 가장 우선순위가 높은 대표 트랜잭션 매니저임을 지정해 둡니다.

실제 서비스에서 사용할때는 단일 데이타 소스를 사용할때와 동일하게 @Transactional 을 사용해 트랜잭션을 지정합니다. 이때 사용되는 트랜잭션 매니저는 ChainedTransactionManager 가 될 것이며 데이타 소스 종류와 상관없이 동일한 트랜잭션 단위로 묶이게 될 것입니다.

@Configuration
public class ChainedTxConfig {
 
    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(PlatformTransactionManager txManager1, PlatformTransactionManager txManager2) {
        return new ChainedTransactionManager(txManager1, txManager2);
    }
}

 

다음글에서는 최종적으로 테스트를 진행해 다중 데이타소스 상에서 정상적인 데이타 커밋과 롤백이 이루어지는지를 확인해 보겠습니다.


Comments