본문 바로가기
Develop/Spring

[Spring] @Transactional 정리

by 코딩의성지 2021. 10. 7.

하이 ..

요즘 지속적으로 JPA 에 대해 공부를 하고 있는데 JPA를 SpringBoot에서 사용하면서 습관적으로 생각하지 않고 CREATE, UPDATE, DELETE API 메서드를 만들때 @Transactional 어노테이션을 사용하였다. 그러다가 문득 @Transcational에 대해서 한번 정리를 해둬야 겠다는 생각을 했따.

@Transactional 어노테이션을 통해 트랜잭션을 처리하는데 먼저 트랜잭션에 대해 구체적으로 알 필요가 있다.
트랜잭션에 대해 잘 기억이 나지 않으신 분들은 아래 링크를 참고 하자.
https://devkingdom.tistory.com/270

 

[Database] 트랜잭션이란 뭘까?

오늘은 트랜잭션에 대해 정리를 해두려한다. 트랜잭션(Transaction)이란? 트랜잭션(Transaction)의 정의를 내려보자면, 트랜잭션은 Database의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작

devkingdom.tistory.com

 

@Transactional


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

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

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


여기서 Transactional에서 사용할 수 있는 다양한 옵션에 대해 알아보자.

@Transactional 옵션

 

1.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 속성을 써야하겠지만 이렇게 격리수준이 높아지면 성능저하의 우려가 있으니 상황에 따라 적절한 속성을 사용해줘야한다.

2.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

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

5) NOT_SUPPORTED

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

6) NEVER

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

7) NESTED

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

3.noRollbackFor, .rollbackFor

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

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

해당 속성을 보기전에 우선 Exception에 대해 조금 알 필요가 있겠다.
https://devkingdom.tistory.com/283

 

[Java] Java Exception 처리하기

자바에서 에러나 예외 클래스의 계층 구조를 그려보면 위에 그려놓은 정보와 같다. 상위에 있는 Throwable 클래스를 기준으로 하여 Error 와 Exception 으로 나눠진다. Exception 은 또 컴파일 단계에서 발

devkingdom.tistory.com


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

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

4. timeout

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

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

 

5. readOnly

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

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

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

끝.

반응형

댓글