스프링의 트랜잭션 관리
스프링의 트랜잭션 관리는 선언적 트랜잭션과 프로그래밍 트랜잭션 두 가지 방법을 제공한다.
선언적 트랜잭션은 트랜잭션 관리를 "선언적"으로 처리하는 방법으로, 주로 애노테이션 또는 XML 설정을 이용해 트랜잭션의 범위, 롤백 규칙 등을 정의하는 방식을 말한다. "선언적"이라는 말은 트랜잭션 관리를 비즈니스 로직에서 분리하고, 트랜잭션 관련 설정을 외부에서 선언하게 함으로써 코드의 가독성과 재사용성을 높이는 장점이 있다.
프로그래밍 트랜잭션은 코드 상에서 트랜잭션을 명시적으로 시작, 커밋, 롤백하는 방식을 말하며, 주로 더 세밀한 제어가 필요한 경우에 사용하지만 일반적으로 사용하는 경우는 드물다.
관점 지향 프로그래밍 (AOP, Aspect-Oriented Programming)
애플리케이션 로직은 크게 '핵심 기능'과 '부가 기능'으로 나눌 수 있다.
'핵심 기능'은 해당 객체가 제공하는 고유의 기능을 말한다.
'부가 기능'은 핵심 기능을 보조하기 위해 제공되는 기능이다. 예를들어 트랜잭션 기능 등이 있다.
보통 부가 기능은 여러 클래스에 거쳐서 함께 사용된다. 예를들어 모든 애플리케이션 호출을 로깅 해야 하는 요구사항을 생각해보면, 이러한 부가 기능은 횡단 관심사가 된다. 즉, 하나의 부가 기능이 여러 곳에서 동일하게 사용된다는 뜻이다. 이런 부가 기능을 여러 곳에 적용하려면 중복된 코드가 많이 만들어지고 부가 기능을 변경하면 많은 수정이 필요하는 단점이 존재한다.
따라서 이러한 부가 기능을 핵심 기능으로부터 분리하고 한 곳에서 관리하도록 했다. 그리고 해당 부가 기능을 어디에 적용할 지 선택하는 기능도 만들었는데, 그것이 바로 Aspect다. Aspect는 부가 기능과 부가 기능을 어디에 적용할 지를 정의한 것이다. AOP는 횡단 관심사를 깔끔하기 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.
AOP 용어
1. 어드바이스
부가 기능으로, 특정 조인 포인트에서 Aspect에 의해 취해지는 조치로, 한마디로 부가 기능을 말한다.
2. 포인트 컷(Pointcut)
어디에 부가 기능을 적용할 지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직으로, 주로 클래스와 메서드 이름으로 필터링 한다.
3. 조인 포인트(Join Point)
추상적인 개념으로 AOP를 적용할 수 있는 모든 지점을 말한다.스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점으로 제한된다.
4. 애스펙트
어드바이스 + 포인트컷을 모듈화 한 것
5. 어드바이저
하나의 어드바이스 + 하나의 포인트컷으로 구성, 스프링 AOP에서만 사용
@Aspect
스프링은 @Aspect 애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.
@Aspect
public class MyAspect {
@Around("execution(* com.myapp.service.MyService.myMethod(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before execution");
try {
Object result = joinPoint.proceed(); // continue on the intercepted method
System.out.println("After execution");
return result;
} catch (Exception e) {
System.out.println("Exception caught");
throw e;
}
}
}
@Aspect 애노테이션은 클래스 레벨에서 사용되며, 해당 클래스가 Aspect임을 선언한다. Aspect는 횡단 관심사를 모듈화한 것으로, Advice와 Pointcut을 포함한다. 스프링 시작시 스프링 빈 팩토리에서 빈의 생성과 초기화가 완료되면 AnnotationAwareAspectJAutoProxyCreator 이라는 빈 후처리기가 실행되는데, 해당 클래스는 @Aspect 애노테이션이 붙은 클래스를 찾아 해당 클래스의 Advice를 적절한 시점에 적용하는 프록시 객체를 생성한다. 또한, @Transactional과 같은 애노테이션도 인식하며, 이 경우에도 해당 애노테이션이 붙은 메소드나 클래스에 대한 프록시 객체를 생성하고, 이 프록시 객체에 트랜잭션 관리를 위한 Advice를 적용한다. 따라서 생성된 프록시 객체는 원래의 타켓 객체를 감싸고 있으며, 프록시 객체를 통해 메서드가 호출되면 먼저 프록시 객체가 인터셉터를 하여 Aspect의 Advice를 실행하고, 이후에 타켓 객체의 실제 메서드를 호출하게 된다.
@Transactional
@Transactional은 선언적 트랜잭션을 제공하는 애노테이션으로, 내부적으로 이미 정의된 트랜잭션 관리 관심사를 수행한다. 따라서 개발자는 별도의 Aspect를 정의할 필요 없이 해당 애노테이션을 사용하기만하면 간단하게 트랜잭션 관리를 수행할 수 있다.
이 애노테이션은 메서드나 클래스에 적용함으로써 해당 메서드 또는 클래스 내의 모든 public 메서드가 하나의 트랜잭션으로 묶이게 된다. 따라서 해당 애너테이션을 사용하면 메서드가 호출될 때 트랜잭션을 시작하고, 메서드가 성공적으로 종료되면 커밋하고, 만약 예외가 발생하면 자동으로 트랜잭션을 롤백하는 기능 등을 제공한다. @Transactional에서 사용할 수 있는 옵션에 대해 알아보자.
1. Propagation
propagation 옵션은 트랜잭션의 전파 방식을 결정하는 옵션이다. 즉 한 메서드에서 다른 메서드를 호출할 때 트랜잭션의 동작을 어떻게 할 것인지를 결정하는 것을 의미한다. 'propagation' 속성은 아래와 같다.
- REQUIRED : 기본값으로, 호출하는 메서드에 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 그렇지 않으면 새로운 트랜잭션을 사용한다.
- SUPPORTS : 호출하는 메서드에 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 그렇지 않으면 트랜잭션 없이 사용한다.
- WANDATORY : 호출하는 메서드에 호출하는 메서드에 트랜잭션이 존재해야 한다. 그렇지 않으면 예외를 발생시킨다.
- REQUIREDS_NEW : 항상 새로운 트랜잭션을 생성한다. 호출하는 메서드에 트랜잭션이 존재하면 해당 트랜잭션을 일시 중단한다. (독립적으로 동작)
- NOT_SUPPORTED : 트랜잭션 없이 메서드를 실행하고, 호출하는 메서드에 트랜잭션이 존재하면 해당 트랜잭션을 일시 중단한다.
- NEVER : 트랜잭션 없이 메서드를 실행한다. 호출하는 메서드에 트랜잭션이 존재하면 예외를 발생시킨다.
- NESTED : 호출하는 메서드에 트랜잭션이 존재하면 중첩 트랜잭션을 시작한다. 그렇지 않으면 REQUIRED와 같이 동작한다.
@Service
public class BookService {
@Autowired
private OtherService otherService;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 코드 블럭 ...
otherService.methodB();
// 코드 블럭 ...
}
}
@Service
public class OtherService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 코드 블럭 ...
}
}
위 옵션들을 사용한 예시를 들어서 위 코드가 어떻게 동작하는지 순서대로 나열해보면,
1. methodA() 호출 : 새로운 트랜잭션 시작
2. methodB() 호출 : methodA 트랜잭션 일시 중단. methodB의 새로운 트랜잭션 시작
3. methodB() 종료 : methodB 트랜잭션 종료 (커밋 또는 롤백)
4. methodA() 재개 : methodA 트랜잭션 재개
5. methodA() 종료 : methodA 트랜잭션 종료 (커밋 또는 롤백)
따라서 이 예시에서는 methodB()에서 오류가 발생하여 롤백이 일어나도 methodA()의 트랜잭션에서는 영향을 주지 않고 각각 독립적으로 처리된다.
2. Isolation Level
트랜잭션의 격리 수준을 지정하는 데 사용된다. 데이터베이스 마다 지원하는 격리 수준이 다르므로 적절하게 선택해야 한다. isolation level에 대한 설명은 이전에 작성한 글을 참조하고, 격리 수준을 설정하는 예제를 간단하게 작성하겠다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void someServiceMethodForReadCommitted() {
// ... service logic here ...
}
3. Rollback Rules
스프링의 @Transactional은 기본적으로 RunTimeException과 Error 유형의 미확인 예외가 발생하면 트랜잭션을 롤백한다. 만약 롤백 동작을 변경하거나 특정 예외 유형에 대한 롤백 동작을 정의하려면 해당 옵션을 사용해 확장할 수 있다.
- rollbackFor : 트랜잭션 내에서 발생하는 특정 예외 유형에 대해 롤백을 수행하도록 설정할 수 있다. 해당 예외 유형이나 그 하위 유형이 트랜잭션에서 발생하면 트랜잭션은 롤백된다.
- noRollbackFor : 트랜잭션 내에서 특정 예외 유형에 대해 롤백을 수행하지 않도록 설정할 수 있다.
@Transactional(rollbackFor = {Exception1.class, Exception2.class},
noRollbackFor = {Exception3.class, Exception4.class})
public void someServiceMethod() {
// ... service logic here ...
}
4. Read-only Flag
트랜잭션이 읽기 전용 모드로 설정할 수 있는 플래그이다. 해당 플래그를 사용하면 데이터베이스 최적화(불필요한 잠금을 걸지 않거나, 쓰기 작업에 필요한 일부 리소스를 할당하지 않는 등)를 통해 성능을 향상시킬 수 있다. 또 해당 트랜잭션이 읽이 전용임을 명시적으로 나타낼 수 있으므로 코드의 가독성을 높일 수 있는 장점도 존재한다.
5. Timeout
트랜잭션이 시작부터 종료까지의 시간을 초단위로 설정하는 옵션이다. 기본적으로는 트랜잭션은 완료될 때까지 계속 실행되지만 경우에 따라서 트랜잭션이 너무 오래 실행되는 것을 막기 위해 타임아웃을 설정할 수 있다. 해당 시간 내에 트랜잭션이 완료되지 않으면, 트랜잭션은 롤백되고 TransactionTimeoutException이 발생한다.
Transaction Manager
트랜잭션 매니저란 트랜잭션 처리를 담당하는 객체로, 트랜잭션을 시작하고 커밋 또는 롤백을 수행하는 기능을 제공한다. @Transactional은 기본적으로 PlatformTransactionManager을 사용한다.
@Autowired
private PlatformTransactionManager transactionManager;
public void performTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 트랜잭션 처리 코드
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
위 예시는 PlatformTransactionManager 구현체인 DataSourceTransactionManager를 사용한 예시이다. 위 코드에서 트랜잭션 매니저를 통해 트랜잭션을 시작하고 커밋 또는 롤백하는 기능을 수행할 수 있다. 따라서 개발자가 직ㅈ버 Connection을 획득하지 않고 트랜잭션 매니저가 알아서 관리하게 도와줄 수 있으므로 개발자는 트랜잭션 처리를 위한 보일러플레이트 코드를 줄일 수 있다.
스프링배치에서의 트랜잭션 관리
추후 작성 예정