티스토리 뷰

Spring

[Spring] Spring에서 트랜잭션의 사용법 - (3/3)

망나니개발자 2021. 7. 9. 19:10
반응형

 

1. @Transactional 어노테이션(선언적 트랜잭션)


[ @Transactional  어노테이션 ]

Spring에서는 클래스나 인터페이스 또는 메소드에 부여할 수 있는 @Transactional이라는 어노테이션을 제공하고 있다.

이 어노테이션이 붙으면 스프링은 해당 타깃을 포인트 컷의 대상으로 자동 등록하며 트랜잭션 관리 대상이 된다. 즉, 이 어노테이션을 통해 포인트 컷에 등록하고 트랜잭션 속성을 부여하는 것이다.

이렇듯 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 해주는 것을 선언적 트랜잭션(declarative transaction)이라고 한다. 반대로 TrnasactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 이용해 직접 코드안에서 사용하는 방법을 프로그램에 의한 트랜잭션(programmatic transaction)이라고 한다. 스프링에서는 두 가지 방식을 모두 지원하지만, 특별한 경우가 아니라면 선언적 트랜잭션을 사용하는 것이 좋다.

왜냐하면 @Transactional 어노테이션을 이용하면 트랜잭션 속성을 메소드 단위로 다르게 지정할 수 있어 매우 세밀한 트랜잭션 속성 제어가 가능할 뿐만 아니라 직관적이므로 이해하기도 좋다.

대신 이 어노테이션을 이용하려면 @EnableTransactionManagement을 추가해주어야 한다.

 

 

[ @Transactional의 롤백 처리 ]

Java에는 체크 예외(Checked Exception)와 언체크 예외(Unchecked Exception)가 있다. 두 가지 예외 종류를 구분하는 것이 중요한 이유는 트랜잭션 롤백 범위가 다르기 때문이다. 체크 예외란 Exception 클래스 하위 클래스이며, 언체크 예외란 Exception 하위의 RuntimException 하위의 예외이다. (Java 예외 종류에 대해 모르면 이 글을 참고해주세요)

스프링의 선언적 트랜잭션(@Transactional) 안에서 예외가 발생했을 때, 해당 예외가 언체크 예외(런타임 예외)라면 자동적으로 롤백이 발생한다. 하지만 체크 예외라면 롤백이 되지 않는다. 체크 예외를 롤백시키기 위해서는 @Transactional의 rollbackFor 속성으로 해당 체크 예외를 적어주어야 한다.

스프링의 트랜잭션이 언체크 예외(런타임 예외)나 에러(Error) 만을 롤백 대상으로 보는 이유는 해당 예외들이 복구 가능성이 없는 예외들이므로 별도의 try-catch나 throw를 통해 처리를 강제하지 않기 때문이다. 스프링의 트랜잭션은 내부적으로 언체크 예외(런타임 예외)이거나 에러(Error) 인지 검사한 후에 맞으면 롤백 여부를 결정하는 rollback-only를 True로 변경하는 로직이 있다. (정확히는 TransactionInfo의 transactionAttribute의 rollbackOn에 의해 검사된다) 

@Override
public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

 

 

그리고 만약 언체크 예외라면 rollback 처리를 진행한다.

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (logger.isTraceEnabled()) {
        logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
    }
    if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
        try {
            txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
        }
    ...
    
}

 

 

 

[ @Transactional의 대체 정책(Fallback Policy) ]

만약 모든 메소드에 @Transactional이 붙어있으면 메소드가 상당히 더러워진다. 그래서 스프링은 메소드 외에도 클래스와 인터페이스에 어노테이션을 붙일 수 있도록 하고 있다. 그리고 트랜잭션 어노테이션을 적용할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스 or 인터페이스) 순으로 @Transactional이 적용되었는지 차례로 확인하고, 가장 먼저 발견되는 속성 정보를 사용한다. 이를 4단계의 대체 정책(fallback policy)라고 부르며, 이를 통해 어노테이션을 최소화하는 동시에 세밀한 제어를 해줄 수 있다.

 

 

 

2. Spring에서 트랜잭션의 사용법


[ Spring에서 트랜잭션의 활용법 ]

1. 비지니스 로직과의 결합

트랜잭션을 중구난방으로 적용하는 것은 좋지 않다. 대신 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 좋은데, 일반적으로 비지니스 로직을 담고 있는 서비스 계층의 메소드와 결합시키는 것이 좋다. 왜냐하면 데이터 저장 계층으로부터 읽어온 데이터를 사용하고 변경하는 등의 작업을 하는 곳이 서비스 계층이기 때문이다. 위와 같이 클래스 레벨에 트랜잭션 어노테이션을 붙여주면 메소드까지 적용이 된다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    public List<User> getUserList() {
        return userRepository.findAll();
    }

}

 

서비스 계층을 트랜잭션의 시작과 종료 경계로 정했다면, 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 한다. 트랜잭션은 보통 서비스 계층의 메소드 조합을 통해 만들어지기 때문에 DAO가 제공하는 주요 기능은 서비스 계층에 위임 메소드를 만들어둘 필요가 있다. 그리고 가능하면 다른 모듈의 DAO에 접근할 때는 서비스 계층을 거치도록 하는 것이 바람직하다.

 

 

2. 읽기 전용 트랜잭션의 공통화

클래스 레벨에는 공통적으로 적용되는 읽기전용 트랜잭션 어노테이션을 선언하고, 추가나 삭제 또는 수정이 있는 작업에는 쓰기가 가능하도록 별도로 @Transacional 어노테이션을 메소드에 선언하는 것이 좋다. 이를 체감하기는 힘들겠지만 약간의 성능적인 이점을 얻을 수 있다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    public List<User> getUserList() {
        return userRepository.findAll();
    }

    @Transactional
    public User signUp(final SignUpDTO signUpDTO) {
        final User user = User.builder()
                .email(signUpDTO.getEmail())
                .pw(passwordEncoder.encode(signUpDTO.getPw()))
                .role(UserRole.ROLE_USER)
                .build();

        return userRepository.save(user);
    }

}

 

 

 

3. 테스트의 롤백

트랜잭션 어노테이션을 테스트에 붙이면 테스트의 DB 커밋을 롤백해주는 기능이 있다.

DB와 연동되는 테스트를 할 때에는 DB의 상태와 데이터가 상당히 중요하다. 하지만 문제는 테스트에서 DB에 쓰기 작업을 하면 DB의 데이터가 바뀌는 것인데, 트랜잭션 어노테이션을 테스트에 활용하면 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 진행하기 전의 상태로 만들어준다. 어떠한 경우에도 커밋을 하지 않기 때문에 테스트가 성공하거나 실패해도 상관이 없으며 심지어 예외가 발생해도 어떠한 문제가 발생하지 않는다. 강제로 롤백시키도록 설정되어 있기 때문이다.

@Transactional
@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmailAndPw() {
        final User user = User.builder()
                .email("email")
                .pw("pw")
                .role(UserRole.ROLE_USER).build();
        userRepository.save(user);

        assertThat(userRepository.findAll().size()).isEqualTo(1);
    }

}

 

하지만 테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고는 싶지만 강제 롤백을 원하지 않을 수 있다. 테스트의 작업을 그대로 DB에 반영하고 싶다면 @Rollback(false)를 이용해주면 된다. @Rollback은 메소드에만 적용가능하므로, 클래스 레벨에 부여하기를 원한다면 @TransactionConfiguration(defaultRollback=false) 를 이용하고, 롤백을 원하는 메소드에 @Rollback(true)를 이용하면 된다.

 

물론 여기서 auto_increment나 sequence 등에 의해 증가된 값은 롤백이 되지 않는다. 그렇기 때문에 테스트를 위해서는 별도의 데이터베이스로 연결을 하거나 또는 H2와 같은 휘발성(인메모리) 데이터베이스를 사용하는 것이 좋다.

 

 

 

 

 

관련 포스팅

  1. 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction(트랜잭션) 핵심 기술 - (1/3)
  2. Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) - (2/3)
  3. Spring에서 트랜잭션의 사용법 - (3/3)
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
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
글 보관함