스프링 DB 1편 - 데이터 접근 핵심 원리 : Transaction 관리, Spring AOP Transaction

jkky98·2024년 8월 21일
0

Spring

목록 보기
34/77

트랜잭션 구현 문제점

컨트롤러를 관리하는 컨트롤러 클래스의 핸들러 메서드는 웹 계층에서 클라이언트의 요청을 처리하고, 응답을 생성하는 책임을 진다. 구체적인 처리 과정인 비즈니스 로직은 다시 서비스 계층을 호출하여 해결한다. 서비스 계층에는 비즈니스 로직을 가지며 DB와의 연결을 통해 데이터를 주고 받기 위해 서비스계층은 다시 리포지토리 계층을 활용한다.

트랜잭션 로직은 분명히 리포지토리의 로직이다. 이러한 로직이 서비스에서 복잡하게 구성되고 있음을 다음 코드를 보며 알 수 있다.

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);
        }
    }

커넥션을 다루는 것은 분명한 리포지토리의 영역이다. 서비스 계층의 메서드이지만 서비스 로직을 다루는 것은 bizLogic(~)한 줄 뿐이다. 또한 JDBC에서의 트랜잭션 시작은 setAutoCommit(false)이지만 JPA에서는 transaction.begin()이 된다.

이렇게 데이터 접근 기술에 따라 트랜잭션 로직이 달라지는 문제와 트랜잭션 로직 자체가 서비스 로직에 존재한다는 것 크게는 두 가지 문제를 이번 챕터에서 해결해본다.

트랜잭션 추상화 - TransactionManager

트랜잭션은 커밋을 통해 DB에 반영하는 것이므로 구현체마다 다른 문제가 있다면 이를 모두 포괄할 인터페이스를 설계해놓고 사용하면 구현체를 갈아끼는 것에 있어 문제가 없을 것이다.(다형성 활용)

이러한 기능을 스프링이 PlatformTransactionManager를 통해 제공한다.

DataSourceTransactionManager tM = new DataSourceTransactionManager(dataSource);

트랜잭션 매니저의 내부에서는 커넥션을 가져오기 위해 동일하게 dataSource가 사용되므로 dataSource를 생성자로 주입해주어야 한다.

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        // 트랜잭션 시작
        // 복잡한 옵션 속성 정할 수 있음 (지금은 디폴트로.(DefaultTransactionDefinition)
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());

        try {
            // 비즈니스 로직 코드
            bizLogic(fromId, toId, money);
            tm.commit(status);
        } catch (Exception e) {
            tm.rollback(status); // 실패시 롤백
            throw new IllegalStateException(e);
        }

이제 우리는 Jpa를 사용하던지 Jdbc를 사용하던지 커밋을 위해서는 tm.commit(status)와 같이 사용할 수 있다.

익숙한 전략 패턴(인터페이스 아래에서 구현, 인터페이스에 의존하여 사용)을 활용한 기술을 보고있다. 하지만 여전히 서비스 계층에 트랜잭션 코드가 남아있다.

Transaction 동기화

이전 챕터에서 같은 커넥션을 사용하기 위해 직접 하나의 커넥션만 생성하고 파라미터로 전달하여 유일성을 유지했다. 트랜잭션 매니저는 트랜잭션 실행시 Transaction synchronization manager(트랜잭션 동기화 매니저)에 데이터소스로 생성한 커넥션을 트랜잭션 상태로 보관시킨다. 동기화 매니저는 이를 ThreadLocal에 보관한다. 쓰레드로컬은 하나의 쓰레드에 대해 종속적으로 존재하는 영역으로 특정 쓰레드에서 생성한 커넥션을 보관할 수 있어 독립성을 유지할 수 있으며 멀티 쓰레드 환경에서도 당연히 쓰레드마다 접근하는 쓰레드로컬이 다르기 때문에 안정적이다.

이렇게 쓰레드 로컬에 트랜잭션 상태의 커넥션이 보관될 경우 리포지토리의 각 기능에서 DataSourceUtils.getConnection()을 통해 동기화 매니저의 쓰레드 로컬에 접근해서 이를 꺼내올 수 있다. 만약 쓰레드 로컬에 커넥션이 없는데 getConnection()이 이루어진다면 DataSource를 통해 커넥션을 가져오게 된다.

Transaction Template

위의 코드에서 tm.getTransaction(), tm.commit(status), tm.rollback(status)과 같이 이 트랜잭션 코드의 구조는 반복될 것을 알 수 있다. 이를 줄이기 위해 템플릿 콜백 패턴이 적용된 TransactionTemplate을 활용할 수 있다.

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        txTemplate.executeWithoutResult((status) -> {
            try {
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }

트랜잭션 템플릿은 비즈니스 로직이 정상 수행되면 커밋을 진행하고, 언체크 예외 발생시 롤백한다(체크 예외는 커밋한다)

Spring 트랜잭션 AOP

이제 트랜잭션 관련 코드를 완전히 분리할 시간이다. 다음과 같이 @Transactional를 사용하면 프록시 도입으로 하여금 Transaction을 적용해준다.

	@Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(fromId, toId, money);
    }

메서드 안에 비즈니스 로직 코드만 남고 트랜잭션에 관련한 로직은 사라지는 것을 볼 수 있다.

이 기술을 프록시를 사용하여 서비스 계층에 순수한 비즈니스 로직만 남긴다.

Proxy
실제로 위의 코드는 실행 시점에 트랜잭션이 추가된 다음과 같은복제 메서드가 실행된다.

//트랜잭션 시작
	TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
	try {
//비즈니스 로직
     bizLogic(fromId, toId, money);
	transactionManager.commit(status); //성공시 커밋 } catch (Exception e) {
	transactionManager.rollback(status); //실패시 롤백
    throw new IllegalStateException(e);
 }

스프링은 @Transactional를 보고 CGLib을 통해 복제된 클래스를 만들어낸다. 그리고 복제된 클래스를 호출하여 트랜잭션이 적용된 복제된 클래스의 메서드를 실행한다.

개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 되는 것이다.

이렇게 애노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션(Declarative Transaction Managment)라고 한다. 현재 실무수준에서는 이 방식을 99% 사용한다. 우리가 이전에 진행한 것처럼 트랜잭션 매니저등을 활용해서 직접 코딩하는 방식을 프로그래밍 방식의 트랜잭션 관리(programmatic transaction management)라고 한다.(거의 사용되지 않음)

당연히 트랜잭션 AOP 방식 또한 트랜잭션 매니저 및 데이터소스를 활용한다. 스프링에 종속적인 기술이므로 트랜잭션 매니저와 데이터소스를 빈으로 등록해줘야 하지만 스프링부트를 통해 자동적으로 등록되어있다.

데이터 소스를 자동 등록하기 위해서는 어플리케이션 설정에 다음과 같은 정보들을 넣어줘야 한다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

만약 JPA를 사용한다면(라이브러리 존재) 트랜잭션 매니저는 JpaTransactionManager를 자동 등록할 것이고 Jdbc를 사용한다면 DataSourceTransactionManager를 등록할 것이다. 둘 다 있다면 Jpa가 등록된다.(JpaTransactionManager가 DataSourceTransactionManager의 기능들을 대부분 지원한다.)

profile
자바집사의 거북이 수련법

0개의 댓글