Background
Transaction에 대한 내용은 열심히 들어왔었지만, 정작 프로젝트 내의 레거시 코드에서 내 의도대로 사용 되지 못하는 경우가 많았다.
- 매번 Transaction 에 대한 설명은 구글링 해보면 "철수와 영희의 돈 거래 과정"을 예시로 알려 준다.
- 하지만 위의 예시를 이해 했더라도 레거시 코드에서는 어떻게 사용하고 있는지 감이 잡히지 않는다.
- Transaction 처리가 필요한 순간 @Transactional annotation 이 마법처럼 해결해주지 않는다. ex) @Transactional annotation 남발하는 메서드들..(Propagation 이 원하는대로 이루어지지 않는 경우가 다반사)
- 과거에 작정 했던 코드(+ 다른 사람이 짰던 코드)에 갑자기 Transaction 처리가 추가적으로 필요한 경우가 종종 찾아온다.
Connection
애플리케이션(WAS, DB 관련 툴 등)에서 데이터베이스와 연결을 위한 일련의 접속 과정을 의미함
DBCP 란?
Database Connection Pool 의 약자로, Spring Framework 로 만들어진 WAS 에서 필요로 하는 시점에 데이터베이스와의 커넥션을 만드는 것이 아닌, 미리 일정한 수의 커넥션을 만들어놓고 필요한 시점에서 애플리케이션에 제공하는 서비스 관리 체계
org.apache.commons.dbcp2.BasicDataSource 와 같은 Apache 에서 제공하는 오픈소스 라이브러리를 사용하고 있음
(DBCP에 대한 자세한 내용은 Commons DBCP 이해하기 - Naver D2 참고)
Session
Database Connection 을 시작으로, 여러 작업을 수행한 후 접속 종료까지의 전체 기간을 의미한다.
Session 은 데이터가 변경, 삭제가 확정될 때까지 해당 데이터 조작을 분리 처리함으로써 데이터 테이블과의 관계를 보존하는 역할
Transaction
Database 에서 일어나는 CRUD 과정의 논리적 기능을 수행하기 위한 작업 단위로 실행이 시작된 후 Commit 이나 Rollback 으로 마무리된다.
이 과정에서 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)을 보장하는 방법(식?)을 의미한다.
Transaction 이 필요한 이유
Transaction 의 기능은 사실 Database 자체에서 지원하는 기술이다. 그렇지만 애플리케이션 레벨에서 서비스 로직이 수행하는 동안에 Transaction 을 관리하는 일들이 다분하게 발생하게 된다.
아래 코드는 전에 자주보던 "철수와 영희의 돈 거래 과정"에서 있는 송금 서비스 로직을 구현해봤다.
내용을 보면 update 라는 쿼리를 두 번 실행 하면서, 결국 두 번의 Transaction 단위로 쿼리를 수행하게 된다.
1
2
3
4
5
6
7
8
|
public void sendMoney(String from, String to, int money) throws Exception {
Member fromMember = memberRepository.findById(from);
Member toMember = memberRepository.findById(to);
memberRepository.update(from, fromMember.getMoney() - money);
// throw new IllegalStateException("Error Occurred!!!");
memberRepository.update(to, toMember.getMoney() + money);
}
|
간단한 로직이지만 만약 6번 라인의 주석처리 된 에러가 발생한다면, 보내는 사람의 돈만 사라지고 받는 사람의 돈은 변하지 않는 불상사가 발생하게 된다.
하지만 아래와 같이 서비스 로직 과정에서 같은 Connection을 사용하고 Transaction 을 관리할 수 있다면 이 문제를 해결할 수 있게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public void sendMoney(String from, String to, int money) throws Exception {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // Transaction 시작
Member fromMember = memberRepository.findById(con, from);
Member toMember = memberRepository.findById(con, to);
memberRepository.update(con, from, fromMember.getMoney() - money);
// throw new IllegalStateException("Error Occurred!!!");
memberRepository.update(con, to, toMember.getMoney() + money);
con.commit(); // 성공한다면 커밋
} catch (Exception e) {
con.rollback(); // 실패하면 롤백
throw new IllegalStateException(e);
} finally {
con.close();
}
}
|
하지만 이러한 일련의 Connection 처리 과정과 Transaction 처리를 매 번 작성해야 한다면 중복된 코드가 남발할 것이다.(실수로 Connection 을 close 하지 않는다면 더 큰 문제가 발생할 수도 있다.)
위와 같이 Transaction 을 유지하려면 시작부터 끝까지 같은 데이터베이스 Connection 을 유지해야 한다. 위의 코드에서는 Connection 을 동기화하기 위해서 파라미터로 Connection 을 전달하는 방법을 사용하고 있다. 하지만, 이 방법은 누가 보기에도 지저분한 방법으로 보일것이다.
물론 Spring 에서 이러한 중복 과정을 AOP 화 했으며 손쉽게 관리가 가능하도록 이미 구현해두었다. 그것이 바로 ...
PlatformTransactionManager
아래 TransactionSynchronizationManager 을 설명하기 전에 트랜잭션 관리 추상화 클래스인 PlatformTransactionManager 를 설명하고자 한다.
PlatformTransactionManager 는 DataSourceTransactionManager 나 JpaTransactionManager 등 과 같은 트랜잭션 관리 기능을 프로젝트에 알맞게 사용하고 특정 트랜잭션 기술에 직접 의존하지 않도록 만든 트랜잭션 관리 추상화 클래스이다.
1
2
3
4
5
6
7
8
|
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
|
PlatformTransactionManager 인터페이스를 가지고 구현해야하는 기능은 총 세 가지에 불과하다. 현재 Transaction 을 가져오는 getTransaction() 기능과 앞서 말한 commit() 과 rollback() 기능 뿐이다.
대표적인(현재 가맹점 서비스에서 사용중인...) PlatformTransactionManager 의 구현체인 DataSourceTransactionManager 를 간단하게 살펴보자면, 클래스 이름 그대로 DataSource 라는 Bean 이 스프링에 등록되어 사용할 수 있는 트랜잭션 매니저이다. JDBC API 를 사용해서 트랜잭션을 관리하고 액세스 하며, ibatis(mybatis) 에서 제공하는 SqlSessionTemplate 에 적용도 가능하다.
TransactionSynchronizationManager
TransactionSynchronizationManager 을 설명하기 위한 빌드업이 드디어 끝났다...
스프링에서 제공하는 Transaction Manager 는 크게 두 가지 역할을 갖고 있다.
- 트랜잭션 추상화
- 위에서 설명한 PlatformTransactionManager 인터페이스를 사용 방식을 말한다.
- 리소스 동기화
- 트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다.
결국 같은 커넥션을 동기화하기 위해서 예시 코드로 보여주었던 파라미터로 Connection 을 전달하는 방법이 있다. 하지만 이 방법은 아까 말했다시피 지저분해지고 중복된 코드를 생성할 수 밖에 없다.
- 트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다.
여기서 리소스 동기화를 위해 스프링에서 제공하는 것이 TransactionSynchronizationManager (트랜잭션 동기화 매니저) 이다. 위에서 설명한 PlatformTransactionManager 가 TransactionSynchronizationManager 에서 보관하는 커넥션을 가져와서 사용하는 방식이라고 이해하면 된다.
TransactionSynchronizationManager 는 내부적으로 ThreadLocal 를 사용하기 때문에 멀티쓰레드 상황에 안전하게 Connection 을 동기화 할 수 있다. 따라서 Connection 이 필요하면 TransactionSynchronizationManager 를 통해 Connection 을 가져오면 되고, 이전처럼 파라미터로 Connection 을 전달할 필요가 없어집니다. 또한, Thread 기준으로 리소스 및 트랜잭션 동기화를 관리해줄 수 있어서 사용하기 편리합니다.
결국 우리는 TransactionSynchronizationManager 를 사용해서 Transaction 이 발생하는 상황의 로직을 컨트롤 할 수 있습니다.
나는 TransactionSynchronizationManager 가 왜 필요했을까?
... 사내 코드 및 로직이라서 자세한건 나중에...
isActualTransactionActive
public static boolean isActualTransactionActive()
Return whether there currently is an actual transaction active. This indicates whether the current thread is associated with an actual transaction rather than just with active transaction synchronization.
To be called by resource management code that wants to discriminate between active transaction synchronization (with or without backing resource transaction; also on PROPAGATION_SUPPORTS) and an actual transaction being active (with backing resource transaction; on PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, etc).
registerSynchronization
public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException
Register a new transaction synchronization for the current thread. Typically called by resource management code.
Note that synchronizations can implement the Ordered interface. They will be executed in an order according to their order value (if any).
Parameters:synchronization - the synchronization object to registerThrows:IllegalStateException - if transaction synchronization is not active
'Spring' 카테고리의 다른 글
Logback 사용하기 (0) | 2022.03.06 |
---|---|
AWS Amplify (4) | 2021.11.20 |
JUnit 5 (2) | 2021.02.12 |
리플렉션 API: 클래스 정보 조회 (1) | 2021.02.01 |
Spring AOP 개념 (3) | 2020.12.02 |