본문 바로가기
Spring

[Spring] Transaction

by Bhinney 2022. 12. 19.
Transaction 이란?

Transaction 이란, 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미한다.

 

트랜잭션은 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이며, 사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업 단위이고, 하나의 트랜잭션은 Commit되거나 Rollback이 이루어진다.

 

Transaction은 ACID의 원칙을 따른다.

 

A는 Atomicity로 원자성을 뜻한다. 트랜잭션에서의 원자성이란, 작업을 더이상 쪼갤 수 없음을 의미한다. 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 한다. 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야 하며, 모두가 완벽히 수행되지 않고 어느하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다.

 

C는 Consistency로 일관성을 뜻한다. 일관성은 트랜잭션이 에러없이 성공적으로 종료될 경우, 비즈니스 로직에서 의도하는대로 일관성있게 저장되거나 변경되는 것을 의미한다. 트랜잭션이 그 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 변환된다. 시스템이 가지고 있는 고정요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후의 상태가 같아야 한다.

 

I는 Isolation로 독립성, 격리성을 뜻한다. 격리성은 여러 개의 트랜잭션이 실행될 경우 각각 독립적으로 실행이 되어야 함을 의미한다. 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 어느 하나의 트랜잭션 실행 중에 다른 트랜잭션의 연산이 끼어들 수 없고, 수행중인 트랜잭션은 완전히 완료될 때까지 다른 트랜잭션에서 수행 결과를 참조할 수 없다.

 

D는 Durablility로 영속성, 지속성을 뜻한다. 트랜잭션이 완료되면 그 결과는 지속되어야 한다는 의미이다. 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.


Spring에서 Transaction을 사용하는 방법은?

첫 번째 방법은 @Transactional 어노테이션 사용이다. 아래의 코드처럼 클래스 레벨 혹은 메서드 레벨에서 사용이 가능하다. 

 

 @Transactional(readOnly=true)로 읽기 전용 트랙잭션을 적용시킬 수 있다. JPA commit 시, 영속성 컨텍스트 flush된다. 읽기 전용 설정 시 내부적으로 영속성 컨텍스트를 flush 하지 않고, 변경 감지를 위한 스냅 샷 생성도 진행하지 않는다. 따라서 조회 메서드에는 readonly 속성을 true로 지정해서 JPA가 자체적으로 성능 최적화 과정을 거치도록 하는것이 좋다.

 

주의할 점은, 메서드에 사용할 경우 @Transactional 어노테이션은 public 메서드에서만 정상 작동한다.

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

   private final MemberRepository memberRepository;
   private final CustomAuthorityUtils authorityUtils;
   private final PasswordEncoder passwordEncoder;
	...
}

 

@Service
@Transactional
@RequiredArgsConstructor
public class ClientService {
	private final ClientRepository clientRepository;
	private final MemberService memberService;

	...

	/* 소비자 조회 */
	@Transactional(readOnly = true)
	public Client findClient(long clientId) {
		correctClient(clientId);
		Client client = findVerifiedClient(clientId);

		return client;
	}
	
	...
}

 

 

두 번째 방법은 AOP를 이용하여 트랜잭션 코드를 감추는 방법이다. Transaction을 사용할 Config 클래스를 만들고 TransactionManager를 DI 받은 후, TransactionInterceptor를 이용하여 트랜잭션을 적용할 수 있다. Attribute를 지정하여 이름과 패턴에 따라 구분 적용이 가능하며, Advisor를 통해 포인트 컷을 지정하여 사용할 수 있다. (예시는 CoffeeService 클래스를 포인트 컷으로 지정하였다.)

@Configuration
public class TxConfig {
    private final TransactionManager transactionManager;

    public TxConfig(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    @Bean
    public TransactionInterceptor txAdvice(){
        NameMatchTransactionAttributeSource txAttributeSource = new NameMatchTransactionAttributeSource();

	//조회 메서드를 제외한 공통 트랜잭션 애트리뷰트
	RuleBasedTransactionAttribute txAttribute = new RuleBasedTransactionAttribute();
        txAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

	//조회 메서드에 적용하기 위한 트랜잭션 애트리뷰
	RuleBasedTransactionAttribute txFindAttribute = new RuleBasedTransactionAttribute();
        txFindAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        txFindAttribute.setReadOnly(true);

	//트랜잭션을 적용할 메서드에 트랜잭션 애트리뷰트 매핑
	// Map의 key를 메서드 이름 패턴으로 지정해서 각각의 트랜잭션 애트리뷰트를 추가
	//트랜잭션 애트리뷰트를 추가한 Map객체를 txAttributeSource.setNameMap(txMethods)으로 넘겨줌
	Map<String, TransactionAttribute> txMethods = new HashMap<>();
        txMethods.put("find*",txFindAttribute);
        txMethods.put("*", txAttribute);
        txAttributeSource.setNameMap(txMethods);

	// TransactionInterceptor의 생성자 파라미터로 transactionManager와 txAttributeSource를 전달
	return new TransactionInterceptor(transactionManager, txAttributeSource);
    }

    @Bean
    public Advisor txAdvisor(){

	//트랜잭션 어드바이스인 TransactionInterceptor를 타겟 클래스에 적용하기 위해 포인트 컷을 지정
	// AspectJExpressionPointcut객체를 생성한 후,포인트 컷 표현식으로 CoffeeService클래스를 타겟 클래스로 지정
	AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* coffee.service.CoffeeService.*(..))");

	// DefaultPointcutAdvisor의 생성자 파라미터로 포인트컷과 어드바이스를 전달
	return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

 

댓글