어떻게 트랜잭셕은 Read only 여부를 알 수 있을까?
- 개발
- 2026. 2. 22. 23:54
read-only일때 replica 사용하게 하기
master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-replica라는 용어로 더 많이 사용이
b-programmer.tistory.com
위 글에서 @Transactional(readOnly = true)인 경우에는 replica를 사용하게 하고, 그렇지 않은 경우에는 primary DB를 사용하게 코드를 작성하였습니다.
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected @Nullable Object determineCurrentLookupKey() {
boolean readOnly =
TransactionSynchronizationManager.isCurrentTransactionReadOnly();
String key = readOnly ? "REPLICA": "PRIMARY";
return key;
}
}
TransactionSynchronizationManager.isCurrentTransactionReadOnly()
다음 코드는
public static boolean isCurrentTransactionReadOnly() {
return (currentTransactionReadOnly.get() != null);
}
1. 현재 트랜잭션의 리드오니 여부를 가져옵니다.
2. 존재하는 경우 true, 존재하지 않는 경우 false를 리턴합니다.
여기서 두 가지 선택지가 존재합니다.
1. 어떻게 현재 트랙션션의 리드 오니 여부를 가져올 수 있는지 확인
2. get()은 어떻게 동작하는지 확인
1번부터 확인하겠습니다.
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
쓰레드 로컬에 저장을 하는 거 같은데 이게 뭘까요? 일단 간단하게 스레드 로컬에 대해 알아봅시다.
간단히 말해서 스레드마다 독립적인 값을 저장할 수 있게 해주는 객체라고 합니다.
무엇보다 특이한점은 NamedThreadLocal라는 것입니다.
그러니까 저 코드는 현재 스레드의 트랜잭션 readOnly 여부를 저장하는 ThreadLocal을 만들고
디버깅할 때 보기 쉽게 이름을 붙여놓은 것입니다. 그러니까 스레드 로컬인 게 중요한 거고 뒤 메시지는 확인 목적이라는 거죠.
어디에서 이 값을 설정하는 걸까요?
TransactionSynchronizationManager는 스프링에서 트랜잭션 관리를 위해 만들어놓은 클래스입니다.
그렇다는 소리는 스크롤을 내려다보면 저장하는 곳이 나오지 않을까 생각이 듭니다.
아래로 내려다보니 setCurrentTransactionReadOnly라는 부분이 존재하였습니다.
해당 코드는 다음과 같습니다.
public static void setCurrentTransactionReadOnly(boolean readOnly) {
currentTransactionReadOnly.set(readOnly ? Boolean.TRUE : null);
}
코드상으로 이해해 본다면, TransactionSynchronizationManager.setCurrentTransactionReadOnly(true);
로 입력하는 걸로 여겨집니다.
하지만 저는 단 한 번도 저 코드를 작성해 본 적이 없습니다. 대신 @Transactional(readOnly = true) 요 코드를 사용하였습니다.
TransactionSynchronizationManager.setCurrentTransactionReadOnly(true) 이 코드와
@Transactional(readOnly = true)의 관계는 무엇일까요?
TransactionSynchronizationManager.setCurrentTransactionReadOnly(true) 이 코드와
@Transactional(readOnly = true)의 관계
@Transactional(readOnly=true)가 붙은 메서드가 호출되면 스프링 AOP(트랜잭션 인터셉터)가 이를 가로채 TransactionManager로 트랜잭션을 시작합니다. 이 과정에서 readOnly 설정이 현재 스레드의 트랜잭션 상태로 반영되며, 내부적으로 TransactionSynchronizationManager의 ThreadLocal에 readOnly=true가 기록된다고 합니다.
그래서 저는 @Transactinal을 확인하기로 하였습니다.
/**
* A boolean flag that can be set to {@code true} if the transaction is
* effectively read-only, allowing for corresponding optimizations at runtime.
* <p>Defaults to {@code false}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction
* but rather silently ignore the hint.
* @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
boolean readOnly() default false;
대략적으로 해석해 보면, readonly는 힌트로 사용이 되며, readonly=true여도 쓰기가 실패가 되는 것이 아니라고 합니다.
그리고 트랜잭션 매니저에 따라 readOnly를 이해하지 못할 수 있다고 합니다. 그 경우는 무시가 됩니다.
관련주제가 있길래 보니 위에서 저희가 확인한 TransactionSynchronizationManager.isCurrentTransactionReadOnly()이 존재하였습니다. 그리고 동시에 org.springframework.transaction.interceptor.TransactionAttribut e.isReadOnly()도 함께 존재하였습니다.
개인적으로 이렇게 알려주는 이유는 분명히 있다고 생각합니다. 그것이 무엇일까요?
그 이유는 다음과 같습니다.
- TransactionAttribute.isReadOnly()는 어떻게 시작할지에 대한 설정
- TransactionSynchronizationManager.isCurrentTransactionReadOnly()는 지금 어떻게 실행 중인지에 대한 상태
하지만 아직도 그 중간을 이어주는 무언가는 보이지 않는 상태입니다. 그래서 TransactionAttribute의 위쪽을 확인해 봤습니다.
그랬더니 다음과 같은 메시지가 등장하였습니다.
/**
* Interface that defines Spring-compliant transaction properties.
* Based on the propagation behavior definitions analogous to EJB CMT attributes.
*
* <p>Note that isolation level and timeout settings will not get applied unless
* an actual new transaction gets started. As only {@link #PROPAGATION_REQUIRED},
* {@link #PROPAGATION_REQUIRES_NEW}, and {@link #PROPAGATION_NESTED} can cause
* that, it usually doesn't make sense to specify those settings in other cases.
* Furthermore, be aware that not all transaction managers will support those
* advanced features and thus might throw corresponding exceptions when given
* non-default values.
*
* <p>The {@linkplain #isReadOnly() read-only flag} applies to any transaction context,
* whether backed by an actual resource transaction or operating non-transactionally
* at the resource level. In the latter case, the flag will only apply to managed
* resources within the application, such as a Hibernate {@code Session}.
*
* @author Juergen Hoeller
* @since 08.05.2003
* @see PlatformTransactionManager#getTransaction(TransactionDefinition)
* @see org.springframework.transaction.support.DefaultTransactionDefinition
* @see org.springframework.transaction.interceptor.TransactionAttribute
*/
public interface TransactionDefinition { ... }
찬찬히 해석해 보면, 다음과 같습니다.
TransactionDefinition은 설정(정책)을 정의하는 인터페이스이고, 그 설정이 실제로 적용될지 여부는 전파 방식과 트랜잭션 매니저 구현에 따라 달라진다고 합니다.
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
위 코드는 PlatformTransactionManager -> getTransaction을 보면 유추가 가능합니다.
아직 잘 모르겠지만, 위에서 정의한 TransactionDefinition을 받아 사용이 되는 거 같습니다.
이를 해석해 보면
1. @Transactional(readOnly=true)와 같은 어노테이션을 읽습니다.
2. 스프링이 이를 TransactionTttribute로 변환합니다.
public interface TransactionAttribute extends TransactionDefinition
TransactionAttribute가 TransactionDefinition으로 만들어졌습니다.
3. 그리고 그것을 getTransaction(txAttr)에서 사용이 됩니다.
이 흐름대로라면,
getTransaction이 트랜잭션을 만드는 느낌입니다. PlatformTransactionManager이 인터페이스로 되어있기 때문에
이것이 아무래도 여러 플랫폼에 맞게 트랜잭션을 변환시켜 주는 모양입니다.
코드를 확인해 보니
PlatformTransactionManager -> AbstractPlatformTransactionManager -> JDBC, JPA....
요런 식으로 흐름이 이어지고 있습니다.
제가 확인하고 싶은 건 각 플랫폼마다 어떻게 readOnly 어떻게 해석하는지가 아니라
트랜잭션이 어떻게 readOnly를 알고 있는지에 대한 내용이기 때문에
AbstractPlatformTransactionManager을 확인해 보겠습니다.
AbstractPlatformTransactionManager
저희는 공통된 코드를 찾는 것이 목적이기 때문에, 밑으로 스크롤을 내리겠습니다. 생각보다 많군요.
그러면 다음과 같은 코드를 확인할 수 있습니다.
if (!definition.isReadOnly()) {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
throw new IllegalTransactionStateException("Participating transaction with definition [" +
definition + "] is not marked as read-only but existing transaction is");
}
}
코드 상에서 만약, 정의된 값이 즉 @Transactional(readonly=true)가 isReadOnly가 false라면 하위 코드가 실행이 되고
현재 트랜잭션의 ReadOnly가 true라면 예외를 터드리라는 메시지입니다.
그러니까 저희가 찾고 있는 코드는 아니라는 뜻이죠. 사실 모르겠습니다. 그래서 디버깅을 돌리기로 결정하였습니다.
API를 실행시켜 보겠습니다. 조금 이상한 부분이 있습니다.

targetDefinition은 readOnly가 true로 보이는데 transaction은 readOnly가 false입니다.
이로서 @Transactional()를 입력한다고 해도 트랜잭션이 실행이 안된다는 사실을 알게 되었습니다.
코드를 확인해 보니
startTransaction(AbstractPlatformTransactionManager) -> prepareSynchronization(AbstractPlatformTransactionManager)
순으로 동작하는 것을 알 수 있습니다.
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
definition.getIsolationLevel() : null);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
TransactionSynchronizationManager.initSynchronization();
}
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
요기에서 현재 @Transactional(readonly=true)의 값을 읽어서 여기에 세팅해주고 있습니다.
이제 @Transactional에서 트랜잭션값을 세팅하는 것을 알게 되었습니다.
이렇게 세팅했기 때문에 초반에 isCurrentTransactionReadOnly확인하였을 때 readOnly의 값이 true인지 알 수 있었습니다.
그렇다면, @Transactional값은 어떻게 읽을 수 있었을까요? 얘는 어노테이션일 뿐인데
이럴 땐 필터, 인터셉터, AOP를 이용해서 어노테이션을 읽습니다. 그래야 어노테이션이 붙은 정보들을 전부 읽을 수 있기 때문이죠.
(AOP에서 해당 코드를 읽는 코드는 생략합니다.)
filter vs interceptor vs AOP
스프링으로 개발하다 보면, "요청이 들어왔을 때 공통적으로 처리해야 하는 로직"을 어디에 둘지 고민하게 됩니다. 예를 들어 인증, 로깅, MDC 세팅, 트래픽 제어, 요청 검증 같은 것들입니다. 스
b-programmer.tistory.com
그렇다면 어째서 AOP를 선택하였을까요?
필터를 선택하기 위해서는 HTTP요청 이어야 합니다. 그리고 인터셉터는 스프링 MVC를 사용해야 사용할 수 있죠.
하지만 트랜잭션은 DB에서 사용됩니다. 그렇기 때문에 HTTP 요청도 스프링 MVC도 아니죠.
심지어 스프링이 존재하지 않아도 사용이 가능합니다. 그렇기 때문에 AOP로 해당 값을 읽게 된 것으로 추측합니다.
결론
@Transactional을 사용한다고 해서 해당 메서드에 자동으로 트랜잭션이 단순히 "붙는 것"은 아닙니다.
@Transactional은 단순한 메타데이터(설정) 일뿐이며, 스프링은 AOP를 통해 해당 어노테이션을 읽어 TransactionAttribute(= TransactionDefinition) 객체로 변환합니다. 이 객체는 PlatformTransactionManager#getTransaction()으로 전달되며,
트랜잭션을 시작하는 과정에서 AbstractPlatformTransactionManager 내부에서 현재 스레드의 실행 콘텍스트에 반영됩니다. 이 과정에서 readOnly 값은 ThreadLocal 기반의 TransactionSynchronizationManager에 저장됩니다. 따라서 이후 실행 중에 TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 호출하면, 현재 스레드에서 실행 중인 트랜잭션의 readOnly 여부를 확인할 수 있습니다.
'개발' 카테고리의 다른 글
| 도커 네트워크 이해해보기 (1) | 2026.02.25 |
|---|---|
| 502 게이트웨이를 없애보자. (이론편) (0) | 2026.02.24 |
| Redis 분산락은 왜 완전히 안전하다고 보기 어려울까? (0) | 2026.02.20 |
| EDA에도 패턴들이 이렇게나 많이 존재 하다니.. (0) | 2026.02.19 |
| 분산락을 적용해보자! (0) | 2026.02.18 |