이번 포스팅에서는 Spring Boot에서 트랜잭션(Transaction)이 어떻게 동작하고, 어떤 방식으로 관리할 수 있는지 알아볼 예정이다 👀
트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적인 작업 단위를 말한다.
여러 작업들을 하나의 단위로 묶어서 '모두 성공' 또는 '모두 실패'로 처리되어야 하는 경우에 사용된다.
우선 개념적인 부분을 조금 더 살펴보자
Atomicity (원자성)
- 트랜잭션은 전체가 성공하거나 전체가 실패해야 한다
- 부분적 성공은 허용되지 않는다
Consistency (일관성)
- 트랜잭션 실행 전과 후의 데이터베이스는 일관된 상태를 유지해야 한다
- 모든 제약조건을 만족해야 한다
Isolation (격리성)
- 동시에 실행되는 트랜잭션들은 서로 영향을 미치지 않아야 한다
- 각각의 트랜잭션은 독립적으로 실행되어야 한다
Durability (지속성)
- 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 한다
- 시스템 장애가 발생하더라도 데이터는 보존되어야 한다
사실 이런 개념적인 부분은 그냥 훑고 넘어가도 되지 않을까 싶다.
코드를 통해 이해해보자!
게시판의 상세 조회 기능을 예시로 살펴보자
상세 조회 기능
- 조회수 증가
- 게시글 정보 조회
우리는 위 두 작업이 하나의 트랜잭션으로 처리되지 않으면 다음과 같은 문제가 발생할 수 있다.
Sample Code
@Override public BoardDto selectBoardDetail(int boardIdx) { boardMapper.updateHitCnt(boardIdx); int i = 10 / 0; // 예외 발생 return boardMapper.selectBoardDetail(boardIdx); }
이 코드에서는 조회수 증가 후 예외가 발생해도 조회수가 증가된 상태로 남게 된다.
즉, 게시물을 정상적으로 조회하지 못했으나 조회수는 올라가는 문제가 발생한다.
이는 데이터 일관성을 해치는 심각한 문제가 될 수 있다!
SpringBoot에서는 크게 두 가지 방식으로 트랜잭션을 관리할 수 있다.
(트랜잭션을 관리하는 방법은 충분히 더 많을 수 있으나, 필자는 2가지 방법에 대해서 소개할 예정이다)
Spring에서 가장 많이 사용되는 방식으로, @Transactional
어노테이션을 사용한다.
Sample Code - 클래스 단위
@Service @Transactional public class BoardServiceImpl implements BoardService { public BoardDto selectBoardDetail(int boardIdx) { boardMapper.updateHitCnt(boardIdx); return boardMapper.selectBoardDetail(boardIdx); } }
이처럼 @Transactional
어노테이션을 클래스에 추가하여 트랜잭션 관리 대상을 지정할 수 있다.
즉, 해당 어노테이션이 붙어있다면 메서드 실행 중에 오류가 발생할 경우, 전체 실패로 처리하게 된다.
따라서, 오류가 발생하더라도 조회수만 증가하는 문제를 해결할 수 있다.
물론 @Transactional
어노테이션을 클래스 단위가 아닌 메서드 단위에 적용할 수도 있다.
Sample Code - 메서드 단위
@Service public class BoardServiceImpl implements BoardService { @Transactional(rollbackFor = Exception.class) public BoardDto selectBoardDetail(int boardIdx) { boardMapper.updateHitCnt(boardIdx); return boardMapper.selectBoardDetail(boardIdx); } }
이렇게 메서드 위에 어노테이션을 선언할 경우, selectBoardDetail 메서드에 대해서만 트랜잭션이 적용된다!
사실 AOP를 활용하면 더욱 유연하고 체계적인 트랜잭션 관리가 가능하다.
우선 예제 코드를 살펴보자
Sample Code
package com.Board.Board.aop; import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.*; import java.util.Arrays; @Configuration public class TransactionAspect { // 트랜잭션 관리자 @Autowired private PlatformTransactionManager transactionManager; // 트랜잭션 인터셉터 정의 // 트랜잭션 관리자를 사용하여 트랜잭션 시작, 커밋, 롤백 등의 처리를 수행한다. @Bean TransactionInterceptor transactionAdvice(){ TransactionInterceptor transactionInterceptor = new TransactionInterceptor(); transactionInterceptor.setTransactionManager(transactionManager); // 모든 메서드에 동일한 트랜잭션 속성을 적용할 때 사용 MatchAlwaysTransactionAttributeSource source = new MatchAlwaysTransactionAttributeSource(); // 트랜잭션 속성을 정의 -> 트랜잭션 이름, 롤백 규칙 적용 RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute(); // 트랜잭션 이름 정의 transactionAttribute.setName("*"); // 롤백 기준을 정의 transactionAttribute.setRollbackRules(Arrays.asList(new RollbackRuleAttribute(Exception.class))); source.setTransactionAttribute(transactionAttribute); transactionInterceptor.setTransactionAttributeSource(source); return transactionInterceptor; }; // AOP 포인트 컷과 어드바이저 설정이 필요하다. @Bean Advisor transactionAdviceAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* com.Board.Board.service.*Impl.*(..))"); return new DefaultPointcutAdvisor(pointcut, transactionAdvice()); } }
AOP를 사용한다고 말했으나, 이전에 우리가 작성한 AOP와는 꽤 많이 달라보인다.
이 부분을 조금 더 살펴보자
트랜잭션 AOP는 우리가 일반적으로 사용하는 AOP와는 조금 다르다.
그 이유는 바로 트랜잭션은 이미 Spring이 제공하는 기능을 설정하는 것이기 때문이다.
따라서, @Aspect
어노테이션을 사용했던 일반적인 AOP와는 다르게, @Configuration
으로 트랜잭션 인터셉터만 정의를 해주면 된다.
주요 컴포넌트들을 살펴보면 다음과 같다.
트랜잭션 AOP 주요 컴포넌트
1. 트랜잭션 매니저
- 실제 트랜잭션을 처리하는 핵심 객체
@Autowired private PlatformTransactionManager transactionManager;
- 트랜잭션 인터셉터
- 메서드 호출을 가로채서 트랜잭션 처리
TransactionInterceptor interceptor = new TransactionInterceptor();
- 트랜잭션 속성
- 롤백 규칙 등 트랜잭션 동작 방식 정의
RuleBasedTransactionAttribute attribute = new RuleBasedTransactionAttribute();
이처럼 트랜잭션 인터셉터에 트랜잭션 매니저, 그리고 트랜잭션 Attribute를 설정하여 넣어주면 설정한 속성에 맞게 트랜잭션이 동작하게 된다.
다음으로 갖는 차이점은 바로 포인트 컷 정의 방법이다.
포인트 컷 정의 방법
1. 일반 AOP@Pointcut("execution(* com.Board.Board..controller.*Controller.*(..))") private void loggerTarget() {}
- 트랜잭션 AOP
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* com.Board.Board.service.*Impl.*(..))");
트랜잭션 AOP는 포인트 컷을 직접 생성하여 주소를 설정하고 빈으로 등록하면 된다.
그 이유는 트랜잭션 AOP가 Spring이 제공하는 TransactionInterceptor를 사용하기 때문에, 일반적인 AOP처럼 Aspect를 직접 정의하지 않고 설정만 수행하면되기 때문이다!
이처럼 AOP를 사용하게 되면 외부 라이브러리에도 트랜잭션을 적용할 수 있다는 점, 그리고 한 파일에서 트랜잭션을 관리할 수 있다는 장점이 존재한다.
이번 포스팅에서는 Spring Boot에서의 트랜잭션 관리 방법에 대해 자세히 알아보았다.
트랜잭션은 데이터의 일관성을 유지하는데 매우 중요한 역할을 하며, 상황에 따라 적절한 관리 방식을 선택하는 것이 중요하다.
일반적으로 @Transaction
어노테이션을 사용하여 관리하지만, AOP를 사용하여 트랜잭션을 관리하는 방법도 코드를 통해 살펴봤다.
사실 아직 필자는 예제 코드가 없다면 혼자서 트랜잭션 AOP를 설정하진 못할 것 같다.
하지만, 차이점과 필요성에 대해서는 기억해두자 👊