김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
➜ PlatFormTransactionManager
라는 인터페이스를 통해 트랜잭션 추상화
@Transactional
을 메서드나 클래스에 붙이면 해당 객체는 트랜잭션 AOP 적용 대상이 되고, 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다
주입을 받을 때도 실제 객체 대신에 프록시 객체가 주입된다
스프링부트는 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다
프록시를 적용하면 트랜잭션을 처리하는 객체와 서비스 객체를 분리할 수 있다
public class TxBasicTest { @Autowired BasicService basicService; static class BasicService { @Transactional public void tx() { ... } public void nonTx() { ... } } }
@Transactional
어노테이션이 특정 클래스나 메서드에 하나라도 있으면 있으면 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록 ➜ 실제 basicService 객체 대신에 프록시인 basicService$$CGLIB
를 스프링 빈에 등록
➜ 프록시는 내부에 실제 basicService 를 참조
txBasicTest
가 스프링 컨테이너에 @Autowired
로 의존관계 주입을 요청하면➜ 스프링 컨테이너에는 프록시가 스프링 빈으로 등록되어 있기 때문에 프록시를 주입
➜ basicService.getClass()
로 확인하면 프록시 클래스의 이름이 출력된다
@Transactional
public void tx() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
public void nonTx() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
basicService.tx()
호출 ➜ 프록시의 tx()
가 호출 ➜ 프록시는 tx()
메서드가 트랜잭션을 사용할 수 있는지 확인
➜ @Transactional
이 붙어있으므로 트랜잭션 적용 대상
➜ 따라서 트랜잭션을 시작한 다음에 실제 basicService.tx()
를 호출
➜ 실제 basicService.tx()
의 호출이 끝나서 프록시로 제어가( 리턴 ) 돌아오면
프록시는 트랜잭션 로직을 커밋하거나 롤백해서 트랜잭션을 종료
basicService.nonTx()
호출 ➜ 프록시의 nonTx()
가 호출 ➜ 프록시는 nonTx()
메서드가 트랜잭션을 사용할 수 있는지 확인
➜ @Transactional
이 없음
➜ 트랜잭션을 시작하지 않고, 실제 basicService.nonTx()
를 호출하고 종료
isActualTransactionActive()
: 현재 Thread에 트랜잭션이 적용되어 있는지 확인@Transactional
의 적용 위치에 따라 우선순위가 다르다
스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다
클래스와 메서드에 모두 붙어 있는 경우 메서드에 붙은 것이 높은 우선순위
인터페이스와 구현 클래스에 붙어 있는 경우 구현한 클래스가 높은 우선 순위
AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기 때문에 구체 클래스에 사용을 권장
static class LevelService {
@Transactional(readOnly = false)
public void write() { ... }
public void read() { ... }
}
기본적으로 트랜잭션은 읽기와 쓰기 모두 가능한 트랜잭션이 생성
@Transactional(readOnly = true)
: 읽기 전용 트랜잭션이 생성
write()
: readOnly = false 가 적용 ( 디폴트 )
read()
: readOnly = true 가 적용
@Transactional
을 사용하면 스프링의 트랜잭션 AOP가 적용되는데 트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용
트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체( Target )을 호출해야 프록시에서 먼저 트랜잭션을 적용하고, 이후에 대상 객체를 호출하게 된다
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다
AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록하기 때문에 일반적으로 대상 객체를 직접 호출하는 일은 발생하지 않는다
but> 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하고 @Transactional이 있어도 트랜잭션이 적용되지 않는다
static class CallService{
public void external() {
printTxInfo();
internal();
}
@Transactional
public void internal() {
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx Active={}", txActive);
}
}
@Transactional
이 있기 때문에 프록시 객체가 만들어지고 실체 객체 대신 주입된다
internal()
을 호출하면 @Transactional
이 있으므로 프록시가 트랜잭션을 적용하기 때문에 txActive = true 가 출력
external()
을 호출하면 txActive = false가 출력되고 내부에서 internal()
을 호출했을 때 txActive = false 가 출력된다
즉, internal() 에서 트랜잭션이 적용되지 않은 것이다
클라이언트인 테스트 코드가 callService.external()
을 호출
callService
의 트랜잭션 프록시가 호출
external()
에는 @Transactional
이 없으므로 프록시는 트랜잭션을 적용하지 않는다
트랜잭션을 적용하지 않고 실제 callService
의 external()
를 호출
external()
은 내부에서 internal()
메서드를 호출하는데 여기서 문제가 발생
실제 대상 객체 내부에서 호출하기 때문에 this.internal()
이 되어버린다
프록시를 거치지 않고 실제 대상 객체( this )의 메서드를 호출하기 때문에 트랜잭션을 적용할 수 없다
즉, 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없는데 이것이 프록시 방식의 AOP 의 한계
static class CallService{
private final InternalService internalService;
public void external() {
printTxInfo();
internalService.internal();
}
}
static class InternalService {
@Transactional
public void internal() {
printTxInfo();
}
}
가장 간단한 해결 방법은 internal()
메서드를 별도의 클래스로 분리하는 것이다
CallService
에는 트랜잭션 관련 코드가 없으므로 트랜잭션 프록시가 적용되지 않는다
InternalService
에는 트랜잭션 관련 코드가 있으므로 트랜잭션 프록시가 적용된다
external()
에서는 txActive = false가 출력되지만 internal()
에서는 txActive = true 가 출력된다
클라이언트인 테스트 코드가 callService.external()
을 호출
callService
는 실제 callService
객체 인스턴스이다
callService
는 주입 받은 internalService.internal()
을 호출
internalService
는 트랜잭션 프록시이며 internal()
메서드에 @Transactional
이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다
트랜잭션 적용 후 실제 internalService
객체 인스턴스의 internal()
을 호출
스프링의 트랜잭션 AOP 기능은 public
메서드에만 적용하도록 기본 설정이 되어 있다
그래서 protected
나 private
에는 트랜잭션이 적용되지 않는다
클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 적용될 수 있는데 그렇게 되면 트랜잭션을 의도하지 않은 곳까지 적용되어 버린다
트랜잭션은 주로 비지니스 로직의 시작점에서 걸기 때문에 대부분 외부에 열어준 곳
( public )을 시작점으로 사용한다
이러한 이유로 public
메서드에서만 트랜잭션을 적용하도록 설정되어 있는 것이다
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
@PostConstruct
와 @Transactional
을 함께 사용하면 트랜잭션이 적용되지 않는다
초기화 코드가 먼저 호출되고, 트랜잭션 AOP가 적용되기 때문에 false 가 출력된다
즉, 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
컨테이너와 AOP 등 모든 스프링이 완성된 후에 메서드가 호출되도록 한다
ApplicationReadyEvent
: 트랜잭션 AOP 를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이 이벤트가 붙은 메서드를 호출해준다
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
}
트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할 지 알아야 한다
코드로 직접 트랜잭션을 사용할 때 트랜잭션 매니저를 주입 받아서 사용한 것처럼 @Transactional
에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다
사용할 트랜잭션 매니저를 지정할 때는 value
, transactionManager
둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어준다
이 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하는데 트랜잭션 매니저가 둘 이상이면 아래처럼 트랜잭션 매니저의 이름을 지정해서 구분해야한다
@Transactional(value = "memberTxManager")
public @interface Transactional {
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
rollbackFor
: 기본 예외 처리 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정
rollbackForClassName
도 동일한 기능을 하는데 이 옵션은 예외 이름을 문자로 넣는다
noRollbackFor
: rollbackFor
처럼 예외 클래스를 직접 지정하지만 기능은 반대이다
ex> @Transactional(rollbackFor=Exception.class)
public @interface Transactional {
Isolation isolation() default Isolation.DEFAULT;
}
트랜잭션의 격리 수준을 지정하며 기본 값은 DB에서 설정한 트랜잭션 격리 수준을 사용하는 DEFALUT
DEFAULT : 데이터베이스에서 설정한 격리 수준을 따른다
READ_UNCOMMITTED : 커밋되지 않은 읽기
READ_COMMITTED : 커밋된 읽기
REPEATABLE_READ : 반복 가능한 읽기
SERIALIZABLE : 직렬화 가능
public @interface Transactional {
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
String[] label() default {};
}
트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정하며 기본 값은 트랜잭션 시스템의 타임아웃을 사용
timeoutString
: 숫자 대신 문자 값으로 지정할 수 있다
label
: 트랜잭션 어노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용할 수 있다
public @interface Transactional {
boolean readOnly() default false;
}
readOnly가 true이면 등록, 수정, 삭제가 불가능한 읽기 전용 트랜잭션이 생성된다
또한 이 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있는데 보통 프레임워크, JDBC 드라이버, DB 3곳에서 적용된다
DB : DB에 따라 읽기 전용 트랜잭션의 경우 읽기 수행하므로 내부에서 성능 최적화가 발생
프레임워크
JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던진다
JPA는 읽기 전용 트랜잭션의 경우 커밋 시점에 플러시를 호출하지 않는다
읽기 전용이니 변경에 사용되는 플러시를 호출할 필요가 없다
변경이 필요 없으니 변경 감지를 위한 스냅샷 객체도 생성하지 않는다
JDBC 드라이버
읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던진다
읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청한다
읽기 전용 트랜잭션의 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용
위 내용들은 DB와 드라이버 버전에 따라 다르게 동작한다
체크 예외인 Exception
과 그 하위 예외가 발생하면 커밋한다
언체크 예외인 RuntimeException
, Error
와 그 하위 예외가 발생하면 롤백한다
정상 응답( 리턴 )하면 트랜잭션을 커밋
체크 예외는 반드시 처리해야하는 예외인 경우, 비지니스 의미가 있을 때 사용
rollbackFor
옵션으로 커밋과 롤백을 선택할 수 있다
메모리 DB를 통해 테스트를 수행하면 테이블 자동 생성 옵션이 활성화 된다
JPA는 엔티티 정보를 참고해서 테이블을 자동으로 생성해준다
테이블 자동 생성은 spring.jpa.hibernate.ddl-auto
옵션으로 조정할 수 있다
none
: 테이블을 생성하지 않는다
create
: 애플리케이션 시작 시점에 테이블을 생성한다