plugins {
id 'java'
id 'org.springframework.boot' version '3.1.9'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 테스트에서 lombok을 사용하기 위한 의존성 추가
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
tasks.named('bootBuildImage') {
builder = 'paketobuildpacks/builder-jammy-base:latest'
}
tasks.named('test') {
useJUnitPlatform()
}
package com.example.springtx.apply;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
BasicService basicService;
@TestConfiguration
static class TxApplyBasicConfig {
@Bean
BasicService basicService() {
return new BasicService();
}
}
@Slf4j
static class BasicService {
@Transactional
public void tx() {
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("txActive: {}", txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("txActive: {}", txActive);
}
}
@Test
void proxyCheck() {
log.info("aop class= {}", basicService.getClass().getName());
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Test
void txTest() {
basicService.tx();
basicService.nonTx();
}
}
로그
2024-03-21T10:58:14.706+09:00 INFO 51573 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : call tx
2024-03-21T10:58:14.706+09:00 INFO 51573 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : txActive: true
2024-03-21T10:58:14.707+09:00 INFO 51573 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : call nonTx
2024-03-21T10:58:14.707+09:00 INFO 51573 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : txActive: false
@Transactional 애노테이션이 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시 객체를 만들어서 IoC Container에 등록합니다. 실제 basicService 객체 대신에 프록시인 basicService$$CGLIB를 스프링 빈에 등록합니다.
그리고 프록시는 내부에 실제 basicService를 참조하게 됩니다. 여기서 핵심은 실제 객체 대신에 프록시가 스프링 컨테이너에 등록되었다는 점입니다.
txBasicTest는 프록시 객체에 요청하게 되고 프록시 객체가 프록시에 수행할 로직 사이에 실제 서비스의 call부분을 호출하게 됩니다. 프록시 객체는 실제 서비스를 상속한 서비스이기 때문에 txBasicTest에서 호출할때에는 프록시 객체에 요청하는지 모릅니다.
트랜잭션 로깅을 확인하기 위해 application.yml에 설정합니다.
logging:
level:
org.springframework.transaction.interceptor: TRACE
트랜잭션 로깅후 로그
2024-03-21T11:07:30.686+09:00 TRACE 51808 --- [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.springtx.apply.TxBasicTest$BasicService.tx]
2024-03-21T11:07:30.686+09:00 INFO 51808 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : call tx
2024-03-21T11:07:30.686+09:00 INFO 51808 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : txActive: true
2024-03-21T11:07:30.686+09:00 TRACE 51808 --- [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.springtx.apply.TxBasicTest$BasicService.tx]
2024-03-21T11:07:30.687+09:00 INFO 51808 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : call nonTx
2024-03-21T11:07:30.687+09:00 INFO 51808 --- [ Test worker] c.e.s.apply.TxBasicTest$BasicService : txActive: false
txBasicTest에서 basicService.tx()를 호출하게 되면 프록시 서비스 내에서 트랜잭션 대상인지 확인하고, 트랜잭션 대상이면 트랜잭션을 활성화해줍니다.
basicService.nonTx()는 트랜잭션 대상이 아니기 때문에 트랜잭션 활성화하지 않습니다.
테스트 코드
package com.example.springtx.apply;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired CallService callService;
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService();
}
}
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("txActive: {}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly: {}", readOnly);
}
}
@Test
void proxyCheck() {
log.info("aop class= {}", callService.getClass().getName());
assertThat(AopUtils.isAopProxy(callService)).isTrue();
}
@Test
void callInternal() {
callService.internal();
}
}
callInternal test
@Test
void callInternal() {
callService.internal();
}
callInternal test 로그
2024-03-21T11:18:43.576+09:00 TRACE 52078 --- [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.springtx.apply.InternalCallV1Test$CallService.internal]
2024-03-21T11:18:43.577+09:00 INFO 52078 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : call internal
2024-03-21T11:18:43.577+09:00 INFO 52078 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : txActive: true
2024-03-21T11:18:43.577+09:00 INFO 52078 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : readOnly: false
2024-03-21T11:18:43.577+09:00 TRACE 52078 --- [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.springtx.apply.InternalCallV1Test$CallService.internal]
callExternal test
@Test
void callExternal() {
callService.external();
}
callExternal test 로그
2024-03-21T11:19:59.290+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : call external
2024-03-21T11:19:59.290+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : txActive: false
2024-03-21T11:19:59.290+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : readOnly: false
2024-03-21T11:19:59.291+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : call internal
2024-03-21T11:19:59.291+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : txActive: false
2024-03-21T11:19:59.291+09:00 INFO 52106 --- [ Test worker] c.e.springtx.apply.InternalCallV1Test : readOnly: false
callInternal은 트랜잭션이 적용되었고,
callExternal은 트랜잭션이 적용안된것을 로그를 통해 파악했습니다.
@Transcational 이 하나라도 있으면 트랜잭션 프록시 객체가 만들어집니다. 그리고 callService 빈을 주입 받으면 트랜잭션 프록시 객체가 대신 주입됩니다.
class에 애노테이션이 없고, internal 메서드에 @Transactional이 붙었고, external 메서드에는 @Transactionl이 붙어있지 않습니다.
이 경우에는 프록시 객체에서 트랜잭션 적용이 안된상태로 실제 callService가 호출되었습니다.
추가로 external() 내부에서 internal()를 호출합니다. 이미 트랜잭션이 적용 안된 상태로 실제 callService에서 호출되기 때문에 interal()에서도 트랜잭션 적용이 안됩니다.
java에서는 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기자신의 인스턴스를 가리킵니다.
테스트 코드
package com.example.springtx.apply;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService(internalService());
}
@Bean
InternalService internalService() {
return new InternalService();
}
}
@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("txActive: {}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly: {}", readOnly);
}
}
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("txActive: {}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly: {}", readOnly);
}
}
@Test
void proxyCheck() {
log.info("aop class= {}", callService.getClass().getName());
assertThat(AopUtils.isAopProxy(callService)).isTrue();
}
@Test
void callExternalV2() {
callService.external();
}
}
테스트 코드 로그
2024-03-21T11:34:49.711+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : call external
2024-03-21T11:34:49.711+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : txActive: false
2024-03-21T11:34:49.711+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : readOnly: false
2024-03-21T11:34:49.728+09:00 TRACE 52506 --- [ Test worker] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.springtx.apply.InternalCallV2Test$InternalService.internal]
2024-03-21T11:34:49.728+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : call internal
2024-03-21T11:34:49.728+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : txActive: true
2024-03-21T11:34:49.728+09:00 INFO 52506 --- [ Test worker] c.e.springtx.apply.InternalCallV2Test : readOnly: false
2024-03-21T11:34:49.728+09:00 TRACE 52506 --- [ Test worker] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.springtx.apply.InternalCallV2Test$InternalService.internal]