티스토리 뷰

728x90
반응형

목차

     

     

     

     

    지난 글에서 트랜잭션의 정의와 규칙, 구체적인 WorkFlow에 대해 알아보았다.

     

    2022.09.05 - [개발/Spring] - [Spring]트랜잭션(Transaction)

     

    [Spring]트랜잭션(Transaction)

    트랜잭션이란 더 이상 쪼갤 수 없는 일처리의 최소 단위를 뜻한다. 이전에 SQL에 대해 알아볼 때 등장한 적이 있다. 2022.08.05 - [개발/Database] - [데이터베이스]SQL [데이터베이스]SQL SQL(Structured Query L.

    gnidinger.tistory.com

     

    요약하면 트랜잭션이란 모두 성공하거나 모두 실패하는(All or Nothing), 쪼갤 수 없는 작업의 최소 단위이며

     

    ACID Principles에 의해 안정성을 보장받고 있다.

     

    또한 CommitRollback 명령어로 제어가 가능하며

     

    commit()의 경우 내부적으로 상당히 복잡한 과정을 거쳐 호출된다.

     

    이번 글에서는 Spring Boot 환경에서 선언형 방식으로 트랜잭션을 적용하는 두 가지 방식에 대해 살펴본다.

     

    관련 설정은 Spring Boot가 내부적으로 처리해주기 때문에, 여기서는 바로 트랜잭션을 적용한다.

     

    시작하기 전에 트랜잭션 과정이 로그에 찍히도록 application.yml에 다음과 같은 설정을 추가한다.

    ...
    
    logging:
      level:
        org:
          springframework:
            orm:
              jpa: DEBUG

     

    @Transactional

     

    스프링에서 트랜잭션을 적용하는 가장 간단한 방법은 필요한 영역에 @Transactional을 추가하는 것이다.

     

    ...
    
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @Transactional // 추가
    public class MemberService {
        private final MemberRepository memberRepository;
    
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        ...
    }

    위와 같이 클래스 레벨에 @Transactional을 추가하면 해당 클래스에서

     

    MemberRepository를 사용하는 모든 메서드에 트랜잭션이 적용된다.

     

    commit 동작 여부 확인

     

    애플리케이션을 실행시키고 Postman을 통해 회원 정보를 입력하면 아래와 같은 로그가 출력된다.

    2022-09-06 11:08:29.014 DEBUG 55436 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager
    : Creating new transaction[...MemberService.createMember]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    
    : Initiating transaction commit
    : Committing JPA transaction on EntityManager [SessionImpl(36635712<open>)]
    : Not closing pre-bound JPA EntityManager after transaction
    : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

    createMember() 메서드가 호출되며 트랜잭션이 생성, commit() 후 종료되는 것을 확인할 수 있다.

     

    rollback 동작 여부 확인

     

    계속해서 예외를 발생시킨 뒤 rollback이 작동하는 로그를 확인하자.

    ...
    
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @Transactional
    public class MemberService {
        ...
    
        public Member createMember(Member member) {
            verifyExistsEmail(member.getEmail());
            Member resultMember = memberRepository.save(member);
    
            if (true) throw new RuntimeException("Rollback Test"); // 예외 발생
    
            return resultMember;
        }
        ...
    }

    회원 정보를 저장하고 결과를 리턴하기 전 강제로 RuntimeException을 발생시켰다.

     

    동일하게 Postman을 통해 요청을 보내면 이번엔 이런 로그가 뜬다.

    2022-09-06 11:24:15.884 DEBUG 56455 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager
    : Initiating transaction rollback
    : Rolling back JPA transaction on EntityManager [SessionImpl(1661184085<open>)]
    : Not closing pre-bound JPA EntityManager after transaction
    
    2022-09-06 11:24:15.951 ERROR 56455 --- [nio-8080-exec-2] c.c.m.s.advice.GlobalExceptionAdvice
    : # handle Exception
    
    java.lang.RuntimeException: Rollback Test
    
    2022-09-06 11:24:16.350 DEBUG 56455 --- [nio-8080-exec-2] o.j.s.OpenEntityManagerInViewInterceptor
    : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

    예외에 따라 rollback이 잘 작동하는 것을 확인할 수 있다.

     

    체크 예외(Checked Exception)의 Rollback

     

    @Transactional은 기본적으로 예상하지 못한 Unchecked Exception, Error 만을 Rollback 한다.

     

    즉, SQLException, DataFormatException과 같은 Checked Exception

     

    @Transactional 만으로는 Rollback이 동작하지 않는다.

     

    Checked Exception의 경우 예상되는 예외이기 때문에 catch 후 예외 전략을 수립해야 하며, 별도의 전략이 필요 없을 시

     

    @Transactional(rollbackFor = Exception.class)

     

    과 같이 해당 예외를 지정하거나 Unchecked Exception으로 감싸주어야 한다.

     

    Method Level에 @Transactional 적용

     

    이번에는 클래스 레벨과 함께 메서드 레벨에도 @Transactional을 적용해보자.

    ...
    
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @Transactional // 클래스 레벨에 적용
    public class MemberService {
        ...
        
        @Transactional(readOnly = true) // 메서드 레벨에 적용
        public Member findMember(long memberId) {
            return findVerifiedMember(memberId);
        }
        ...
    }

    findMember() 메서드 레벨에도 @Transactional이 적용된 것을 확인할 수 있다.

     

    (readOnly = true) 속성은 해당 트랜잭션을 읽기 전용으로 설정한다.

    2022-09-06 11:49:06.957 DEBUG 57607[...MemberService.findMember]
    : PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
    
    : Committing JPA transaction on EntityManager [SessionImpl(574318919<open>)]

    실행시키고 로그를 확인하면 트랜잭션 설정이 readOnly인 것을 볼 수 있다.

     

    @Transactional(readOnly = true)

     

    위 로그에서 또 하나 확인할 수 있는 건 읽기 전용임에도 commit을 진행한다는 점이다.

     

    그럼에도 불구하고 DB의 상태가 변하지 않는 이유는, 간단하게도 flush를 진행하지 않기 때문이다.

     

    flush는 이전에 JPA에 대해 처음 알아보던 글에서 등장한 적이 있는데,

     

    2022.08.31 - [개발/Spring] - [Spring]JPA(Java Persistence API)

     

    [Spring]JPA(Java Persistence API)

    지난 글에서 JDBC의 단점을 극복하며 나온 ORM에 대해 다루었다. 2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM [Spring]SQL Mapper vs. ORM 지난 글에선 웹 앱의 계층 구조와 JDBC Workflow에 대해..

    gnidinger.tistory.com

    flush란 한 마디로 말하면 영속성 컨텍스트와 DB의 동기화 과정이다.

     

    따라서 읽기 전용 트랜잭션은 commit을 진행하지만 DB의 상태가 변하지 않으며,

     

    이를 대략적으로 나타내면 아래 그림과 같다.

     

     

    Class Level / Method Level @Transactional 적용 순서

     

    • 클래스 레벨에만 적용된 경우 - 해당 클래스의 메서드에 @Transactional 일괄 적용
    • 클래스 레벨과 메서드 레벨에 함께 적용된 경우 - 메서드 레벨 → 클래스 레벨 순으로 @Transactional 적용

     

    여러 작업을 하나의 트랜잭션으로 묶기

     

    위 그림은 커피 주문 앱에서 주문 정보를 DB에 저장하는 상황이다.

     

    주문 정보를 저장하는 작업과 스탬프를 업데이트하는 작업이 독립적으로 진행되고 있는데,

     

    이때 updateStamp()에서 예외가 발생할 경우 주문 정보는 저장되지만 스탬프는 업데이트되지 않는 상황이 벌어질 것이다.

     

    따라서 위 그림처럼 두 개의 작업을 하나의 트랜잭션으로 묶을 필요성이 생기는데,

     

    이 경우 updateStamp()에서 예외가 발생하면 두 클래스의 작업이 묶여있으므로 한 번에 Rollback 처리가 된다.

     

    코드와 동작 로그를 확인하자.

    ...
    
    @Service
    @Transactional
    public class OrderService {
        ...
    
        public Order createOrder(Order order) {
            verifyOrder(order);
            Order savedOrder = saveOrder(order); 
            updateStamp(savedOrder); // updateStamp() 호출 -> Transaction Binding
            
    
            throw new RuntimeException("Rollback Test"); // 예외 발생
    
    //        return savedOrder;
        }
    
        public void updateStamp(Order order) {
            Member member = memberService.findMember(order.getMember().getMemberId());
            int stampCount = calculateStampCount(order);
    
            Stamp stamp = member.getStamp();
            stamp.setStampCount(stamp.getStampCount() + stampCount);
            member.setStamp(stamp);
    
            memberService.updateMember(member);
        }
    
        private int calculateStampCount(Order order) {
            return order.getOrderCoffees().stream()
                    .map(orderCoffee -> orderCoffee.getQuantity())
                    .mapToInt(quantity -> quantity)
                    .sum();
        }
    
        private Order saveOrder(Order order) {
            return orderRepository.save(order);
        }
        ...
    }
    ...
    
    @Service
    @Transactional
    public class MemberService {
        private final MemberRepository memberRepository;
        ...
    
        @Transactional(propagation = Propagation.REQUIRED) // Propagation 속성 지정
        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);
        }
        ...
    }
    2022-09-06 14:24:25.020 DEBUG 63635 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager
    : Initiating transaction rollback
    : Rolling back JPA transaction on EntityManager [SessionImpl(142478777<open>)]
    : Not closing pre-bound JPA EntityManager after transaction
    : # handle Exception
    
    java.lang.RuntimeException: Rollback Test
    
    2022-09-06 14:24:25.090 DEBUG 63635 --- [nio-8080-exec-5] o.j.s.OpenEntityManagerInViewInterceptor
    : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

    앱을 실행시키고, 회원과 커피 정보를 등록한 후 주문을 넣으면 위와 같은 로그를 확인할 수 있다.

     

    두 작업이 OrderService에서 시작된 트랜잭션으로 묶여있기 때문에 모두 Rollback 처리된다.

     

    트랜잭션 전파(Transaction Propagation)

     

    트랜잭션 전파란 위 경우처럼 특정 트랜잭션 내부에서 다른 트랜잭션을 실행할 경우 처리하는 방식을 의미한다.

     

     @Transactional 애너테이션의 propagation 속성으로 지정할 수 있으며,

     

    대표적으로 아래와 같은 유형의 전파가 존재한다.

     

    종류 설명 다이어그램
    REQUIRED(default) 부모 트랜잭션이 존재하면 합류
    없다면 새로운 트랜잭션 생성
    예외 발생시 전체 Rollback
    REQUIRES_NEW 무조건 새로운 트랜잭션 생성
    기존의 트랜잭션은 새 트랜잭션 종료까지 정지
    MANDATORY 부모 트랜잭션이 존재하면 합류
    없다면 예외 발생
    NOT_SUPPORTED 부모 트랜잭션이 있다면 메서드 이후까지 보류
    없다면 트랜잭션 생성하지 않음
    NEVER 부모 트랜잭션이 있다면 예외 발생
    없다면 트랜잭션 생성하지 않음

    트랜잭션 고립 수준(Isolation Level)

     

    트랜잭션 고립 수준은 ACID Princlples의 Isolation 원칙을 조정할 수 있는 속성이다.

     

    @Transactional(isolation = Isolation.DEFAULT)과 같이 사용한다.

     

    조금 더 정확하게는 여러 개의 트랜잭션이 실행 중일 때 특정 트랜잭션이 다른 트랜잭션에 접근할 수 있는 정도를 나타낸다.

     

    대략 아래와 같은 종류가 존재한다.

     

    종류 설명
    REPEATABLE_READ(default) SELECT시 현재 시점의 스냅샷을 만들고 조회한다.
    즉, 반복해서 조회해도 같은 결과를 얻는다.
    트랜잭션 내에서 일관성을 보장한다.
    READ_UNCOMMITTED Commit 되지 않은 데이터에 다른 트랜잭션이 접근할 수 있다.
    Commit 이나 Rollback에 상관없이 현재의 데이터를 읽어온다.
    Rollback 될 데이터도 읽어올 수 있으므로 주의가 필요하다.
    READ_COMMITTED Commit 된 데이터에 다른 트랜잭션이 접근할 수 있다.
    구현 방식의 차이로 Query 수행 시점의 데이터와 정확하게 일치하지 않을 수 있다.
    대량의 데이터를 이동하거나 복제할 때 사용된다.
    SERIALIZABLE 동일 데이터에 대해 두 개 이상의 트랜잭션이 수행될 수 없다.
    가장 단순하고 엄격한 격리수준이다.

     

    AOP

     

    AOP는 위에 알아본 @Transactional을 사용하지 않고도 트랜잭션을 적용하는 방법이다.

     

    CoffeeService에 적용하는 예를 확인하자.

     

    상위 폴더에 config 패키지를 생성해 트랜잭션 Config 파일을 생성한다.

    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.*;
    import java.util.HashMap;
    import java.util.Map;
    
    
    @Configuration // @Bean 등록을 위한 애너테이션
    public class TxConfig {
        private final TransactionManager transactionManager;
    
        public TxConfig(TransactionManager transactionManager) { // 트랜잭션 적용을 위해 TransactionManager 객체 DI
            this.transactionManager = transactionManager;
        }
    
        @Bean
        public TransactionInterceptor txAdvice() { // 트랜잭션 어드바이스용 TransactionInterceptor 빈 등록
            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); // Map의 key를 메서드 이름 패턴으로 지정
            txMethods.put("*", txAttribute);
    
            txAttributeSource.setNameMap(txMethods); // 위의 Map 객체를 txAttributeSource()에게 전달
    
            return new TransactionInterceptor(transactionManager, txAttributeSource);
            // TransactionInterceptor의 생성자 파라미터로 transactionManager, txAttributeSource 지정
        }
    
        @Bean
        public Advisor txAdvisor() {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); // 포인트 컷 지정
            pointcut.setExpression("execution(* com.codestates.coffee.service." + "CoffeeService.*(..))");
    
            return new DefaultPointcutAdvisor(pointcut, txAdvice());
            // Advisor 객체 생성 및 파라미터로 포인트컷과 어드바이스 지정
        }
    }

    이어서 CoffeeService 클래스의 createCoffee()를 아래와 같이 수정하고

    public Coffee createCoffee(Coffee coffee) {
            String coffeeCode = coffee.getCoffeeCode().toUpperCase();
    
            verifyExistCoffee(coffeeCode);
            coffee.setCoffeeCode(coffeeCode);
    
            if (true) throw new RuntimeException("Rollback Test");
    
            return coffeeRepository.save(coffee);
        }

    요청을 보내면 Rollback이 실행되는 것을 쉽게 확인할 수 있다.

    반응형
    댓글
    공지사항
    최근에 올라온 글
    최근에 달린 댓글
    Total
    Today
    Yesterday
    링크
    «   2024/11   »
    1 2
    3 4 5 6 7 8 9
    10 11 12 13 14 15 16
    17 18 19 20 21 22 23
    24 25 26 27 28 29 30
    글 보관함