Categories: 개발

[SpringBoot] 스프링 트랜젝션(@Transactional) 사용

스프링에서의 트랜잭션

스프링에서는 트랜젝션을 적용하려면 어떻게 해야할까?
스프링에서는 간편하게 트랜젝션을 적용할수 있다. 바로 @Transactional 어노테이션을 사용하면된다.
트랜젝션이 적용을 하고 싶은 서비스의 class 나 method에 @Transactional 을 붙여준다면 알아서 트랜젝션이 적용된다.

소스 구성

먼저 Mybatis 를 사용한 예시를 보여드리겠습니다.

TestController -> TestService -> TestMapper 구조입니다.

먼저 Controller 입니다.

@RestController
@RequiredArgsConstructor
public class TestController {

    private final TestService testService;

    @GetMapping("/test")
    public void test(){
        testService.test();
    }
}

Serivce 이구요.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    public void test(){
        testMapper.insert();
    }
}

Mapper 입니다.

@Mapper
public interface TestMapper {

    void insert();

}

생성한 Test Table 입니다.

정말 단순한 테이블 입니다. id 란 auto increment(자동증가) 컬럼 한개 갖고 있는 테이블 입니다.
이 테이블에 insert 를 해봄으로서 트랜젝션을 확인해보겠습니다.

사용되는 쿼리입니다. 정말 단순하죠?

insert into test (id) values (null)

처음 API 를 실행하면 당연히 데이터 하나가 추가 됩니다.

@Transactional 테스트

자 이제 처리 과정 중에 에러가 나도록 해보겠습니다.
test method 에 @Transactional 어노테이션을 주고 insert 후 에러가 발생하도록 코드를 작성했습니다.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional
    public void test() {
        testMapper.insert();
        int[] a = new int[1];
        int b = a[3]; // 배열 에러 발생
    }
    
}

자 이렇게 하고 API 를 실행해보겠습니다.
예상 결과 : id = 2 데이터가 insert 되지 않는다.

console 창에는 insert query 가 잘 돌았다는게 log 가 찍힙니다.

그 후 예상대로 배열 에러가 발생하게 됩니다.

예상대로 @Transactional 어노테이션 덕분에 test 테이블에는 데이터가 추가되지 않았습니다.
만약 트랙젝션을 걸어두지 않았다면 데이터가 추가되었을 것 입니다.

Try-Catch 문 주의사항

자 위에서 본 것처럼 트랜젝션을 거는건 너무 쉽습니다.
다만 주의할께 try-catch 문 입니다.

만약 제가 이렇게 try-catch 문을 걸었다면 어떻게 될까요?

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional
    public void test() {
        try{
            testMapper.insert();
            int[] a = new int[1];
            int b = a[3]; // 배열 에러 발생
        }catch (ArrayIndexOutOfBoundsException e){
            log.debug("ERROR");
        }
    }

}

예상 결과 : @Transactional 어노테이션을 줬으니 에러때문에 Rollback 이 된다.

하지만 insert 는 이루어지게 됩니다. 즉 Rollback이 되지 않았습니다.

저도 처음 사용할때 왜 안될까 고민을 많이 했었지만 사실 답은 간단합니다.
우리가 @Transactional 어노테이션만 주고 아무것도 안하잖아요..?
사실 어노테이션을 줌으로서 이미 에러가 발생하면 예외 처리를 알아서 해줬던거죠.
즉 Rollback 처리까지 알아서 해준겁니다.
하지만 여기선 catch로 잡은 순간! 예외처리는 우리가 직접 맡은 것이니 Rollback 처리가 안되어있으므로
Rollback 안되는게 맞는겁니다. 정말 주의해야 합니다.!

그럼 어떻게 해야할까요?
바로 catch 부분에서 처리 할거 처리하고 다시 Exception을 발생시켜주면 알아서 Rollback 까지 해줍니다.
Catch 에서 Exception 재처리 안하면 Rollback 안되니 꼭 잘 생각해주세요.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional
    public void test(){
        try{
            testMapper.insert();
            int[] a = new int[1];
            int b = a[3]; // 배열 에러 발생
        }catch (ArrayIndexOutOfBoundsException e){
            log.debug("ERROR");
            throw new ArrayIndexOutOfBoundsException(); // catch 잡은 후 다시 에러 발생
        }
    }

}

Rollback이 안되는 경우

혹시 @Transactional 도 했는데 Rollback 안되는 경우가 있으신가요?
그 이유는 바로 모든 Exception 에 Rollback 이 안되기 때문입니다.

먼저 밑에 service 를 보시죠.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional
    public void test() {
        testMapper.insert();
        throw new RuntimeException();
    }

}

이 method 에서는 Rollback이 이루어 집니다.

다음 service 를 보시죠.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional
    public void test() throws Exception{
        testMapper.insert();
        throw new Exception();
    }
}

이 method 에서는 Rollback 이 이루어지지 않습니다.

바로 Exception에 따라 Rollback 처리가 다릅니다.

Checked ExceptionUnchecked Exception
정의Exception의 상속받는 하위 클래스 중 Runtime Exception을 제외한 모든 ExceptionRuntime Exception 하위 Exception
Rollback 여부rollback 안됨rollback 됨
대표 ExceptionIOException FileNotFoundExceptionArrayIndexOutOfBoundsException NullPointerException

위에 표에서 보듯이 Checked Exception 에 대해서는 Rollback이 이루어지지 않습니다.
그럼 Checked Exception 이 발생하면 그냥 Rollback 못하는 것이냐? 아닙니다.

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestMapper testMapper;

    @Transactional(rollbackFor = Exception.class) // 추가해주면
    public void test() throws Exception{
        testMapper.insert();
        throw new Exception();
//        int[] a = new int[1];
//        int b = a[3]; // 배열 에러 발생
    }

}

@Transactional 어노테이션에 rollbackFor 에 원하는 Exception에 대해 Rollback을 시켜줄수 있습니다.
저는 Exception.class 를 추가해주어서 묻지도 말고 따지지도 말고 무조건 Rollback 하도록 해주었습니다.
로직에 따라서 커스텀 해주면 되겠죠?

트랜젝션을 부여하는건 쉽지만 쉬운만큼 복잡하게 커스텀하려면 잘 확인하고 써야합니다.
여러가지 로직이 섞여들어간다면 꼼꼼하게 테스트를 잘 하면서 확인해보시길 바랍니다.

mana

Recent Posts

[여행] 1박 2일 경주 뚜벅이 여행기

경주시티투어 이번 여행에 앞서 뚜벅이 여행객들에게 추천하는게 바로 경주시티투어 이다. 경주시티투어 사이트로이동 경주시티투어는 여러가지가 있다.1.…

8개월 ago

[공공데이터] 공공 API 사용하기 – 지하철 실시간 도착 정보

열린데이터 광장 공공데이터 사이트로 이동 서울 열린데이터 광장에서는 서울에 관한 여러가지 공공 API 를 제공한다.그…

8개월 ago

[Vue] Props 와 Emit – 부모 함수 사용하기

Props props 는 쉽게 생각해서 부모가 자식에게 주는 데이터이다.먼저 부모에게 물려받을 데이터를 자식 컴포넌트에 정의한다.…

8개월 ago

이자 계산기

이번에는 이자 계산기 사이트를 만들어봤습니다. 이자 계산기 바로가기 vue로 하다보니 금액이나 이자율 같은 input 태그의…

8개월 ago

[SpringBoot+Nginx+vite]유틸 사이트를 만들어봤습니다.feat.핫딜 모음

유틸 사이트로 이동 웹사이트 개설 나의 블로그의 접속자 수는 처참하지만 내가 블로그를 시작하면서생각했던 계획은 내…

9개월 ago

[GitHub] Actions로 서버에 자동배포하기

과정 시작하기에 앞서 과정부터 설명해드리겠습니다.master branch에 push 가 되면 GitHub Actions 에서소스를 build 를 하고…

9개월 ago