스프링 트랜잭션
- JDBC로 트랜잭션을 사용하는 것과 JPA로 트랜잭션을 사용하는 코드는 다르기에 기술을 변경하면 코드도 변경해야 한다.
- 이런 문제를 해결하기 위해 스프링에서 PlatformTransactionManager 인터페이스를 통해 트랜잭션 추상화를 제공한다.
- 스프링은 트랜잭션 추상화 기능뿐만 아니라 트랜잭션 매니저의 구현체도 제공한다.
- 스프링 부트는 사용하는 데이터 접근 기술을 자동으로 인식해서 트랜잭션 매니저를 선택해서 스프링 빈으로 등록해주기에 트랜잭션 매니저를 선택하고 등록하는 과정도 생략할 수 있다.
- PlatformTransactionManager를 사용하는 방법 2가지
- 선언적 트랜잭션 관리
- @Transactional 어노테이션 하나만 선언해서 편리하게 트랜잭션을 적용하는 것
- 해당 로직에 트랜잭션을 적용하겠다라고 선언하기만 하면 트랜잭션이 적용되는 방식
- 프로그래밍 방식 트랜잭션 관리
- 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것
- 프로그래밍 방식의 트랜잭션 관리를 사용하면 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합됨
선언적 트랜잭션 관리는 훨씬 간편하고 실용적이기에 실무에서 대부분 사용하는 방식이다.
- 선언적 트랜잭션 관리
- @Transactional을 통한 선언적 트랜잭션 관리 방식을 사용하면 프록시 방식의 AOP가 적용된다.
- 트랜잭션을 처리하기 위한 프록시를 적용하면 트랜잭션을 처리하는 객체와 비지니스 로직을 처리하는 객체를 명확하게 분리할 수 있다.
- 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.
- 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 어노테이션만 붙여주면 된다.
트랜잭션 적용 확인
- @Transactional 어노테이션이 특정 클래스나 메소드에 하나라도 있으면 트랜잭션 AOP는 해당 클래스의 프록시를 만들어서 컨테이너에 등록한다.
- 의존 관계를 주입받을 때도 실제 객체 대신 프록시 객체가 주입된다.
- 프록시는 실제 클래스를 상속받아서 만들어지기 때문에 다형성을 활용할 수 있다.
- 실제 객체 대신 프록시 객체를 스프링 빈에 등록하며 프록시는 내부에서 실제 객체를 참조한다.
- 핵심은 실제 객체 대신 프록시가 스프링 컨테이너에 등록된다는 것이다.
- 의존 관계를 주입받을 때도 실제 객체 대신 프록시 객체가 주입된다.
@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired
BasicService basicService;
@Test
void proxyCheck() {
log.info("aop class={}", basicService.getClass());
assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Test
public void txTest() {
basicService.tx();
basicService.nonTx();
}
@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("tx active={}", txActive);
}
public void nonTx() {
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
- 트랜잭션 적용 여부를 확인하는 테스트 예제 코드
- proxyCheck()
- AopUtils.isAopProxy() : 프록시 여부를 확인할 수 있다.
- txTest()
- basicService.tx() 호출 => 프록시의 tx()가 호출
- tx() : @Transactional 어노테이션을 사용함으로 트랜잭션 적용 대상이 되며 BasicService 클래스의 프록시가 만들어지고 실제 BasicService 객체 대신 프록시 객체가 스프링 빈으로 등록된다.
- 트랜잭션을 시작 -> 실제 basicService.tx()를 호출 -> 트랜잭션 커밋/롤백 -> 트랜잭션 종료
- basicService.nonTx() 호출 => 프록시의 nonTx()가 호출
- nonTx() : @Transactional이 없으므로 트랜잭션 적용 대상이 아니기에 트랜잭션을 시작하지 않고 실제 basicService.nonTx()를 호출 후 종료한다.
- isActualTransactionActive() : 현재 쓰레드에 트랜잭션이 적용되어 있는지 여부를 확인한다.
- 트랜잭션 적용 여부를 가장 확실하게 확인할 수 있다.
- basicService.tx() 호출 => 프록시의 tx()가 호출
- proxyCheck()
logging.level.org.springframework.transaction.interceptor=TRACE
- application.properties 속성에 해당 내용 추가 시 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확하게 로그로 확인할 수 있다.
트랜잭션 적용 위치
- 스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
- ex) 메소드와 클래스에 어노테이션을 붙인다면 더 구체적인 메소드가 높은 우선순위를 가진다.
인터페이스와 클래스는 더 구체적인 클래스가 더 높은 우선순위를 가진다.
- ex) 메소드와 클래스에 어노테이션을 붙인다면 더 구체적인 메소드가 높은 우선순위를 가진다.
@SpringBootTest
public class TxLevelTest {
@Autowired
LevelService service;
@Test
public 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.isSynchronizationActive();
log.info("tx active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}", readOnly);
}
}
}
- 트랜잭션 적용 위치에 대한 테스트 예제 코드
- LevelService 클래스에 @Transactional(readOnly = true)가 있다.
- write()에 @Transactional(readOnly = false가 있다.
- 이때 클래스에는 readOnly 속성이 true지만 메소드는 속성이 false이다.
클래스보다 더 구체적인 메소드가 우선순위가 높기에 readOnly 속성은 false가 된다.
- 이때 클래스에는 readOnly 속성이 true지만 메소드는 속성이 false이다.
- read()에는 @Transactional이 없지만 상위인 클래스 계층에 @Transactional(readOnly = true)가 있기에 마찬가지로 트랜잭션이 적용되고 readOnly 속성은 true가 된다.
- TransactionSynchronizationManager.isCurrentTransactionReadOnly() : 현재 트랜잭션에 적용된 readOnly 옵션의 값을 반환한다.
- 내부 클래스에 추가로 @Slf4j를 사용함으로 외부 클래스와 별도로 로그를 관리할 수 있기 때문이다.
- 스프링 @Transactional 2가지 규칙
- 우선순위 규칙
- 더 구체적인 것이 우선순위가 높다.
- 클래스에 적용하면 메소드는 자동 적용
- 우선순위 규칙
- 인터페이스에 @Transactional 적용하는 경우
- 구현 클래스의 메소드 (우선순위가 가장 높다)
- 구현 클래스
- 인터페이스의 메소드
- 인터페이스 (우선순위가 가장 낮다)
- 인터페이스에 @Transactional을 사용하는 것은 AOP가 적용되지 않는 경우가 있기에 권장하지 않는 방식이다.
- 가급적으로 구체 클래스에 사용하는 것을 권장한다.
트랜잭션 AOP 주의 사항 - 프록시 내부 호출
- @Transactional 사용 시 스프링 트랜잭션 AOP가 적용된다.
- 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용한다.
- @Transactional 적용 시 프록시 객체가 요청을 받아서 트랜잭션을 먼저 처리하고 실제 객체를 호출한다.
- 트랜잭션 적용을 위해 항상 프록시를 통해 대상 객체(Target)을 호출해야 한다.
- AOP를 적용하면 스프링은 대상 객체 대신 프록시를 스프링 빈으로 등록하기에 의존관계 주입 시 실제 객체 대신 프록시 객체를 주입한다.
- 만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 트랜잭션도 적용되지 않는다.
- @Transactional이 있더라도 대상 객체 내부에서 메소드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
public void internalCall() {
callService.internal();
}
@Test
public void externalCall() {
callService.external();
}
@TestConfiguration
static class InternalCallV1TestConfig {
@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.isSynchronizationActive();
log.info("tx active={}", txActive);
}
}
}
- 대상 객체 내부에서 메소드 호출 문제 발생 예제 코드
- callService의 메소드인 internal()에 @Transactional이 사용되었기에 callService는 스프링 빈으로 프록시 객체가 등록된다.
- external()은 @Transactional이 없지만 내부에 @Transactional을 사용하는 internal()을 호출한다.
- internalCall() 동작 흐름
- callService.internal() 호출
- callService의 트랜잭션 프록시 호출
- @Transactional이 internal()에 사용되었기에 트랜잭션 적용
- 트랜잭션 적용 후 실제 callService 객체의 internal() 호출
- 실제 callService 객체가 처리를 완료하면 응답이 프록시 객체로 돌아오고 트랜잭션을 완료한다.
- externalCall() 동작 흐름
- callService.external() 호출
- callService의 트랜잭션 프록시 호출
- external()에는 @Transactional이 없기에 프록시는 트랜잭션을 적용하지 않는다.
- 트랜잭션을 적용하지 않고 실제 callService 객체의 external() 호출
- external()은 내부에서 internal()을 호출
- 자바에서는 메소드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다.
자기 자신의 내부 메소드를 호출하는 this.internal()을 호출 즉, 실제 대상 객체의 internal()이 호출된다.- 이런 내부 호출은 프록시를 거치지 않기에 트랜잭션을 적용할 수 없다.
- 자바에서는 메소드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다.
- 프록시 방식의 AOP 한계
- @Transactional을 사용하는 트랜잭션 AOP는 프록시를 사용하는데 프록시를 사용하면 메소드 내부 호출에 프록시를 적용할 수 없다.
- 가장 단순한 문제 해결 방법은 내부 호출을 피하기 위해 메소드를 별도의 클래스로 분리하는 방법이 있다.
- 다른 해결 방법도 있지만 복잡하기에 실무에서 주로 별도의 클래스로 분리하는 방법을 사용한다.
@Slf4j
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
public void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
public void externalCallV2() {
callService.external();
}
@TestConfiguration
static class InternalCallV1TestConfig {
@Bean
InternalService internalService() {
return new InternalService();
}
@Bean
CallService callService() {
return new CallService(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.isSynchronizationActive();
log.info("tx active={}", txActive);
}
}
@Slf4j
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isSynchronizationActive();
log.info("tx active={}", txActive);
}
}
}
- 대상 객체 내부에서 메소드 호출 문제 해결 예제 코드 (별도의 클래스로 분리)
- 이전 예제와 달리 @Transactional을 사용한 internal()을 별도의 클래스로 분리함으로 메소드 내부 호출을 외부 호출로 변경했다.
- CallService는 @Transactional이 없기에 프록시가 적용되지 않고 InternalService는 @Transactional에 의해 프록시가 적용된다.
- externalCallV2() 동작 흐름
- callService.external() 호출
- 실제 callService 객체가 호출
- internalService.internal() 호출
- internalService의 트랜잭션 프록시 호출
- internal()에 @Transactional을 사용했기에 트랜잭션 적용
- 실제 internalService 객체의 internal() 호출
- 스프링 트랜잭션 AOP 기능은 public 메소드에만 트랜잭션을 적용하도록 설정이 되어있다.
- protected, private, default에는 예외가 발생하지는 않고 트랜잭션 적용만 되지 않는다.
- 클래스 레벨에 트랜잭션을 적용하면 과도하게 모든 메소드에 트랜잭션이 걸릴 수 있기에 public 메소드에만 트랜잭션이 적용되도록 설정되어 있다.
트랜잭션 AOP 주의 사항 - 초기화 시점
- 스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.
- ex) 초기화 코드(@PostConstruct)와 @Transactional을 함께 사용하는 경우
=> 초기화 코드가 먼저 호출되고 그 다음 트랜잭션 AOP가 적용되기 때문에 초기화 시점에는 해당 메소드에서 트랜잭션을 획득할 수 없다.
- ex) 초기화 코드(@PostConstruct)와 @Transactional을 함께 사용하는 경우
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
public void go() {
//초기화 코드는 스프링이 초기화 시점에 호출한다.
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
}
- 스프링 초기화 시점에 트랜잭션 적용 예제 코드
- go()
- 초기화 코드를 테스트하는 것이기 때문에 초기화 코드는 별도로 호출해주지 않아도 초기화 시점에 스프링이 호출해주므로 직접 호출X
- initV1()
- @PostConstruct, @Transactional을 함께 사용
- 스프링 초기화 코드가 먼저 호출되기 때문에 트랜잭션이 적용되지 않는다.
- initV2()
- @EventListener(ApplicationReadyEvent.class) : 트랜잭션 AOP를 포함한 스프링 컨테이너가 완전히 생성되고 난 후 이벤트가 붙은 메소드를 호출해준다.
- @EventListener(ApplicationReadyEvent.class), @Transactional을 사용함으로 트랜잭션을 적용시킬 수 있다.
- 트랜잭션을 적용해야 하는 초기화 코드의 경우에는 @EventListener(ApplicationReadyEvent.class)를 사용하고 트랜잭션이 없는 초기화 코드의 경우 @PostConstruct를 사용하면 된다.
- go()
트랜잭션 옵션
public @interface Transactional {
String value() default "";
String transactionManager() default "";
Class<? extends Throwable>[] rollbackFor() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
String[] label() default {};
}
- 트랜잭션 옵션
- value, transactionManager : 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정할 때 둘 중 하나에 트랜잭션 매니저의 스프링 빈 이름을 적어주면 된다.
- @Transactional 어노테이션 속성이 하나인 경우 value는 생략하고 바로 값을 넣을 수 있다.
- 생략하는 경우 기본으로 등록된 트랜잭션 매니저를 사용하기에 보통 생략한다.
- 트랜잭션 매니저가 둘 이상인 경우에 트랜잭션 매니저의 이름을 지정해서 구분해야 한다.
- rollbackFor : 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할지 예외 클래스를 직접 지정할 수 있다.
- 스프링 트랜잭션의 기본 정책 : 언체크 예외(RuntimeException, Error)가 발생하면 롤백하고 체크 예외(Exception)가 발생하면 커밋한다.
- noRollbackFor : rollbackFor와 반대로 롤백하면 안되는 예외 클래스를 지정할 수 있다.
- propagation : 트랜잭션 전파에 대한 옵션이다. (추후 따로 정리)
- isolation : 트랜잭션 격리 수준을 지정할 수 있다.
- 기본 값은 DB에서 설정한 격리 수준을 따르는 DEFAULT로 대부분 DB에서 설정한 기준을 따른다.
- timeout : 트랜잭션 수행 시간에 따른 타임아웃을 초 단위로 지정한다.
- 기본 값은 트랜잭션 시스템의 타임아웃을 사용한다.
- label : 일반적으로 사용하지 않으며 트랜잭션 어노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용한다.
- readOnly : 트랜잭션은 기본적으로 읽기, 쓰기가 모두 가능한 트랜잭션이 생성되지만 readOnly = true 옵션을 사용하면 읽기 전용 트랜잭션이 생성되며 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다.
- readOnly 옵션 사용 시 읽기에서 다양한 성능 최적화가 발생할 수 있다.
- 상황에 따라, DB에 따라서 느려질 수도 있다.
- readOnly 옵션은 크게 3곳에서 적용된다.
- 프레임워크
- JdbcTemplate은 읽기 전용 트랜잭션의 경우 변경 기능을 실행하면 예외가 발생
- JPA는 커밋 시점에 플러시를 호출하지 않고 변경 감지를 위한 스냅샷 객체를 생성하지 않기에 최적화가 발생한다.
- JdbcTemplate은 읽기 전용 트랜잭션의 경우 변경 기능을 실행하면 예외가 발생
- JDBC 드라이버 (DB와 드라이버 버전에 따라 상이)
- 읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던진다.
- 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청하며 읽기 전용 트랜잭션의 경우
읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용한다.
- 데이터베이스
- 데이터베이스에 따라 읽기 전용 트랜잭션의 경우 읽기만 하면 되므로 내부에서 성능 최적화 발생
- 프레임워크
- readOnly 옵션 사용 시 읽기에서 다양한 성능 최적화가 발생할 수 있다.
- value, transactionManager : 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정할 때 둘 중 하나에 트랜잭션 매니저의 스프링 빈 이름을 적어주면 된다.
예외와 트랜잭션 커밋, 롤백
- 예외 발생 시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
- 언체크 예외가 발생하는 경우 트랜잭션을 롤백한다.
- 체크 예외가 발생하는 경우 트랜잭션을 커밋한다.
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
- application.properties에 추가 시 트랜잭션이 커밋 / 롤백 여부를 로그로 확인할 수 있다.
- 해당 내용을 추가하지 않으면 트랜잭션 시작, 완료 여부만 알 수 있을 뿐 커밋되었는지 롤백되었는지는 알 수 없다.
static class MyException extends Exception {...}
//런타임 예외 발생 : 롤백
@Transactional
public void runtimeException() {
log.info("call runtimeException");
throw new RuntimeException();
}
//체크 예외 발생 : 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}
//체크 예외 rollbackFor 지정 : 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call rollbackFor");
throw new MyException();
}
- 체크, 언체크 예외 발생 시 트랜잭션 처리 예제
- runtimeException()
- 언체크 예외인 RuntimeException이 발생했기 때문에 트랜잭션을 롤백한다.
- checkedException()
- MyException은 체크 예외이기에 필수적으로 throws 예외를 선언해줘야한다.
- 체크 예외의 하위 타입 MyException이 발생했기 때문에 트랜잭션을 커밋한다.
- rollbackFor()
- MyException은 체크 예외이기에 필수적으로 throws 예외를 선언해줘야한다.
- 체크 예외의 하위 타입 MyException이지만 해당 예외를 rollbackFor 옵션으로 지정했기에 트랜잭션을 커밋하지 않고 롤백한다.
- runtimeException()
- 스프링에서 체크 예외는 커밋하고 언체크 예외는 롤백하는 이유
- 스프링에서 기본적으로 체크 예외는 비지니스 의미가 있을 때 사용하고 언체크 예외는 복구 불가능한 예외로 가정한다.
- ex) 주문 시스템에서 시스템의 문제가 아닌 결제 잔고가 부족한 경우와 같이 비지니스 상황으로 발생한 예외로 주문 데이터를 저장하고 결제 상태를 대기로 처리한 후 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내
- 비지니스 예외는 매우 중요하고 반드시 처리해야하는 경우가 많기 때문에 체크 예외를 고려할 수 있다.
- 스프링에서 기본적으로 체크 예외는 비지니스 의미가 있을 때 사용하고 언체크 예외는 복구 불가능한 예외로 가정한다.
- 실무에서 정상 케이스를 제외하고 예외 케이스는 항상 시스템 예외와 비지니스 예외 2가지 케이스로 구분해야 한다.
- 메모리 DB를 통해 테스트 수행 시 테이블 자동 생성 옵션이 활성화 된다.
- JPA는 엔티티 정보를 참고해서 테이블을 자동으로 생성해준다.
#JPA SQL
logging.level.org.hibernate.SQL=DEBUG
#테이블을 생성하지 않음
spring.jpa.hibernate.ddl-auto = none
#테이블을 애플리케이션 시작 시점에 생성
spring.jpa.hibernate.ddl-auto = create
- application.properties
- ~ SQL 추가 시 JPA가 실행하는 SQL을 로그로 확인 가능
- ~ ddl-auto = none 추가 시 자동으로 테이블을 생성하지 않는다.
- ~ ddl-auto = create 추가 시 애플리케이션 시작 시점에 테이블을 자동으로 생성한다.
- 비지니스 예외를 통해 예외가 발생했음을 알림으로 예외를 마치 리턴 값처럼 사용할 수도 있다.
정리
- 트랜잭션 AOP 동작 흐름
- 클라이언트가 프록시 호출 -> 프록시에서 트랜잭션 매니저 획득 후 트랜잭션 시작 -> 트랜잭션에서 커넥션 생성
-> 커넥션을 수동 커밋 모드로 설정함으로 트랜잭션 시작 -> 커넥션을 트랜잭션 동기화 매니저에 보관
-> 레포지토리에서 트랜잭션 동기화 매니저에 보관된 커넥션을 사용
- 클라이언트가 프록시 호출 -> 프록시에서 트랜잭션 매니저 획득 후 트랜잭션 시작 -> 트랜잭션에서 커넥션 생성
- @Transactional 어노테이션이 특정 클래스나 메소드에 하나라도 있으면 트랜잭션 AOP는 해당 클래스의 프록시를 만들어서 컨테이너에 스프링 빈으로 등록한다.
- 의존관계 주입 시에도 실제 객체가 아닌 프록시 객체가 주입된다.
- 프록시가 트랜잭션을 동작시키고 실제 객체를 호출하는 방식으로 동작한다.
- 스프링 @Transactional 2가지 규칙
- 우선순위 규칙
- 더 구체적인 것일수록 우선순위가 높다.
- 클래스에 적용하면 메소드는 자동 적용
- 우선순위 규칙
- 프록시를 거치지 않고 내부에서 메소드를 호출하는 등의 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 트랜잭션도 적용되지 않는다.
- 문제를 해결하는 가장 단순한 방법은 내부에서 호출하는 메소드를 별도의 클래스로 분리시키는 것이다.
- 스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.
- ex) 초기화 코드(@PostConstruct)와 @Transactional을 함께 사용하는 경우
=> 초기화 코드가 먼저 호출되고 그 다음 트랜잭션 AOP가 적용되기 때문에 초기화 시점에는 해당 메소드에서 트랜잭션을 획득할 수 없다. - 스프링 초기화 시점에 트랜잭션을 적용시키려면 @EventListener(ApplicationReadyEvent.class)를 사용하면 된다.
- ex) 초기화 코드(@PostConstruct)와 @Transactional을 함께 사용하는 경우
- 스프링에서 기본적으로 체크 예외는 비지니스 의미가 있을 때 사용하고 언체크 예외는 복구 불가능한 예외로 가정한다.
- 체크 예외는 트랜잭션을 커밋한다.
- 언체크 예외는 트랜잭션을 롤백한다.
출처 : [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
스프링 DB 2편 - 데이터 접근 활용 기술 강의 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드
www.inflearn.com
'Spring > [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 스프링 트랜잭션 전파 - 활용 (1) | 2024.11.30 |
---|---|
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 스프링 트랜잭션 전파 - 기본 (0) | 2024.11.29 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - 활용 방안 (2) | 2024.11.27 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - Querydsl (0) | 2024.11.26 |
[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2024.11.26 |