
스프링에서는 @Transactional 어노테이션을 제공한다. 이러한 어노테이션을 사용하는 트랜잭션 방식을 선언적 트랜잭션이라 불리며 스프링 AOP가 이를 인식하고 트랜잭션 프록시 객체가 생성해 자동으로 commit 혹은 rollback을 시행한다. 메서드에 붙여도 되고 클래스에 붙여도 되지만 클래스에 붙으면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다.
스프링부트는 데이터소스와 트랜잭션 매니저를 자동으로 등록한다.

앞서 말했듯이 @Transactional이 달려있으면 트랜잭션 AOP는 트랜잭션 프록시 객체를 만들어 스프링 빈에 등록한다. 스프링 부트의 디폴트 프록시 생성 라이브러리인 CGLIB가 담당한다.
당연히 주입 받을 때도 실제 객체 대신 프록시 객체가 주입된다.
프록시 객체가 요청을 가로채 트랜잭션을 처리하고 실제 객체를 호출한다.
따라서 트랜잭션을 적용하려면 항상 프록시를 통해서 타깃을 호출해야 한다. 부가 기능을 담당하는 것이다.

만약 타깃 메서드를 내부 호출한다면 @Transactional이 달려있어도 트랜잭션이 적용되지 않는다. 이는 프록시 방식의 AOP의 한계다.
다음 코드를 보자.
@Service
public class CallService {
@Transactional
public void external() {
internal(); // 내부 메서드 호출
}
@Transactional
public void internal() {
// 트랜잭션이 적용되지 않는다.
}
}
external()를 호출하면 internal()도 함께 호출되지만, 트랜잭션은 external()에만 적용된다.
프록시 방식에서 external()를 호출하면 프록시 객체가 트랜잭션을 관리하고 이 메서드를 가로채서 트랙잭션을 적용한다. 하지만 external()가 internal()를 호출하는 순간, 프록시가 관여하지 않는 직접적인 메서드 호출이 되어 internal()에는 트랜잭션이 적용되지 않는다.
가장 간단한 방법은 내부 호출을 피하기 위해 internal() 메서드를 별도의 클래스로 분리해 내부 호출을 외부 호출로 만들어주는 것이다.

@Service
public class CallService {
@Autowired
private InternalService internalService;
@Transactional
public void external() {
internalService.internal(); // 외부 클래스 메서드 호출
}
}
@Service
public class InternalService {
@Transactional
public void internal() {
// 트랜잭션 적용
}
}
@Transactional의 기본적인 롤백 원칙은 다음과 같다.
Exception, IOException 등)이 발생하면 트랜잭션은 롤백되지 않고 커밋된다.롤백을 지정하고 싶다면 rollbackFor을 사용해서 지정해주면 된다.
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
throw new MyException();
}
그렇다면 왜 스프링은 체크(컴파일) 예외는 커밋하고 체크되지 않은(런타임) 예외는 롤백할까?
스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임(언체크) 예외는 복구 불가능한 예외로 가정 한다.
💡만약 결제 잔고가 부족해 NotEnoughMoneyException 라는 체크 예외가 발생했다 가정하자.
이 예외는 시스템 상의 문제가 있어서 발생하는 예외인가? 아니다!
오히려 시스템이 문제 없이 동작했기 때문에 발생하는 비즈니스적 예외인 것이다.
따라서 이러한 체크 예외는 시스템이 정상이라는 증거이기도 하기 때문에, 커밋되는 것이 맞다.
롤백하면 오히려 결제 요청 자체가 사라지는 이상한 상황이 된다.
만약 그래도 롤백하고 싶을 경우를 위해 스프링은 친절히 rollbackFor 옵션을 제공한다.
isolation : 트랜잭션의 격리 수준 설정. 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성을 유지하는 방법을 정의한다.DEFAULT: 데이터베이스의 기본 격리 수준을 사용합니다.READ_UNCOMMITTED: 다른 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있습니다.READ_COMMITTED: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있습니다.REPEATABLE_READ: 트랜잭션 동안 읽은 데이터가 변경되지 않도록 보장합니다.SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션 간에 완전한 직렬화를 보장합니다.propagation : 트랜잭션 전파 수준 설정. 메소드가 트랜잭션 내에서 호출될 때 트랜잭션을 어떻게 처리할지 정의한다.REQUIRED: 기본값으로, 메소드가 트랜잭션 내에서 실행되어야 하며 기존 트랜잭션이 없으면 새로운 트랜잭션을 생성합니다.REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며 기존 트랜잭션이 있으면 일시 중단됩니다.MANDATORY: 반드시 기존 트랜잭션이 존재해야 하며, 그렇지 않으면 예외를 발생시킵니다.SUPPORTS: 트랜잭션이 있으면 그 안에서 실행하고, 없으면 트랜잭션 없이 실행합니다.NOT_SUPPORTED: 트랜잭션 없이 실행합니다. 기존 트랜잭션이 있으면 일시 중단됩니다.NEVER: 트랜잭션이 있으면 예외를 발생시킵니다.NESTED: 중첩 트랜잭션을 허용합니다.timeout: 트랜잭션 지속 시간 설정, 시간 내에 트랜잭션이 완료되지 않으면 롤백된다.readOnly: 읽기 전용 트랜잭션 명시. 주로 데이터 조회용으로만 사용하는 트랜잭션에 설정한다.rollBackFor : 특정 예외 발생시 롤백할지 여부 설정한다.noRollBackFor: 특정 예외 발생시 롤백하지 않도록 설정한다.