본문 바로가기
서버/JPA

AbstractRoutingDataSource 를 통한 DB replication 환경 설정과 Spring Data JPA에서의 Transactional 이슈 해결을 위해 스프링 구현 코드를 까보는 (약간은 바보 같은) journey..

by 베어 그릴스 2023. 10. 22.
320x100

문제 상황

Spring Data Jpa에서 메서드 명만 작성하면 알아서 jpql을 만들어내주는 신기한 기능을 많이들 써봤을 것이다. 나 또한 그랬고, 이번에도 어김없이 사용하고 있었다. 다만, 현재 하고 있는 프로젝트에서는 현재 트랜잭션이 read-only일 시에는 read-only DB,  데이터 변경 관련 트랜잭션일 경우에는 write 전용 DB를 바라보게 하고 있었다. (전형적인 DB replication 구조)

 

이때 클라이언트 단에서 post로 데이터를 작성하고, 직후 get을 통해 데이터를 조회할 시 read DB와 write DB의 동기화 시간 차이로 인한 문제로 not found exception이 발생했고 새로고침하면 동기화가 되어 다시 데이터가 정상적으로 조회되는 상황이었다.

 

해결?

사실 해결방법은 간단했다. get할 때 트랜잭션의 read-only를 꺼서 write DB에서 조회를 하게 하면 됐다.

 

AbstractRoutingDataSource 를 통한 DB replication 환경 설정

우선, 천천히 환경 설정부터 보자.

스프링 데이터 JPA에서는 아래와 같은 방식으로 현재 트랜잭션이 ReadOnly인지 알 수 있다.

TransactionSynchronizationManager.isCurrentTransactionReadOnly()

 

위 정보를 통해 DataSource 빈을 정의해주면 된다.

우리는 Routing이 필요하므로 AbstractRoutingDataSource() 를 상속 받는 클래스를 하나 만들어준다.

public class DataSourceRouting extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) "read" else "write"
	}
}

 

이제, 위 클래스를 사용하여 DataSource를 재정의해주면 아래와 같이할 수 있다.

	@Bean
	@Primary
	public DataSource dataSource() {
		DataSourceRouting dataSourceRouting = new DataSourceRouting();
		dataSourceRouting.setTargetDataSources(targetDataSources());
		dataSourceRouting.setDefaultTargetDataSource(readDataSource());
		return dataSourceRouting;
	}

	private Map<Object, Object> targetDataSources() {
		Map<Object, Object> targetDataSources = new HashMap<>();
		targetDataSources.put("write", writeDataSource());
		targetDataSources.put("read", readDataSource());
		return targetDataSources;
	}
    
    // writeDataSource() , readDataSource() 생성 ...

 

이제 구현된 환경 설정으로 트랜잭션이 read면 readDB, write면 writeDB를 바라볼 수 있다!

 

 

다시 문제로

자 각설하고 원래 문제를 다시 상기하자.

원래 Spring Data Jpa를 사용해서 구현한 나의 코드는 다음과 같았다.

@Transactional(readOnly = true)
Order findById(id: Long)

 

위 findById가 현재는 전에 설명한 라우팅 설정으로 인한 read-only DB를 바라보고 있는 상태이기 때문에 writeDB를 바라보게 해야한다.

 

처음에는 단순히.. 저 위에 명시해놓은 @Transactional(readOnly = true) 를 지우면 되겠지 라고 생각했는데 디버깅을 해보니 여전히 read-only는 true 상태였다.

 

사실 그냥 @Transactional을 붙이면 해결되겠지라는 막연한 생각이 있었지만, '왜 아무것도 없는 Default 상태에서까지 read-only true인가?, 어디에서 read-only true를 걸어주는가?' 를 알고 넘어가고 싶어서 저 메서드가 실행될 때 Transaction에 read-only 설정이 도대체 언제 붙는가 디버깅 해보았다.

 

접근법

트랜잭션을 실행하려면 적절한 DataSource로부터 커넥션을 얻어와야한다. 즉, 트랜잭션을 실행하기 위해서는 위에 언급했던 TransactionSynchronizationManager.isCurrentTransactionReadOnly()이 불릴 수 밖에 없다.

 

TransactionSynchronizationManager의 안쪽으로 들어가 디버깅을 시작해보자.

자, 이렇게 디버깅 포인트를 찍어두고 어디서 read-only가 찍히는지 한번 확인해보자.

위에서 currentTransactionReadOnly를 set해주는 쪽에서 메서드 콜 스택을 쭉 보면 어디서 read-only가 설정되는지 알 수 있을 것이다.

 

좌측 아래의 콜스택들을 찬찬히 보면서 read only 설정이 어디서 되는건지 살펴보자.

 

콜스택을 하나하나 눌러보며 쭉 따라가보면 invokeWithInTransaction에서 readOnly라고 설정되는 것을 볼 수 있다.

이제 다시 readOnly가 설정되는 부분인 txAttr 부분에 브레이킹 포인트를 찍고 쭉 들어가보자.

 

위 메서드에서 getTransactionAttribute를 통해 지금 트랜잭션이 readOnly인지 아닌지 정보를 가져온다.

 

쭉 들어가보면 해당 메서드의 트랜잭션 정보에 대해 캐싱된 것이 있는지 확인하고 지금은 당연히 첫 실행이므로 트랜잭션 정보를 얻기 위해 더 들어간다.

 

즉, 우리는 computeTransactionAttribute를 확인해보면 된다.

 

computeTransactionAttribute 내부에 findTransactionAttribute까지 들어간다.

해당 메서드는 현재 메서드 위에 생성된 Transactional Annotation이 있는지 확인한다.

결과는 슬프게도 아직 null..

 

다음으로 이제 클래스에 어노테이션이 붙어있어 거기서 확인할 수 있는지 찾는다.

 

SimpleJpaRepository..? 사실 여기서 이미 직감이 왔다. 아.. JPARepository 구현체인 SimpleJpaRepository 의 상단에 어노테이션이 걸려있고, 그 정보를 가져오는구나.. 그래도 쭉쭉 들어가보자.

드디어 찾았다 이자식.. SimpleJpaRepository 클래스 상단에서 readOnly 정보를 가져오고 있었다.

 

구현체에 직접가서 확인까지 마쳤다..

 

위에서 봤다 싶히 우리가 메서드에 먼저 Transactional 어노테이션을 걸어준다면 그걸 우선적으로 확인해서 return하기 때문에 findById 메서드 상단에 @Transactional을 걸고 wirte db를 보도록 할 수 있었다.

 

결론

음..사실 Jpa에 대한 기본기가 조금만 탄탄했더라도 금방 알 수 있었던건데 디버깅을 깊게 해보다 보니 시간이 오래걸렸다.

다만, 이 시간이 아깝진 않았던게 직접 스프링 코드를 까보면서 aop가 진행되며 어떻게 메서드 단의 어노테이션에 우선순위를 높게 둬서 가져오는지, 트랜잭션의 정보가 어떻게 전달되는지 등을 보다 명확하게 알 수 있었기 때문이다.

시간이 된다면 뭔가 오류가 발생했을 때 이렇게 구현체까지 까보면서 보는 것은 상당히 도움이 되는 것 같긴 하다. (맞나..? ㅋㅋㅋ)

 

그래도 꼭 그냥 read only니까 Transactional 걸면 되겠지~ 하면서 넘어가지 말고 도대체 왜 이렇게 나오는지 확인해보는 습관을 들였으면 좋겠다. (다음번엔 구현체부터 보면 훨빨랐겠다..)

 

그냥 끄적끄적

디버깅도 그렇고 기본적인 클린코드 하는법도 그렇고 여러가지 단축키를 활용하는 법도 그렇고 요즘 상당히 기본기가 많이 부족하다는 느낌을 많이 받는다. 애초에 확인하지 않고 머리로만 개발하는 안좋은 습관을 들여놔서 더 그런거 같기도하다. 앞으로는 겉멋보다는 기본기를 더 갈고 닦는 시간을 많이 가져야지..

 

- 끝 -

 

728x90

'서버 > JPA' 카테고리의 다른 글

[JPA] 단방향, 양방향 연관관계 매핑과 주의점  (0) 2022.10.31
[JPA] 영속성 컨텍스트  (0) 2022.10.29