Spring 에서는 @Transactional 이라는 어노테이션을 통해 트랜잭션을 쉽게 사용할 수 있다.

스프링에서는 이를 사용하는 방식을 “선언적 트랜잭션을 사용한다" 라고 표현한다.

내부적으로는 AOP 를 사용하여 구현된다.

  • 이미 다들 알겠지만.. AOP 는 “cross-cutting-concern” 즉, 어플리케이션 전반에 걸친 공통적인 관심사 (대표적으로, 커넥션을 가져오고 커넥션을 반환하여 트랜잭션 설정 하는 것 ) 를 비즈니스 로직 으로 부터 분리 시켜 줄 수 있다.

스프링에서 AOP 는 “메소드 레벨 단위로만 적용 가능" 하다는 특징이 있다.

여기서 @Transactional 역시 메소드 레벨로 적용되는 AOP 로 구현되어있다 보니 생기는 문제점은

트랜잭션의 범위를 좁게 유지할 수 없다.

는 것이다.

만약

@Transactional
void A() {
	// stmt1
	// API call  
	// stmt2
	..
}

이와 같은 메소드가 존재하는 경우, stmt1 에서는 Connection Pool 로부터 커넥션을 하나 가져온다.

A() 메소드 전체는 하나의 트랜잭션 단위이기 때문에, API 를 호출하는 동안에서 stm1 에서 가져온 커넥션을 유지하고 있는다. 그리고 stm2 역시 이 커넥션을 사용하여 DB 에 접근한다.

따라서, Connection Pool 의 커넥션들이 빠르게 고갈될 수 있다. 

Network I/O 는 같은 머신 내의 저장공간에 대한 입출력도 아닌, 네트워크를 통해 요청을 하고 (필요시에는 ) 이에 대한 응답을 받아오는 과정이다. 

따라서 상당한 시간이 걸릴 수 있다. 

 

💥 만약 A() 를 호출해야하는 요청이 동시에 여러 번 일어난다면??

--> A() 메소드에 묶여있는 커넥션들이 채 반환되기도 전에 수많은 요청이 들어오는 것이다.

따라서 커넥션 풀의 커넥션들이 빠르게 고갈될 것이다. 

 

사실 DB I/O 를 위와 같이 다른 종류의 I/O 와 묶어둘 경우 이 문제가 일어날 위험이 높아진다고 볼 수 있다. ( 커넥션을 잡아두고는 사용도 안하니까.. idle 하게 놀고 있게 되는 커넥션들이 생기는 거임 ㅠ-ㅠ idle 한 코어 처럼 생각하면 됨 ) 


 

 

그래서 이번에는 선언적 트랜잭션이 아닌 programmatic transaction 을 사용해보기로 했다.

따라서 아래 클래스가 필요하다. 

PlatformTransactionManager 의 TransactionalTemplate

트랜잭션을 수동으로 관리할 수 있는 일련의 callback 기반의 API 들을 제공한다.

이를 사용하려면 PlatformTransactionManager 를 통해 초기화 하는 것이 필요 하다.

 

 

TransactionTemplate 이 제공하는 exeucte() 메소드에 코드블럭을 전달하면, 이 코드 블락은 하나의 트랜잭션 내에서 수행되게 된다.

이 TransactionCallback 은 함수형 인터페이스이기에 람다를 통해 코드 블럭을 전달해줘도 된다.

또한 여기서 사용되는 트랜잭션은, PlatformTransactionManager 를 통해 TransactionTemplate 을 만들 때, isolation_level, propagation level 등을 세팅 해 줄 수 있다.

@Service
public class TestUserService {
    private final TestUserRepository userRepository;
    private final TransactionTemplate transactionTemplate;

    public TestUserService(
        TestUserRepository userRepository,
        PlatformTransactionManager transactionManager) {
        
        this.userRepository = userRepository;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

 

예시 상황)

updateXXX 메소드 --> updateOne 메소드  

이런식으로 호출되는 상황이다. 

 

updateXXX 메소드에서는 updateOne 을 두 번 호출하여, 각각의 user 의 월급을 업데이트 한다. 

 

참고로 아래 코드들은 개인적인 궁금증들을 해결하기 위해 작성된 극단적인 코드들임에 유의해주면 좋겠다. ex)List 가져와서 사이즈 확인도 안하고 바로 get(index) 해와서 update 하는 코드..

예시 ) 하나의 클래스 내부에서 “트랜잭션설정 안한 메소드" → 선언적 트랜잭션으로 “REQUIRES_NEW” 의 전파레벨을 가진 메소드 호출 시

self-invocation 상황에서 스프링 AOP 는 예상한대로 적용되지 않는다.

Self-invocation 과 @Transactional

 

Spring AOP 는 프록시를 사용하여 구현되는데, 같은 클래스 내부의 메소드를 호출 하는 것은 묵시적으로 this 참조를 통해 self invocation 을 하는 것이다.

따라서 이 경우에는 “프록시" 가 아닌 “this 참조 객체" 에 대해 호출을 하는 것이 된다.

이로 인해, 해당 메소드는 트랜잭션이 설정되지 않는다. 

 

 

따라서 “호출되는 메소드(called method)" 가 @Transactional(propagation = REQUIRED_NEW) 이었다면, self-invocation 으로 인해 " 새로운 트랜잭션 범위에서 실행되지 않고(우리가 annotate 한 @Transactional 이 적용되지가 않기 때문에)" 호출하는 메소의 범위에서 실행된다.

이 경우, 호출하는 메소드에는 어떤 트랜잭션도 설정하지 않았기 때문에, 결국 이 전체 과정은 어떤 트랜잭션 위에서도 실행되는 연산들이 아니게 된다. 

따라서, 호출되는 메소드에서 DB 에 대한 update 를 하더라도 commit 되지 않는다.

 

    public void updateWithoutTransaction(int amount) {
        List<TestUser> users = userRepository.findAll();

        updateOne(users.get(0).getId(), amount);
        updateOne(users.get(1).getId(), amount);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    protected void updateOne(Long id, int updateAmount) {

        userRepository.findById(id)
            .orElseThrow(() ->
                new RuntimeException("해당하는 id 의 사용자가 없습니다"))
            .updateSalary(updateAmount);
    }
        @Test
        @DisplayName("트랜잭션이 존재 하지않는 같은 클래스 메소드 -> @Transactional(propagation = REQUIRES_NEW) 에서는 업데이트가 발생하지 않는다(아예 트랜잭션이 생성되지 않는 상황인 것)")
        void test1() {
            userService.updateWithoutTransaction(beforeSalaryAmount);

            List<TestUser> users = userRepository.findAll();

            Assertions.assertThat(users.get(0).getSalary())
                .isEqualTo(beforeSalaryAmount);
            Assertions.assertThat(users.get(1).getSalary())
                .isEqualTo(beforeSalaryAmount);
        }

 

예시 ) 하나의 클래스 내부에서 “트랜잭션설정 안한 메소드" → programmatic transaction 으로 “REQUIRES_NEW” 의 전파레벨을 가진 메소드 호출 시

Spring AOP 를 통해 트랜잭션을 설정하지 않고, 트랜잭션 템플릿을 통해 트랜잭션을 사용하는 상황이다. 

 

따라서, this 로 호출한 메소드 내부에서 트랜잭션 템플릿을 통해 트랜잭션 내부에서 수행되는 연산을 갖고 있다면, 이는 정상적으로 수행된다. 여기에서 설정한 트랜잭션 옵션도 모두 적용되어, 매 번 새로운 트랜잭션 범위에서 연산이 수행된다. 

따라서 index 0 에 위치한 user 의 월급을 업데이트하는데 성공하고 

index 1 에 위치한 user 의 월급을 업데이트 하는 과정에서는 런타임 예외가 발생하더라도 

다른 트랜잭션에서 이미 커밋 완료된 Index 0 은 롤백 되지 않는다. 

    
    public void updateWithoutTransactionWithCallingTemplateMethod(int amount) {
        List<TestUser> users = userRepository.findAll();

        updateOneThrowingExceptionByConditionWithTemplate(false, users.get(0).getId(), amount);
        updateOneThrowingExceptionByConditionWithTemplate(true, users.get(1).getId(), amount);
    }
    
    protected void updateOneThrowingExceptionByConditionWithTemplate(boolean isThrowException,
        Long id, int updateAmount) {
        transactionTemplate.execute(status -> {
            userRepository.findById(id)
                .orElseThrow(() ->
                    new RuntimeException("해당하는 id 의 사용자가 없습니다"))
                .updateSalary(updateAmount);

            if (isThrowException) {
                throw new RuntimeException("THROW NEW RUNTIME_EXCEPTION");
            }

            return null;
        });
    }
        @Test
        @DisplayName("@Transactional -> TransactionTemplate 을 사용한 메소드 - 에서 호출된 메소드에서 런타임 예외 발생 시 별개의 트랜잭션에서 수행되어 , 이미 업데이트 된 부분은 롤백되지 않는다(즉 내부 호출된 메소드의 트랜잭션이 적용된 상황!원래는 self-invocation 으로 적용이 안되는데, 프로그래밍적 트랜잭션 사용하여 적용이 가능해진 것)")
        void test4() {
            Assertions.assertThatExceptionOfType(RuntimeException.class)
                .isThrownBy(() -> userService.updateWithoutTransactionWithCallingTemplateMethod(
                    beforeSalaryAmount));

            List<TestUser> users = userRepository.findAll();

            Assertions.assertThat(users.get(0).getSalary())
                .isEqualTo(beforeSalaryAmount * 2);
            Assertions.assertThat(users.get(1).getSalary())
                .isEqualTo(beforeSalaryAmount);
        }

 

참조

https://www.baeldung.com/spring-programmatic-transaction-management

 

Programmatic Transaction Management in Spring | Baeldung

Learn to manage transactions programmatically in Spring and why this approach is sometimes better than simply using the declarative Transactional annotation.

www.baeldung.com

https://multifrontgarden.tistory.com/289

 

TransactionTemplate 을 이용한 트랜잭션 제어

spring framework 를 이용하여 트랜잭션을 제어할 일이 생기면 대부분 @Transactional 애노테이션을 이용할 것이다. 이 방식은 가장 간단하기도하며 spring 을 공부하는 자료들에선 대부분 선언적으로 트

multifrontgarden.tistory.com

 

복사했습니다!