트랜잭션이란?
데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다.
메소드에 트랜잭션을 적용시키면 메소드가 수행하는 작업의 단위가 된다.
메소드 실행 중에 생기는 쿼리들은 프록시 객체에 저장되고 완료되면 기존 객체와 변화한 점을 확인 후 데이터베이스에 한꺼번에 커밋한다.
스프링 트랜잭션 사용 방식
선언적 트랜잭션 관리 vs 프로그래밍 방식의 트랜잭션 관리
- 선언적 트랜잭션 관리 : @Transaction 어노테이션 하나만 선언해서 트랜잭션을 적용하는 것
- 프로그래밍 방식의 트랜잭션 관리 : 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것
프로그래밍 방식의 트랜잭션 관리를 사용하게 되면 애플리케이션 코드가 트랜잭션 코드와 강하게 결합된다.
그리고 선언적 트랜잭션 관리가 간편하기 때문에 실무에서는 선언적 트랜잭션 관리를 사용한다.
선언적 트랜잭션과 AOP
@Transaction을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용된다.

트랜잭션을 처리하기 위한 프록시를 적용하면 트랜잭션을 처리하는 객체와 비지니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
@Transactional 을 메서드나 클래스에 붙이면 해당 객체는 트랜잭션 AOP 적용의 대상이 되고, 결과적으로
실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다. 그리고 주입을 받을 때도 실제 객
체 대신에 프록시 객체가 주입된다.
예시로 클라이언트가 @Autowired BasicService basicService 로 의존관계 주입을 요청하면 스프링 컨테이너에는 실제 객체 대신에 프록시가 스프링 빈으로 등록되어 있기 때문에 프록시를 주입한다.
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
위와 같이 @Transactional이 메소드에만 적용되어 있는 경우에도 Service 객체는 실제 객체 대신에 프록시 객체가 주입된다.
트랜잭션의 적용위치
클래스에 적용한 어노테이션은 메소드에 자동 적용된다.
그렇다면 클래스와 메소드 동시에 어노테이션이 적용되어 있다면 어떻게 적용될까?
스프링에서 우선순위는 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
이러한 우선순위는 같은 어노테이션에서 옵션을 달리 줄때 중요하다. 트랜잭션은 다양한 옵션을 사용할 수 있어 우선순위를 따지는 것이 중요하다.
1. 클래스의 메소드(우선순위 높음)
2. 클래스의 타입
3. 인터페이스의 메소드
4. 인터페이스의 타입(우선순위 낮음)
프록시 내부 호출 문제

@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall() {
callService.external();
}
@TestConfiguration
static class InternalCallV1Config {
@Bean
CallService callService() {
return new CallService();
}
}
@Slf4j
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
트랜잭션이 적용되어있는 internal()은 정상 작동되나 external()에서 불러온 internal()에서는 트랜잭션이 작동안하는 오류가 발생한다.
external() 호출을 순서대로 분석해보자.
1. 클라이언트인 테스트 코드는 callService.external() 을 호출한다. 여기서 callService 는 트랜잭션 프록시이다.
2. callService 의 트랜잭션 프록시가 호출된다.
3. external() 메서드에는 @Transactional 이 없다. 따라서 트랜잭션 프록시는 트랜잭션을 적용하지않는다.
4. 트랜잭션 적용하지 않고, 실제 callService 객체 인스턴스의 external() 을 호출한다.
5. external() 은 내부에서 internal() 메서드를 호출한다. 여기서 문제가 발생한다.
자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다.
결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데, 여기서 this 는 자기 자신을 가리키므로, 실제 대상 객체( target )의 인스턴스를 뜻한다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 트랜잭션을 적용할 수 없다. 결과적으로 target 에 있는 internal() 을 직접 호출하게 된 것이다.
따라서 스프링 트랜잭션 AOP는 메서드 내부 호출에 프록시를 적용할 수 없다.
이 문제를 해결하는 가장 간단한 방법은 internal() 메소드를 별도의 클래스로 분리하는 것이다.
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
void externalCallV2() {
callService.external();
}
@TestConfiguration
static class InternalCallV2Config {
@Bean
CallService callService() {
return new CallService(innerService());
}
@Bean
InternalService innerService() {
return new InternalService();
}
}
@Slf4j
@RequiredArgsConstructor
static class CallService {
private final InternalService internalService;
public void external() {
log.info("call external");
printTxInfo();
internalService.internal();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
@Slf4j
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
변경사항
- InternalService 클래스를 만들고 internal() 메서드를 여기로 옮겼다.
- 이렇게 메서드 내부 호출을 외부 호출로 변경했다.
- CallService 에는 트랜잭션 관련 코드가 전혀 없으므로 트랜잭션 프록시가 적용되지 않는다.
- InternalService 에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용된다.

변경된 external() 호출을 순서대로 분석해보자.
1. 클라이언트인 테스트 코드는 callService.external() 을 호출한다.
2. callService 는 실제 callService 객체 인스턴스이다.
3. callService 는 주입 받은 internalService.internal() 을 호출한다.
4. internalService 는 트랜잭션 프록시이다. internal() 메서드에 @Transactional 이 붙어있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
5. 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 을 호출한다.
'Java > Spring' 카테고리의 다른 글
그거 아셨나요? - Reflection과 기본 생성자가 필요한 이유 (0) | 2024.08.10 |
---|---|
트랜잭션(@Transaction) (2) - 트랜잭션 옵션 (0) | 2024.08.07 |
그거 아셨나요? - 체크 예외(Check Exception) (0) | 2024.07.28 |
Spring @어노테이션 (0) | 2024.07.28 |
Spring - 스프링 빈 (0) | 2024.07.23 |