트랜잭션 전파란 특정 A프랜잭션이 처리되는 과정 안에서 또다른 B트랜잭션 이 처리되는 경우 에러가 발생할 경우 각 트랜잭션에 에러전파 하는것을 뜻한다.
전파방식
의미
REQUIRED(default)
트랜잭션 상황에서 실행되어야 한다. 진행 중인 트랜잭션이 있다면 이 트랜잭션에서 실행된다. 없는 경우에는 트랜잭션이 새로 시작된다.
MANDATORY
호출 전에 반드시 진행 중인 트랜잭션이 존재해야 한다. 진행 중인 트랜잭션이 존재하지 않을 경우 예외 발생
REQUIRED_NEW
자신만의 트랜잭션 상황에서 실행되어야 한다. 이미 진행 중인 트랜잭션이 있으면 그 트랜잭션은 해당 메소드가 반환되기 전에 잠시 중단된다.
SUPPORTS
진행 중인 트랜잭션이 없더라도 실행 가능하고, 트랜잭션이 있는 경우에는 이 트랜잭션 상황에서 실행된다.
NOT_SUPPORTED
트랜잭션이 없는 상황에서 실행 만약 진행 중인 트랜잭션이 있다면 해당 메소드가 반환되기 전까지 잠시 중단한다.
NEVER
트랜잭션 진행 상황에서 실행 될 수 없다. 만약 이미 진행 중인 트랜잭션이 존재하면 예외 발생
NESTED
이미 진행 중인 트랜잭션이 존재하면 중첩된 트랜잭션에서 실행되어야 함을 나타낸다. 중첩된 트랜잭션은 본 트랜잭션과 독립적으로 커밋되거나 롤백될 수 있다. 만약 본 트랜잭션이 없는 상황이라면 이는 REQUIRED와 동일하게 작동한다. 그러나 이 전파방식은 DB 벤더 의존적이며, 지원이 안되는 경우도 많다.
트랜잭션 전파방삭에 따라 A, B 의 rollback 결정이 달라진다.
@Transactional
@Transactional 어노테이션이 있으면 Spring AOP 가 알아서 TransactionManager 기반으로 commit, rollback 을 진행한다. @Transactional 어노테이션을 사용하는 메서드에서 데이터 소스에 접근하는 쿼리를 실행할 때 락이 걸린다.
rollbackFor: 특정 Exception 발생 시 rollback 하도록 설정
noRollbackFor: 특정 Exception 발생 시 rollback 하지 않도록 설정
@Transactional 은 모든 예외발생시 rollback 하지 않고 RuntimeException, Error 를 상속한 예외 발생시에만 rollback 한다. 위 전제조건을 토대로 상황에 맞게 rollbackFor, noRollbackFor 을 사용한다.
propagation: 위 트랜잭션 전파 참고하여 설정, Propagation.REQUIRED 가 default
isolation: 트랜잭션 격리레벨 설정, Isolation.DEFAULT 가 default
Isolation.DEFAULT 는 DBMS 에 설정된 격리수준을 사용한다는 뜻
TransactionManager
TransactionManager 는 DB 영역에서 트랜잭션 기능을 추상화 시킨 클래스이다.
JPA 의 경우 EntityManager 의 begin, commit 를 통해 트랜잭션을 진행하고, JDBC 의 경우 dataSource.getConnection 의 setAutoCommit(false), commit 을 통해 트랜잭션을 진행한다.
스프링에선 이를 아래와 같은 TransactionManager 인터페이스로 트랜잭션 과정을 추상화 시켰다.
publicabstractclassAbstractPlatformTransactionManager implementsPlatformTransactionManager, ConfigurableTransactionManager, Serializable { ... } // 아래와 같은 AbstractPlatformTransactionManager 구현체들이 있음. // JpaTransactionManager // DataSourceTransactionManager // JmsTransactionManager - java message system 을 같이 사용할 경우 이용
JpaTransactionManager 를 사용하는 상황에서 @Transactional 어노테이션을 만나면 Spring AOP 가 알아서 추상화 처리된 AbstractPlatformTransactionManager 의 동작대로 DB 와의 연결 및 트랜잭션 작업을 수행한다.
트랜잭션 시작, 아래함수를 순서대로 호출 TransactionAspectSupport.createTransactionIfNecessary AbstractPlatformTransactionManager.getTransaction AbstractPlatformTransactionManager.startTransaction
DataSource 로부터 Connection 흭득 및 ThreadLocal 에 등록 AbstractPlatformTransactionManager.doBegin TransactionSynchronizationManager.bindResource - ThreadLocal 등록
트랜잭션 내부에서 수행되는 Repository 메서드들은 ThreadLocal 로부터 Connection 을 가져와서 쿼리를 실행
트랜잭션 종료(commit or rollback) TransactionAspectSupport.cleanupTransactionInfo
DataSource 에 Connection 반환
readOnly = true 가 특별한 유형의 연결을 생성하지는 않지만, 읽기 전용 트랜잭션의 이점을 활용하여 ORM의 성능을 최적화할 수 있다. 수정요청시 에러를 발생시키도록 하거나 Hibernate 의 영속성 플러시 작업을 추가적으로 하지않아 성능을 최적화 할 수 있다. 이를 통해 데이터 일관성을 보장하고, 읽기 전용 작업의 성능을 극대화할 수 있다.
// @Transaction 이 없을 때 public List<Board> findAll() { return repository.findAll(); }
1 2 3 4
@Transactional(readOnly = true) public List<Board> findAll() { return repository.findAll(); }
위와 같이 @Transactional(readOnly = true) 있는것과 없는 메서드 실행시 아래 6개의 general_log 가 출력된다.
set session transaction read only 세션의 읽기 전용 지정
SET autocommit=0 세션에서 호출될 쿼리들이 자동커밋되지 않고 트랜잭션으로 묶이는 것을 의미, 트랜잭션 시작을 의미.
select b1_0.bno,… from tbl_boards b1_0 쿼리 수행
commit 쿼리 커밋, 트랜잭션 종료
SET autocommit=1 autocommit 원복
set session transaction read write 세션 읽기 쓰기 지정 원복
@Transactional 을 지정하지 않으면 repository 메서드 호출마다 session 에 대한 설정을 수행하기 때문에 위 예제의 경우 @Transactional(readOnly = true) 설정한것과 동일하다.
@Transactional(readOnly=false) 의 경우 4개의 general_log 가 출력된다.
1 2 3 4 5 6 7 8
@Transactional(readOnly=false) public List<Board> findAll() { return repository.findAll(); } // SET autocommit=0 // "select b1_0.bno,... from tbl_boards b1_0" // commit // SET autocommit=1
@Transactional(readOnly=true) 를 설정할 경우 영속성 레이어에서 추가작업을 하지 않아 어플리케이션 레이어에선 부하가 줄어들겠지만, session transaction 의 read only, read write 작업을 추가적으로 수행하기 때문에 DB 레이어에선 부하가 증가한다.
DataSource 에서 autocommit 설정을 disable 처리하고, @Transactional 만 지정된 메서드를 수행하면 단 2개의 general_log 가 출력된다.
Entity 에 @Version 만 지정하면 별도의 어노테이션을 사용하지 않아도 version 정보를 기반으로 UPDATE 하기 때문에 Lost Update 문제가 발생하지 않는다.
1 2 3 4 5 6 7 8 9
-- OrderService->patch start! select order0_.order_number as order_nu1_1_, order0_.state as state2_1_, order0_.version as version3_1_ from purchase_order order0_ where order0_.order_number = ? -- UPDATE 시 version 체크 update purchase_order set state=?, version=? where order_number=? and version=? -- OrderService->patch end!
쿼리 메서드에 별도로 @Lock 어노테이션을 사용해 낙관적 락에 대한 추가설정을 할 수 있다.
OPTIMISTIC 트랜잭션 종료 시점에 한번 더 버전정보를 체크한다. 만약 종료시점에서 검색된 version 이 다를경우 OptimisticLockException 을 발생시킨다. 현재 스레드의 Dirty Read, Lost Update 상황을 방지한다.
OPTIMISTIC_FORCE_INCREMENT 단순 SELECT 요청도 version 을 증가시킨다. 변경까지 한다면 version 이 2 증가한다. 타 스레드의 Dirty Read, Lost Update 상황을 방지한다.
1 2 3 4 5 6 7
@Lock(LockModeType.OPTIMISTIC) @Query("SELECT o FROM Order o WHERE o.orderId = :orderId") Optional<Order> findByIdOptimistic(OrderId orderId);
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) @Query("SELECT o FROM Order o WHERE o.state = :state") List<Order> findAllByOrderStateOptimistic(OrderState state);
LockModeType.OPTIMISTIC 을 사용했다면 초기 SELECT 한 Entityversion 과 트랜잭션 종료 직전 조회한 version 이 일치하지 않는다면 OptimisticLockException 가 발생한다.
1 2 3 4 5 6 7 8 9 10 11 12
-- OrderService->findByIdOptimistic start! 함수 트랜잭션 시작 -- service function start! select order0_.order_number as order_nu1_1_, order0_.state as state2_1_, order0_.version as version3_1_ from purchase_order order0_ where order0_.order_number = ?
-- service function end! select version as version_ from purchase_order where order_number =? -- OrderService->findByIdOptimistic end! 함수 트랜잭션 종료 전 version 검사 -- 일치하지 않으면 OptimisticLockException
LockModeType.OPTIMISTIC_FORCE_INCREMENT 을 사용했다면 SELECT 로 조회한 모든 Entityversion 을 증가시킴.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
-- OrderService->findAllByOrderState start! select order0_.order_number as order_nu1_1_, order0_.state as state2_1_, order0_.version as version3_1_ from purchase_order order0_ where order0_.state = ?
update purchase_order set version=? where order_number=? and version=? update purchase_order set version=? where order_number=? and version=? update purchase_order set version=? where order_number=? and version=? update purchase_order set version=? where order_number=? and version=? update purchase_order set version=? where order_number=? and version=? -- SELECT 로 조회된 purchase_order 개수만큼 수행 -- OrderService->findAllByOrderState end!
LockModeType.OPTIMISTIC_FORCE_INCREMENT 는 부하를 유발시키는 설정이긴 하지만 first-commiter win 과 같은 형태로 운영할 수 있다.
낙관적 락의 단점은 DB 락을 가져올수 있는지 즉시 체크하지 못하기 때문에 데이터 일관성 체크를 커밋 시점에야 가능하다는 것이다. 낙관적 락 과 연계된 쿼리가 있다면 별도의 처리를 해줘야할 수 도 있다.
비관적 락(Pessimistic Lock)
비관적 락에선 @Lock(LockModeType.PESSIMISTIC...) 을 사용한다.
DBMS 마다 다르지만 MySQL 의 경우 비관적 락 을 설정하면 쿼리 마지막에 FOR SHARE, FOR UPDATE 키워드가 붙는다.
PESSIMISTIC_READ FOR SHARE 키워드를 사용, [UPDATE, DELETE] 를 막는다.
PESSIMISTIC_WRITE FOR UPDATE 키워드를 사용, [SELECT, UPDATE, DELETE] 를 막는다. 현재 스레드의 Dirty Read, Lost Update 를 막는다.
PESSIMISTIC_FORCE_INCREMENT PESSIMISTIC_WRITE 와 동일한 기능에 더불어 잠금 흭득시 @Version 을 증가시킨다.
비관적 락 방식의 경우 락에 의한 교착상태가 발생가능하니 타임아웃 설정을 권장한다. DBMS 레이어에서 Lock Timeout 을 설정해도 된다. innodb_lock_wait_timeout=50(default)
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT) // javax.persistence.lock.timeout @QueryHints(@QueryHint(name = AvailableSettings.JPA_LOCK_TIMEOUT, value ="5000")) @Query("SELECT o FROM Order o WHERE o.state = :state") List<Order> findAllByOrderState(OrderState state); }
1 2 3 4 5 6 7 8 9 10 11 12 13
-- findAllByOrderState 실행 -- 리스트 개수만큼 version update 가 추가실행된다. select order0_.order_number as order_nu1_1_, order0_.state as state2_1_, order0_.version as version3_1_ from purchase_order order0_ where order0_.state = ? forupdate;
update purchase_order set version=? where order_number = ? and version = ?; update purchase_order set version=? where order_number = ? and version = ?; ... update purchase_order set version=? where order_number = ? and version = ?;
비관적 락을 사용하는 대부분 이유가 Dirty Read 이후 이어지는 Lost Update 를 막기 위함이기 때문에 PESSIMISTIC_WRITE 를 주로 사용한다.
아래와 같이 api 100번 연속 호출시 분산락 내부 임계영역이 정확히 몇번 호출되는지 테스트하면 된다.
1
for i in {1..100}; do curl -s http://localhost:8080/distribute-lock/test & done; wait
아래 명령으로 현재 사용중인 메타데이터 락을 확인 가능.
1
SELECT*FROM performance_schema.metadata_locks;
하지만 MySQL 의 메타데이타 락 은 코드의 임계영역을 동시에 한번 실행시키진 않는다. 동일 세션의 GET_LOCK 호출은 이미 락을 흭득했다 보고 성공코드를 돌려주기 때문.
SELECT GET_LOCK('testLock', 10); 해당 코드를 같은 DB 콘솔에서 여러번 실행하면 동일 세션이기 때문에 모두 1(성공) 이 출력된다.
즉 어플리케이션 단위로 메타데이타 락 을 가져간다고 봐야한다. 어플리케이션 레이어에서 메타데이타 락 과 함께 로컬 Lock 를 관리하거나 synchronized 키워드를 사용하면 분산환경에서도 임계영역을 지정할 수 있다.
open-in-view
1
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
보통 Service 에서 @Transactional 을 사용해 영속성 컨텍스트를 생성하고 Controller 나 외부 컴포넌트에선 준영속 컨텍스트가 될거라 새각하지만
open-in-view=true 의 경우 영속성 컨텍스트의 생존 법위가 스레드의 종료까지 이어진다 (REST API 의 Response 완료까지) default true 이기 떄문에 컨트롤러에서 Lazy Loading 을 통해 엔티티를 통해 객체를 찾고 DB 에서 가져올 수 있다.
open-in-view=false 일 경우 준영속 컨텍스트에선 지연로딩 사용이 불가능하다. 지연로딩 기법을 사용한다면 @Transactional 외부에서 영속공간에 접근하는 내용을 제거해야한다. Transaction 안에서만 Lazy Loading 을 수행할 수 있고, 컨트롤러 코드에서 접근시 no session 에러가 발생하게 된다.
Criteria
JPA의 공식 동적 쿼리 생성 API. 타입 안전한 쿼리를 작성할 수 있지만 코드가 복잡하고 가독성이 떨어진다.
// QueryDsl 쿼리 타입 생성 (QClass 생성 시 @Entity 탐색) annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta" // java.lang.NoClassDefFoundError:javax/persistence/Entity 에러 방지 annotationProcessor "jakarta.persistence:jakarta.persistence-api" annotationProcessor "jakarta.annotation:jakarta.annotation-api" }
// QueryDSL QClass 생성 디렉토리 설정 def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile //def querydslDir = file("src/main/generated") # src/main/generated 위치에 저장
sourceSets { main.java.srcDirs += [querydslDir] }
// annotationProcessor 에서 생성할 코드 위치 지정 project.tasks.named('compileJava') { options.generatedSourceOutputDirectory = file(querydslDir) }
// QueryDSL QClass 생성 디렉토리 정리 태스크 task cleanQuerydslGenerated(type: Delete) { delete querydslDir } // clean 태스크에 QueryDSL 생성 파일 삭제 추가 clean { delete querydslDir }