일하다 보면 쓰는 것만 쓰게 되는 경향이 있어서 안다고 생각한 것도 사실 잘 모르는 경우가 더 많다.
좀 더 자세히 들여다보면서 Spring Framework 기능을 제대로 사용할 때까지 공부를 좀 더 해야지 ^_ㅠ
오늘은 Spring @Transactional과 그 주요 설정에 대해 알아보려고 한다.
Spring에서는 @Transactional 어노테이션을 통해 트랜잭션을 설정할 수 있다. (선언적 트랜잭션)
트랜잭션을 적용하고자 하는 클래스나 메서드 위에 @Transactional을 선언하면 해당 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성되고, 이 객체는 @Transactional이 포함된 메소드가 호출되는 경우 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고 정상 여부에 따라 Commit 또는 Rollback 하여 트랜잭션을 수행한다.
트랜잭션을 적용할 대상에게 어노테이션만 붙여도 되겠지만 Spring은 좀 더 세부적인 설정들이 많다.
propagation
우선 이 글을 쓰게 된 이유인 @Transactional 어노테이션의 propagation이라는 옵션이 있다.
'전파', '전달' 이라는 뜻으로 트랜잭션의 실행 범위를 지정하는 옵션이다.
트랜잭션을 가져올 때 PlatformTransactionManager에서 getTransaction을 사용하게 되는데
이때 propagation 세팅 내용을 기반으로 트랜잭션을 가져오게 된다.
(PlatformTransactionManager (Spring Framework 5.3.20 API) 에서 getTransaction에 대한 Docs 확인 가능)
옵션명 | 설명 |
REQUIRED (기본값) | 항상 트랜잭션이 실행된다. 기존 트랜잭션이 있으면 해당 트랜잭션을 사용하여 실행되고, 없으면 새로운 트랜잭션을 생성한다. |
SUPPORTS | (있으면 쓰고 없으면 말고) 기존 트랜잭션이 있으면 해당 트랜잭션을 사용하여 실행되고, 없으면 트랜잭션 없이 실행된다. |
NOT_SUPPORTED | 항상 트랜잭션 없이 실행된다. 기존 트랜잭션이 있으면 일시중단 (보류) 시킨다. |
REQUIRES_NEW | 항상 새로운 트랜잭션에서 실행된다. 기존 트랜잭션이 있으면 일시중단 (보류) 시킨다. |
MANDATORY | 항상 트랜잭션이 실행된다. - REQUIRED와 비슷하게 기존 트랜잭션이 있으면 이를 이용한다. - 차이점은 기존 트랜잭션이 없으면 새로 시작하는 대신 예외를 발생시킨다. - 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다. |
NEVER | 항상 트랜잭션 없이 실행된다. - 기존 트랜잭션도 존재하면 안됨! 있다면 예외를 발생시킨다. |
NESTED | 항상 트랜잭션이 실행된다. - 기존 트랜잭션이 있으면, 해당 트랜잭션에 SAVE POINT를 만든다. - 더불어 기존 트랜잭션 내에 중첩 트랜잭션을 만든다. (트랜잭션 안에 다시 트랜잭션 생성) - 이 과정에서 비즈니스 로직에 예외 발생 시, SAVE POINT로 ROLLBACK한다. - 하위 트랜잭션 (중첩) 은 상위 트랜잭션에 영향을 받지만, 상위 트랜잭션은 하위 트랜잭션에게 영향을 덜 받도록 구축하는 형태임 기존 트랜잭션이 없는 경우, REQUIRED처럼 새로운 트랜잭션을 생성한다. |
사용 방법은 @Transactional(propagation=Propagation.SUPPORTS)의 형태로 사용한다.
isolataion
isolation은 격리라는 뜻으로 사용되며, @Transactional에서는 해당 속성을 통해 격리 수준을 지정한다.
격리라는 단어가 와닿지 않지만 동시에 여러 개의 트랜잭션에 의한 변경사항을 어떻게 적용될지에 대한 설정이다.
속성의 세부적인 옵션에 대해 설명하기에 앞서
동시에 발생하는 트랜잭션으로 인해 발생할 수 있는 문제에 대한 이해가 필요하다.
- Dirty Read
: 변경사항이 반영되지 않은 값을 다른 트랜잭션에서 읽도록 허용할 경우 발생하는 데이터 불일치
: 트랜잭션A에서 seq가 2인 데이터의 값을 30에서 40으로 바꾸고 commit하지 않은 상태인데 다른 트랜잭션 B에서 조회 시 40으로 조회됨
: 문제가 없어보이지만, Commit을 하지 않고 Rollback 하게 되는 경우 트랜잭션 B는 잘못된 데이터를 사용하게 됨 - Non-Repeatable Read
: 한 트랜잭션 내에서 값을 조회할 때, 동시성 문제로 인하여 같은 쿼리가 다른 결과를 반환하는 경우
즉, 트랜잭션이 끝나기 전에 수정사항이 반영되어, 트랜잭션 내에서 쿼리 결과가 일관성을 가지지 못하는 경우
: 트랜잭션A가 특정 데이터를 조회해오고 같은 트랜잭션 내에서 또 같은 데이터를 조회할 예정임.
근데 그 사이에 다른 트랜잭션 B가 해당 데이터를 수정/삭제하게 되면 트랜잭션 A는 두 번의 조회 결과가 다르게 됨 - Phantom Read
: 외부에서 수행되는 입력/삭제 작업으로 인해 트랜잭션 내에서의 동일한 쿼리가 다른 값을 반환하는 경우
: 트랜잭션A가 특정 범위의 데이터를 읽어오고 또 같은 범위의 데이터를 조회할 예정임 (0부터 10 사이의 값)
근데 그 사이에 다른 트랜잭션 B가 해당 범위 내에 신규 데이터를 추가해버리면 트랜잭션 A는 두 번의 조회 결과가 다르게 된다.
** Non-Repeatable Read와 Phantom Read가 좀 헷갈리는데 ^_ㅠ
Non-Repeatable Read는 기존에 조회한 행의 내용을 두 번째에서는 변경된 형태로 조회한다는 얘기고
Phantom Read는 기존 조회결과에 있던 게 없어지거나 없던 게 생기는 것을 의미함.
여러 개의 트랜잭션이 발생하는 경우, 생길 수 있는 동시성 문제를 해결하는 isolation 속성은 아래와 같다.
옵션명 | 설명 |
DEFAULT (기본값) | 별도의 값을 설정하지 않는 경우, DBMS의 Isolation Level을 따름 - DB를 변경하게 되는 경우, 주의 필요. |
READ_UNCOMMITED | 가장 낮은 Isolation Level - COMMIT 되지 않은 데이터, 트랜잭션 처리 중인 데이터에 대한 읽기를 허용 - 동시성 부작용 모두 발생 (Dirty Read, Non-Repeatable Read, Phantom Read) - Postgres는 READ_UNCOMMITTED를 허용하지 않고 READ_COMMITTED를 사용하도록 함 - Oracle은 READ_UNCOMMITTED를 지원하지 않거나 허용하지 않음 |
READ_COMMITED | 트랜잭션에서 COMMIT 된 확정 데이터만 읽기 허용 - 동시성 부작용 중 Dirty Read 방지 - Postgres, Oracle, SQL Server의 기본 수준 |
REPEATABLE_READ | 트랜잭션이 완료될 때까지 SELECT문이 사용하는 모든 데이터에 Shared Lock 처리 - 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능하다. - 동시성 부작용 중 Dirty Read, Non-Repeatable Read 방지 - 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제가 불가능 하기때문에 같은 데이터를 두 번 쿼리했을 때 일관성 있는 결과를 리턴한다. - Mysql의 기본 수준이며, Oracle은 REPEATABLE_READ를 지원하지 않는다. |
SERIALIZABLE | 최고 수준의 Isolation Level - 동시성 부작용 모두 방지 (Dirty Read, Non-Repeatable Read, Phantom Read) - 동시 호출을 순차적으로 실행하도록 제한하는 설정이므로, 성능 저하의 우려가 있음 - 데이터의 일관성 및 동시성을 위해 MVCC(Multi Version Concurrency Control)을 사용하지 않음 (MVCC는 다중 사용자 데이터베이스 성능을 위한 기술로 데이터 조회 시 LOCK을 사용하지 않고 데이터의 버전을 관리해 데이터의 일관성 및 동시성을 높이는 기술) - 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능하다. |
트랜잭션 isolation 수준을 높이는 경우, 데이터의 일관성은 향상되나 동시성은 저하되어 성능 저하의 우려가 있으므로 해당 내용을 고려하여 isolation 설정을 진행하면 된다.
사용방법은 @Transactional(isolation=Isolation.READ_COMMITED) 의 형태로 사용한다.
readOnly
@Transactional은 readOnly 속성을 통해 해당 트랜잭션을 읽기 전용으로 설정할 수 있다.
기본적으로 default는 readOnly=false 상태이다.
readOnly=true로 설정하게 되면 Spring은 해당 트랜잭션의 FlushMode를 NEVER로 설정하게 된다.
flush가 일어나지 않으면, COST 절감이 가능하고 생성/수정/삭제가 일어나지 않기 때문에 스냅샷을 만들 필요가 없어 성능 상의 이점이 있다.
- 성능 최적화 목적으로 사용
- 특정 트랜잭션 작업 내에서 쓰기 작업을 의도적으로 방지하고자 사용
(단, 일부 TransactionManager의 경우 readOnly 속성을 무시하고 쓰기 작업을 허용하기도 한다고 함) - @Transactional(readOnly=true) 형태의 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다.
- AOP/TX 스키마로 트랜잭션 선언을 할 때는 이름 패턴을 이용해 읽기 전용 속성으로 만드는 경우가 많다.
보통 get이나 find 같은 이름의 메소드를 모두 읽기 전용으로 만들어 사용하면 편리하다.
사용방법은 @Transactional(readOnly=true) 의 형태로 사용한다.
[참고]
[Spring] Transactional 정리 및 예제 (tistory.com)
@Transactional 어노테이션의 다양한 옵션 활용 (tistory.com)
스프링 부트 트랜잭션: 트랜잭션 전파 이해 - DZone Java
트랜잭션 수준 읽기 일관성 - [종료]구루비 DB 스터디 - 개발자, DBA가 함께 만들어가는 구루비 지식창고! (gurubee.net)
'PROGRAM > JAVA / JSP' 카테고리의 다른 글
[JAVA] equals 함수 사용 순서, NullPointerException 방지 (2) | 2022.12.19 |
---|---|
도로명 주소 API 연동방식, 외부 API 연동 시 유의사항 (0) | 2020.12.14 |
for 반복문 중첩 빠져나오기 - break label (0) | 2020.04.13 |
Maven project 제대로 사용하기 : pom.xml (0) | 2019.05.23 |
QR코드 생성방식 (Google Chart API 대체) (0) | 2019.03.19 |