스프링과 트랜잭션

ksh98·2024년 3월 4일

스프링 DB

목록 보기
4/8

스프링 DB 1편 - 데이터 접근 핵심 원리 강의의 내용을 저의 말로 정리한 것입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1

서비스 계층에서 트랜잭션을 시작하면 생기는 문제점

애플리케이션들은 보통 웹, 서비스, 리포지토리 세개의 계층으로 나눠진다.

서비스 계층은 핵심 비지니스 로직이 들어가고 웹, 디비 기술이 바뀌어도 코드를 변경하지 않기 위해 다음을 지키며 작성된다.

  • 순수한 자바 코드로 작성하기
  • 리포지토리의 인터페이스에 의존하기
  • 모든 jdbc 코드는 리포지토리에 넣기

하지만 트랜잭션을 사용하려면 트랜잭션을 서비스 계층에서 시작해야 한다. 따라서 어쩔 수 없이 jdbc 등에 의존한 코드가 서비스 계층에 있다. 서비스 계층이 특정 기술에 의존하므로 기술이 변경되면 서비스 계층의 코드도 바뀐다.

트랜잭션 매니저

트랜잭션 매니저는 트랜잭션을 추상화하고 리소스를 동기화한다.

트랜잭션 추상화

위에 문제를 해결하기 위해서 스프링에서는 트랜잭션을 시작하고 커밋 롤백하는 등 트랜잭션 관련 작업을 추상화 했다.

스프링에서는 PlatformTransactionManager 라는 트랜잭션을 처리하는 인터페이스를 만들고 기술별 구현체도 모두 만들어 놓았다.

이제 서비스 계층은 이 인터페이스에 의존하여 특정 기술에 대해 의존하지 않을 수 있다.

인터페이스는 다음과 같이 생겼다

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

	void commit(TransactionStatus status) throws TransactionException;

	void rollback(TransactionStatus status) throws TransactionException;
}

즉 트랜잭션을 처리하고 싶으면 기술 별로 getTransacation을 구현해 트랜잭션을 시작하고 commit과 rollback을 구현해 트랜잭션을 종료할 수 있어야 한다는 것이다.

리소스 동기화

리소스 동기화란 트랜잭션 시작과 끝에서 같은 커넥션을 유지하는 것을 말한다.

트랜잭션 매니저가 등장하기 전에는 생성한 커넥션은 매개변수를 통해 넘겨주었다. 하지만 이 방법은 메소드를 중복해서 만들어야 하고 코드가 지저분해진다는 단점이 있다.

스프링에서는 트랜잭션 동기화 매니저를 이용해서 리소스 동기화를 한다. 커넥션을 생성하면 동기화 매니저에 저장하고 한 트랜잭션 동안 커넥션이 필요한 경우 꺼내서 쓴다.

동작 방식

  1. 트랜잭션을 시작하면 트랜잭션 매니저는 매개변수로 받은 데이타소스를 이용해 커넥션을 생성하고 트랜잭션을 시작한다.

  2. 트랜잭션 매니저가 생성된 커넥션을 트랜잭션 동기화 매니저에 보관한다.

  3. 리포지토리에서 커넥션이 필요하다면 동기화 매니저에서 저장된 커넥션을 꺼내서 쓴다.

  4. 일을 다 하면 동기화 매니저에 보관된 커넥션을 이용해서 트랜잭션을 종료하고 커넥션을 닫는다.

적용

서비스 계층

이제 서비스 계층은 트랜잭션 매니저 인터페이스에 의존한다.

public class MemberService {

	private final PlatformTransactionManager transactionManager;
	private final MemberRepository memberRepository;

	public void transaction() throws SQLException {
		TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            bizLogic();
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw new IllegalStateException(e);
    	}
	}
}

트랜잭션 매니저는 getTrancation 으로 트랜잭션을 시작한다. 이걸 호출하면 트랜잭션 매니저는 내부의 데이터소스를 이용해서 커넥션을 생성한다.

이후 수동 커밋 모드로 바꾸면서 트랜잭션을 시작하고 커넥션을 동기화 매니저에 보관한다.

비지니스 로직이 모두 수행되면 트랜잭션 매니저가 커밋 또는 롤백을 하여 트랜잭션을 종료하기 위해 commit 또는 rollback을 호출한다. 이 메소드를 호출하면 동기화 매니저에서 커넥션을 가져와 커넥션을 이용해 커밋 또는 롤백을 한다.

마지막으로 자동 커밋 모드로 바꾸고 커넥션을 종료하며 리소스를 정리하고 끝이 난다.

리포지토리 계층

비지니스 로직을 수행하면서 리포지토리 계층은 쿼리를 날릴 일이 있을 것이다. 그러려면 트랜잭션 동기화 매니저에 있는 커넥션을 꺼내와야 한다. 트랜잭션 동기화 매니저를 사용하려면 DataSourceUtils를 이용하면 된다.

커넥션을 얻어오려면 DataSourceUtils의 getConnection을 다시 동기화 매니저에 넣으려면 DataSourceUtils의 releaseConnection을 사용하면 된다.

getConnection은 서비스가 트랜잭션 매니저를 사용하여 동기화 매니저가 관리하는 커넥션이 있다면 해당 커넥션을 반환한다. 반면 트랜잭션 매니저를 사용하지 않는다면 새로운 커넥션을 생성해서 준다.

releaseConnection은 커넥션을 바로 닫는 것이 아니다. 커넥션이 트랜잭션 매니저가 동기화 매니저에 넣어 뒀던 커넥션이라면 닫지 않고 유지하고 트랜잭션 매니저를 사용하지 않아 관리되지 않는 커넥션이라면 닫는다.

아래는 이후 트랜잭션을 종료하는 과정에서 일어나는 일을 그림으로 표현한 것이다.

트랜잭션 템플릿

트랜잭션을 사용하는 코드에는 반복이 많다.
예를 들어

  • 비지니스 로직을 수행하고
  • 성공하면 커밋
  • 실패하면 롤백
  • try catch finally

등등이 계속 반복된다. 항상 달라지는 부분은 비지니스 로직 정도이다. 이를 템플릿 콜백 패턴을 이용해서 해결해주는 것이 트랜잭션 템플릿이다. 스프링에서는 TransactionTemplate이라는 템플릿 클래스를 제공한다.

public class TransactionTemplate {
	private PlatformTransactionManager transactionManager;
	
    public <T> T execute(TransactionCallback<T> action){..}
	void executeWithoutResult(Consumer<TransactionStatus> action){..}
}

이 클래스를 사용하려면 트랜잭션 매니저가 필요하다. 따라서 생성자를 통해서 받아야 한다.

execute와 executeWithoutResult 메소드를 호출하면 알아서 트랜잭션을 시작하고 비지니스 로직을 수행하고 커밋이나 롤백을 한다. 둘의 차이는 앞은 응답값이 있고 뒤는 응답값이 없다.

이제부터는 트랜잭션 템플릿을 만들기만 했다면 메소드 호출 하나로 트랜잭션을 시작하고 진행하고 끝낼 수 있다.

public void transaction() throws SQLException {
    txTemplate.executeWithoutResult((status) -> {
		try {
			bizLogic();
		} catch (SQLException e) {
			throw new IllegalStateException(e);
		}
	});
}

하지만 여전히 트랜잭션 관련 코드가 남아있다.

트랜잭션 AOP 적용

이제는 트랜잭션으로 묶어줄 곳에 @Transactional이라고 붙여주기만 하면 된다. 그러면 서비스에는 비지니스 로직만 있을 수 있다.

또한 프록시 객체가 만들어지는데 프록시 객체는 원래 서비스 클래스를 상속받아서 만들어진다. 여기에는 서비스에 없는 트랜잭션 관련 코드들이 추가되어 있고 클라이언트는 프록시 객체에 의존하게 된다.

따라서 프록시 객체의 비지니스 로직 메소드를 호출하게 되고 프록시는 실제 서비스의 메소드를 호출한다.

@Transactional은 클래스 레벨과 메소드 레벨 모두에 붙일 수 있는데 클래스에 붙이면 클래스 내 모든 public 메소드에 적용이 된다.

이렇게 @Transactional을 붙여 이 부분을 하나의 트랜잭션을 처리하겠다고 하는 것을 선언적 트랜잭션이라고 한다.

스프링 부트의 자동 리소스 등록

스프링 부트를 이용하면 데이터소스와 트랜잭션 매니저를 자동으로 빈으로 등록해준다. 따라서 개발자가 직접 등록할 필요가 없다.

데이터소스

  • dataSource라는 이름으로 등록해준다.
  • 개발자가 직접 등록하면 직접 등록한게 우선순위가 높다.
  • 기본으로 등록되는 데이터소스는 HikariDataSource이다.
  • application.properties의 spring.datasource.url을 보고 기본 등록 데이터소스를 변경할 수 있다.
  • spring.datasource.url이 없다면 메모리 db를 이용하려고 시도한다.

트랜잭션 매니저

  • transactionManager 라는 이름으로 등록해준다.
  • 직접 등록한 것이 우선 순위가 더 높다.
  • 현재 다운받은 디비 기술 라이브러리를 보고 적당한 매니저를 등록해준다.
profile

0개의 댓글