데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다. - wikipedia
DBMS는 각각의 트랜잭션에 대해 ACID를 보장한다.
간단하게 읽어보자.
From wikipedia
트랜잭션은 일반적으로 다음과 같은 순서로 실행된다.
중간에 쿼리 하나가 실패한다면, 전체 트랜잭션 혹은 실패한 쿼리를 롤백한다.(설정하기 나름이긴 하다)
대부분의 major한 RDBMS에서는 SAVEPOINT 를 이용해서 오류 복구 처리를 복잡하게 실행할 수 있게 지원한다.
(이를 이용해서 특정 지점으로 롤백해서 재실행하기도 한다.)
스프링에서는 트랜잭션 처리를 보통 2가지 방법으로 지원한다.
선언적 트랜잭션 처리를 이용하면 컨테이너가 자동으로 트랜잭션을 처리할 수 있다고 한다.
xml 내에 세팅함으로써 특정 메소드들에 트랜잭션을 걸 수 있는 방법이다.
트랜잭션 관련 설정을 할 때 스프링에서 지원하는 다양한 트랜잭션 관리자 중 하나를 선택하게 된다.
이 트랜잭션 관리자"들"은 같은 인터페이스를 구현한 클래스이다.
public interface PlatformTransactionManager{
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TrnasactionStatus status) throws TransactionException;
}
흥미롭게도 위에서 기술한 트랜잭션 실행에 필요한 내용이 있다.(commit, rollback)
트랜잭션 처리를 위해서 이와 같은 트랜잭션 관리자/매니저(DatasourceTransactionManager-JDBC 관련, JPATransactionManager-JPA 관련 등)를 Bean으로 등록하게 된다.
참고로 이 트랜잭션 매니저는 @Transactional에서도 중추적인 역할을 한다.
이제 어떤 메소드에, 어떻게 처리해야할지를 AOP 개념을 이용하여 처리한다.
즉, 포인트컷(어느 메소드에 실행되어야 할지) + 어드바이스(어떻게 처리해야 할지) 조합을 이용해 설정하면 끝이 난다.
해당 문서를 보면 조금 더 이해가 쉽다!
프로그래밍으로 관리하는 방법은 TransactionTemplate을 쓴다거나, PlatformTransactionManager을 직접 코드로 구현해서 하는 방법인데, 생각보다 되게 귀찮다.
솔직히 이렇게까지 복잡하게 하고 싶지는 않다😅
@Transactional의 중요성을 한 번 깨닫고 가는 의미에서 보는 정도로 하자.
원래는 Spring에서
의 과정이 필요하지만, 스프링 부트는 알아서 자동으로 해준다.
(트랜잭션 매니저와 관련해서, 덧붙이자면 일반 JDBC를 쓰거나 하이버네이트를 쓰거나 JPA를 쓰는 경우에 구현체가 각기 다르다. 하지만 결론적으로는 모두 JDBC를 쓰기 때문에 사실상 트랜잭션 실행은 모두 JDBC 방식으로 실행된다. 만약 JPA를 쓰다가 트랜잭션 관리 부분이 궁금하다면, 하이버네이트에서 어떻게 트랜잭션을 관리하는지 좀 더 살펴보도록 하자.)
아무튼 이 과정을 거치면 스프링은 @Transactional 어노테이션이 달린 public 메소드에 대해 내부적으로 데이터베이스 트랜잭션 코드를 실행해준다. (스프링 프록시는 프록시를 통해 들어오는 외부 메서드의 호출에서만 가로챌 수 있다. public 메소드라는 것에 유의하자. private 메소드에서도 처리하고 싶다면 AspectJ와 같은 AOP 도구를 이용해야 한다.)
대략적으로 이런 코드가 삽입된다.
아니 잠깐만, 이런 코드들은 어떻게 삽입되는데?!
컴파일할 때 뚝딱 넣어지진 않는 거 같던데?!!
그렇다.
이 과정에서 Dynamic Proxy ~~(이름은 끝내주게 멋있어보이지만 사실 별거 없는)~~를 이용한다.
Proxy?
여기서는 간단하게 어떤 일을 대리로 시키는 객체라는 의미로 사용하였다.
말 그대로 Wrapper.
(GoF의 Proxy Pattern이라고 생각하면 편하다.)
Runtime에 이 프록시를 만든다는 뜻이다. 진짜 별거 없죠? 라고 할 뻔
(또한 이 프록시 빈은 IoC 컨테이너에 의해 생성됩니다.)
참고로, 동적으로 생성된 Proxy Bean은 타깃의 메소드가 호출되는 시점에 부가기능을 추가해주기도 한다.
부가기능을 미리 추가해둔 것이 아니라,
호출 시점에 동적으로 피한다고 하여 런타임 위빙(Runtime Weaving)이라고 부른다. (엄밀히 말하면 GoF의 Proxy 패턴과 아주 동일하지는 않다. 데코레이터 패턴을 덧붙였다고 생각하자)
위에서 진짜 별거 없다고 하지만, 실제로 내부 구현을 까보면 쉽지 않다.
내부에서는 프록시 객체가 JDK Dynamic Proxy와 CGLib 방식에 따라 생성되는데....
솔직히 그냥 넘어가고 싶은데 이거 무조건 설명해야 할 필요성을 느꼈다.
참고로 이 글이 큰 도움이 되었다.
(개인적으로 해당 글을 꼭 읽어보는 것을 추천한다. 꼭!)
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
글을 요약하자면,
좋다. 프록시는 이정도로 다루면 충분한 것 같다. 본론으로 돌아가보자.
그러니깐 @Transactional을 이용하는 UserService라는 객체가 있다고 치자.
이런 형식이 된다.
여기까지 정리하면서 깨달았다.
스프링... 너 멋진 녀석이구나...?
우리는 이제 @Transactional이 어떻게 동작하는지 "조금은" 깨달았다.
원래는 우리가 @Transactional을 붙였을 때 쿼리가 어떻게 날아가는지에 대해서 설명하려고 했다.
하지만 생각보다 Spring이 하는 일이 많아서 깜짝 놀라버렸다.
그래도 원래의 목적을 달성하기 위해 플로우를 한 번 써보았다.
SpringBoot를 쓰다보면 @Transactional의 구현체가 두 개인것을 확인할 수 있다.
하나는 javax 패키지 내에, 하나는 org.springframework 패키지 내에 작성되어 있는데,
두 패키지의 차이는 추가적인 기능을 제공하느냐, 안 하느냐 정도의 차이다.
당연히 org.springframework 내에 있는 구현체가 추가적인 기능(propagation, isolation level 등)을 제공한다.
Jib란? (+ M1 Mac에서 빌드하기 & Kotlin DSL) (2) | 2021.09.06 |
---|---|
[Gradle] implementation vs compile (4) | 2020.02.25 |
댓글 영역