[Spring] @Transactional with try~catch 롤백(Rollback) 에 관하여

동민·2022년 5월 4일
0

@Transactional

  1. 조회 로직만 있는 경우
    @Transactional(readOnly=true)
@Transactional(readOnly=true)
public String getData(Request request) {
	// 조회 biz. 로직
    Response response = XXXRepository.select(request);
}
  • @Transactional(readOnly=true) 어노테이션으로 SELECT 성능을 최적화 시킬 수 있다.

  1. 삽입/삭제/수정 로직이 있는경우
    @Transactional 또는 @Transactional(readOnly=false)
@Transactional
public void cudData(Request Request) {
	// 삽입/수정/삭제 biz. 로직
    XXXRepository.insert(request); // A
    XXXRepository.update(request); // B
    XXXRepository.delete(request); // C
}
  • @Transactional 어노테이션의 default는 readOnly=false 이다.
  • 만약 A, B 는 성공하였지만 C 에서 익셉션이 발생한 경우, A, B 동작 모두 Rollback 된다. (DB에 반영되지 않음)

만약...

@Transactional(readOnly=true)
public String getData() {
	// 조회 biz. 로직
    ... 
    
    Repository.insert(DTO); // INSERT - Exception 발생
}
  • @Transactional(readOnly=true) 어노테이션이 붙어있는 클래스 또는 메소드 내에서 INSERT, UPDATE, DELETE 를 실행할 경우 예외가 발생한다.

@Transactional + try~catch 문 ROLLBACK 에 관하여

@Transactional
public void cudData(Request Request) {
	// 삽입/수정/삭제 biz. 로직
    try {
    	XXXRepository.insert(request); // A
    	XXXRepository.update(request); // B
    	XXXRepository.delete(request); // C
    } catch (Exception e) {
    	System.out.println("[Error] A, B, C 로직 처리중 에러가 발생하였습니다."); // 예외를 던지지 않는 구문
    }
}

Q1) 만약 위처럼 트랜잭션@Transactionaltry~catch 구문을 같이 사용한다면??
A1) @Transactional 에 의한 트랜잭션 처리는 동작하지 않는다.
각각의 서비스 A, B, C 에서 발생하는 모든 예외(예외의 최상위 계층인 Exception을 catch함)를 try~catch 문으로 처리하도록 정의했기 때문에, 발생하는 모든 예외는 catch 블록에서 처리된다.

@Transactional에 의한 트랜잭션 처리 루틴으로 예외는 전파되지 않아 @Transactinal은 아무런 의미를 가지지 않게 된다.
즉, 일부 서비스 예외에 의한 롤백(Rollback)은 절대 일어나지 않는다.

Q2) 그래도 @Transactionaltry~catch 문을 같이 사용하고자 한다면?
A2) catch문에라도 Exception을 발동 시켜야 한다.

@Transactional
public void cudData(Request Request) {
	// 삽입/수정/삭제 biz. 로직
    try {
    	XXXRepository.insert(request); // A
    	XXXRepository.update(request); // B
    	XXXRepository.delete(request); // C
    } catch (Exception e) {
    	throw new Exception(); // 예외를 던지는 구문
    }
}

@Transactional + private/public 접근제어자

@Transactional // private 접근제어자에 트랜잭션 정책 적용 안됨
private String method1() { // IntelliJ 에서는 컴파일 에러 발생
	...
}

@Transactional // public 접근제어자에 트랜잭션 정책 적용 됨
public String method2() {
	...
}
  • 스프링 트랜잭션의 기본 모드에서는 private 접근제어자 메소드에 @Transactional 을 붙여도 스프링이 트랜잭션을 관리해 주지 않는다.
  • @Transactional 어노테이션을 non-public (protected, private, package, no-modifier) 메소드에 적용할 경우에 런타임 에러는 발생하지 않지만, 트랜잭션 정책이 적용되지 않는다. (IntelliJ 에서는 컴파일 에러를 뱉음)
  • @Transactional 어노테이션은 public 메소드에 사용!

@Transactional Commit/Rollback 단위 묶기

@Transactional
public Result biz(Dto dto) {

	// 1. UPDATE 수행
	this.update(dto); // SUCCESS
        
	// 2. INSERT 수행
	this.insert(dto); // Exception 발생
}

private void update(Dto dto) throws Exception {
	Dao.update(dto); // SUCCESS
}

private void insert(Dto dto) throws Exception {
	Dao.insert(dto); // Exception 발생
}

하나의 API 호출에서 1.UPDATE, 2.INSERT 동작이 모두 COMMIT 되어야 운영상 이슈가 없는 상황을 가정해본다면... (둘 중 하나만 COMMIT 되면 운영 이슈)

1) 예시 코드와 같이 @Transactional 어노테이션으로 1.UPDATE2.INSERT 를 하나의 트랜잭션으로 묶었을 때,
만약, 1.UPDATE 동작 정상 수행 후 2.INSERT 동작에서 익셉션이 발생한다면 1.UPDATE 결과도 DB에 COMMIT 되지 않는다. 1.UPDATE, 2.INSERT 모두 정상 동작해야 하나의 트랜잭션에서 COMMIT
예외를 발생시키면, ROLLBACK 처리를 자동 수행해준다.

2) @Transactional 어노테이션으로 하나의 트랜잭션으로 묶지 않았을 때,
만약, 1.UPDATE 동작 정상 수행 후 2.INSERT 동작에서 익셉션이 발생한다면 1.UPDATE 결과만 COMMIT 되어 운영 이슈 발생. ex) 결제 시스템 등..

이처럼 하나의 COMMIT/ROLLBACK 단위(?)로 트랜잭션으로 묶기 위해 사용하며...
사실 @Transactional 어노테이션의 기능은 더 많지만.. 간단히 정리해 보았다.

@Transactional 전파 레벨

1) Propagation.REQUIRED (Default)

@Transactional(propagation = Propagation.REQUIRED)
public void test() { ... }
  • 메소드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않았다면 트랜잭션를 새로 시작한다. (새로운 연결을 생성하고 실행)
  • 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 로직을 실행한다. (동일한 연결 안에서 실행)
  • 예외가 발생하면 Caller/Callee 에 모두 전파되어 롤백된다. (하나의 트랜잭션)
  • Propagation.REQUIRED 는 디폴트이기에 생략해도 된다.

2) Propagation.REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test() { ... }
  • Propagation.REQUIRES_NEW는 매번 새로운 트랜잭션을 시작한다. (새로운 연결을 생성하고 실행)
  • 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면, 기존 트랜잭션은 메소드가 종료될 때까지 잠시 대기 상태로 두고 자신의 트랜잭션을 실행한다.
  • 새로운 트랜잭션 안에서 예외가 발생해도 호출한 곳에는 롤백이 전파되지 않는다. (즉, 새로 생성된 N개의 트랜잭션은 독립적인 단위로 동작)

3) Propagation.NESTED

@Transactional(propagation = Propagation.NESTED)
public void test() { ... }
  • Propagation.NESTEDPropagation.REQUIRED와 동일하게 작동하나 SAVEPOINT를 지정한 시점까지 부분 롤백이 가능하다는 차이점이 있다.
  • 단, 데이터베이스가 SAVEPOINT 기능을 지원해야 사용이 가능하다. (대표적으로 Oracle)

Transactional 전파 레벨 예제

https://oingdaddy.tistory.com/28

혹시, 틀린 내용이 있다면, 댓글 주시면 감사드리겠습니다. 🙇🏻‍♂️

참고

https://rlawls1991.tistory.com/entry/spring-transaction-roll-back-%EC%B2%98%EB%A6%AC

https://okky.kr/questions/1231694

https://jsonobject.tistory.com/467

profile
BE Developer

0개의 댓글