본문 바로가기
Spring

트랜잭션의 역사

코동이 2022. 9. 4.

개요


스프링에서 당연하게 사용해오는 기술인 @Transactional의 역사를 알아보겠습니다. 스프링에서 트랜잭션의 사용 방법 및 스프링의 추상화 전략을 살펴보면서 개발자가 핵심 개발에만 집중 할 수 있게 된 배경을 알아보겠습니다..

 

본 글은 김영한 님의 강의를 바탕으로 작성했습니다

 

 

데이터베이스 세션


데이터베이스에서 커넥션을 얻기 위해서 데이터베이스 서버는 내부에서 세션을 이용합니다. 세션은 트랜잭션을 시작하고 커밋 또는 롤백을 통해서 트랜잭션을 종료합니다. 즉, 개발자가 SQL을 전달하면, 커넥션에 연결되어 있는 세션이 SQL을 실행합니다.

 

 

 

트랜잭션이 없는 롤백


 롤백은 취소, 복구와도 같은 의미로 롤백이 발생하는 원인은 다양하며, 보통 예외가 발생했을 때 롤백 처리를 합니다.

 

 비지니스 로직을 짜면서 데이터베이스에서 중요한 것은, 롤백 범위를 적절하게 선택하고 롤백 유무를 정하는 것입니다.

 

 트랜잭션은 데이터베이스의 작업 단위로, 롤백 시 같은 트랜잭션 범위 내에 있는 작업을 일괄 취소(복구)시킬 수 있습니다.(물론 예외 처리 방식에 따라 달라질 수 있습니다.) 그렇다면 트랜잭션 없이 중간에 롤백이 되면 같은 단위로 묶인 기존 작업들은 어떻게 될까요? 당연히 롤백되지 않으며, 롤백이 발생하는 시점 이전까지의 작업은 정상적으로 반영이 됩니다.

 

트랜잭션이 없는 롤백으 예시는 다음과 같습니다.

 

예를 들어, 사용자 1, 사용자 2가 각각 10000원을 가지고 있다고 가정합니다.

사용자 1이 사용자 2에게 2000원을 보냈습니다. 그렇다면 사용자 1은 8000원, 사용자 2는 12000원이 되어야 합니다.

그런데, 사용자 1이 2000원을 보낸 직후, 사용자 2가 2000원을 받기 바로 직전에 전산에서 오류가 난다면?

일반적인 상식으로, 

전산 오류는 DB관점에서 작업 취소(롤백)로 볼 수 있으며, 사용자1은 10000원으로 복구되어야 합니다.

하지만 사용자 1은 2000원이 빠져나가 8000원, 사용자 2는 10000원인 상태에서 송금 작업이 종료됩니다.

 

 

송금 과정이 트랜잭션으로 묶여 있지 않기 때문에, 롤백이 발생해도 모든 작업들이 복구되지는 않습니다.

 

작업 범위에 트랜잭션을 선언 할 수 있으며, 이를 통해 적절하게 복구 전략을 세울 수 있습니다.

 

 

코드를 통해서 오류 과정을 살펴보겠습니다.

 

송금하는 accountForTransfer() 메서드는 아래와 같습니다. MemberServiceV1에는 아무런 @Transactional이 없습니다. 따라서 2개의 memberRepository.update()는 개별적인 @Transactional이 설정되어 있고 예외가 발생하더라도 다른 작업들을 같이 복구시키지 않습니다.

 

 

 

의도적으로 memberService.accountTransfer()함수에서 예외를 발생시켜 보겠습니다. 사용자 A가 2000원을 송금하고 나서, 예외를 발생시키면 사용자 1과 사용자 2의 최종 금액은 어떻게 될까요?

 

아래의 코드 72,73줄을 보면, 사용자 A는 2000원이 차감되어 8000원, 사용자 B는 10000원 그대로입니다.

 

 

이 테스트가 성공한 이유는, 사용자 A가 송금하고나서 예외가 발생했는데 트랜잭션이 없기 때문에, 사용자 A의 2000원 송금이 복구되지 않습니다. 또한 사용자 B의 2000원 증가는 실행되지도 않습니다. 이것이 DB 작업을 할 때, 트랜잭션을 설정해야 하는 이유입니다.

 

 

따라서, 롤백 시, 모든 작업을 다시 복구하기 위해서 트랜잭션을 사용해 개선해보겠습니다.

 

 

Connection으로 트랜잭션의 적용


Connection을 통한 트랜잭션 사용법을 알아보겠습니다.

 

트랜잭션의 범위는 어디에서 설정을 해야 할까요?

 

비지니스 로직이 잘못되면, 일괄적으로 비지니스 로직을 모두 롤백 시켜야 하기 때문에 서비스 계층에 트랜잭션을 사용합니다.

 

 

 

 비지니스 로직에서 롤백이 된다면 같은 트랜잭션 범위에 있는 비지니스 작업이 복구 될 수 있으므로, [3. 비지니스 로직 계좌이체]를 같은 트랜잭션 범위로 설정해야 합니다.

 

 DB 트랜잭션을 사용하기 위해서, 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야하고, 그래야 같은 세션을 이용할 수 있습니다. 같은 커넥션을 유지하기 위해서, 커넥션을 생성한 이후 각 비지니스 로직마다 커넥션을 매개변수로 넘겨주도록 합니다.

 

 

 Connection의 con.commit()과 con.rollback()의 명령어로 트랜잭션의 시작, 완료, 복구를 설정할 수 있습니다.

 

 bizLogic()은 비지니스 로직을 따로 메서드로 정의한 것입니다. 아래 코드에서, 매개변수의 Connection con이 각 저장소의 update() 쿼리문에 모두 전달 되었습니다. 이를 통해, 비지니스 로직의 작업은 같은 커넥션을 사용합니다. 이 방식은 송금 중간에 오류가 발생하더라도, con.rollback()으로 bizLogic() 내부의 모든 작업을 롤백하기 때문에 사용자 1과 사용자 2 모두 10000원의 금액을 가지게 됩니다.

 

 

 

 하지만, 다음과 같은 문제점이 있습니다.

  • 같은 트랜잭션을 위해서 Connection을 매개변수로 이용해야 한다
  • 자원의 할당과 해체 모두 비지니스로직에서 반복적으로 사용된다.
  • Connection을 사용한다는 것 자체가 Jdbc 기술에 의존하고 있다.

 

서비스 계층은 특정 기술에 종속되지 않아야 합니다.

 

 

스프링에서 트랜잭션 사용을 통해 이 문제를 개선해보겠습니다.

 

 

 

PlatformTransactionManager로 트랜잭션 사용


기존의 Jdbc 트랜잭션을 사용하다가 JPA로 스펙이 변경되면 코드를 얼마나 고쳐야 할까요? 아래 코드를 비교해보겠습니다.

 

//Jdbc 기술
public void accountTransfer(String fromId, String toId, int money) throws
SQLException {
 Connection con = dataSource.getConnection();
 try {
 	con.setAutoCommit(false); //트랜잭션 시작
 	//비즈니스 로직
 	bizLogic(con, fromId, toId, money);
 	con.commit(); //성공시 커밋
 } catch (Exception e) {
 	con.rollback(); //실패시 롤백
 	throw new IllegalStateException(e);
 } finally {
  	release(con);
 }
}

//Jpa 기술
public static void main(String[] args) {
 	//엔티티 매니저 팩토리 생성
 	EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
 	EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
 	EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득
 try {
 	tx.begin(); //트랜잭션 시작
 	logic(em); //비즈니스 로직
 	tx.commit();//트랜잭션 커밋
 } catch (Exception e) {
 	tx.rollback(); //트랜잭션 롤백
 } finally {
 	em.close(); //엔티티 매니저 종료
 }
 	emf.close(); //엔티티 매니저 팩토리 종료
}

 

 문제점은, 각각 기술에 의존성이 있어 Jdbc의 경우 Connection을, Jpa의 경우 EntityTransaction을 통해서 각각 commit()과 rollback()을 수행한다는 것입니다. 이 기술 뿐 아니라 다른 구현체로 변경을 위해 또다시 해당 기술에 의존성을 가져야 합니다.

 

 스프링에서는 이 문제를 해결하기 위해 추상화 기술을 사용합니다. 한 곳의 기술이 변경되거나 코드가 변경될 때, 다른 곳에서 동시다발적으로 변경이 발생하는 것을 방지합니다. 실제 동작은 추상화된 인터페이스를 구현하는 구현체에 따라 달라지지만, 구현체가 바뀐다고 해서 코드 자체가 바뀌지는 않습니다.

 

추상화를 통해 코드의 변경작업을 줄인다

 

 PlatformTransactionManger 라는 추상화를 통해서, 별도의 코드 변경 없이, 내가 사용하는 트랜잭션 관리의 구현체를 등록하면 됩니다.

 

PlatformTransactionManager 인터페이스의 함수는 아래와 같습니다. getTransaction()으로  트랜잭션을 획득 할 수 있습니다.

 

package org.springframework.transaction;

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

 

 

 아래 사진에서, 스프링에서 제공하는 기술로 트랜잭션 매니저와 트랜잭션 동기화 매니저가 있습니다. 트랜잭션 매니저 내부에 트랜잭션 동기화 매니저가 있으며, 트랜잭션 매니저를 트랜잭션의 관리를, 트랜잭션 동기화 매니저는 데이터 소스에서 얻은 커넥션을 저장하는 역할을 합니다.

 

트랜잭션 동기화 매니저는 쓰레드 로컬(Thread Local)을 사용하기 때문에, 멀티 쓰레드 환경에서 안전하게 동기화가 가능합니다.

 

트랜잭션 매니저와 트랜잭션 동기화 매니저

 

 트랜잭션의 동작 방식은 간단히 다음과 같습니다.

 

1. 트랜잭션을 사용하기 위해서 먼저 커넥션이 필요하므로, 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만듭니다. 이 커넥션을 트랜잭션 동기화 매니저에 저장하고 트랜잭션을 시작합니다.

2. 리포지토리는 트랜잭션 동기화 매니저에 있는 커넥션을 꺼내서 사용합니다. 따라서 더이상 파라미터로 커넥션을 넘기지 않아도 됩니다.

3. 트랜잭션이 모두 끝나면, 트랜잭션 매니저는 트랜잭션 동기화 매니저에  보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫습니다.

 

 

코드로 예시를 확인하겠습니다.

 

아래는 레포지토리 클래스에서 커넥션을 연결하고 해제하는 코드입니다.

 

 

DataSourceUtils.getConnection(dataSource)를 사용해, 트랜잭션 동기화 매니저에 있는 커넥션을 사용할 수 있습니다.

또한 DataSourceUtils.releaseConnection(con, dataSource)를 사용해, 커넥션을 해제하는 것에 주의해야 합니다. 한 트랜잭션 단위 안에 여러개의 커넥션 호출 및 해제가 있다면, 트랜잭션을 유지하기 위해서 커넥션 또한 종료되지 않고 유지되어야 합니다.

 

따라서, jdbcUtils.closeConnection(con)으로 바로 커넥션을 닫는 것이 아니라, 남은 트랜잭션을 위해서 동기화된 커넥션을 계속 살아있도록 트랜잭션 동기화 매니저에 반납해야 합니다. 만약, 트랜잭션 동기화 매니저에서 관리하고 있는 커넥션이 아닌 경우에만 해당 커넥션을 닫습니다.

 

 

아래 코드에서 이제 더이상 데이터소스를 직접 사용하지 않고, PlatformTransactionManager를 통해서 커넥션에 접근합니다. transactionManager.getTransaction() 메서드를 통해서 트랜잭션을 시작 뿐 아니라 커넥션 관리도 추상화 되었습니다. 구현체에 따라서 세부 동작방식은 달라집니다.

 

 

 

아래 테스트 코드에서, DataSourceTransactionManager를 활용하여 Jdbc 트랜잭션 관리를 사용하도록 했습니다. 만약, Jpa로 변경하고 싶다면 단순하게 JpaTransactionManager로만 변경하면 됩니다. 추상화 구현 방식 덕분에, 세부적인 비지니스 로직을 수정 할 필요가 없습니다.

 

 

 

PlatformTransactionManager를 사용해 좀 더 효율적으로 추상화된 커넥션 관리가 가능해 졌지만, 여전히 서비스 계층 코드에 트랜잭션 메서드인 getTransaction(), commit(), rollback()이 사용되고 있습니다. 즉, 서비스 계층에서는 비지니스 코드만 있어야 하지만 여전히 트랜잭션 코드가 있습니다. 

 

스프링에서 트랜잭션 기술을 좀 더 추상화된 형태인 @Transactional로 만들어 해당 문제를 개선해보겠습니다.

반응형

'Spring' 카테고리의 다른 글

스프링 AOP (2) - AOP 구현 및 예제  (0) 2023.01.12
스프링 트랜잭션 이해  (0) 2022.12.01
@Controller @RestContrller  (0) 2022.08.25
@Controller, @Service, @Repository 차이  (0) 2022.08.25
DI(Dependency Injection)  (0) 2022.07.14