스프링과 데이터베이스[2] - 트랜잭션 사용
들어가기에 앞서
데이터베이스를 다루면서 트랜잭션은 절대적으로 놓쳐서는 안되는 중요한 요소이다. 개발자가 직접 트랜잭션 코드를 모두 작성해서 사용하면 반복되는 코드도 많아지고 자칫하면 실수가 발생할 수도 있다. 지금의 스프링은 트랜잭션을 아주 간단하고 편하게 사용할 수 있도록 설정되어있고 개발자는 간단하게 사용하기만 하면 되는 환경이 되었다. 여기서는 트랜잭션을 직접 사용할려면 어떻게 해야하는지와 스프링이 이러한 불편함을 어떻게 해결하고 지금은 얼마나 편해졌는지를 알아보도록 하자.
트랜잭션의 개념
트랜잭션은 어떤 로직의 수행이 모두 이루어지거나 모두 이루어지지 않도록 해주는 것이다. 특정 로직이 수행되었는데 중간에 에러가 발생해서 종료되더라도 수행된 부분까지는 데이터를 저장하는 것이 아니라 해당 로직이 실행되기 전의 상태로 돌아가는 것이다.
가장 많이 드는 예시로 계좌이체가 있다.
A가 B한테 돈을 1000원 보내야 할 경우 간단하게 다음과 같은 과정들이 이루어 질 것이다.
1. A계좌에서 1000원이 줄어든다.
2. B계좌에서 1000원이 추가된다.
위와 같은 상황에서 만약에 1번이 이루어지고 에러가 발생하면 A는 1000원이 없어졌는데 B는 1000원을 받지못한 끔찍한 일이 일어나게 된다. 이러한 경우는 트랜잭션이 적용되지 않은 경우이다. 1번과 2번은 하나의 로직에서 수행되는데 로직이 모두 수행되거나, 모두 수행되지 않거나도 아닌 중간까지만 수행되었기 때문이다.
트랜잭션을 사용하면 중간까지 진행한 상태에서 에러가 발생하더라도 중간까지 진행한 상황을 다시 돌려줌으로서 로직이 모두 수행되지 않은 상태로 되돌릴 수 있다. 이렇게 다시 되돌리는 작업을 롤백(rollback)이라고 한다.
만약 중간에 어떤 에러도 발생하지 않아서 로직이 끝까지 성공했다면 마지막에 커밋(commit)을 수행함으로서 모든 로직을 성공시키고 트랜잭션을 종료한다.
이렇게 트랜잭션은 커밋과 롤백으로 로직의 수행을 관리하며 데이터를 보호하는 역할을 하고있다.
트랜잭션 사용
데이터베이스는 기본적으로 자동 커밋이 디폴트로 설정되어있다. 그렇기 때문에 우리가 데이터베이스에 쿼리를 수행하면 바로 실행이 되는 것이다. 이것을 수동커밋으로 변경해주면 트랜잭션이 시작된다.
트랜잭션을 수동으로 변경하는 명령어는 DB마다 다르다.
start transaction; // mysql
set autocommit false; // h2
...
트랜잭션은 하나의 커넥션에만 적용할 수 있다. 커넥션이 여려개면 트랜잭션도 여러개가 생성되기 때문에 이 점을 주의해야한다.
우리가 직접 트랜잭션을 관리하고 싶을 때는 주의해야 할 점이 있다. 스프링은 커넥션을 커넥션풀을 동작시켜서 관리하고 있다. 우리가 사용한 커넥션을 수동 커밋으로 변경한 뒤 그대로 커넥션을 반환하면 커넥션 풀에는 수동 커밋으로 설정된 커넥션이 존재하게 된다. 만약 이후에 이 커넥션을 다른 로직에서 사용한다면 분명히 문제가 발생할 수도 있을 것이다. 그렇기 때문에 트랜잭션이 종료되면 해당 커넥션을 다시 자동 커밋으로 변경해주어야 한다. 코드를 보면 아래와 같이 되어야 한다.
val con = dataSource.connection
try {
con.autoCommit = false
비지니스 로직 실행
con.commit()
} catch (e: Exception) {
con.rollback()
} finally {
con.autoCommit = true
con.close()
}
하지만 이 모든것도 결국 스프링이 자동으로 처리해준다.
스프링이 관리하는 트랜잭션 매니저
현재 자바단에서 사용하는 데이터 엑세스 기술은 정말 다양하다. JDBC, MyBatis, Hibernate, JPA등 다양한 기술들이 있다. 그런데 각 기술들마다 트랜잭션 실행, 커밋, 롤백의 방법이 다양하다. 그렇다면 사용하는 기술이 바뀔 때마다 매번 트랜잭션과 관련된 코드를 바꿔줘야 하는데 굉장히 광범위하게 퍼져있는 코드들을 전부 수정할 수는 없을 것이다. 이러한 문제를 해결하기 위해 트랜잭션과 관련된 동작들을 추상화 시켰는데 그것이 바로 트랜잭션 매니저 이다.
스프링이 PlatformTransactionManager이라는 인터페이스를 중심으로 다양한 종류의 기술들에 맞는 구현체를 만들어 놨다. PlatformTransactionManager에는 3개의 메소드가 있다.
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) // 트랜잭션 상태를 가져옴
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException; // 트랜잭션 커밋
void rollback(TransactionStatus status) throws TransactionException; // 트랜잭션 롤백
getTransaction을 실행하면 TransactionStatus을 가져오게 된다. 이것이 왜 필요할까?
트랜잭션은 하나의 커넥션에서만 이루어질 수 있다. 코드로 트랜잭션을 선언했더라도 그 안에 2개의 커넥션이 있다면 트랜잭션이 제대로 이루어질 수가 없다. 그렇다면 처음 실행한 하나의 커넥션을 데이터가 필요할 때 마다 파라미터로 보내서 사용해야 하는데 이는 너무 번거롭고 불필요한 코드가 생성된다. 이러한 상황을 위해서 스프링은 트랜잭션 동기화라는 작업을 진행한다.
실행된 커넥션을 쓰레드 로컬에 저장함으로서 다른곳에서 로직실행을 위해서 객체의 위치들이 변하더라도 같은 커넥션을 사용함으로서 트랜잭션을 유지하고 사용할 수 있게 되는 것이다.
모든 작업이 끝나고 커밋과 롤백을 해야할 때 처음 getTransaction으로 가져온 TransactionStatus를 파라미터로 넣어줌으로서 해당 트랜잭션만 커밋하고 롤백할 수 있도록 동작시키는 것이다.
그래서 위의 코드에서 commit과 rollback을 살펴보면 파라미터에서 TransactionStatus를 받고있는 것을 알 수 있다.
스프링은 이런방식으로 트랜잭션을 동기화 시켜서 여러 하나의 커넥션을 이용한 트랜잭션 관리를 하고있다.
스프링 트랜잭션의 최종형태 - @Transactional
개발자가 직접 다뤄주어야 하는 많은 코드들을 제거하고 아주 간단하게 어노테이션 하나로 모두 처리할 수 있도록 만든 것이 선언적 트랜잭션 @Transactional이다.
먼저 사용방법을 알아보면 아래의 코드와 같다.
@Transactional
fun transactionTest() {
비즈니스 코드
}
이렇게 아주 단순한 방법으로 사용할 수 있다. 기존에 사용하던 커넥션을 직접 종료시킬 필요도 없고, 자동커밋 상태를 직접 변경할 필요도 없고, try - catch도 사라졌다. 데이터베이스와 관련된 그 어떠한 코드도 개발자가 직접 작성하지 않고 오로지 스프링이 전부 맡아서 진행해준다. 개발자는 단지 트랜잭션을 선언하고자 하는 함수 위에 @Transactional만 선언해주면 된다. 물론 클래스 단위로도 설정할 수 있다.
@Transactional이 선언된 함수가 동작하는 원리는 바로 AOP이다.
애플리케이션이 실행되면 스프링은 전체적으로 초기화 작업을 진행할 것이다. 빈을 만들거나 DI를 진행하는 등의 작업이다. 그때 @Transactional이 선언된 클래스를 찾아서 프록시 객체를 만들게 된다. 프록시는 '대신한다' 라는 의미를 가지고 있어서 기존의 객체를 대신해서 동작하도록 설계되어있다.
프록시 객체는 기존의 객체를 상속해서 기존 객체에 있는 기능을 사용할 수 있으면서 스프링이 원하는 코드를 추가적으로 가지고 있게 된다. 스프링은 정말 많은 종류의 프록시 객체를 생성하는데 @Transactional이 붙은 프록시는 아마도 아래와 같을 것이다.
fun transactionTest() {
--- 트랜잭션 시작
--- 비즈니스 코드
--- 트랜잭션 종료
return
}
정말 말 그대로 트랜잭션을 만들어주는 것이다. 아주아주 간단하게 표현해서 이런 모양이지만 아마도 정말 많은 코드들이 들어있을 것이다. 실제로 @Transactional이 선언된 클래스의 로직을 디버깅해서 살펴보면 아래 사진과 같은 순서로 동작하게 된다.
사진을 보면 먼저 프록시 객체가 실행되고 그 위에 프록시 객체가 호출한 진짜 객체가 실행되고 있는 것을 볼 수 있다.
프록시 객체에서 스프링이 만든 트랜잭션이 실행되고 실제 객체를 호출해서 우리가 동작시키고자하는 비지니스 로직이 실행되는 것이다. 자세히 보면 프록시 객체의 이름이 MeberServiceV3$$FastClassBySpringCGLIB$$... 으로 나타나는 것을 볼 수 있는데 이 이름에서 중요한 부분은 CGLIB이다. 스프링은 버전마다 프록시를 만드는 방법들이 조금씩 바뀌었는데 지금은 최종적으로 CGLIB로 만들어지고 있다. 고로 CGLIB가 붙은 클래스는 스프링이 만든 프록시 객체라고 생각하면 된다.
개발자가 직접 관리하던 트랜잭션을 스프링이 제공하는 선언적 트랜잭션으로 아주 편하게 사용할 수 있게 되었다.
@Transactional이 가지고 있는 설정들도 정말 다양하게 있다. 특별한 경우가 아니라면 디폴트 설정만으로도 원하는 트랜잭션 효과를 볼 수 있겠지만 상황에 따라서는 직접 설정을 해주어야 하는 경우도 있을 것이다. @Transactional과 연관되서 같이 사용할 수 있는 @TransactionalEventListener도 있는데 트랜잭션 내에서 이벤트를 발생시키는 코드가 있을 때 함께 사용하면 유용하다.
'Spring > Core' 카테고리의 다른 글
@Transactional 어노테이션 알아보기[1] (0) | 2023.02.18 |
---|---|
스프링과 데이터베이스[1] - JDBC와 커넥션 풀 (0) | 2022.11.08 |
스프링 주입의 다양한 방법들 (0) | 2022.06.13 |
스프링 빈 등록의 다양한 방법들 (0) | 2022.06.09 |
[3/3] Spring과 싱글톤 패턴 - @Configuration의 비밀 (0) | 2022.06.07 |
댓글
이 글 공유하기
다른 글
-
@Transactional 어노테이션 알아보기[1]
@Transactional 어노테이션 알아보기[1]
2023.02.18 -
스프링과 데이터베이스[1] - JDBC와 커넥션 풀
스프링과 데이터베이스[1] - JDBC와 커넥션 풀
2022.11.08 -
스프링 주입의 다양한 방법들
스프링 주입의 다양한 방법들
2022.06.13 -
스프링 빈 등록의 다양한 방법들
스프링 빈 등록의 다양한 방법들
2022.06.09