[Section3] Spring MVC - 트랜잭션(Transaction)

2023. 6. 26. 15:58

🧑🏻‍💻 TIL(Today I Learned)


✔️ 트랜잭션(Transaction)

 

💡 트랜잭션(Transaction)?

➡️ 여러 개의 작업들을 하나의 그룹으로 묶어서 처리하는 단위 

➡️ 물리적으로는 여러 개의 작업, 논리적으로는 하나의 작업으로 인식

예를 들어서 A의 5000을 B에게 계좌이체를 한다고 했을 때 A의 잔고는 5000원 감소, B의 잔고는 5000원 증가해야 함
  • 위의 예시처럼 계좌이체라는 거래는 2가지 작업이 합쳐져서 하나의 작업으로 동작해야 함 
  • 만약 둘 중 하나가 문제가 생겨 실패한다면 심각한 문제가 발생한다. 

➡️ 전부 성공하든가 전부 실패하든가(All or Nothing)의 둘 중 하나로만 처리되어야 함 
: 이러한 트랜잭션 처리 방식은 애플리케이션에서 사용하는 데이터의 무결성을 보장하는 핵심적인 역할을 함

 

 

🔎 ACID 원칙 (트랜잭션의 특징)

➡️ 데이터베이스 내에서 일어나는 하나의 트랜잭션의 안전성을 보장하기 위해 필요한 것 

 

1. 원자성(Atomicity)

➡️ 작업을 더 이상 쪼갤 수 없음을 의미

➡️ 논리적으로 하나의 작업으로만 인식해서 둘 다 성공하든가 둘 다 실패하든가 중에서 하나로만 처리되는 것이 보장되어야 함

 

2. 일관성(Consistency)

➡️ 트랜잭션이 에러없이 성공적으로 종료될 경우 비즈니스 로직에서 의도하는 대로 일관성 있게 저장되거나 변경되는 것을 의미

 

3. 격리성(Isolation)

➡️ 여러 개의 트랜잭션이 실행될 경우 다른 트랜잭션에 영향을 주지 않고 독립적으로 실행이 되어야 함을 의미 

 

4. 지속성(Durablity)
➡️ 데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미

 

(참고 : https://reeeemind.tistory.com/87)

 

 

🔎 커밋(commit), 롤백(rollback)

  • 커밋(commit)
    • 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어, commit 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장됨
    • commit 명령을 수행하지 않으면 작업 결과가 데이터베이스에 최종적으로 반영되지 않음
    • commit 명령을 수행하면 하나의 트랜잭션 과정은 종료하게 됨
  • 롤백(rollback)
    • 작업 중 문제가 발생했을 때 트랜잭션 내에서 수행된 작업들을 취소함
    • 트랜잭션 시작 이전의 상태로 되돌아감 

 

💡Spring에서 트랜잭션 적용하기

➡️ 트랜잭션은 크게 로컬 트랜잭션과 분산 트랜잭션으로 구분할 수 있으며, Spring에서 사용되는 방식은 선언형 트랜잭션 방식과 프로그래밍 코드 베이스 트랜잭션 방식이 있음 

➡️ 트랜잭션은 애플리케이션의 핵심 로직이 아닌 부가 기능이기 때문에 AOP의 적용 대상 중 하나라고 볼 수 있음

 

🔎 선언형 방식의 트랜잭션 적용

➡️ 선언형 방식으로 트랜잭션 적용하는 방법에는 두 가지가 있음

  • 작성한 비즈니스 로직에 애너테이션을 추가하는 방법
  • AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 코드를 감추는 방법

 

✔️ 만약 Spring Boot 를 사용하고 있지 않다면 아래와 같은 코드를 Spring Configuration에 추가해야 함

→ Spring Boot를 사용하고 있다면 트랜잭션 설정은 내부적으로 알아서 해줌 

@Configuration
@EnableTransactionManagement
public class JpaConfig{

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(){
        final LocalContainerEntityManagerFactoryBean em = 
				new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource());
        ...
        ...

        return em;
    }

    @Bean // 📍
    public DataSource dataSource() {
        final DriverManagerDataSource dataSource = new DriverManagerDataSource();
				
				...
				...

        return dataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager(){
        JpaTransactionManager transactionManager
                = new JpaTransactionManager(); // 📍
        transactionManager.setEntityManagerFactory(
                entityManagerFactoryBean().getObject() );
        return transactionManager;
    }
}
  • 트랜잭션은 기본적으로 데이터베이스와의 인터렉션 관계에 있기 때문에 데이터 커넥션 정보를 포함하고 있는 DataSource가 기본적으로 필요함 
  • Spring에서 기본적으로 트랜잭션은 PlatformTransacrionManager에 의해 관리되며 이 인터페이스를 구현하여 해당 데이터 액세스 기술에 맞게 유연하게 트랜잭션 기술을 적용할 수 있도록 추상화되어 있음
  • 사용하는 액세스 기술이 JPA이기 때문에 구현 클래스인 JpaTransacrionManger 를 사용하고 있는 것을 알 수 있음

 

1. 작성한 비즈니스 로직에 애너테이션을 추가하는 방법

➡️ @Transactional 애너테이션을 트랜잭션이 필요한 영역에 붙여주면 됨 

 

❓ 실습하던 중 애너테이션을 작성하고 있는데  Transaction에 관한 import 구문이 두 개가 존재함

import org.springframework.transaction.annotation.Transactional; // (1)
import javax.transaction.Transactional; // (2)

(1)번의 경우 : Spring Framework에 특화된 어노테이션으로, Spring 애플리케이션 내에서 트랜잭션 관리 기능을 제공
(2)번의 경우: JTA 표준 Java API로, 분산 트랜잭션을 관리하기 위한 Java EE 애플리케이션에서 활용됨

     

@Service
@Transactional 
// 클래스 레벨에 추가하면 기본적으로 이 클래스에서 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션 적용됨
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
       
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }

 ...
 ...
  • 왜 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용될까? 
    : MemberRepository가 데이터베이스와 직접적인 연관이 있기 때문에 

 

🏁 트랜잭션이 어떻게 적용되는지 확인하기 위한 로그 설정하기 

JPA 로그 레벨 설정

➡️ JPA 로그 레벨 코드를 application.yml에 추가하기 

➡️ DEBUG 레벨로 설정하면 JPA 내부에서 DEBUG 로그 레벨을 설정한 부분의 로그를 확인할 수 있음 


 

➡️ 로그 레벨까지 설정했다면 애플리케이션을 실행시키고 포스트맨으로 postMember() 핸들러 메서드를 호출한 뒤 로그 확인하기

  • MemberService의 createMember 가 호출되면서 새로운 트랜잭션을 생성하고 있음을 확인
  • 트랜잭션에서 commit 이 일어나는 것을 알 수 있음 : Committing JPA transaction on EntityManager ~
  • 트랜잭션 종료됨을 알 수 있음 : Not closing pre-bound JPA EntityManager after transaction
  • JPA EntityManager가 종료됨을 알 수 있음 : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

 

✔️ rollback 동작 확인

➡️ 기존 코드를 아래와 같이 수정하고 애플리케이션 실행한 뒤 똑같이 포스트맨으로 postMembers() 핸들러 메서드 호출한 뒤 확인하기

    public Member createMember(Member member) {
        // 이미 등록된 이메일인지 확인
        verifyExistsEmail(member.getEmail());
        Member resultMember = memberRepository.save(member);
        
        if(true) { // 메서드가 종료되기 전에 강제로 RuntimeException이 발생하도록 수정
            throw new RuntimeException("Rollback test");
        }
        return resultMember;
    }

 

(중간 로그 생략)

  • rollback 잘 진행된 뒤 EntityManager가 닫히는 것까지 확인 가능 
java.lang.RuntimeException 상속받는 언체크 예외는 @Transactional 애너테이션 추가해서 rollback 가능하지만 
체크 예외의 경우 애너테이션만 추가한다고 해서  rollback이 되지 않음

➡️ 캐치(catch)해서 예외를 복구할지 회피할지 적절한 예외 전략을 고민하거나 별도의 예외 전략이 필요하지 않은 경우 
@Transactional(rollbackFor = {SQLException.class, DataFormatException.class} ) 와 같이 예외를 직접 지정해주거나 감싸서 rollback이 동작하도록 할 수 있음 

 

✔️ 메서드 레벨에서 @Transactional 적용

@Transactional(readOnly = true) // 읽기 전용 트랜잭션
    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }

 

Creating new transaction with name [com.codestates.member.service.MemberService.findMember]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly

  • 트랜잭션 설정이 readOnly인 것을 알 수 있음 
조회 메서드에 @Transactional(readOnly = true)로 설정하는 이유
: 기본적으로 JPA에서 commit 이 호출되면 영속성 컨텍스트가 flush됨 하지만 위와 같이 readOnly=true로 설정하면 JPA 내부적으로 영속성 컨텍스트를 flush 하지 않음 또한 변경 감지를 위한 스냅샷도 생성하지 않음

➡️ 전반적으로 불필요한 추가 동작을 줄일 수 있음
➡️ 조회 메서드에서 readOnly 속성을 true로 지정해서 JPA가 자체적으로 성능 최적화를 거치도록 하는 것이 좋음  

 

✔️ 클래스 레벨과 메서드 레벨의 트랜잭션 적용 순서

  • 클래스 레벨에만  @Transactional 애너테이션을 추가할 때
    • 클래스 레벨의  @Transactional 애너테이션이 메서드에 일괄 적용
  • 클래스 레벨과 메서드 레벨에 함께 적용된 경우
    • 메서드 레벨의 @Transactional 애너테이션 적용
    • 만약 메서드 레벨에 해당 애너테이션 없을 경우 클래스 레벨의 @Transactional 애너테이션 적용됨

 

🔎 여러 작업이 하나의 트랜잭션으로 묶이는 경우

➡️ 음료를 주문하면 주문한 개수만큼 스탬프가 찍힌다고 가정했을 때 주문 정보 저장을 위한 트랜잭션이 하나 시작되고, memberService에서 스탬프 업데이트를 위한 트랜잭션이 하나 더 시작될 것

이렇게 각각의 트랜잭션이 독립적으로 실행된다면 updateStamp() 동작에서 예외가 밸생한 경우 스탬프 도장은 찍히지도 않았는데 주문정보는 저장되는 이상한 경우 생김 

 

@Service
@Transactional
public class OrderService {
    private final MemberService memberService;
    private final OrderRepository orderRepository;
    private final CoffeeService coffeeService;
    public OrderService(MemberService memberService,
                        OrderRepository orderRepository,
                        CoffeeService coffeeService) {
        this.memberService = memberService;
        this.orderRepository = orderRepository;
        this.coffeeService = coffeeService;
    }

    public Order createOrder(Order order) {
        verifyOrder(order);
        Order savedOrder = saveOrder(order);
        updateStamp(savedOrder);

        // (2)
        throw new RuntimeException("rollback test");
//        return savedOrder;
    }
@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    ...
		...

	// 📍아래와 같이 애트리뷰트 지정하면 메서드 실행 시 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 사용
    // 존재하지 않으면 새 트랜잭션을 생성하도록 해줌
    @Transactional(propagation = Propagation.REQUIRED) 
    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Optional.ofNullable(member.getName())
                .ifPresent(name -> findMember.setName(name));
        Optional.ofNullable(member.getPhone())
                .ifPresent(phone -> findMember.setPhone(phone));
        Optional.ofNullable(member.getMemberStatus())
                .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));

        return memberRepository.save(findMember);
    }

    @Transactional(readOnly = true)
    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }
		...
		...
}
  • OrderService에서 createOrder() 메서드를 호출하면 트랜잭션이 하나 생성되며 createOrder() 메서드 내에서 updateMember() 메서드를 호출하면 현재 OrderService에서 진행 중인 트랜잭션에 참여함
  • 트랜잭션이 하나로 묶여있기 때문에 MemberService의 updateStamp() 메서드 작업을 처리하는 도중에 예외가 발생해도 두 클래스에서 작업을 처리하는 메서드들이 모두 하나의 트랜잭션 경계 내에 있으므로 모두 rollback 됨
  • 실제로 실행시켜봐도 OrderService에서 강제로 발생시킨 예외로 인해 rollback이 진행됨 
    → h2 콘솔로 확인해 보면 주문 정보와 스탬프 정보가 반영되지 않았음을 확인할 수 있음

 

✔️ 트랜잭션 전파(Transaction Propagation)

➡️ 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때 어떻게 동작할 것인지 결정하는 방식 

➡️ propagation 애트리뷰트를 통해서 설정할 수 있음

 

  1. Propagation.REQUIRED
    : 일반적으로 가장 많이 사용되는 propagation 유형의 디폴트 값
    : 진행 중인 트랜잭션이 없으면 새로 시작하고 진행 중인 트랜잭션이 있으면 해당 트랜잭션에 참여

  2. Propagation.REQUIRES_NEW
    : 이미 진행 중인 트랜잭션과 무관하게 새로운 트랜잭션 시작
    : 기존에 진행 중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때까지 중지됨

  3. Propagation.MANDATORY
    : 진행 중인 트랜잭션이 없으면 예외를 발생시킴
  4. Propagation.NOT_SUPPORTED
    : 트랜잭션을 필요로 하지 않음을 의미
    : 현재 진행 중인 트랜잭션이 있는 경우 해당 트랜잭션 일시적으로 중지
    : 메서드가 실행되는 동안에는 트랜잭션을 시작하지 않으며 트랜잭션 컨텍스트에 접근하지 않음
    : 메서드 내에서 수행되는 데이터베이스 작업은 트랜잭션없이 독립적으로 실행됨
    : 즉, 특성 메서드를 트랜잭션 범위에서 벗어나게 하고자 할 때 사용함 

 

✔️ 트랜잭션 격리 레벨(Isolation Level)

➡️ 트랜잭션은 다른 트랜잭션에 영향을 주지 않고 독립적으로 실행되어야 하는 격리성이 보장되어야 함 

➡️ Spring은 이러한 격리성을 조정할 수 있는 옵션을 @Transactional 애너테이션의 isolation 애트리뷰트를 통해 제공함

 

  1. Isolation.DEFAULT
    : 데이터베이스에서 제공하는 기본값
  2. Isolation.READ_UNCOMMITTED
    : 다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용함
  3. Isolation.READ_COMMITED
    : 다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용함
  4. Isolation.REPEATABLE_READ
    : 트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 함
  5. Isolation.SERIALIZABLE
    : 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 함

 

 

2. AOP 방식의 트랜잭션 적용

package com.codestates.config;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

import java.util.HashMap;
import java.util.Map;

@Configuration // AOP 방식으로 트랜잭션을 적용하기 위한 Configuration 클래스 정의
public class TxConfig {
    // TransactionManager : 마커 인터페이스, 단순히 트랜잭션을 처리하는 최상위 명세, 하위 인터페이스들을 구분하기 위한 용도
    private final TransactionManager transactionManager;


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

    @Bean
    // TransactionInterceptor : AOP 기반 트랜잭션 관리 인터셉터. 메서드 실행전후에 트랜잭션을 시작하고 커밋 또는 롤백하는 트랜잭션 관련 작업 수행
   // 설정 파일에서 메서드 레벨 또는 클래스 레벨에 트랜잭션 애너테이션을 지정하면 해당 메서드를 감싸는 형태로 AOP 적용
    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<String, TransactionAttribute> txMethods = new HashMap<>();
        txMethods.put("find*", txFindAttribute);
        txMethods.put("*", txAttribute);

        txAttributeSource.setNameMap(txMethods);

        return new TransactionInterceptor(transactionManager, txAttributeSource);
    }

    @Bean
    public Advisor txAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        // 포인트컷 지정 --> CoffeeServiece 클래스를 타깃 클래스로 지정함
        pointcut.setExpression("excution(*com.codestates.coffee.service." +
                "CoffeeService.*(..))");

        // AOP에서 어떤 메서드에 어떤 어드바이스를 적용할지 결정하는 역할을 하는 클래스
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

 

BELATED ARTICLES

more