[SpringBoot] 게시판 구현하기 10 (스프링의 다양한 기능 살펴보기 - 트랜잭션)
FRAMEWORK/Spring

[SpringBoot] 게시판 구현하기 10 (스프링의 다양한 기능 살펴보기 - 트랜잭션)

반응형

들어가기 전에

하기 포스팅은 "스프링부트 시작하기(김인우 저)" 책을 공부하며 적은 포스팅입니다. 이번 포스팅에서는 트랜잭션에 대해 살펴보도록 하겠습니다.

트랜잭션

먼저 돈을 송금하는 과정을 예로 들어 트랜잭션 개념을 간단히 알아보겠습니다. 돈을 송금하는 과정은 아래와 같습니다.

  1. 송금하고자 하는 계좌와 금액을 선택합니다.
  2. 이체하는 계좌에서 돈이 출금됩니다.
  3. 이체받는 계좌에 돈이 입금됩니다.
  4. 거래가 정상적으로 완료됩니다.

거래가 정상적으로 처리되면 문제가 없지만, 중간에 문제가 발생할 수 있습니다. 예를 들어, 2번과 3번 과정 사이에서 문제가 발생했다고 가정합니다. 그렇게 되면, 돈은 출금되었는데 실제로 이체 받을 사람 계좌에는 돈이 들어오지 않게 됩니다. 따라서, 돈을 송금하는 과정에서 문제가 발생하면 이와 관련된 모든 과정이 취소되고 원래 상태로 돌아와야 합니다.

 

트랜잭션이란, 데이터베이스의 상태를 변화시킬 때 더 이상 분리할 수 없는 작업의 단위를 의미합니다. 여기서 예를 들은 송금의 경우 돈이 출금되고 입금되는 과정이 분리되면 안되는 것입니다. 즉, 하나의 트랜잭션에서 일련의 작업이 처리되어야 합니다.

 

트랜잭션 속성(ACID)

ACID 설명
원자성(Atomicity) 트랜잭션은 하나 이상의 관련된 동작을 하나의 작업 단위로 처리합니다.
트랜잭션이 처리하는 하나의 작업 단위는 그 결과가 성공 또는 실패할 경우 관련된 동작은 모두 동일한 결과가 나옵니다.
즉, 작업 중 하나라도 실패한다면 관련 트랜잭션 내 먼저 처리된 동작들은 모두 롤백됩니다.
일관성(Consistency) 트랜잭션이 성공적으로 처리되면 데이터베이스의 관련 모든 데이터는 일관성을 유지해야 합니다.
고립성(Isolation) 트랜잭션은 독립적으로 처리되며, 처리되는 중간에 외부에서의 간섭은 없어야 합니다.
서로 다른 트랜잭션이 동일 데이터에 동시 접근할 경우 적절한 동시 접근 제어가 필요합니다.
지속성(Durability) 트랜잭션이 성공적으로 처리되면, 그 결과는 지속적으로 유지되어야 합니다.

 

스프링에서 트랜잭션을 처리하는 방식은 XML 설정과 어노테이션을 이용하는 방식, 그리고 AOP를 이용하는 방식으로 나눌 수 있습니다. 여기서는 어노테이션과 AOP를 이용해 트랜잭션을 처리하는 방법에 대해 알아보겠습니다.

@Transaction 어노테이션을 이용해 트랜잭션 설정하기

스프링은 데이터베이스 연동 뿐 아니라 코드 기반 트랜잭션과 선언적 트랜잭션 처리를 지원합니다. 먼저, 어노테이션을 이용해 트랜잭션 처리를 해보기 위해 DatabaseConfiguration 클래스에 아래 코드를 추가합니다.

...
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
...
@Configuration
@PropertySource("classpath:/application.properties")
@EnableTransactionManagement
public class DatabaseConfiguration {
...
	@Bean
	public PlatformTransactionManager transactionManager() throws Exception {
		return new DataSourceTransactionManager(dataSource());
	}
}
  • @EnableTransactionManager: 스프링에서 제공하는 어노테이션 기반 트랜잭션을 활성화합니다.
  • public PlatformTransactionManager transactionManager() throws Exception: 스프링에서 제공하는 트랜잭션 메니저를 등록합니다.

그 후, 트랜잭션을 처리하기 원하는 곳에 다음과 같이 @Transactional 어노테이션을 추가하면 됩니다.

...
import org.springframework.transaction.annotation.Transactional;
...
@Service
@Transactional
public class BoardServiceImpl implements BoardService {
  • @Transactional: 인터페이스나 클래스, 메소드에 사용할 수 있습니다. 어노테이션이 적용된 대상은 설정된 트랜잭션 빈에 의해 트랜잭션이 처리됩니다.

AOP를 이용해 트랜잭션 설정하기

어노테이션을 이용한 트랜잭션은 단순히 어노테이션만 사용하면 되므로 쉽게 설정할 수 있습니다. 또한, 원하는 클래스 또는 메소드 단위로 트랜잭션을 설정할 수 있는 장점이 있습니다. 하지만, 어노테이션을 이용한 트랜잭션은 새로운 클래스 또는 메소드 등을 만들 때마다 @Transactional 어노테이션을 붙여야 한다는 단점이 있습니다. 따라서, 프로젝트가 커지면 어노테이션이 누락되거나 일관되지 않아 문제가 발생할 수 있습니다. 또한 외부 라이브러리를 사용하면 해당 라이브러리의 코드를 편집할 수 없기 때문에 트랜잭션이 적절하게 처리되지 않을 수 있습니다. 따라서 그러한 문제를 해결할 수 있는 스프링의 AOP 기능을 이용해 트랜잭션을 설정해 보겠습니다.

 

먼저, aop 패키지에 TransactionAspect 클래스르 생성하고 하기 내용을 작성합니다.

package board.aop;

import java.util.Collections;

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.TransactionManager;
import org.springframework.transaction.interceptor.MatchAlwaysTransactionAttributeSource;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

@Configuration
public class TransactionAspect {
	private static final String AOP_TRANSACTION_METHOD_NAME = "*";
	private static final String AOP_TRANSCTION_EXPRESSION = "execution(* board..service.*Impl.*(..))";
	
	@Autowired
	private TransactionManager transactionManager;
	
	@Bean
	public TransactionInterceptor transactionAdvice() {
		MatchAlwaysTransactionAttributeSource source = new MatchAlwaysTransactionAttributeSource();
		RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute();
		transactionAttribute.setName(AOP_TRANSACTION_METHOD_NAME);
		transactionAttribute.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
		source.setTransactionAttribute(transactionAttribute);
		
		return new TransactionInterceptor(transactionManager, source);
	}
	
	@Bean
	public Advisor transactionAdviceAdvisor() {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression(AOP_TRANSCTION_EXPRESSION);
		
		return new DefaultPointcutAdvisor(pointcut, transactionAdvice());
	}
	
}
  • private static final String AOP_TRANSACTION_METHOD_NAME = "*": 트랜잭션을 설정할 때 사용되는 설정값을 상수로 선언합니다.
  • transactionAttribute.setName(AOP_TRANSACTION_METHOD_NAME): 트랜잭션 이름을 설정합니다. 트랜잭션 모니터에서 트랜잭션 이름을 확인할 수 있습니다.
  • transactionAttribute.setRollbackRules(Collectection.singletonList(new RollbackRullAttribute(Exception.class))): 트랜잭션에서 롤백을 하는 룰을 설정합니다. 여기서는 예외가 발생했을 때 롤백이 되도록 지정했습니다. Exception.class를 롤백 룰로 등록하면, 자바의 모든 예외는 Exception 클래스를 상속받으므로 어떠한 예외가 발생하더라도 롤백이 수행됩니다.
  • pointcut.setExpression(AOP_TRANSATION_EXPRESSION):AOP 포인트컷을 설정합니다. 여기서는 비즈니스 로직이 수행되는 모든 ServiceImpl 클래스의 모든 메소드를 지정했습니다.

AOP를 이용해 트랜잭션을 설정하면 새로운 클래스나 메소드가 추가될 때 따로 어노테이션을 붙이지 않아도 자동적으로 트랜잭션 처리가 됩니다. 따라서, 어노테이션 누락이나 잘못된 사용에 따른 문제를 미연에 방지할 수 있습니다.

 

코드 내용 참고 사항

위 코드의 경우 기존 코드와 달리 transactionManager 이름의 객체가 속한 클래스를 PlatformTransactionManager 대신 TransactionManager을 사용했습니다. 그 이유는, 아래 그림에서 알 수 있듯이, return 시 사용하는 TransactionInterceptor 생성자 중 PlatformTransactionManager를 파라미터로 사용하는 생성자가 deprecated되었기 때문입니다.

TransactionInterceptor Constructor deprecated

// AS-IS
...
import org.springframework.transaction.PlatformTransactionManager;
...
	@Autowired
	private PlatformTransactionManager transactionManager;
...

// TO-BE
...

import org.springframework.transaction.TransactionManager;
...
	@Autowired
	private TransactionManager transactionManager;
...

 트랜잭션 결과 확인하기

먼저, 위에서 작성한 트랜잭션이 예외가 발생했을 때 롤백되도록 구현했기 때문에 임의로 조회주 증가 메소드에 예외를 발생시키도록 하겠습니다. BoardServiceImpl 클래스 내에 하기와 같이 코드를 추가합니다.

...
	@Override
	public BoardDto selectBoardDetail(int boardIdx) throws Exception {
		boardMapper.updateHitCount(boardIdx);
		int i = 10 / 0; // ADD
		BoardDto board = boardMapper.selectBoardDetail(boardIdx);
		
		return board;
	}
...

0으로 나누는 것은 로직상 말이 안 되는 것이므로 해당 부분에서 예외가 발생할 것입니다. 이때, 트랜잭션의 동작 유무를 보기 위해 TransactionAspect 클래스의 @Configuration 어노테이션을 잠시 주석을 건 후 테스트를 진행합니다.

 

먼저, 게시글 목록(localhost:8080/board/openBoardList.do)을 호출해 조회수를 체크한 후, 해당 게시글을 클릭해 예외를 발생시킵니다. 그 후, 다시 게시글 목록을 호출하여 조회수를 확인합니다. 

이 경우, 트랜잭션으로 동작하지 않아 조회수 증가 메소드가 롤백되지 않음을 확인할 수 있습니다.

@Configuration 주석 처리

이제 @Configuration 어노테이션의 주석을 해제하여 테스트를 진행해봅니다. 위와 동일한 과정으로 테스트하면 조회수 증가 로직이 다시 롤백되어 조회수가 증가되지 않음을 확인할 수 있습니다.

@Configuration 주석 해제

@Transaction 어노테이션과 AOP를 이용한 트랜잭션 처리 방식의 장단점

  @Transaction 어노테이션을 이용한 트랜잭션 AOP를 이용한 트랜잭션
장점 * 특별한 설정 없이 쉽게 사용 가능
* 원하는 곳에만 트랜잭션 설정하여 성능 영향 최소화
* 공통으로 트랜잭션이 적용되어 누락될 일 없음
* 외부 라이브러리 역시 적용 가능
단점 * 어노테이션이 누락되거나 여러 메소드에 걸쳐 사용될 경우 트랜잭션이 적용되지 않을 수 있음
* 외부 라이브러리에 사용 어려움
* 트랜잭션이 필요 없는 곳까지 적용되어 성능에 영향
* 원하는 곳에만 트랜잭션 적용하기 어려움. 상황에 따라 처리 중 에러가 발생해도 해당 시점까지 데이터만 저장되어야 할 수 있는데, 트랜잭션으로 설정되어 있으면 롤백되어 버림

추가적으로, 두 가지 방법 모두를 하나의 어플리케이션에 사용하는 것은 불가능합니다. 따라서, 하나의 트랜잭션이 설정된 상태에서 다른 트랜잭션을 설정하려 하면 에러가 발생하므로 조심해야 합니다.

 

반응형