스프링 트랜잭션
스프링 트랜잭션 사용 방식
스프링은 트랜잭션 추상화 인터페이스인 PlatformTransactionManager 를 제공하여 다양한 DataSource에 맞게 트랜잭션을 관리할 수 있게 한다.
선언적 트랜잭션 관리
(Declarative Transaction Management)
@Transactional 어노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리라고 한다.
이름 그대로 해당 로직에 트랜잭션을 적용하고 싶으면 선언하기만 하면 트랜잭션이 적용되는 방식으로 트랜잭션 관리가 프로그래밍 방식에 비해 훨씬 간편하고 실용적이기 때문에 실무에서는 대부분 선언적 트랜잭션 관리를 사용한다.
선언적 트랜잭션과 AOP
@Transactional 을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용된다.
트랜재션을 처리하기 위한 프록시를 적용하게 되면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
따라서, 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져가게 되고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다. 덕분에 서비스 계층에는 순수한 비즈니스 로직만 남길 수 있다.
트랜잭션은 항상 트랜잭션 매니저를 통해 동작한다 !
- 트랜잭션은 커넥션에 con.setAutocommit(false) 를 지정하면서 시작한다.
- 같은 트랜잭션을 유지하려면 같은 데이터베이스 커넥션을 사용해야 한다.
- 이를 위해 스프링 내부에서는 트랜잭션 동기화 매니저가 사용된다.
- JdbcTemplate 를 포함한 대부분의 데이터 접근 기술들은 트랜잭션을 유지하기 위해 내부에서 트랜잭션 동기화 매니저를 통해 리소스(커넥션)를 동기화 한다.
프로그래밍 방식 트랜잭션 관리
(Programmatic Transaction Management)
트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것을 프로그래밍 방식의 트랜잭션 관리라고 한다.
해당 방식은 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합될 수 있다.
트랜잭션 적용 위치
스프링의 @Transactional 은 두 가지 규칙이 있다.
- 우선순위 규칙
- 클래스에 적용하면 메서드는 자동 적용
💡스프링에서 우선순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
예를 들어서 메서드와 클래스에 어노테이션을 붙일 수 있다면 더 구체적인 메서드가 더 높은 우선순위를 가진다.
또한, 인터페이스와 해당 인터페이스를 구현한 클래스에 어노테이션을 붙일 수 있다면 더 구체적인 클래스가 더 높은 우선순위를 가진다.
우선순위
- 클래스의 메서드 (우선순위가 가장 높다)
- 클래스의 타입
- 인터페이스의 메서드
- 인터페이스의 타입 (우선순위가 가장 낮다)
트랜잭션을 사용할 때 다양한 옵션을 사용할 수 있다. 어떤 경우에는 옵션을 주고, 어떤 경우에는 옵션을 주지 않기를 원할 때가 있다.
@Transactional(readOnly = true)
static class LevelService {
@Transactional(readOnly = false)
public void write() {
}
public void read() {
}
}
타입에 @Transactional(readOnly = true) 가 적용되어 있고, 해당 메서드에 @Transactional(readOnly = false) 이 적용되어 있는 경우 클래스 보다는 메서드가 더 구체적이기 때문에 메서드에 있는 @Transactional(readOnly = false) 옵션을 사용한 트랜잭션이 적용된다.
따라서, 위의 경우 write() 메서드는 readOnly = false 가 적용이 됐고, read() 메서드의 경우 @Transactional 이 없기 때문에 더 상위인 클래스를 확인하게 된다. 이때 클래스에는 readOnly = true 가 적용 되었기 때문에 해당 옵션을 사용하게 된다. (클래스에 적용하면 메서드는 자동 적용)
인터페이스에 @Transactional 를 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이다. AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용이 되지 않는 경우도 있기 때문이다.
따라서, 가급적 구체 클래스에 @Transactional 를 사용하자.
트랜잭션 AOP (중요)
@Transactional 을 사용하면 기본적으로 스프링 프록시 방식의 트랜잭션 AOP가 적용된다.
따라서, @Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아 트랜잭션을 처리하고, 실제 객체를 호출한다. 따라서 트랜잭션을 적용하려면 항상 프록시를 통해 대상 객체(Target)을 호출해야 한다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다.
AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.
따라서 스프링은 의존관계 주입시에 항상 실제 객체 대신에 프록시 객체를 주입한다. 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않는다. 하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
( 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하면 @Transactional 이 있어도 트랜잭션이 적용되지 않는다. )
- 클라이언트가 프록시를 호출한다.
- callService.external() 를 호출하면 트랜잭션 프록시인 callSerivce 가 호출된다.
- external() 메서드에는 @Transactional 어노테이션이 없기 때문에 트랜잭션 프록시는 트랜잭션을 적용하지 않는다.
- 트랜잭션을 적용하지 않고 실제 callService 객체 인스턴스의 external() 를 호출한다.
- external() 은 내부에서 internal() 메서드를 호출한다.
자바 언어에서는 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 위의 경우에도 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되므로 실제 대상 객체(target)의 인스턴스를 뜻한다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않아 트랜잭션을 적용할 수 없다.
즉, target 에 있는 internal() 을 직접 호출하게 된 것이다.
프록시 방식의 AOP 한계
메서드 내부 호출에 프록시 적용 불가능
@Transactional 를 사용하는 트랜잭션 AOP는 프록시를 사용하며 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.
따라서, 실무에서는 가장 단순한 방법으로 내부 호출을 피하기 위해 internal() 메서드를 별도의 클래스로 분리해준다.
public 메서드만 트랜잭션 적용
스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있다.
따라서 protected, private, package-visible 에는 트랜잭션이 적용되지 않는다.
- 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있다. 그렇게 되면 의도하지 않는 곳 까지 트랜잭션이 과도하게 적용된다. 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다. 이런 이유로 public 메서드에만 트랜잭션을 적용하도록 설정되어 있다.
- public 이 아닌 곳에 @Transactional 이 붙어 있으면 예외가 발생하지는 않고 트랜잭션 적용만 무시된다.
@Transactional 옵션
value, transactionManager
rollbackFor
isolation
timeOut
readOnly
스프링 트랜잭션 예외
스프링 트랜잭션 전파
트랜잭션이 이미 진행중인데, 이곳에 추가적인 트랜잭션을 수행하면 어떻게 될까 ?
(즉, 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것)
기존 트랜잭션과 별도의 트랜잭션을 진행해야 하는지 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야 하는지, 어떻게 동작할지 결정하는 것을 트랜잭션 전파(propagation)라 한다.
외부 트랜잭션(처음 시작된 트랜잭션)이 수행중일 때 내부 트랜잭션(외부에 트랜잭션이 수행되고 있는 도중에 호출된 트랜잭션)이 수행되는 경우, 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다.
내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다. 이것이 기본 동작(REQUIRED) 이다.
스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다.
논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다.
( 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다. )
물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다.
( 실제 커넥션을 통해서 트랜잭션을 시작 setAutoCommit(false) 하고, 실제 커넥션을 통해서 commit, rollback 하는 단위이다. )
논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다. 단순히 트랜잭션이 하나인 경우 둘을 구분하지 않는다.
전파 기본 원칙
- 모든 논리 트랜잭션이 commit 되어야 물리 트랜잭션이 commit 된다.
- 하나의 논리 트랜잭션이라도 rollback 이 되면 물리 트랜잭션은 rollback 된다.
즉, 모든 트랜잭션 매니저를 commit 해야 물리 트랜잭션이 commit 된다. 하나의 트랜잭션 매니저라도 rollback 하면 물리 트랜잭션은 rollback 된다.
외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋해야 한다. 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문에 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다. 따라서 내부 트랜잭션은 데이터베이스 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다.
스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다. 이를 통해 트랜잭션 중복 커밋 문제를 해결한다.
트랜잭션 전파 요청 동작 과정
트랜잭션 전파 응답 과정
트랜잭션 참여
내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따름
⇒ 다른 관점에서 보면 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
⇒ 외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
⇒ 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것