본문 바로가기

Back-end/SpringBoot

[SpringBoot] @Transactional 트랜잭션 사용법 및 옵션

Transactional

스프링 환경에서는 @Transactional을 이용하여 메서드, 클래스, 인터페이스의 트랜잭션 처리가 가능한데 이러한 방식을 선언적 트랜잭션이라 부른다. 내부적으로 좀 뜯어보면 트랜잭션 기능이 들어가 있는 프록시 객체가 생성되어 자동적으로 커밋이나 롤백을 해준다고 생각하면 된다.

 

간단한 예시로 MemberService에서 MemberDTO 클래스를 insert를 하는 Service 내부의 메서드를 만든다고 가정해보면 아래처럼 구현을 할 수가 있다.

 

@Service 
public class MemberService { 
``` 
	@Transactional
	public void addMember(MemberDto memberDto) throws Exception { 
    	// 멤버 삽입 로직 구현 
  	  } 
``` 
}

 

 Transactional 옵션


🔍 isolation
- isolation 옵션을 트랜잭션에서 일관성없는 데이터를 어떻게 허용할지에 대한 허용 수준을 정할수 있는 옵션


아래와 같은 형태로 설정을 해주면 된다.

@Service
public class MemberService { 
``` 
	@Transactional(isolation=Isolation.DEFAULT) 
    	public void addMember(MemberDto memberDto) throws Exception { 
    	// 멤버 삽입 로직 구현 
     } 
```

 

  1. DEFAULT
    - 기본적인 격리 수준이고, 기존 DB의 Isolation Level을 따르게 된다.
  2. READ_UNCOMMITED (Level 0)
    - 커밋되지 않는 데이터에 대한 읽기를 허용하는 방식이다.
    이렇게 설정하면 Dirty Read라는 문제가 발생할 수 있다.

    예를 들어 사용자 A가 1이라는 데이터를 2로 변경한다고 했을때, 사용자 B가 아직 완료되지 않은 (Uncommitted or Dirty) 데이터를 읽을 수 있는데, 만약 사용자가 A 가 수행한 데이터가 정상커밋되지 않아 롤백될 경우 사용자 B가 읽은 데이터는 잘못된 데이터가 되는 것이다.

  3. READ_COMMITED (Level 1)
    커밋된 데이터에 대해 읽기 를 허용하는 방식이다. 즉, 사특정 사용자가 데이터를 변경하는 동안 다른 사용자는 해당 데이터에 접근이 불가하다.
    이렇게 하면 Dirty Read 문제는 방지할 수 있으나, Non-Repeatable Read 문제가 생긴다.

    예를들어 사용자 A 가 1번 데이터를 조회하고 있을 때 사용자 B가 1번 데이터를 수정하고 커밋을 해버리면, 사용자 A가 같은 트랜잭션에서 다시 1번 데이터를 조회한다면 수정된 데이터가 조회되어 버려서 반복해서 데이터를 읽을 수 없는 경우를 의미한다.

  4. REPEATABLE_READ (Level 2)
    - 동일 필드에 대해 다중으로 접근할 때 동일한 결과를 보장하는 방식을 의미한다. 즉, 트랜잭션이 완료될 때까지 SELECT 문장이 사용되는 모든 데이터들에 대해 Shared Lock이 걸려 다른 사용자가 그 데이터에 대한 접근이 불가능해진다.
    그러므로 어떤 트랜잭션이 수행될 때 다른 트랜잭션이 앞선 트랜잭션이 사용 중인 데이터에 대해 갱신하거나 삭제하는게 불가능하기에 트랜잭션 내에서 여러번 데이터를 접근한다해도 데이터의 일관성을 보장할 수 있다.

    이렇게 하면 Non-Repeatable Read 문제는 방지할 수 있으나, Phantom Read 문제가 생긴다.

    예를들어 사용자 A 가 특정 범위의 데이터 [1,2,3,4] 를 2번 읽는 트랜잭션이 수행된다고 가정하자. 두번째 읽기전에 사용자 B가 수행한 트랜잭션이 [5,6,7,8] 이라는 데이터를 추가해 버리면 첫번째 쿼리와 두번째 쿼리의 결과가 달라진다.

    즉, 한 트랜잭션에서 일정한 범위의 레코드를 두번 이상 읽을 때, 데이터가 불일치하는 문제가 바로 Phantom Read 문제이다.

  5. SERIALIZABLE (Level 3)
    - 해당 설정은 데이터의 일관성과 동시성을 유지하기 위해 MVCC(Multi Version Concurrency Control) 을 사용 하지 않는다. (MVCC 는 다중 사용자 데이터 베이스 성능 을 위한 기술로 데이터를 조회할때 LOCK 을사용하지 않고 데이터의 버전을 관리하여 데이터 일관성과 동시성을 높이는 기술을 의미)
    그리고 트랜잭션이 완료될 때 까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock 을 걸어 다른 사용자가 해당 영역에 있는 모든 데이터에 대한 수정과 입력이 불가능하게 만들어 Phantom Read를 방지한다.

    설명만 보기에는 SERIALIZABLE 속성을 써야하겠지만 이렇게 격리수준이 높아지면 성능저하의 우려가 있으니 상황에 따라 적절한 속성을 사용해줘야한다.

🔍 propagation
- propagation 옵션은 트랜잭션이 동작할 때 다른 트랜잭션이 호출되면 어떻게 처리할지를 정하는 옵션이다.
즉 피호출 트랜잭션에서 호출한 트랜잭션을 그대로 사용할지 새로 생성할지를 정하는 옵션이라고 생각하면된다. 설정방법은 아래와 같이 적용해주면 된다.

 

@Service
public class MemberService { 
``` 
	@Transactional(propagation=Propagation.REQUIRED)
    public void addMember(MemberDto memberDto) throws Exception { 
    	// 멤버 삽입 로직 구현 
    } 
``` 
}

 

 

  1. REQUIRED
    - 디폴트인 속성이고, 부모 트랜잭션 내에서 실행하게 하고 만약 부모 트랜잭션이 없으면 새로운 트랜잭션을 생성하게 하는 설정이다.

  2. SUPPORTS
    - 이미 시작된 트랜잭션이 있으면 그 트랜잭션에 참여하여 처리하게 하고, 없으면 트랜잭션없이 진행하게 하는 설정이다.
  3. REQUIRES_NEW
    -부모 트랜잭션이 있어도 그냥 새롭게 트랜잭션을 생성하게 하는 설정이다.

  4. MANDATORY
    - REQUIRED 처럼 이미 시작한 트랜잭션에 참여하지만 , 없으면 새로 생성하는게 아니라 예외를 발생시킨다. 보통 독립적으로 트랜잭션이 진행되면 안되는 경우에 해당 옵션을 사용한다.

  5. REQUIRES_NEW
    - 항상 새로운 트랜잭션으로 작업을 수행한다.. 만악에 진행중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.

  6. NOT_SUPPORTED
    - 트랜잭션 없이 작업을 수행한다. 만약 진행중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.

  7. NEVER
    - 트랜잭션을 사용하지 않게 강제한다. 이미 진행중인 트랜잭션이 있다면 Exception을 발생시키고 트랜잭션이 진행중이지 않을때 작업을 수행한다.

  8. NESTED
    - 이미 진행중인 트랜잭션이 있으면 중첩된 트랜잭션을 실행하고 없으며 REQUIRED와 동일하게 새로운 트랜잭션을 생성하여 진행한다. 중첩 트랜잭션을 말 그대로 트랜잭션안에 트랜잭션을 만드는 것이다. 트랜잭션안의 트랜잭션이 커밋되거나 롤백되어도, 바깥의 트랜잭션에게 영향을 주지 않는다


🔍 noRollbackFor, .rollbackFor

- 특정 예외 발생 시, rollback을 하거나 하지않게 하는 옵션이다.
사용방법은 아래와 같다.

 

@Service
public class MemberService { 
``` 
	@Transactional(noRollvackFor=CustomException.class) 
    public void addMember(MemberDto memberDto) throws Exception { 
    	// 멤버 삽입 로직 구현 
    }
``` 
}

 

선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백을 하도록한다. 반면 체크 익셉션이 발생하면 커밋을 한다. 스프링에서는 Data Acces 기술의 예외가 런터임 예외로 전환되어 던져지기 때문에 런타임 예외만 롤백대상이 되지만 기본동작을 바꿀수 가 있는데 그 옵션이 바로 rollbackFor 속성이다.

그리고 반대로 특정 예외 발생시 Rollback 처리되지 않게 하는 옵션이 noRollbackFor 속성이다.

 

🔍 timeout

지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback 하게 하는 옵션이다. -1 이 default 값이고 이는 no timeout을 의미한다.
사용 방법은 아래와 같다.

@Service
public class MemberService { 
``` 
	@Transactional(timeout=10)
    public void addMember(MemberDto memberDto) throws Exception { 
    	// 멤버 삽입 로직 구현 
    } 
```
}

 

🔍 readOnly

트랜잭션을 읽기전용으로 설정하는 옵션이다. true로 설정하면 insert, update, delete 실행할 때 예외가 발생한다.
default 값은 false이다.
사용방법은 아래와 같다.

 

@Service
public class MemberService {
``` 
	@Transactional(readOnly =true)
    public List<Member> findAllMember() throws Exception { 
    	// 전체 멤버 검색 로직 구현 
      } 
``` 
}

 

 

보통 get 이나 find 같은 이름의 메서드 앞에 이런식으로 설정이 되어있다. 성능을 최적화 하기위해서도 사용할 수 있다고하지만 나는 보통 특정 트랜잭션 작업에서 누군가 쓰기 작업을 하는 걸 의도적으로 방지하기 위해 사용해왔다.