[Spring] Spring의 트랜잭션 관리 (feat: @Transactional)
📑 목차
- 트랜잭션이란?
- 트랜잭션 예제
- 트랜잭션 ACID
- 트랜잭션 관리의 종류
- 비즈니스 로직과 트랜잭션 코드의 분리
- 선언적 트랜잭션 관리 vs 프로그래밍적 트랜잭션 관리
- @Transactional이란?
- AOP
- @Transactional 예제
- @Transactional의 동작 원리
- 테스트와 @Transactional
😮 트랜잭션이란?
: DB 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위
또는 한꺼번에 모두 수행되어야 할 일련의 연산들
📚 트랜잭션 예제
온라인 쇼핑몰에서 결제할 때를 생각해보자.
연로그는 친구의 생일을 앞두고 생일 선물을 사려고 한다.
선물을 골라 결제할 때 총 2가지 작업이 이루어진다.
- 판매처에 돈 보내기
- 판매처에서 돈 받기
1만 성공하거나 2만 성공하는 경우가 발생해서는 안된다. 돈 보내는데 실패하면 판매처는 돈을 받을 수 없어야하고, 반대로 돈을 보냈는데 판매처가 돈을 못받는 상황이 발생하면 안된다. 모든 작업이 성공적으로 완료되어야 작업 결과를 적용하고, (commit) 작업 중에 어느 한 곳에서라도 오류가 발생하면 작업 실행 전의 상태로 돌아가야하는 것 (rollback)이 트랜잭션의 개념이다.
✨ 트랜잭션 ACID
- Atomicity; 원자성: 트랜잭션 내의 작업들은 모두 성공 또는 모두 실패한다.
- Consistency; 일관성: 모든 트랜잭션은 일관성 있는 DB 상태를 유지한다. (ex: DB의 무결성 제약 조건 항상 만족)
- Isolation; 격리성: 동시에 실행되는 트랜잭션들은 서로 영향을 미치지 않는다. (ex: 동시에 같은 데이터 수정 X)
- Durability; 지속성: 트랜잭션이 성공적으로 끝나면 그 결과는 항상 기록되어야 한다.
여기서 조금 더 신경써야하는 포인트는 격리성이다. 격리성을 완벽하게 보장하려면 트랜잭션을 순서를 정해 하나씩 실행해야 한다. 하지만 이런 경우 성능이 너무 나빠지기 때문에 트랜잭션 격리 수준이 존재한다.
🔻 트랜잭션 격리 수준; Isolation Level
- READ UNCOMMITED: 커밋되지 않은 데이터 읽기
- READ COMMITED: 커밋된 데이터만 읽기
- REPEATABLE READ: 한 트랜잭션 내에서 같은 읽기 보장
- SERIALIZABLE: 팬텀 리드 현상 개선
해당 개념은 데이터베이스에 포함된 개념이라 이 글에서는 자세한 설명을 생략한다.
참고할만한 링크: geeks for geeks, english wikipedia
😋 트랜잭션 관리의 종류
💡 비즈니스 로직과 트랜잭션 코드의 분리
비즈니스 로직과 트랜잭션 처리 로직이 동시에 존재한다면?
코드의 중복이 생길 수 있고 비즈니스 로직에만 집중하기 어렵다.
비즈니스 로직에만 집중하기 위해 Spring은 크게 2가지 트랜잭션 기술을 지원해준다.
1. TransactionTemplate
데이터 접근 기술에는 JdbcTemplate, JPA 등 여러가지가 존재한다. 이 기술이 바뀌면 트랜잭션을 사용하는 방법도 달라진다. Spring에서는 이러한 상황을 고려해 트랜잭션에 대한 추상화를 지원한다. PlatformTransactionManager 인터페이스를 이용하면 된다. 해당 인터페이스는 총 3가지 메서드를 제공한다.
- getTransaction(): 현재 TransactionStatus를 return
- commit(): 변경 내역 커밋
- rollback(): 변경 내역 롤백
TransactionTemplate은 위의 Transaction Manager를 주입 받아 사용한다. 해당 클래스를 이용해 개발자가 트랜잭션 시작/종료 지점을 명시적으로 결정할 수 있게 된다.
2. @Transactional 어노테이션
또는 @Transactional 어노테이션을 지원해준다. 우리는 DB와 관련된, 트랜잭션이 필요한 클래스 혹은 메서드에 @Transactional 어노테이션을 달기만 하면 된다.
(+참고로 클래스에 다는 경우, 해당 클래스 및 하위 클래스까지 적용된다.)
🔻 @Transactional의 우선 순위 (클래스 vs 메서드)
@Transactional은 클래스, 메서드 모두에 적용 가능하다.
이 경우, 메서드 레벨의 @Transactional 선언을 우선 적용한다.
Spring은 기본적으로 더 좁은 범위를 우선시 해준다는 것을 기억해두면 좋다.
💥 프로그래밍적 트랜잭션 관리 vs 선언적 트랜잭션 관리
- 프로그래밍 트랜잭션 관리; Programmactic Transaction Management
- ex: TransactionManager, TransactionTemplate
- 트랜잭션 관련 코드를 직접 작성하는 방법
- 선언적 트랜잭션 관리; Declarative Transaction Management
- ex: @Transactional
- 과거엔 xml을 이용해 설정하기도 함
- 프로그래밍 트랜잭션 관리에 비해 간편
😲 @Transactional이란?
🔎 AOP; Aspect-Oriented Programming
애플리케이션 기능은 크게 핵심 기능과 부가 기능으로 나눌 수 있다.
- 핵심 기능: 해당 객체가 제공하는 고유한 기능 (ex: CustomerService - 구매 로직)
- 부가 기능: 핵심 기능을 보조하는 기능 (ex: 로그 추적, 트랜잭션 처리 등)
하나의 부가 기능은 여러 곳에서 동일하게 사용되는 경우가 많다. sql 호출 실패하면 rollback, 성공하면 commit.. 반복적이고 지루한 작업들! 똑같은 로직을 여러 곳에 적용하기란 너무 번거롭다. 부가 기능에 수정 사항 발생하면 이 로직들을 죄다 수정해야할 수도 있다. 그렇다면 어떻게 부가 기능을 한번에 관리할 수 있을까? 부가 기능을 분리하고 이 기능을 어디에 적용할지 선택하는 기능을 합쳐 하나의 모듈로 만든 것이 Aspect다.
우리 말로는 '관점'이라는 뜻인데 애플리케이션을 바라볼 때 핵심 기능만 바라볼 수 있도록 도와준다.
@Transactional은 트랜잭션 AOP를 위해 Spring에서 지원해주는 어노테이션이다.
📚 @Transactional 예제
userDao의 loseMoney 또는 sellerDao의 gainMoney 둘 중 하나라도 실패하면 전체 작업을 취소한다.
모든 작업이 성공할 경우 DB에 해당 변경 내역이 반영된다.
➕ CheckedException이 발생하는 경우는 트랜잭션이 롤백되지 않는다. (참고 링크: SLiPP)
@Transactional
public void buy(Long money) {
userDao.loseMoney(money);
sellerDao.gainMoney(money);
}
💻 @Transactional의 동작 구조
스프링의 트랜잭션 AOP는 @Transactional을 인식하여 트랜잭션 프록시를 적용한다.
프록시는 '대리인'이라는 뜻인데 Aspect와 @Transactional을 적용한 클래스 or 메서드인 Target을 연결해주는 역할을 한다.
- Client가 API 호출
- 프록시 실행
- 트랜잭션 코드 실행
- 비즈니스 로직 실행
- 트랜잭션 코드 실행 (commit / rollback)
🔻 코드로 확인해보기
@Transactional 어노테이션이 달린 메서드에 브레이크 포인트를 잡고 디버깅을 시작했다.
시작하자마 Proxy 관련 클래스로 이동한 것을 확인했다.
코드가 너무 세세하고 복잡해서 하나하나 살피기는 힘들었다.
트랜잭션 코드 -> 비즈니스 로직 -> 트랜잭션 코드 순서대로 잘 동작되는지 정도만 확인해보겠다.
트랜잭션 코드를 다루는 PlatformTransactionManager의 모든 메서드와 비즈니스 로직의 일부가 있는 dao 메서드에 브레이크 포인트를 잡아보았다.
가장 먼저 호출되는 것은 AbstractPlatformTransactionManager의 getTransaction()이었다.
그 다음은 비즈니스 로직에 따라 dao의 findAll()이 호출되었고
마지막으로 TransactionAspectSupport의 commitTransactionAfterReturning()을 통해 AbstractPlatformTransactionManager의 commit()이 호출되었다.
🤔 테스트와 @Transactional
테스트 환경에서는 @Transactional이 약간 다르게 동작한다.
성공/실패 결과와는 다르게 테스트 메서드가 종료되면 무조건 롤백된다.
TestTransaction
: @Transactional이 적용된 테스트 메서드의 트랜잭션을 확인할 수 있는 클래스
(테스트가 아닌 메인에서 확인하고 싶다면 TransactionSynchronizationManager 클래스 활용)
TestTransaction.isActive(); // 동작 중인 트랜잭션이 있는지
TestTransaction.isFlaggedForRollback(); // rollback flag
참고
- 테코블 - @Transactional
- spring docs - @Transactional
- inflearn - 김영한님 스프링 DB 1편