spring boot/기술 적용

@Transactional의 정의, 같은 클래스에서 호출, 예외처리

ballde 2022. 6. 22. 11:16

트랜잭션이 무엇인가? 그리고 특징은? @Transaction을 같은 클래스에서 호출할 경우? @Transaction이 CheckedException, UnCheckedException에서 어떻게 동작하는지?

트랜잭션이란 무엇인가?

  • 트랜잭션이란 데이터 베이스의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들을 의미
  • 상태변경 - insert, update, select, delete

트랜잭션의 특징(ACID)

  • 원자성(Atomicity)
    • 트랜잭션이 데이터베이스에 모두 반영되던가 전혀 반영이 되지 않아야한다.
  • 일관성(Consistency)
    • 트랜잭션 처리 결과가 항상 일관성 있어야한다.
  • 독립성(Isolation)
    • 둘 이상의 트랜잭션이 동시에 실행되고 있을 경우 다른 트랜잭션의 연산에 끼어들 수 없다.
  • 지속성(Durability)
    • 트랜잭션이 성공적으로 완료됐을 경우, 결과는 영구적으로 반영이 되어야 한다.

@Transaction을 같은 클래스에서 호출할 경우?

  • transaction 처리
    • aop 방식으로 한 클래스에서 일괄적으로 처리
    • 각 메서드나 클래스 범위에 transaction 처리

예시 transaction처리를 각 메서드에서 처리해주는데 여러개 update요청한다. 예를 들어 상점아이디 5개가 들어왔는데 3번째에서 실패했을 경우 실패 이전의 상점 이름 변경을 성공처리 한다. ⇒ 반복을 통해서 update() 시킨다.

@Service
@RequiredArgsConstructor
public class StoreSystemService {

    private final StoreRepository storeRepository;

    public void updateStore(TestRequest request) {
        for (Long id : request.getStoreList()) {
            update(id);
        }
    }

    @Transactional
    public void update(Long id) {
        Store store = storeRepository.findStoreById(id)
                .orElseThrow(NotFoundException::new);
        store.updateStoreName();
    }

}
  • 처음에는 updateStore를 호출하고 안에 있는 update에서만 @Transaction 처리 해주면 될 줄 알았다…
  • 하지만 한 클래스 내 @Transactional이 설정되어 있지 않은 메서드에서 @Transactional이 설정된 메서드를 호출할 경우 (위와 같은 경우) @Transactional이 작동하지 않는다. ⇒ @Transactional 는 Proxy 기반이고 AOP로 구성되어 있다. 이는 Method 혹은 Class가 실행되기 전/후 등의 단계에서 자동으로 트랜잭션을 묶는다. 그렇기 때문에 @Transactional 은 인스턴스에서 처음으로 호출하는 메서드나 클래스의 속성을 따라가게 된다. 그래서 동일한 Bean안에 상위 메서드가 @Transactional 가 없으면 하위에는 선언이 되었다 해도 전이되지 않는다고 한다. (https://lemontia.tistory.com/878)

해결 코드

@Service
@RequiredArgsConstructor
public class StoreSystemService {

    private final TestService testService;

    @Transactional
    public void updateStore(TestRequest request) {
        for (Long id : request.getStoreList()) {
            testService.update(id);
        }
    }
}

@Service
@RequiredArgsConstructor
public class TestService {

    private final StoreRepository storeRepository;

    @Transactional
    public void update(Long id) {
        Store store = storeRepository.findStoreById(id)
                .orElseThrow(NotFoundException::new);
        store.updateStoreName();
    }
}
// exception 발생 이전꺼는 성공처리가 된다.

@Transaction이 CheckedException, UnCheckedException에서 어떻게 동작하는지?

  • Error는 시스템이 비정상적인 상황에서 발생
  • 일단 RuntimeException을 상속받는 것이 uncheckedException

일단 checkedException에서는 rollback이 안된다고 하는데 알아보자.

checkedException일 때 rollback이 일어나지 않는지 확인 예시(예시로 IOException이 발생했다고 가정)

@Service
@RequiredArgsConstructor
public class StoreSystemService {

    private final StoreRepository storeRepository;
    private final TestService testService;

    @Transactional
    public void updateStore(TestRequest request) throws IOException {
        for (Long id : request.getStoreList()) {
            testService.update(id);
        }
    }

}

@Service
@RequiredArgsConstructor
public class TestService {

    private final StoreRepository storeRepository;

    @Transactional
    public void update(Long id) throws IOException {
        Optional<Store> storeById = storeRepository.findStoreById(id);
        if (storeById.isEmpty()) {
            throw new IOException();
        }
    }

}
  • checkedException이 일어났을 경우 롤백되지 않고 예외 발생
  • ⇒ 이유는 spring 의 transactional의 기본 정책이 Unchecked Exception과 Errors로 @Transactional 은 @Transactional(rollbackFor = {RuntimeException.class,Error.class})와 같다.
  • 해결 ⇒ Checked Exception을 처리하기 위해서는 throws를 이용해 피호출 메소드에서 호출하는 메소드로 예외를 던진다고 말할 수 있다. 이 "던짐(throws)"은 해당 예외를 처리할 수 있는 메소드까지 던져지게 된다. 하지만 이런 무분별한 throws의 활용은 코드의 가독성을 떨어트림과 동시에, 어떤 메소드의 어떤 부분에서 예외가 발생했는지 알기 어렵게 만든다. ⇒ try catch를 활용해서 uncheckedException을 발생시키자
@Service
@RequiredArgsConstructor
public class StoreSystemService {

    private final StoreRepository storeRepository;
    private final TestService testService;

    @Transactional
    public void updateStore(TestRequest request) {
        for (Long id : request.getStoreList()) {
            testService.update(id);
        }
    }

}

@Service
@RequiredArgsConstructor
public class TestService {

    private final StoreRepository storeRepository;

    @Transactional
    public void update(Long id) {
        Optional<Store> storeById = storeRepository.findStoreById(id);
        try {
            if (storeById.isEmpty()) {
                throw new IOException();
            }
        } catch (IOException e) {
            throw new NotFoundException();
        }
    }

결론

  • 트랜잭션에 대해 모르는게 많았다…
    • CheckedExcpetion에서 rollback이 안된다는 것 ⇒ chekcedExcpetion이 발생했을 때 rollback 작성하는 것보다 unCheckedException 발생시켜서 rollback 시키는게 더 좋을듯? 하다.
    • 한 클래스 내 @Transactional이 설정되어 있지 않은 메서드에서 @Transactional이 설정된 메서드를 호출할 경우