[Spring Boot] 트랜잭션, 롤백은 고려하고 있니?

이동엽·2023년 8월 30일
20

spring

목록 보기
11/21

개요

얼마 전 입사를 하고, 파일럿 프로젝트를 진행하며 기존 레거시 API 서버들을 차차 분석하며 새롭게 구현하고 있다. 그러던 중 파일 이미지를 저장하고, 수정하는 기능을 구현하다가 이사님께 지적을 받았다.


단순히 ‘파일을 MultipartFile 타입으로 받고, 저장하면 되겠지’ 라고 간단하게 생각했던 나는, 성공 케이스만을 고려한 듯한 코드를 작성했던 게 문제!


내가 생각했던 파일 저장 플로우

파일 저장 요청
→ 저장소(로컬 혹은 클라우드)에 파일 저장
→ DB에 파일 정보(이름 및 경로 등등) 저장

위에서 나타낸 저장소에 파일 저장 과정을 1번, DB에 파일 정보 저장 과정을 2번이라고 칭하고 지적받은 내용을 함께 살펴보자.


우선 1번 과정에서 에러가 발생할 수 있는 상황들을 가정해보자.

  • 파일 저장 요청시 하나의 파일 당 최대 크기(max-file-size)를 초과했을 경우

  • 파일 저장 요청시 총 요청 크기가 최대 크기(max-request-size)를 초과했을 경우

  • 파일의 이름이 겹쳤을 경우
    • 덮어쓰기를 할 것인가? 아니면 새로 들어온 파일을 버릴 것인가?
    • 아니면 이름에 랜덤 변수를 추가하여 겹치는 상황을 피하게 할 것인가?

  • 여러 사진을 저장하다가 중간에 한 사진만 저장에 실패한다면?
    • 모두 롤백할 것인가? 아니면 해당 사진만 저장하지 않을 것인가?
    • = 트랜잭션의 전파 및 격리 수준을 어떻게 정할 것인가?

등등의 경우를 생각해보아야 하지만, 에러는 아니지만 이외에도 고려해야 할 사항들이 존재한다.

  • 한 저장소에 파일을 모두 보관하는 것은 무리가 있을 수 있다.
    • 그렇다면 어떤 전략으로 저장소(혹은 위치)를 나눌 것인가?
  • 최근에는 핸드폰도 카메라 화질이 좋아져, 사진의 크기가 굉장히 크다.
    • 클라우드에 사진을 저장할 경우에는 이게 곧 비용으로 돌아온다.
    • 이럴 경우 리사이징을 고려해야 한다.


이후, 2번 과정에서 예외가 발생했을 경우에 어떻게 처리해야 할 지를 고민해보자.

  • 저장소(로컬 혹은 클라우드)에는 저장을 완료했고, DB에 저장하다가 예외가 발생했다면?

    • 단순히 DB만 롤백하는 건 당연히 쉽다.
    • 다만, 이때 에러 응답을 제공하고 끝내는게 아니라 방금 저장소에 저장한 사진도 지워야 한다.
    • 클라우드에 저장한다면 이런 사진들이 쌓여 역시 비용으로 돌아온다.
  • 또한 위와 동일한 과정에서, 여러 사진들 중 한 사진만 DB 저장에 실패했다면?

    • = 트랜잭션의 전파 및 격리 수준을 어떻게 정할 것인가?


마찬가지로 파일 저장이 아닌 수정에서도 마찬가지다.

파일 수정 플로우

파일 저장 요청 → 저장소에 파일 저장 → DB에 파일 정보 수정 → 기존 파일 삭제
  • 새로운 파일을 저장했고, DB에 파일 정보 수정도 완료했다면, 기존 파일은 삭제하자.

💡 트랜잭션은 요청 완료까지가 아니라, 응답까지 완료되어야 잘 끝나야 완료되는 것임을 기억해야 한다!



💡 그렇다면 스프링에서 트랜잭션 전파는 어떻게 관리할까?

Spring이 제공하는 @Transactional 의 장점 중 하나는 여러 트랜잭션을 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점이다.

작업을 하다보면 기존에 트랜잭션이 진행 중일 때 추가적인 트랜잭션을 진행해야 하는 경우가 있다.
→ 이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것이 전파 속성(Propagation)이다.


전파 속성에 따라 아래와 같은 선택을 할 수도 있다.

  • 기존의 트랜잭션에 참여
  • 별도의 트랜잭션으로 진행
  • 에러를 발생시키기

전파 속성에 따라서 외부 트랜잭션과 내부 트랜잭션이 동일한 트랜잭션을 사용할 수도 있다.
하지만 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이다.

스프링은 논리 트랜잭션이라는 개념을 추가하여 아래 두 트랜잭션을 구분한다.
1. 실제 데이터베이스 트랜잭션
2. 스프링이 처리하는 트랜잭션


물리 트랜잭션 개념과 논리 트랜잭션

  • 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
  • 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위

@Transactional의 Propagation 전파속성

전파 속성에는 총 7가지가 있다.



1. REQUIRED

제일 많이 사용하는 설정인 디폴트 값이다.
자식/부모에서 Rollback이 발생하면 부모와 자식 모두 Rollback이 된다.


1-1. 만약 부모에서 자식에서 발생한 예외를 처리하는 try-catch문을 작성한다면?

여전히 부모/자식 모두 롤백된다.



2. REQUIRES_NEW

2-1. 자식에서 예외가 발생한다면?

항상 자식에서 새로운 트랜잭션을 만든다는 성질로, 자식에서 예외가 발생한다고 해도
부모에서 이전에 저장한 1번 Member가 저장될 줄 알았는데 같이 롤백되었다.

이는, 트랜잭션이 전파되는 것과 예외가 전파되는 것은 다르다. 라는 의미로 해석이 된다.


2-2. 그럼 부모에서 자식의 예외를 처리한다면?

트랜잭션은 전파되지 않았고, 자식에서 발생한 예외는 try-catch 문으로 처리했기에 예외가 발생하기 전에 저장한 1번 회원은 저장이 된 것을 볼 수 있다.

트랜잭션 전파와 예외 전파는 별도임을 다시 한번 명심하자!


2-3. 그럼 부모에서 예외가 발생하면?

부모 트랜잭션에서 예외가 발생하는 것은 자식은 영향을 받지 않으므로, 2번 회원만 저장되었다.

REQUIRES_NEW는 부모의 영향을 받지 않고, 자식 트랜잭션에서 반드시 커밋되어야 하는 상황에 적절할 것 같다.



3. MANDATORY

MANDATORY는 부모 트랜잭션이 있으면 종속되고, 없으면 예외를 발생시킨다.
현재 부모 메서드에 트랜잭션이 없기에, 예외가 발생했어도 롤백이 되지 않았다.

따라서 1번 회원만 저장이 되어 있는 것을 볼 수 있다.


마무리

지금까지 스프링에서 제공하는 @Transactional 애너테이션을 통해 트랜잭션 전파에 속성에 대해 간단하게 알아보았다. 총 7가지 옵션 중 3가지만 먼저 알아보았지만, 나머지도 직접 실습을 해본다면 충분히 이해하기 쉬울 거라고 생각한다.

모든 소스 코드는 Github 링크에 있으니, 클론하여 test.http 로 실행해볼 수 있도록 작성해놓았다.
또한 위에서 코드로 확인해보았던 단계별로 커밋에 Step을 나누어 작성해놓았으니 참고하면 좋다!


단순하게 트랜잭션을 하나로 묶는다고만 생각하지는 말자.
대용량 작업의 경우에는 롤백 비용이 더 비쌀 수 있다는 점을 명심하자.

profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

1개의 댓글

comment-user-thumbnail
2023년 9월 4일

좋은 정보 감사합니다.
제가 짠 코드를 보니 저도 문제점이 보이네요!

답글 달기