본문 바로가기
Spring

Spring - Transaction Manager

by icblue21 2022. 11. 21.
728x90

스프링에서는 @Transaction 어노테이션을 이용한 트랜잭션 처리를 지원합니다. 이번 포스트에서는 트랜잭션이란 무엇이고 스프링에서 트랜잭션 처리를 어떻게 하는지에 대해 살펴보겠습니다.

트랜잭션이란?

트랜잭션은 DB 상태를 변화시키기 위해 수행하는 작업의 논리적인 단위를 말합니다. 여기서 DB 상태를 변화시킬 수 있는 행동이란 SELECT, UPDATE, INSERT, DELETE, CREATE, DROP 같은 행위를 말합니다. 여기서 SELECT는 데이터베이스 상태를 변화시키진 않지만 트랜잭션으로 관리하면 좋습니다. 왜냐하면 SELECT를 수행하는 동안 해당 테이블을 업데이트할 수 없도록 하고, 보통 SELECT를 한 결과를 UPDATE,INSERT,DELETE의 매개변수로 이용할 수 있기 때문입니다.

 

작업은 트랜잭션 단위로 COMMIT(커밋) 혹은 ROLLBACK(롤백) 됩니다. 커밋은 하나의트랜잭션이 성공적으로 끝나고 데이터베이스가 일관성있는 상태가 되었다고 알려주는 연산이며, 롤백은 하나의 트랜잭션 처리가 비정상적으로 종료되어 트랜잭션을 처음부터 다시 시작하거나, 트랜잭션의 부분적으로만 연산된 결과를 다시 취소시키는 연산입니다.

 

트랜잭션은 ACID라 불리는 4가지 성질을 가지고 있습니다.

  • 원자성(Atomicity) - 한 트랜잭션 내에 실행한 연산들은 하나의 작업을 봄 (모두 성공하거나 실패)
  • 일관성(Consistency) - 작업 처리 결과 항상 일관성이 있어야 함. 데이터베이스 상태를 일관성 있게 유지
  • 독립성(Isolation) - 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않아야 함
  • 지속성(Durability) - 트랜잭션 작업이 성공하면 결과는 영구적으로 반영되어야 함

스프링에서의 트랜잭션 처리

일반적으로 하나의 서비스 로직 단위로 트랜잭션 처리를 합니다. 하나의 서비스 함수는 다양한 DAO 함수 호출을 통해 하나의 트랜잭션을 구성합니다. 예외발생시 예외발생 전에 실행된 모든 DAO 명령들을 롤백하거나 정상동작한 경우 커밋할 수 있습니다.

 

스프링에서 테스트 클래스에 @Transactional 을 붙여줄 경우 메서드마다 데이터를 롤백합니다. 여기서 주의할 점은 AUTO_INCREMENT 된 값은 트랜잭션 외부에서 작동하기 때문에 롤백되지 않는다는 점입니다. 만약 특정 열을 AUTO_INCREMENT 한 경우 이를 바로 뷰 쪽에 보여주면 정확하지 않은 데이터를 표시할 수 있습니다.

 

스플링에서 트랜잭션은 다음과 같은 순서로 처리됩니다.

  1. @Transactional 적용되어있을 경우 해당 클래스에 대한 트랜잭션 기능 적용된 프록시 객체 생성
  2. 해당 프록시 객체는 @Transactional 포함된 메소드 호출될 경우 PlatformTransactionManager를 사용하여 트랜잭션을 시작. 메소드가 호출될 경우 트랜잭션 단위로 묶습니다.
  3. 정상 여부에 따라 Commit 또는 Rollback. 정상 여부는 RuntimeException 발생 여부로 판단합니다.

스프링에서 트랜잭션 적용 방법

  1. Transaction Manager를 스프링 빈으로 등록합니다.
    @Bean 
    public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManager() {
    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
    dataSourceTransactionManager.setDataSource(dataSource());
    return dataSourceTransactionManager;
    }
  2. @Transactional 어노테이션을 트랜잭션 관리하고 싶은 메소드 단에 작성합니다.
    • 클래스 단이 아니라 DAO를 통해 CRUD를 실행하는 서비스 단에 붙여주는 것이 좋습니다.
    • 클래스 단에 작성할 경우 DB이외의 다른 외부 자원을 이용하는 경우 DB까지 영향을 미칠 수 있고, 각 메서드와 상황에 따라 트랜잭션을 어떻게 처리할 지 달라지기 때문입니다.
  3. 만약 DB에 있는 데이터를 Read만 하는 경우에도 필요하면 @Transactional 을 붙일 수 있습니다.
    • 보통 이런 경우 @Transactional(readOnly = true) 로 값을 지정합니다
      @Transactional(readOnly = true) // CUD를 할 경우 예외발생
      public Member findById(String uId) {
      // ... select ....
      }

주의사항!

서비스 로직에서 외부 API를 호출하거나 DB 이외의 외부 자원에 접근하는 경우 트랜잭션으로 묶는 것은 좋지 않습니다. 다수 요청이 같은 트랜잭션으로 묶이면 동일한 DB커넥션이 오랫동안 물려있어서 DB커넥션 타임아웃같은 이슈가 발생할 수 있기 때문입니다.

 

JPA는 CrudRepository 자체적으로 @Transactional 을 가집니다.

@Transactional 옵션

  • isolation : 트랜잭션에서 일관성없는 데이터 허용 수준을 설정
    • 여러 개의 트랜잭션이 동시에 데이터베이스 접근했을 때 해당 트랜잭션들을 어떻게 처리할 것인지에 대한 설정 ( 동시에 처리 or 하나씩 처리 등 )
    • 기본값은 default로 DB의 기본값을 따라감
      @Transactional(isolation = Isolation.REPEATABLE_READ)
      public Member findById(String uId) {
      // ...
      }
  • propagation : 트랜잭션 동작 도중 다른 트랜잭션을 호출할 때 어떻게 할 것인지 지정하는 옵션
    @Transactional(propagation=Propagation.REQUIRED)
    public void addMember(MemberDTO dto) throws Exception {
        // 로직 구현
    }
  • noRollbackFor : 특정 예외 발생 시 롤백하지 않음
    @Transactional(noRollbackFor=Exception.class)
    public void addMember(MemberDTO dto) throws Exception {
        // 로직 구현
    }
  • rollbackFor : 특정 예외 발생 시 강제로 롤백
    @Transactional(rollbackFor=Exception.class)
    public void addMember(MemberDTO dto) throws Exception {
        // 로직 구현
    }
  • timeout : 지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback, -1일 경우 timeoout을 사용하지 않는 다는 의미 (default)
    @Transactional(timeout=10)
    public void addMember(MemberDTO dto) throws Exception {
        // 로직 구현
    }
  • readOnly : 트랜잭션을 읽기 전용으로 처리 , true : CUD시 예외 발생, false : 기본 값
    @Transactional(readonly = true)
    public void addMember(MemberDTO dto) throws Exception {
        // 로직 구현
    }

checked vs unchecked 예외 전략

checked exception은 반드리 처리해야하는 예외로, 예외를 무조건 핸들링 해야한다. 반면에, unchecked exception은 명시적인 예외처리를 강제하지 않아 예외를 핸들링 하지 않아도 상관없다. 스프링에서는 이 두가지 예외의 성질을 이용하여 하나의 전략을 세울 수 있다.

@Service
public class MemberService {

        private final MemberRepository memberRepository;

        // (1) RuntimeException 예외 발생 - 롤백 ⭕️
        @Transactional
        public Member createUncheckedException() {
          final Member member = memberRepository.save(new Member("yun"));
          if (true) {
            throw new RuntimeException(); // Unchecked Exception
          }
          return member;
        }

        // (2) IOException 예외 발생 - 롤백 ❌
        @Transactional
        public Member createCheckedException() throws IOException {
          final Member member = memberRepository.save(new Member("wan"));
          if (true) {
            throw new IOException(); // Checked Exception
          }
          return member;
        }

}

위 코드에서 Checked Exception은 예외가 발생해도 롤백되지 않는다. 왜그럴까?


바로 스프링의 매커니즘 때문이다. 스프링에서는 Checked Exception은 개발자가 복구 작업을 했을 것이라 생각하여 롤백을 하지 않는 매커니즘을 가지고 있다. 개발자들은 이러한 특징을 이용하여 특정 이미지 파일을 찾아서 전송해주는 함수에서 이미지를 찾지 못했거나 파일을 찾지 못한 경우 FileNotFouondException 기본 이미지를 전송하는 등의 전략을 세우기 시작했다.

 

하지만 현실적으로 Checked Exception 발생 시 복구 전략을 가지고 복구할 수 있는 경우는 많지 않다. 예를들어 유니크해야 하는 이메일 값이 중복되어 SQLException이 발생하는 경우, 유저가 입력했던 이메일 + 난수를 입력해서 다시 삽입하면 가능 하겠지만, 현실에서는 RuntimeException을 발생시키고 사용자가 입력을 다시하도록 유도하는 것이 현실적이다. 따라서, SQLException이 발생할 경우 이를 그대로 전달하지 않고 DuplicateEmailException과 같은 unchecked exception으로 구체적인 정보를 전달하는 예외 클래스로 랩핑해서 전달하는 게 좋다.

@Transactional 이 작동하지 않는 경우

  1. 메서드의 접근제어자가 public이 아닌 경우 트랜잭션 매니저가 @Transactional을 관리할 수 없다
  2. @Transactional이 없는 메소드에서 @Transactional 이 붙은 다른 메소드를 호출하는 경우
    • 스프링에서는 인스턴스에서 처음 호출하는 메서드나 클래스 속성을 따라간다.
    • 아래 예시는 method1이 method2의 상위 메서드이기 때문에 @Transactional이 작동하지 않는다
@Service
public class MemberService {
        // 트랜잭션 매니저 프록시 객체    


    public void method1() {
        method2(); // method1()에서 method2()를 호출해도 트랜잭션 처리되지 않음
    }

    @Transactional
    public void method2() { 
        // ...
    }
}

트랜잭션으로 처리하려면 상위 메서드에 @Transactional을 붙이거나 클래스를 분리하는 방법이 있다.

'Spring' 카테고리의 다른 글

Spring - Filter / Interceptor  (0) 2022.11.23
Spring - MyBatis  (0) 2022.11.21
Spring - JdbcTemplate  (0) 2022.11.21
Spring - ConnectionPool  (0) 2022.11.21
Spring - Log4j2  (0) 2022.11.21

댓글