상세 컨텐츠

본문 제목

@Transactional 파헤치기

Log.Develop/SpringBoot

by bluayer 2022. 2. 15. 15:20

본문

아 일단 트랜잭션이 뭔데?

데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다. - wikipedia

 

DBMS는 각각의 트랜잭션에 대해 ACID를 보장한다.

간단하게 읽어보자.

  • 원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 즉, 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다
  • 일관성(Consistency)은 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 의미한다.
  • 독립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 공식적으로 고립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다. 성능관련 이유로 인해 이 특성은 가장 유연성 있는 제약 조건이다.
  • 지속성(Durability)은 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다. 시스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 함을 의미한다. 전형적으로 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.

From wikipedia

트랜잭션은 일반적으로 다음과 같은 순서로 실행된다.

  • 트랜잭션 시작(Begin)
  • 트랜잭션 실행 (Execute, but not applied)
  • 트랜잭션 커밋(Commit, applied!)

중간에 쿼리 하나가 실패한다면, 전체 트랜잭션 혹은 실패한 쿼리를 롤백한다.(설정하기 나름이긴 하다)

대부분의 major한 RDBMS에서는 SAVEPOINT 를 이용해서 오류 복구 처리를 복잡하게 실행할 수 있게 지원한다.

(이를 이용해서 특정 지점으로 롤백해서 재실행하기도 한다.)

 

스프링에서 트랜잭션 처리

스프링에서는 트랜잭션 처리를 보통 2가지 방법으로 지원한다.

  • XML 혹은 프로그래밍 트랜잭션 처리
  • 어노테이션 트랜잭션 처리

 

XML 혹은 프로그래밍 트랜잭션 처리

선언적 트랜잭션 처리를 이용하면 컨테이너가 자동으로 트랜잭션을 처리할 수 있다고 한다.

xml 내에 세팅함으로써 특정 메소드들에 트랜잭션을 걸 수 있는 방법이다.

트랜잭션 관련 설정을 할 때 스프링에서 지원하는 다양한 트랜잭션 관리자 중 하나를 선택하게 된다.

이 트랜잭션 관리자"들"은 같은 인터페이스를 구현한 클래스이다.

 

PlatformTransactionManager의 메소드 요약. 공식 문서에서 가져왔다.

 

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 개념을 이용하여 처리한다.

즉, 포인트컷(어느 메소드에 실행되어야 할지) + 어드바이스(어떻게 처리해야 할지) 조합을 이용해 설정하면 끝이 난다.

해당 문서를 보면 조금 더 이해가 쉽다!

 

https://velog.io/@ehdrms2034/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%B2%98%EB%A6%AC

 

스프링 MVC - 트랜잭션 처리

본 글은 스프링 MVC에 대해 지식을 정리하고 나중에 헷갈릴 때 다시 보기 위한 글입니다 👀 > 본 게시글은 Spring MVC Quick Start를 참조하여 정리한 글입니다. 📖 👀 > 본 게시글은 Spring MVC Documentation

velog.io

 

프로그래밍으로 관리하는 방법은 TransactionTemplate을 쓴다거나, PlatformTransactionManager을 직접 코드로 구현해서 하는 방법인데, 생각보다 되게 귀찮다.

솔직히 이렇게까지 복잡하게 하고 싶지는 않다😅

@Transactional의 중요성을 한 번 깨닫고 가는 의미에서 보는 정도로 하자.

 

어노테이션 트랜잭션 처리

원래는 Spring에서

  • Configuration에 @EnableTransactionManagement 를 붙이고
  • Spring Configuration에서 트랜잭션 매니저를 지정한다.

 

의 과정이 필요하지만, 스프링 부트는 알아서 자동으로 해준다.

(트랜잭션 매니저와 관련해서, 덧붙이자면 일반 JDBC를 쓰거나 하이버네이트를 쓰거나 JPA를 쓰는 경우에 구현체가 각기 다르다. 하지만 결론적으로는 모두 JDBC를 쓰기 때문에 사실상 트랜잭션 실행은 모두 JDBC 방식으로 실행된다. 만약 JPA를 쓰다가 트랜잭션 관리 부분이 궁금하다면, 하이버네이트에서 어떻게 트랜잭션을 관리하는지 좀 더 살펴보도록 하자.)

아무튼 이 과정을 거치면 스프링은 @Transactional 어노테이션이 달린 public 메소드에 대해 내부적으로 데이터베이스 트랜잭션 코드를 실행해준다. (스프링 프록시는 프록시를 통해 들어오는 외부 메서드의 호출에서만 가로챌 수 있다. public 메소드라는 것에 유의하자. private 메소드에서도 처리하고 싶다면 AspectJ와 같은 AOP 도구를 이용해야 한다.)

대략적으로 이런 코드가 삽입된다.

  • 커넥션을 가져옴
  • 트랜잭션 시작(tx.begin())
  • 메소드 끝나면 커밋(tx.commit())
  • 예외 발생시 rollback(tx.rollback())

 

아니 잠깐만, 이런 코드들은 어떻게 삽입되는데?!

컴파일할 때 뚝딱 넣어지진 않는 거 같던데?!!

그렇다.

이 과정에서 Dynamic Proxy ~~(이름은 끝내주게 멋있어보이지만 사실 별거 없는)~~를 이용한다.

 

Proxy?

여기서는 간단하게 어떤 일을 대리로 시키는 객체라는 의미로 사용하였다.

말 그대로 Wrapper.

(GoF의 Proxy Pattern이라고 생각하면 편하다.)

 

Dynamic Proxy?

Runtime에 이 프록시를 만든다는 뜻이다. 진짜 별거 없죠? 라고 할 뻔

(또한 이 프록시 빈은 IoC 컨테이너에 의해 생성됩니다.)

참고로, 동적으로 생성된 Proxy Bean은 타깃의 메소드가 호출되는 시점에 부가기능을 추가해주기도 한다.

부가기능을 미리 추가해둔 것이 아니라,

호출 시점에 동적으로 피한다고 하여 런타임 위빙(Runtime Weaving)이라고 부른다. (엄밀히 말하면 GoF의 Proxy 패턴과 아주 동일하지는 않다. 데코레이터 패턴을 덧붙였다고 생각하자)

 

권투 용어로도 쓰이는 그 위빙 맞다.

 

위에서 진짜 별거 없다고 하지만, 실제로 내부 구현을 까보면 쉽지 않다.

내부에서는 프록시 객체가 JDK Dynamic ProxyCGLib 방식에 따라 생성되는데....

솔직히 그냥 넘어가고 싶은데 이거 무조건 설명해야 할 필요성을 느꼈다.

참고로 이 글이 큰 도움이 되었다.

(개인적으로 해당 글을 꼭 읽어보는 것을 추천한다. 꼭!)

https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

 

JDK Dynamic Proxy와 CGLIB의 차이점은 무엇일까?

Moon

gmoon92.github.io

 

스프링 공식 문서에서 발췌한 AOP Proxy에 관한 내용.

 

글을 요약하자면,

  • JDK Dynamic Proxy : 인터페이스 없으면 프록시 생성 안 된다. 즉, 인터페이스 타입으로 DI 받아줘야 한다. Spring은 JDK Dynamic Proxy 방식이 default다.
  • CGLib : 타겟 클래스 상속 받아서 Proxy 생성한다. 인터페이스 없어도 된다! 심지어 JDK Dynamic Proxy보다 성능도 좋잖아?! 아 근데 한계가 있네.. 디폴트 생성자가 필요하고, 타깃 생성자가 2번이나 호출되고, 별도의 의존성도 필요해ㅠㅠ
  • 잠만, 나 스프링 부트 쓸 때 구체 클래스로 했는데?! 왜 스프링부트는 CGLib처럼 되는 거 같지?!
  • Spring 3.2부터는 CGLib이 Spring Core에 들어와서 의존성 추가하지 않아도 되고, Spring 4.x부터는 디폴트 생성자 없이도 프록시를 만들 수 있고, 생성자가 2번 호출되던 상황도 같이 개선되었다.

 

좋다. 프록시는 이정도로 다루면 충분한 것 같다. 본론으로 돌아가보자.

그러니깐 @Transactional을 이용하는 UserService라는 객체가 있다고 치자.

  • @Transactional 발견 → 다이나믹 프록시 생성 (이 과정에서 팩토리 빈, 특히 ProxyFactoryBean를 사용한다.)
  • 관련 객체 메소드 콜 → 프록시 메소드 콜 → 프록시가 트랜잭션 매니저에게 처리 위임 → 결과 반환

이런 형식이 된다.

 

여기까지 정리하면서 깨달았다.

스프링... 너 멋진 녀석이구나...?

 

너가 하는 일이 이 정도로 복잡할지는 몰랐다... 친구야.. 코쓱

 

우리는 이제 @Transactional이 어떻게 동작하는지 "조금은" 깨달았다.

 

후기

원래는 우리가 @Transactional을 붙였을 때 쿼리가 어떻게 날아가는지에 대해서 설명하려고 했다.

하지만 생각보다 Spring이 하는 일이 많아서 깜짝 놀라버렸다.

그래도 원래의 목적을 달성하기 위해 플로우를 한 번 써보았다.

  1. 메소드 콜
  2. 프록시 메소드 콜
  3. 프록시에서 트랜잭션 매니저에게 위임
  4. HikariCP의 커넥션 풀에서 JDBC 드라이버가 커넥션을 가져옴 (참고로 HikariCP는 Spring Boot 2.0부터 default JDBC Connection Pool이 되었다.)
  5. JDBC 드라이버를 통해 쿼리 전달 및 결과 값 받아오기

 

추가적으로,

SpringBoot를 쓰다보면 @Transactional의 구현체가 두 개인것을 확인할 수 있다.

하나는 javax 패키지 내에, 하나는 org.springframework 패키지 내에 작성되어 있는데,

두 패키지의 차이는 추가적인 기능을 제공하느냐, 안 하느냐 정도의 차이다.

당연히 org.springframework 내에 있는 구현체가 추가적인 기능(propagation, isolation level 등)을 제공한다. 

관련글 더보기

댓글 영역

페이징