오늘 하루 평온하게 보내고 있었지만 같이 공부하고 있었던 분들 중 Spring에서 Transaction을 어떻게 관리하는지 알고 있냐고 물어보시고 이것에 대해 공부해본적이 있냐며, Spring에서 Transaction을 어떻게 관리하는지 공부해오라는 숙제를 주셨다.
그냥 쓰려면 쉽지만 제대로 쓰려면 어렵다고 말씀하시는 것으로 보아..
깊게 파보라는 말씀(?) 같다..
삽질은.. 익숙하니까 한번 시작해봐야겠다!
1. Transaction에 대한 이해
❓ Transaction이란 무엇인가 ?
- 여러 작업을 진행하다가 문제가 생겼을 경우 이전 상태로 rollback하기 위해 사용되는 것이 Transaction이다.
- Transaction이란 더 이상 쪼갤 수 없는 최소 작업 단위를 의미한다.
- Transaction은 commit으로 성공하거나 rollback으로 실패 이후 취소되어야 한다.
💡 Transaction의 마무리 작업 단계
트랜잭션의 마무리 작업으로는 크게 2가지가 있다.
- Commit : 작업이 마무리 됨 → 데이터 베이스에 반영
- Rollback : 작업을 취소함 → 데이터 베이스에 반영되지 않음
🐦 Transaction의 4가지 속성 - ACID
- 원자성 (Atomicity)
- Transaction의 작업이 부분적으로 실행되거나 중단되지 않는 것을 보장하는 것을 말한다.
- All or Nothing의 개념으로서 작업 단위를 일부분만 실행하지 않는다는 것을 의미한다.
- 일관성 (Consistency)
- Transaction이 성공적으로 완료되면 일관적인 DB상태를 유지하는 것을 말한다.
- 일관성이란 예를 들어, 송금을 했을때 금액의 데이터 타입이 정수형에서 갑자기 문자열이 되지 않는다는 것을 말한다.
- 송금 전후 모든 금액의 데이터 타입은 정수형이여야 한다는 것을 일관성이라고 한다.
- 격리성 (Isolation)
- Transaction 수행시 다른 Transaction의 작업이 끼어들지 못하도록 보장하는 것을 말한다.
- Transaction끼리는 서로를 간섭할 수 없다.
- 지속성 (Durability)
- 성공적으로 수행된 Transaction은 영원히 반영되는 것을 말한다.
- commit을 하면 현재 상태는 영원히 보장된다.
2. Spring이 제공하는 Transaction 핵심 기술
- Spring은 트랜잭션과 관련된 3가지 핵심 기술을 제공하고 있다
- Transaction 동기화
- Transaction 추상화
- AOP를 이용한 Transaction 분리
🔧 Transaction 동기화란 무엇일까 ❓
- JDBC를 이용하는 개발자가 직접 여러개의 작업을 하나의 Transaction으로 관리하려면 Connection 객체를 공유하는 등의 상당히 불필요한 작업이 많이 생길 것이다.
- Spring은 이러한 문제를 해결하고자 Transaction Synchronization(트랜잭션 동기화) 기술을 제공하고 있다.
- 트랜잭션 동기화란 트랜잭션을 시작하기 위한 Connection 객체를 특별한 저장소에 보관해두고 필요할 때 꺼내쓸 수 있도록 하는 기술이다.
- 트랜잭션 동기화 저장소는 작업 쓰레드마다 Connetion 객체를 독립적으로 관리하기 때문에, 멀티쓰레드 환경에서도 충돌이 발생하지 않는다.
🔧 Transaction 동기화 - 예제 코드
TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();
💡 개발자가 JDBC가 아닌 Hibernate와 같은 기술을 쓴다면 위의 코드 처럼 JDBC에 종속적인 트랜잭션 코드들은 문제를 유발하게 된다.
- Hibernate에서는 Connection이 아닌 Session이라는 객체를 사용하기 때문에 이런 문제들이 발생한다.
- 기술 종속적인 문제를 해결하기 위해 Spring은 트랜잭션 관리 부분을 추상화한 기술을 제공하고 있다.
❓ Transaction 추상화는 무엇일까 ❓
- Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다.
- Spring은 트랜잭션 추상화 기술을 제공함으로서, 애플리케이션에 각 기술(JDBC, JPA, Hibernate 등) 종속적인 코드를 이용하지 않고도 일관되게 트랜잭션을 처리할 수 있게 되었다.
- Spring이 제공하는 트랜잭션 경계 설정을 위한 추상 인터페이스는 PlatformTransactionManager 이다.
- 만약 JDBC의 로컬 트랜잭션을 이용한다면 DataSourceTxManager를 이용하면 된다.
- 사용하는 기술과 무관하게 PlatformTransactionManager를 통해 아래의 코드와 같이 트랜잭션을 공유하고 커밋하고, 롤백할 수 있게 되었다.
public Object invoke(MethodInvoation invoation) throws Trowable {
TransanctionStatus status =
this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = invoation.proceed();
this.transactionManager.commit(status);
return ret;
} catch(Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
🆘 하지만 위와 같은 트랜잭션 관리 코드들이 비즈니스 로직 코드와 결합되어 2가지 책임을 가지고 있다. Spring 에서는 AOP를 이용해 이러한 트랜잭션 부분을 핵심 비즈니스 로직과 분리하였다.
🐙 AOP를 이용한 트랜잭션(Transaction) 분리
예를 들어 아래와 같이 트랜잭션 코드와 비즈니스 로직 코드가 복잡하게 얽혀있는 코드가 있다.
🚫 @Transactional 적용 전 코드
public void addUsers(List<User> userList) {
TransactionStatus status =
this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
for(User user : userList) {
if(isEmailNotDuplicated(user.getEmail())) {
userRepository.save(user);
}
}
this.transactionManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
- 위의 코드는 여러 책임을 가지고, 서로 성격도 다르며 주고받는 것도 없으므로 분리하는 것이 적합하다.
- 트랜잭션 코드와 같은 부가 기능 코드를 클래스 밖으로 빼내서 별도의 모듈로 만드는 AOP를 적용한 트랜잭션 애너테이션(@Transactional)을 지원하게 되었다.
👍🏼 @Transactional 적용 후 코드
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserRepository userRepository;
public void addUsers(List<User> userList) {
for (User user : userList) {
if(isEmailNotDuplicated(user.getEmail())) {
userRepository.save(user);
}
}
}
}
⚡ 트랜잭션 애너테이션을 적용하면, 핵심 비즈니스 로직만 남기고 트랜잭션에 관련된 코드들을 분리할 수 있다.
🐘 Spring Transaction의 세부 설정
Spring의 DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 Transaction의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다.
- Transaction의 동작방식에 영향을 줄 수 있는 네 가지 속성
- 트랜잭션 전파
- 격리수준
- 제한시간
- 읽기전용
🐳 트랜잭션 전파
트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랙잭션이 있거나 없을 때 어떻게 동작할 것인가를 결정하는 방식을 의미한다.
예를 들어 A 작업에 대한 트랜잭션이 진행중이고 B 작업이 시작될 때 B 작업에 대한 트랜잭션을 어떻게 처리할까에 대한 부분이다.
-
A의 트랜잭션에 참여(PROPAGATION_REQUIRED)
- B의 코드는 새로운 트랜잭션을 만들지 않고 A에서 진행중인 트랜잭션에 참여할 수 있다.
- 이 경우 B의 작업이 마무리 되고나서, 남은 A의 작업을 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다.
-
독립적인 트랜잭션 생성(PROPAGATION_REQUIRES_NEW)
- B의 트랜잭션은 A의 트랜잭션과 무관하게 만들 수 있다.
- B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고 A에는 어떠한 영향도 주지 않는다.
- A가 작업을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.
-
트랜잭션 없이 동작(PROPAGATION_NOT_SUPPORTED)
- B의 작업에 대해 트랜잭션을 걸지 않을 수 있다.
- B의 작업이 단순 데이터 조회라면 굳이 트랜잭션을 사용하지 않아도 될 것이다.
🐾 Spring이 제공하는 트랜잭션 전파 속성(Propagation)
- Spring이 제공하는 선언전 트랜잭션의 장점 중 하나는 여러 트랜잭션 적용 범위를 묶어서 하나의 커다란 트랜잭션 경계를 만들 수 있다는 점이다.
- Spring이 지원하는 전파 속성은 7가지가 있다.
- REQUIRED
- Default 속성으로써 모든 트랜잭션 매니저가 지원한다.
- 이미 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
- 하나의 트랜잭션이 시작된 후 다른 트랜잭션 경계가 설정된 메서드를 호출하면 같은 트랜잭션으로 묶인다.
- SUPPORTS
- 이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행한다.
- 트랜잭션이 없어도 해당 경계 안에서 Connection 객체나 하이버네이트의 Session 등은 공유 할 수 있다.
- MANDANTORY
- 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 새로 시작하는 대신 없으면 예외를 발생시킨다.
- MANDANTORY는 혼자서 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용한다.
- REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작해야 하는 경우에 사용한다.
- 이미 진행중인 트랜잭션이 있으면 이를 보류시키고 새로운 트랜잭션을 만들어 시작한다.
- NOT_SUPPORTED
- 이미 진행중인 트랜잭션이 있으면 이를 보류시키고, 트랜잭션을 사용하지 않도록 한다.
- NEVER
- 이미 진행중인 트랜잭션이 있으면 예외를 발생시키고, 트랜잭션을 사용하지 않도록 강제한다.
- NESTED
- 이미 진행중인 트랜잭션이 있으면 중첩(자식) 트랜잭션을 시작한다.
- 중첩 트랜잭션은 트랜잭션 안에 트랜잭션을 만드는 것을 말한다.
- 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다르다.
- NESTED에 의한 중첩 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백시에는 영향을 받지만 자신의 커밋과 롤백에 의해 부모 트랜잭션은 영향을 받지 않는다.
- NESTED는 모든 트랜잭션 매니저에 적용 가능하지 않으므로 사용하는 트랜잭션 매니저와 드라이버 또는 WAS 등을 확인해야 한다.
💡 선언전 트랜잭션에서는 @Transactional 애너테이션의 propagation 엘리먼트로 원하는 전파속성을 지정할 수 있다. 기본값은 REQUIRED로 설정되어 있다.
🤯 트랜잭션 격리수준 - Isolation Level
- 모든 DB 트랜잭션은 격리수준을 가지고 있어야 한다.
- 여러 개의 트랜잭션이 동시에 진행될 경우에 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면 안전하겠지만 성능이 크게 떨어질 수 밖에 없다.
- 트랜잭션 격리수준은 JDBC 드라이버나 DataSource 등에서 재설정할 수 있고, 트랜잭션 단위로 격리 수준을 조정할 수도 있다.
- DefaultTransactionDefiniton에 설정된 격리수준은 ISOLATION_DEFAULT로, DataSource에 정의된 격리 수준을 따른다는 뜻이다.
Spring은 5가지 격리수준 속성을 지원한다.
-
Default
- 사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다.
- 일반적으로 드라이버의 격리 수준은 DB의 격리 수준을 따른다.
- 대부분의 DB는 READ_COMMITED를 기본 격리수준으로 가진다.
- 일부 DB는 디폴트 값이 다른 경우도 있으므로 드라이버와 DB 문서를 참고해야 한다.
-
READ_UNCOMMITTED
- 가장 낮은 격리수준으로써, 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제점이 있다.
- 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용한다.
-
READ_COMMITTED
- Spring은 기본 속성이 DEFAULT이며, DB는 일반적으로 READ_COMMITED가 기본 속성이다.
- READ_UNCOMMITED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다.
- 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있어서 처음 트랜잭션이 같은 로우를 읽으면 다른 내용이 발견될 수 있다.
-
REPETABLE_READ
- 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없다.
- 다른 트랜잭션이 새로운 로우를 추가하는 것은 막지 않는다.
- SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.
-
SERIALIZABLE
- 가장 강력한 트랜잭션 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시켜준다.
- 그러므로 SERIALIZABLE은 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스 할 수 없다.
- SERIALIZABLE은 가장 안전하지만 가장 성능이 떨어지므로 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용하지 않아야 한다.
⚡ 선언적 트랜잭션에서는 @Transactional 애너테이션의 isolation 엘리먼트로 원하는 전파 속성을 지정할 수 있다. 기본은 DEFAULT로 설정되어 있다.
🐼 읽기 전용 - Read Only
트랜잭션을 읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있으며, 데이터 엑세스 기술에 따라 성능이 향상될 수 있다.
- Spring에서는 트랜잭션을 2가지 목적(성능 최적화와 쓰기방지)으로 읽기 전용(ReadOnly)로 설정할 수 있다.
- 읽기 전용으로 설정함으로써 성능을 최적화한다.
- 쓰기 작업이 일어나는 것을 의도적으로 방지한다.
읽기 전용으로 설정이 되어 있으면 트랜잭션을 준비하면서 트랜잭션 매니저에게 정보가 전달 되고, 읽기전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE 작업이 진행되면 예외가 발생한다.
💡 일부 트랜잭션 매니저의 경우 읽기전용 속성을 무시하고 쓰기 작업을 허용할 수도 있으므로 주의해서 사용해야 한다.
SQLD를 응시할때나, RDBMS를 공부할 때 ACID의 개념과 Isolation Level에 대해서 들어본 적이 있지만 오늘이 포스팅을 하며 공부한 것을 계기로 알고 있던 것보다 더 많은 것들을 세밀히 컨트롤 하고 있다는 것을 느꼈다.
트랜잭션 전파수준을 지정하고, Isolation Level도 옵션으로 다룰 수 있다는 것은 오늘 처음 알았다.
앞으로 백엔드 개발자로서 알아가야 할 것이 너무 많은 것 같지만 언제나 할 것이 많고 공부해야 할 날들이 많아 즐겁다.
참고 : 망나니개발자님 티스토리
https://mangkyu.tistory.com/154
https://mangkyu.tistory.com/169