티스토리 뷰
아래의 내용은 토비의 스프링의 2권 2장을 참고해서 작성하였습니다.
1. Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등)
[ 전파 속성(Propagation) ]
Spring이 제공하는 선언적 트랜잭션(트랜잭션 어노테이션, @Transactional)의 장점 중 하나는 여러 트랜잭션 적용 범위를 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점이다. 우리는 Spring이 트랜잭션을 어떻게 진행시킬지 결정하도록 전파 속성을 전달해야 하는데, 이를 통해 새로운 트랜잭션을 시작할지 또는 기존의 트랜잭션에 참여할지 등을 결정하게 된다.
Spring이 지원하는 전파 속성은 다음의 7가지가 있다.
- REQUIRED
- SUPPORTS
- MANDATORY
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- NESTED
1. REQUIRED
- 디폴트 속성으로써 모든 트랜잭션 매니저가 지원함
- 이미 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작
- 하나의 트랜잭션이 시작된 후 다른 트랜잭션 경계가 설정된 메소드를 호출하면 같은 트랜잭션으로 묶임
REQUIRED는 기본 속성으로써 모든 트랜잭션 매니저가 지원하며, 대부분의 경우 이 속성이면 충분하다. REQUIRED는 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다. 가장 간단하고 자연스러운 트랜잭션 전파 방식이지만 매우 강력하며 유용하다. REQUIRED 속성일 때 하나의 트랜잭션이 시작된 후 다른 트랜잭션 경계가 설정된 메소드를 호출하면 자연스럽게 같은 트랜잭션으로 묶인다.
2. SUPPORTS
- 이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행함
- 트랜잭션이 없어도 해당 경계 안에서 Connection 객체나 하이버네이트의 Session 등은 공유 할 수 있음
SUPPORTS는 이미 시작 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행하게 만든다. 트랜잭션이 없기는 하지만 해당 경계 안에서 Connection 객체나 하이버네이트의 Session 등은 공유를 할 수 있다.
3. MANDATORY
- 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 새로 시작하는 대신 없으면 예외를 발생시킴
- MANDATORY는 혼자서 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용
MANDATORY 역시 이미 시작된 트랜잭션이 있으면 참여한다. 하지만 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다. 즉, MANDATORY는 혼자서 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용할 수 있다.
4. REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작해야 하는 경우에 사용
- 이미 진행중인 트랜잭션이 있으면 이를 보류시키고 새로운 트랜잭션을 만들어 시작함
REQUIRES_NEW는 항상 새로운 트랜잭션을 시작해야 하는 경우에 사용할 수 있다. 만약 이미 시작된 트랜잭션이 있으면 트랜잭션을 잠시 보류시키고 새로운 트랜잭션을 만들어 시작하게 된다. 만약 JTA 트랜잭션 매니저를 사용한다면 서버의 트랜잭션 매니저에 트랜잭션 보류가 가능하도록 설정되어 있어야 한다.
5. NOT_SUPPORTED
- 이미 진행중인 트랜잭션이 있으면 이를 보류시키고, 트랜잭션을 사용하지 않도록 함
NOT_SUPPORTED는 이미 진행중인 트랜잭션이 있으면 이를 보류시키고, 트랜잭션을 사용하지 않도록 한다.
6. NEVER
- 이미 진행중인 트랜잭션이 있으면 예외를 발생시키며, 트랜잭션을 사용하지 않도록 강제함
NEVER는 이미 진행중인 트랜잭션이 있으면 예외를 발생시키며, 트랜잭션을 사용하지 않도록 강제한다.
7.NESTED
- 이미 진행중인 트랜잭션이 있으면 중첩(자식) 트랜잭션을 시작함
- 중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것으로, 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다름
- NESTED에 의한 중첩 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않음
NESTED는 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것으로, 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다르다. NESTED에 의한 중첩 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.
예를 들어 어떤 중요한 작업을 진행하면서 작업 로그를 DB에 저장해야 한다고 해보자. 그런데 로그를 저장하는 작업은 실패를 하더라도 메인 작업의 트랜잭션까지는 롤백하지 말아야 하는 경우가 있다. 왜냐하면 힘들게 처리한 중요한 작업을 로그를 남기지 못해서 모두 실패로 만들 수 없기 때문이다. 반면에 핵심 작업에서 예외가 발생한다면 이때는 저장된 로그도 제거해야 한다.
이렇듯 부모의 트랜잭션은 자식의 작업에 영향을 줘야하지만 자식의 트랜잭션은 부모에 영향을 주지 않아야 할 때 NESTED 전파 속성을 이용할 수 있다.
그리고 이러한 중첩 트랜잭션은 JDBC 3.0 스펙의 저장포인트(SavePoint)를 지원하는 드라이버와 DataSourceTransactionManager를 이용할 경우에 적용할 수 있다. 또는 중첩 트랜잭션을 지원하는 일부 WAS의 JTA 트랜잭션 매니저를 이용할 때도 적용할 수 있다. 즉, NESTED는 모든 트랜잭션 매니저에 적용 가능하지 않으므로 사용하는 트랜잭션 매니저와 드라이버 또는 WAS 등을 확인해야 한다.
선언적 트랜젝션에서는 @Transactional 어노테이션의 propagation 엘리먼트로 원하는 전파 속성을 지정할 수 있으며, 기본은 REQUIRED로 설정되어 있다. 또한 Spring은 6가지 전파 속성 방법을 지원하고 있지만 해당 속성을 지원하지 않는 트랜잭션 매니저와 데이터 액세스 기술이 있을 수 있다. 그러므로 변경이 필요한 경우 별도의 설정이 필요한지 확인을 해봐야 한다.
[ 격리 수준(Isolation) ]
트랜잭션 격리수준은 동시에 여러 트랜잭션이 진행될 때 트랜잭션의 작업 결과를 여타 트랜잭션에게 어떻게 노출할 것인지를 결정한다. 스프링은 다음의 5가지 격리수준 속성을 지원한다.
- DEFAULT
- READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
- SERIALIZABLE
1. DEFAULT
- 사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따름
- 일반적으로 드라이버의 격리 수준은 DB의 격리 수준을 따르며, 대부분의 DB는 READ_COMMITED를 기본 격리수준으로 가짐
- 일부 DB는 디폴트 값이 다른 경우도 있으므로 DEFAULT를 사용할 때는 드라이버와 DB의 문서를 참고해서 기본 격리수준을 확인해야 함
DEFAULT는 사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다. 물론 일반적으로 드라이버의 격리 수준은 DB의 격리 수준을 따르며, 대부분의 DB는 READ_COMMITED를 기본 격리수준으로 갖는다. 하지만 일부 DB는 디폴트 값이 다른 경우도 있으므로 DEFAULT를 사용할 경우에는 드라이버와 DB의 문서를 참고해서 기본 격리수준을 확인해야 한다.
2. READ_UNCOMMITTED
- 가장 낮은 격리수준으로써 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있음
- 하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용함
READ_UNCOMMITTED는 가장 낮은 격리수준으로써 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있다. 하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용한다.
3. READ_COMMITTED
- Spring은 기본 속성이 DEFAULT이며, DB는 일반적으로 READ_COMMITED가 기본 속성이므로 가장 많이 사용됨
- READ_UNCOMMITTED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없음
- 대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있어서 처음 트랜잭션이 같은 로우를 다시 읽을 때 다른 내용이 발견될 수 있음
READ_COMMITTED는 가장 많이 사용되는 격리수준이다. Spring은 기본적으로 DEFAULT로 지정되어 있고, DB는 일반적으로 READ_COMMITED로 되어있기 때문이다. READ_COMMITTED는 READ_UNCOMMITTED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다. 이 때문에 처음 트랜잭션이 같은 로우를 다시 읽을 때 다른 내용이 발견될 수 있다.
4. REPEATABLE_READ
- 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없도록 막아주지만 새로운 로우를 추가하는 것은 막지 않음
- 따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있음
REPEATABLE_READ는 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없도록 막아준다. 하지만 새로운 로우를 추가하는 것은 막지 않는다. 따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.
5. SERIALIZABLE
- 가장 강력한 트랜잭션 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시켜줌
- 그러므로 SERIALIZABLE은 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스할 수 없음
- SERIALIZABLE은 가장 안전하지만 가장 성능이 떨어지므로 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용해서는 안됨
SERIALIZABLE은 가장 강력한 트랜잭션 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시켜준다. 그렇기 때문에 SERIALIZABLE은 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스할 수 없다.
SERIALIZABLE은 가장 안전하지만 가장 성능이 떨어지는 격리수준이기 때문에 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용해서는 안된다.
선언적 트랜젝션에서는 @Transactional 어노테이션의 isolation 엘리먼트로 원하는 전파 속성을 지정할 수 있으며, 기본은 DEFAULT로 설정되어 있다.
[ 읽기 전용(readOnly) ]
Spring에서는 트랜잭션을 다음의 2가지 목적(성능 최적화와 쓰기 방지)으로 읽기 전용(readOnly)으로 설정할 수 있다.
- 읽기 전용으로 설정함으로써 성능을 최적화함
- 쓰기 작업이 일어나는 것을 의도적으로 방지함
읽기 전용으로 설정이 되어 있으면 트랜잭션을 준비하면서 트랜잭션 매니저에게 이러한 정보가 전달되며 그에 따라 트랜잭션 매니저가 적절한 작업을 수행한다. 그런데 일부 트랜잭션 매니저의 경우 읽기전용 속성을 무시하고 쓰기 작업을 하용할 수도 있으므로 주의해야 한다. 물론 일반적으로 읽기전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE의 작업이 진행되면 예외가 발생한다.
[ 롤백/커밋(rollback/commit) 예외 ]
선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백하고, 예외가 발생하지 않았거나 체크 예외가 발생하였다면 커밋한다. 여기서 체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기 보다는 반환값을 대신해 비지니스적인 의미를 담은 결과로 많이 사용되기 때문이다. 스프링에서는 데이터 액세스 기술의 예외는 런타임 예외로 전환하여 던지므로 런타임 예외만 롤백 대상으로 삼은 것이다.
하지만 롤백/커밋의 동작 방식의 변경을 원한다면 설정을 통해 동작 방식을 바꿀 수 있는데, 커밋 대상이지만 롤백을 발생시킬 예외나 클래스 이름은 각각 rollbackFor 또는 rollbackForClassName으로 지정할 수 있으며, 반대로 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정하기 위해서는 noRollbackFor 또는 noRollbackForClassName을 이용할 수 있다.
[ 제한 시간(timeout) ]
timeout 속성을 이용하면 트랜잭션에 제한시간을 지정할 수 있다. 값은 int 타입의 초 단위로 지정할 수 있는데 문자열로 지정하기를 원한다면 timeoutString을 사용하면 된다.
만약 별도로 값을 설정해주지 않는다면 트랜잭션 시스템의 제한시간을 따르며, 제한 시간을 직접 지정했는데 트랜잭션 매니저에서 이 기능을 지원하지 못한다면 예외가 발생할 수 있다.
위에서 설명한 모든 트랜잭션 속성들은 각각의 트랜잭션 경계설정 속성에 지정할 수 있다. 하지만 보통은 readOnly 속성 정도만 활용하고, 나머지는 디폴트로 사용하는 경우가 많다. 세밀한 속성은 DB나 WAS의 트랜잭션 매니저 설정을 이용해도 되기 때문이다.
관련 포스팅
- 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction(트랜잭션) 핵심 기술 - (1/3)
- Spring 트랜잭션의 세부 설정(전파 속성, 격리수준, 읽기전용, 롤백/커밋 예외 등) - (2/3)
- Spring에서 트랜잭션의 사용법 - (3/3)
'Spring' 카테고리의 다른 글
[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1) (73) | 2021.07.14 |
---|---|
[Spring] Spring에서 트랜잭션의 사용법 - (3/3) (6) | 2021.07.09 |
[Spring] 빈을 찾기 위한 다양한 의존성 검색 방법, DL(Dependency LookUp) (0) | 2021.07.07 |
[Spring] 설정 값 분리의 필요성과 @Value의 사용법 및 동작 과정 (0) | 2021.07.04 |
[Spring] AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 이해 - (1/3) (4) | 2021.06.02 |