Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- @Aspect
- 스프링 고급 - 스프링 aop
- 스프링 db1 - 스프링과 문제 해결
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch2
- 스프링 mvc1 - 서블릿
- 스프링 입문(무료)
- 게시글 목록 api
- 자바의 정석 기초편 ch14
- 타임리프 - 기본기능
- 스프링 mvc2 - 검증
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch12
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch9
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 로그인 처리
- 2024 정보처리기사 시나공 필기
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch7
Archives
- Today
- Total
나구리의 개발공부기록
스프링 트랜잭션 이해, 스프링 트랜잭션 소개 및 프로젝트 생성, 트랜잭션 적용 확인 및 위치, 트랜잭션 AOP 주의 사항(프록시 내부 호출 / 초기화 시점) 본문
인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 2편 - 데이터 접근 활용
스프링 트랜잭션 이해, 스프링 트랜잭션 소개 및 프로젝트 생성, 트랜잭션 적용 확인 및 위치, 트랜잭션 AOP 주의 사항(프록시 내부 호출 / 초기화 시점)
소소한나구리 2024. 9. 24. 14:00 출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
1. 스프링 트랜잭션 소개 및 프로젝트 생성
- 스프링 DB1편 스프링과 문제 해결 - 트랜잭션 강의에서 배운 내용을 간략히 복습
- https://nagul2.tistory.com/308
- https://nagul2.tistory.com/309
1) 스프링 트랜잭션 추상화
- 각각의 데이터 접근 기술들은 트랜잭션을 처리하는 방식에 차이가 있었음
- JDBC 기술을 사용하다가 JPA 기술로 변경하게 되면 트랜잭션을 사용하는 코드도 모두 함께 변경했어야 하는데 스프링이 제공하는 PlatformTransactionManager라는 인터페이스를 통해 트랜잭션을 추상화하여 서로다른 기술을 동인한 방식으로 사용할 수 있었음
- 트랜잭션은 시작, 커밋, 롤백으로 단순화게 추상화가 가능해 졌으며, 실무에서 주로 사용하는 데이터 접근 기술에 대한 트랜잭션 매니저의 구현체도 제공하여 개발자는 필요한 구현체를 스프링 빈으로 등록하고 주입 받아서 사용하면 됨
- 여기에 더해서 스프링 부트는 어떤 데이터 접근 기술을 사용하는지를 자동으로 인식해서 적절한 트랜잭션 매니저를 선택해서 스프링빈으로 등록해주므로 트랜잭션 매니저를 선택하고 등록하는 과정조차도 생략할 수 있음
- JdbcTemplate, MyBatis를 사용하면 DataSourceTransactionManager(스프링 5.3 부터는 기능이 확장된 JdbcTransactionManager를 제공함), JPA를 사용하면 JpaTransactionManager를 스프링 빈으로 등록해줌(JpaTransactionManager 는 JdbcTemplate, MyBatis도 지원)
2) 스프링 트랜잭션 사용 방식
- PlatformTransactionManager를 사용하는 2가지 방법
(1) 선언적 트랜잭션 관리(Declarative Transaction Management)- 99% 사용
- @Transaction 애노테이션 하나만 선언해서 편리하게 트랜잭션을 적용하는 것
- 해당 로직에 적용하겠다고 어딘가에 선언만 하면 트랜잭션이 적용되는 방식
- 훨씬 간편하고 실용적이기 때문에 실무에서 대부분 선언적 트랜잭션 관리를 사용함
(2) 프로그래밍 방식의 트랜잭션 관리(Programmatic Transaction Management)
- 트랜잭션 매니저 or 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것
- 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합하게 됨
3) 선언적 트랜잭션과 AOP
- @Transactional을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용됨
- 프록시 도입 전에는 트랜잭션을 처리하기 위해 서비스의 로직에서 트랜잭션을 시작했지만 프록시를 적용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리 할 수 있음
- 트랜잭션은 커넥션에 con.setAutocommit(false)을 지정하면서 시작함
- 같은 트랜잭션을 유지하려면 같은 데이터베이스 커넥션을 사용해야하는데 스프링 내부에서 트랜잭션 동기화 매니저가 사용되어 커넥션을 관리하며 대부분의 데이터 접근 기술들은 트랜잭션 유지를 위해 트랜잭션 동기화 매니저를 통해 리소스(커넥션)을 동기화 함
4) 스프링이 제공하는 트랜잭션 AOP
- 스프링의 트랜잭션은 전세계 누구나 다 사용할 정도로 매우 중요한 기능이기에 스프링은 트랜잭션 AOP를 처리하기위한 모든 기능을 제공함
- 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기위한 스프링 빈들도 자동으로 등록해주기 때문에 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션을 처리하는 프록시를 적용해줌
5) 프로젝트 생성
- 테스트에서 lombok을 사용하기위해 build.gradle에 의존관계 추가 후 gradle 리프레쉬
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
- 앱 실행 후 로그에 Started SpringtxApplication in 1.043 seconds (process running for 1.301)와 같이 Startd가 뜨면 성공
2. 트랜잭션 적용 확인
- @Transactional을 통해 선언적 트랜잭션 방식을 사용하면 애노테이션 하나로 트랜잭션을 적용할 수 있지만 트랜잭션 관련 코드가 눈에 보이지 않고 AOP를 기반으로 동작하기 때문에 실제 트랜잭션이 적용되고 있는지 아닌지를 가시적으로 확인하기 어려운데 테스트 코드를 통해 적용 여부를 확인
1) TxBasicTest - 트랜잭션 적용 여부를 확인하기 위한 코드
- TransactionSynchronizationManager.isActualTransactionActive(): 트랜잭션이 적용되면 true 반환, 미적용되면 false 반환
package hello.springtx.apply;
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
BasicService basicService;
@Test
void proxyCheck() {
log.info("ap[ class={}", basicService.getClass());
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Test
void txTest() {
basicService.tx();
basicService.nonTx();
}
@TestConfiguration
static class TxAppBasicConfig {
@Bean
BasicService basicService() {
return new 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);
}
}
}
(1) 테스트 실행 결과 로그
- proxyCheck()의 로그에 $$SpringCGLIB&&0 처럼 프록시가 적용된 것이 확인할 수 있음
- txText()의 로그에 tx()는 트랜잭션이 적용되고 nonTx()는 트랜잭션이 적용 안된 로그를 확인할 수 있음
(2) proxyCheck() 실행
- AopUtils.isAopProxy(): @Transactional을 메서드나 클래스에 붙히면 해당 객체는 트랜잭션 AOP 적용의 대상이되어 결과적으로 실제 객체 대신에 트랜잭션을 처리해 주는 프록시 객체가 스프링 빈에 등록되고 주입 받을 때도 실제 객체 대신에 프록시 객체가 주입됨
- 여기에서의 핵심은 실제 BasicService객체 대신에 프록시 객체가 스프링빈에 등록되고 프록시객체는 내부에 실제 basicService를 참조한다는 것
- 클라이언트인 txBasicTest가 스프링 컨테이너에 @Autowired BasicService basicService로 의존관계 주입을 요청하면 스프링 컨테이너는 실제 객체 대신 프록시가 스프링 빈으로 등록되어있으므로 프록시를 주입함
- 프록시는 BasicService를 상속해서 만들어지기 때문에 다형성을 활용할 수 있어서 실제 객체 대신 프록시를 주입할 수 있음
- @Transactional은 클래스 대상으로 만들어지기 때문에 메서드에만 붙어있던 클래스에 붙어있던 프록시는 만들어짐
** 프록시는 스프링 핵심원리 고급편에서 자세히 다룸
(3) txText()
- application.properties에 아래의 코드를 추가하면 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확하게 로그로 확인할 수 있음
logging.level.org.springframework.transaction.interceptor=TRACE
- tx()
- 클라이언트가 tx()를 호출
- 프록시의 tx()가 호출되고 @Transactional이 붙어있으므로 트랜잭션 적용 대상이기 때문에 트랜잭션이 시작
- 실제 basicService.tx()를 호출하고, 호출이 끝난 후 프록시로 제어가 돌아옴
- 프록시는 트랜잭션 로직을 커밋하거나 롤백해서 트랜잭션을 종료함
- nonTx()
- 클라이언트가 nonTx()호출
- 프록시의 nonTx()가 호출되고 @Transactional이 없으므로 트랜잭션을 시작하지않고 실제 basicService.nonTx()를 호출하고 종료함
- 해당 로그를 보면 tx() 메서드 호출시에는 트랜잭션 프록시가 트랜잭션을 시작하고 완료한 내용을 확인할 수 있는데 nonTx()에는 프록시 호출 로그가 없음
3. 트랜잭션 적용 위치
- 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가짐
- 이것을 기억하면 스프링에서 발생하는 대부분의 우선순위를 쉽게 기억할 수 있음(더 구체적인 것이 더 높은 우선순위를 가지는 것은 상식적으로 자연스러움)
- 예를 들어 메서드와 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 메서드가 더 높은 우선순위를 가지며, 인터페이스와 인터페이스를 구현한 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 클래스가 더 높은 우선순위임
- @Transactional에 옵션을 적용하여 우선순위와 적용 위치를 파악할 수 있음
1) TxLevelTest
- TransactionSynchronizationManager.isCurrentTransactionReadOnly: 적용된 트랜잭션의 readOnly 옵션의 값을 반환함
(1) @Transactional의 규칙
- 우선순위 규칙
- 클래스에 적용하면 메서드는 자동 적용
(2) 우선순위
- 트랜잭션을 사용할 때는 다양한 옵션을 사용할 수 있는데, 옵션을 주었을때와 주지 않았을 때의 우선순위를 비교
- LevelService 클래스에 @Transactional(readOnly = true)를 적용
- write() 메서드에 @Transactional(readOnly = false)를 적용(디폴드 값이므로 옵션을 안써도 됨)
- 클래스와 클래스의 메서드에 @Transactional이 각각 다른 옵션으로 적용되어있으면 메서드가 더 구체적이므로 메서드의 @Transactional(readOnly = false)가 붙은 트랜잭션이 먼저 적용됨
- 참고로 쓰기 전용 트랜잭션은 없음
(3) 클래스에 적용하면 메서드는 자동 적용
- read(): 해당 메서드에는 @Transactional이 없는데, 이런 경우에는 더 상위인 클래스를 확인해서 @Transactional이 있는지 확인함
- 클래스에 @Transactional(readOnly = true)가 적용되어있으므로 읽기 전용 트랜잭션이 적용됨
package hello.springtx.apply;
@Slf4j
@SpringBootTest
public class TxLevelTest {
@Autowired
LevelService service;
@Test
void orderTest() {
service.write();
service.read();
}
@TestConfiguration
static class TxLevelTestConfig {
@Bean
LevelService levelService() {
return new LevelService();
}
}
@Slf4j
@Transactional(readOnly = true) // 읽기 전용 트랜잭션으로 생성
static class LevelService {
@Transactional(readOnly = false) // 기본값 -> 실무에선 안적음
public void write() {
log.info("call write");
printTxInfo();
}
public void read() {
log.info("call read");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();// 읽기 전용 트랜잭션인지 확인
log.info("tx readOnly={}", readOnly);
}
}
}
(4) 실행 결과
- 테스트 실행 로그를 확인해보면 메서드에 @Transactional이 붙은 write()0가 먼저 실행되어 읽기,쓰기 트랜잭션이 먼저 적용되어 출력됨
- 그 다음 read()에서 상위 클래스의 트랜잭션이 적용되어 읽기 전용 트랜잭션이 적용된 로그를 확인할 수 있음
2) 인터페이스에 @Transactional 적용
- 인터페이스에서 @Transactional을 적용할 수 있으며 메서드, 클래스, 인터페이스 중에서 가장 우선순위가 낮음
(1) 우선순위
- 클래스의 메서드
- 클래스의 타입
- 인터페이스의 메서드
- 인터페이스의 타입
- 인터페이스에 @Transactional을 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않은 방법임
- AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기 때문에 가급적 구체 클래스에 @Transactional을 사용하는 것을 권장함
** 참고
- 스프링 5.0에서 인터페이스에 @Transactional을 사용하는 방식을 많이 개선하였음
- 과거에는 구체클래스를 기반으로 프록시를 생성하는 CGLIB 방식을 사용하면 인터페이스에 있는 @Transactional을 인식하지 못했는데 이 부분을 개선하여 인터페이스에 있는 @Transactional도 인식 되도록 변경하였음
- 그러나 다른 AOP 방식에서 또 적용이 되지 않을 수 있기 때문에 가급적 공식 메뉴얼대고 구체 클래스에 @Transactional을 사용하는 것을 권장함
- CGLIB 방식은 스프링 핵심원리 고급편에서 다룸
4. 트랜잭션 AOP 주의사항 - 프록시 내부 호출1 ** 매우 중요함
** 참고
- 해당 내용은 스프링 핵심원리 고급편 13. 실무 주의사항 -프록시와 내부 호출 문제에서 다루는 내용과 같은 문제를 다루지만 여기에서는 트랜잭션 AOP의 관점에서 설명함
- 이렇게 다른 강의에서 한번더 다루는 이유는 실무에서 많이 만나는 상황이기도 하고 많은 개발자들이 이 문제를 이해하지 못해서 고통을 받고 있음
- 면접질문으로도 많이 나오는 내용임
1) @Transactional의 문제 발생
- @Transactional을 사용하면 스프링 트랜잭션 AOP가 적용되며 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용함
- 위에서 다룬 것처럼 @Transactional을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고 실제 객체를 호출해주기 때문에 트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체(Target)을 호출 해야함
- 만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 트랜잭션도 적용되지 않음
- AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록하고 스프링은 의존관계 주입시에 항상 실제 객체 대신에 프록시 객체를 주입함
- 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않음
- 그러나, 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생함
- 이렇게 되면 @Transactional이 있어도 트랜잭션이 적용되지 않음
- 이 문제는 실무에서 반드시 한번은 만나서 고생하는 문제이며, 이 문제로 이슈가 발생하면 파급효과가 너무 크기 때문에 꼭 이해하고 넘어가야함
1) InternalCallV1Test
- CallService에 트랜잭션이 없는 external()과 트랜잭션이 있는 internal() 메서드를 작성
- @Transactional이 하나라도 있으면 트랜잭션 프록시 객체가 만들어지고 callService 빈을 주입받으면 트랜잭션 프록시 객체가 대신 주입됨
- 트랜잭션이 없는 external()메서드는 내부에서 internal()메서드를 한번더 호출하도록 작성되어있음
- printProxy() 테스트를 실행해보면 CGLIB...이 붙은 프록시 객체가 주입받은 로그를 확인할 수 있음
package hello.springtx.apply;
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void internalCall() { // 트랜잭션이 적용된 internal() 호출
callService.internal();
}
@Test
void externalCall() {
/*
트랜잭션이 적용되지 않은 external() 호출
external()에 트랜잭션인 적용된 internal()을 호출함
internal()은 트랜잭션에 적용되는 것을 기대하고 실행
그러나 실행 결과는 트랜잭션이 적용되어있지 않음 -> 내부호출은 프록시를 거치지 않음
*/
callService.external();
}
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService();
}
}
@Slf4j
static class CallService {
// external()은 트랜잭션이 없음
public void external() {
log.info("call external");
printTxInfo();
internal(); // external()이 실행되면 internal()이 실행됨
}
// internal()은 트랜잭션이 있음
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
(1) internalCall() 실행
- 클라이언트인 테스트 코드가 트랜잭션 프록시인 callService의 internal()을 호출
- internal() 메서드에 @Transactional이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용함
- 트랜잭션 적용 후 실제 callService 객체 인스턴스의 internal()을 호출
- 실제 callService가 처리를 완료하면 응답이 트랜잭션 프록시로 돌아오고 트랜잭션 프록시는 트랜잭션을 완료함
- 테스트 실행 로그를 보면 transaction for ... 로 트랜잭션 프록시가 적용된 것을 확인할 수 있고 tx active = true로 트랜잭션도 적용된 것을 알 수 있음
- 여기까지는 바로 위에서 학습한 내용과 동일함
(2) externalCall() 실행 - 문제 발생
- 클라이언트인 테스트 코드가 트랜잭션 프록시인 callService의 external()을 호출함
- external() 메서드에는 @Transactional이 없으므로 트랜잭션 프록시는 트랜잭션을 적용하지 않고 실제 callService 객체 인스턴스의 external()을 호출함
- 호출된 실제 callService 객체의 인스턴스의 external()이 내부에서 internal() 메서드를 호출함 <- 여기서 문제가 발생
- 실행 결과 로그를 보면 트랜잭션 관련 코드가 전혀 보이지 않고, tx active = false로 트랜잭션도 적용되지 않았음
(3) 문제 원인
- 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리키는데, 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal()됨
- 여기서 this는 자기 자신을 가리키므로 트랜잭션 프록시객체가 아닌 실제 대상 객체(target)의 인스턴스를 뜻하며, 트랜잭션이 적용되지않은 실제 callService 객체의 internal()이 호출 된 것
- 결과적으로 이러한 내부 호출은 프록시를 거치지 않으므로 트랜잭션을 적용할 수 없음
(4) 프록시 방식의 AOP 한계
- @Transactional을 사용하는 트랜잭션 AOP는 프록시를 사용하기 때문에 메서드 내부 호출에 프록시를 적용할 수 없다는 것을 확인했음
- 복잡하지만 나머지를 하나도 건들지 않아도 되는 해결 방법은 스프링 핵심원리 고급편에서 다룰예정
- 가장 단순하고, 실무에서 많이 적용하는 방법으로는 internal() 메서드를 별도의 클래스로 분리하는 것
4. 트랜잭션 AOP 주의사항 - 프록시 내부 호출2 - 단순한 해결방법
- 메서드 내부 호출 때문에 트랜잭션 프록시가 적용되지 않았으니, 이를 해결하기 위해 internal() 메서드를 외부 호출로 사용하도록 별도의 클래스로 분리
1) InternalCallV2Test
- InternalService 클래스를 생성해서 internal() 메서드를 옮기고 스프링 빈으로 InternalService를 등록하고 주입
- external() 메서드에서 호출하는 internal()메서드는 이제 내부에서 호출되는 게 아니라 외부에서 주입받은 internalService.internal()이 호출됨
- CallService에는 트랜잭션 관련 코드가 전혀 없으므로 트랜잭션 프록시가 적용되지 않고 external()를 호출하고, InternalService에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용되고 internal()를 호출함
package hello.springtx.apply;
@Slf4j
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void externalCallV2() {
callService.external();
}
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService(internalService());
}
@Bean
InternalService internalService() {
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);
}
}
}
(1) 흐름 분석 및 결과
- 클라이언트인 테스트 코드는 callService.external()을 호출하며 callService는 프록시가 아닌 실제 callService 객체임
- callService는 주입받은 internalService.internal()을 호출
- InternalService의 internal() 메서드에 @Transactional이 붙어있으므로 internalService는 프록시로 등록되고 주입되며 트랜잭션도 적용함
- 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal()을 호출함
- 로그를 보면 external() 메서드의 로그에서는 프록시 관련 로그도 없고 tx active = false 로 트랜잭션도 적용 되지 않았음
- internal() 메서드의 로그는 InternalService로 실행되었고, 프록시로 실행된 로그도 확인할 수 있으며 tx active = true로 트랜잭션도 적용되었음
** 참고
- 여러가지 다른 해결방안도 있지만 보통 실무에서는 이렇게 별도의 클래스로 분리하는 방법을 주로 사용함
- 더 다양한 해결 방안은 스프링 핵심원리 고급편에서 다룸
2) public 메서드만 트랜잭션 적용 -> 스프링 부트 3.0 부터는 private에만 트랜잭션이 적용안됨
- 스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있었느나 스프링 부트 3.0 부터는 private을 제외한 public, protected, defualt(package-visible) 접근제한자인 경우에는 트랜잭션이 적용됨
- 지금은 공식 사이트에서 스프링 부트 2.x 버전은 생성할 수 없어서 보통은 스프링 부트 3.0을 사용하겠지만, 다운그레이드해서 3.0 미만 버전을 사용하는 경우에는 클래스 레벨에 @Transactional이 있을 경우 public 메서드에만 트랜잭션이 적용됨
@Transactional
public class Hello {
public method1(); // 트랜잭션 적용됨
method2(): // 스프링부트 3.0부터 트랜잭션 적용됨
protected method3(); // 스프링부트 3.0부터 트랜잭션 적용됨
private method4(); /* 트랜잭션 적용 안됨 */
}
** 참고
- 과거에는 트랜잭션을 의도하지 않는 곳 까지 트랜잭션이 과도하게 적용될 것을 우려하여 외부에 열어둔 public 메서드에만 트랜잭션을 적용한 것으로 보이나 protected, package-visible도 외부에서 호출이 가능하기 때문에 스프링 부트 3.0부터는 변경된 것으로 보임
- 트랜잭션이 적용되지 않았을 때는 예외가 발생되는 것은 아니고 트랜잭션 적용만 무시가 됨
5. 트랜잭션 AOP 주의사항 - 초기화 시점
- 스프링 초기화 시점에 트랜잭션 AOP가 적용되지 않을 수 있음
1) InitTxTest
(1) init1()
- @PostConstruct로 스프링 빈 등록 초기화 -> 트랜잭션과 관련된 AOP가 적용이 안될 수 있음
- @Transactional로 트랜잭션을 적용
(2) itni2()
- @EventListener(ApplicationReadyEvent.class)로 애플리케이션이 완전히 초기화 되고 애플리케이션이 준비되면 이벤트 발생
- @Transactional로 트랜잭션 적용
package hello.springtx.apply;
@Slf4j
@SpringBootTest
public class InitTxTest {
@Autowired Hello hello;
@Test
void go() {
hello.init1(); // 직접 호출하면 트랜잭션 적용 됨
}
@Test
void go2() {
// 초기화 코드는 스프링이 초기화 시점에 호출함
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct
@Transactional
public void init1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void init2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
}
2) 테스트 실행
(1) go()
- 이렇게 클라이언트가 직접 hello.init1()을 호출하면 빈이 다 띄워지고 애플리케이션이 모두 준비되고 다시 init1()을 호출하기때문에 트랜잭션이 적용됨
- 최종적으로 적용된 것이지 로그를 살펴보면 애플리케이션 초기화시점에는 @PostConstruct tx =false로 로그가 찍혀있는것으로 확인했을때 트랜잭션이 적용되지 않았었음을 알 수 있음
(2) go2()
- init1(), init2() 호출 메서드 없이 단순히 이벤트와 초기화로 트랜잭션이 적용되는지 확인
- 실행 결과를 확인해보면 @PostConstruct를 적용해서 초기화를 한 itni1()메소드는 트랜잭션이 적용이 안되었음
- 하지만 @EventListener(ApplicationReadyEvent.class) 를 적용하면 스프링 빈이 완전히 초기화되고 애플리케이션이 최종 준비를 마친 뒤 이벤트를 발생시키기 때문에 init2()메소드의 로그에는 트랜잭션이 적용된 것으로 확인이 됨
3) 정리
- 초기화 코드가 먼저 호출되고 그 다음에 트랜잭션 AOP가 적용되기 때문에 초기화 시점에는 해당 메서드에서 트랜잭션을 획득 할 수 없음
- 가장 확실한 대안으로는 @EventListener(ApplicationReadyEvent.class)를 사용해서 트랜잭션 AOP를 포함한 스프링 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출하면 됨