4. 스프링과 문제 해결 - 트랜잭션

ChoiJaewoo·2026년 2월 23일

Spring DB 1편

목록 보기
4/6
  • 문제점들
  • 트랜잭션 추상화
  • 트랜잭션 동기화
  • 트랜잭션 문제 해결 - 트랜잭션 매니저1
  • 트랜잭션 문제 해결 - 트랜잭션 매니저2
  • 트랜잭션 문제 해결 - 트랜잭션 템플릿
  • 트랜잭션 문제 해결 - 트랜잭션 AOP 이해
  • 트랜잭션 문제 해결 - 트랜잭션 AOP 적용
  • 트랜잭션 문제 해결 - 트랜잭션 AOP 정리
  • 스프링 부트의 자동 리소스 등록

문제점들

애플리케이션 구조

여러가지 애플리케이션 구조가 있지만, 가장 단순하고 많이 사용하는 방법은 역할에 따라 3계층 으로 나누는 것.

  • 프레젠테이션 계층
    • UI 와 관련된 처리 담당
    • 웹 요청과 응답
    • 사용자 요청을 검증
    • 주 사용 기술 : 서블릿과 HTTP 같은 웹 기슬, 스프링 MVC
  • 서비스 계층
    • 비즈니스 로직 담당
    • 주 사용 기술 : 특정 기술에 의존하지 않고, 순수 자바 코드로 작성
  • 데이터 접근 계층
    • 실제 데이터베이스에 접근하는 코드
    • 주 사용 기술 : JDBC, JPA, File, Redis …

→ 서비스 계층 : 핵심 비즈니스 로직

비즈니스 로직이 최대한 변경 없이 유지되는 것이 목표이다!

이렇게 하려면 서비스 계층을 특정 기술에 종속적이지 않게 개발해야 한다.

MemberService2 예시

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {
    private final DataSource dataSource;
    private final MemberRepositoryV2 memberRepository;

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

    private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(con, fromId);
        Member toMember = memberRepository.findById(con, toId);

        memberRepository.update(con, fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(con, toId, toMember.getMoney() + money);
    }

    private static void release(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true);
                con.close();
            } catch (Exception e) {
                log.info("error", e);
            }
        }
    }

    private static void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}
  • 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다.
  • 그런데 문제는 트랜잭션을 사용하기 위해 javax.sql.DataSource, java.sql.Connection, java.sql.SQLException 같은 JDBC 기술에 의존해야 한다.
  • 향후 JDBC 에서 JPA 같은 기술로 바꾸어 사용해야 하면, 서비스 코드도 모두 함께 변경해야 한다.
  • 핵심 비즈니스 로직과 JDBC 기술이 섞여 있어서 유지보수에 어렵다.

문제 정리

  • 트랜잭션 문제
  • 예외 누수 문제
  • JDBC 반복 문제

트랜잭션 추상화

현재 서비스 계층은 트랜잭션을 사용하기 위해 JDBC 기술에 의존하고 있다.

구현 기술에 따른 트랜잭션 사용법

  • 트랜잭션은 원자적 단위의 비즈니스 로직을 처리하기 위해 사용된다.
  • 구현 기술마다 트랜잭션을 사용하는 방법이 다르다.
    • JDBC : con.setAutoCommit(false);
    • JPA : transaction.begin();

JDBC 트랜잭션 의존 상황

JDBC → JPA 기술로 변경 상황

트랜잭션 추상화

이를 해결하기 위해 트랜잭션 기능을 추상화 하면 된다.

스프링의 트랜잭션 추상화

스프링이 제공하는 트랜잭션 추상화 기술을 사용하면 된다.

심지어 데이터 접근 기술에 따른 트랜잭션 구현체도 만들어두어서 가져다 사용하기만 하면 된다.

스프링 트랜잭션 추상화의 핵심은 PlatformTransactionManager 인터페이스이다.

  • org.springframework.transaction.PlatformTransactionManager

PlatfromTransactionManager interface

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;
  }
  • getTransaction() : 트랜잭션을 시작한다.
  • commit() : 트랜잭션을 커밋한다.
  • rollback() : 트랜잭션을 롤백한다.

트랜잭션 동기화

  • 트랜잭션 추상화
  • 리소스 동기화

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

  • 스프링은 트랜잭션 동기화 매니저를 제공한다. → 트랜잭션 매니저 내부에서 트랜잭션 동기화 매니저를 사용한다.
  • 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.

트랜잭션 문제 해결 - 트랜잭션 매니저

MemberRepositoryV3

package hello.jdbc.repository;

import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;

/**
 * 트랜잭션 - 트랜잭션 매니저
 * DataSourceUtils.getConnection()
 * DataSourceUtils.releaseConnection()
 */
@Slf4j
public class MemberRepositoryV3 {

    private final DataSource dataSource;

    public MemberRepositoryV3(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values (?,?)";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, member.getMemberId());
            preparedStatement.setInt(2, member.getMoney());
            preparedStatement.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);

            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Member member = new Member();
                member.setMemberId(resultSet.getString("member_id"));
                member.setMoney(resultSet.getInt("money"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, resultSet);
        }
    }

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id=?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setString(2, memberId);
            int resultSize = preparedStatement.executeUpdate();
            log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public void delete(String memberId) {
        String sql = "delete from member where member_id = ?";
        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try{
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);
            preparedStatement.executeUpdate();
            log.info("delete memberId={}", memberId);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private void close(Connection connection, Statement statement, ResultSet resultSet) {
        JdbcUtils.closeResultSet(resultSet);
        JdbcUtils.closeStatement(statement);
        DataSourceUtils.releaseConnection(connection, dataSource);
    }

    private Connection getConnection() throws SQLException {
        //주의! 트랜잭션 동기화를 사용하려면, DataSourceUtils 를 사용해야 한다.
        // 트랜잭션 동기화 매니저를 사용하는 DataSourceUtils,,,
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info("get connection={}, class={}", con, con.getClass());
        return con;
    }

    //getConnection <-> releaseConnection 으로 열고 닫는다.

}
  • DataSourceUtils.getConnection()
    • getConnection() 에서 DataSourceUtils.getConnection() 을 사용하도록 변경된 부분을 주의해야 한다.
    • DataSourceUtils.getConnection() 은 다음과 같이 동작한다.
      • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
      • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.
  • DataSourceUtils.releaseConnection()
    • close() 에서 DataSourceUtils.releaseConnection() 을 사용하도록 변경된 부분을 특히 주의해야 한다.
    • 커넥션을 con.close()로 닫으면, 커넥션이 유지되지 않는다.
    • 해당 커넥션은 커밋 또는 롤백 할 때 까지 살아있어야 한다.

MemberServiceV3_1

package hello.jdbc.service;

import hello.jdbc.domain.Member;

import hello.jdbc.repository.MemberRepositoryV3;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * 트랜잭션 - 트랜잭션 매니저
 */

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_1 {
    private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        //트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            //비즈니스 로직 실행
            bizLogic(fromId, toId, money);
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw new IllegalStateException(e);
        }
    }

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private static void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}
  • private final PlatformTransactionManager transactionManager;
    • 트랜잭션 매니저를 주입 받는다.
    • PlatfromTransactionManager ← JDBC 를 사용한다? 그러면 DataSourceTransactionManager 구현체를 주입 받으면 된다.
      • 어떻게?
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        
        memberRepository = new MemberRepositoryV3(dataSource);
        
        memberService = new MemberServiceV3_1(transactionManager, memberRepository);
    • 추후 JPA 를 사용한다? 그러면 JpaTransactionManager 를 주입 받으면 된다.
  • 트랜잭션 시작
    • transactionManager.getTransaction()

      TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    • 트랜잭션을 시작한다

    • TransactionStatus 를 반환한다. 현재 트랜잭션 상태 정보가 포함되어 있고,

    • 이후 트랜잭션 커밋, 롤백 시 필요하다.

    • PlatformTransactionManager - getTransaction method

      TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
  • new DefaultTransactionDefinition()
    • 트랜잭션과 관련된 옵션을 지정할 수 있다.
  • transactionManager.commit(status)
    • 트랜잭션이 성공하면, 해당 로직을 호출하여 커밋한다.
  • transactionManager.rollback(status)
    • 문제 발생 시 해당 로직 호출하여 트랜잭션을 롤백한다.

MemberServiceV3_1Test

package hello.jdbc.service;

import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * 트랜잭션 - 트랜잭션 매니저
 */

@Slf4j
class MemberServiceV3_1Test {

    private MemberRepositoryV3 memberRepository;
    private MemberServiceV3_1 memberService;

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    @BeforeEach
    void before() {
//        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        //커넥션 풀링
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);

        memberRepository = new MemberRepositoryV3(dataSource);

        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);

        memberService = new MemberServiceV3_1(transactionManager, memberRepository);
    }

    @AfterEach
    void after() {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() throws SQLException {
        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        //when
        log.info("START TX");
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(12000);

    }

    @Test
    @DisplayName("비정상 이체 - 이체 중 예외 발생")
    void accountTransferEx() throws SQLException {
        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        //when

        assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000)).isInstanceOf(IllegalStateException.class);

        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberEx = memberRepository.findById(memberEx.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(10000);
        assertThat(findMemberEx.getMoney()).isEqualTo(10000);

    }
}

트랜잭션 문제 해결 - 트랜잭션 템플릿

트랜잭션 사용 코드에서 비즈니스로직을 감싸고 있는 try-catch 문이 각각의 서비스에 반복될 것 이다.

트랜잭션을 시작하고, 실행 이후 커밋 또는 롤백하는 코드는 재사용 가능해야 하고, 반복적으로 사용될 것 이다.

→ Template Call Back Pattern 을 사용하면 반복 문제를 깔끔하게 해결할 수 있다!

TransactionTemplate

public class TransactionTemplate {
      private PlatformTransactionManager transactionManager;
      
      public <T> T execute(TransactionCallback<T> action){..}
      void executeWithoutResult(Consumer<TransactionStatus> action){..}
  }
  • execute() : 응답 값이 있을 때 사용 → commit

  • executeWithoutResult() : 응답 값이 없을 때 사용한다.

  • 트랜잭션 템플릿 기본 동작

    • 비즈니스 로직이 정상 수행되면 → commit
    • 언체크 예외가 발생하면 롤백, 그외 → commit
    • 따라서 SQLException 은 체크 예외인데, 이 상황에서도 롤백하기 위해 catch 에서 언체크 예외 IllegalStateException 으로 바꿔 던지도록 한다.

MemberServiceV3_2

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

import java.sql.SQLException;

/**
 * 트랜잭션 - 트랜잭션 탬플릿
 */

@Slf4j
public class MemberServiceV3_2 {
//    private final PlatformTransactionManager transactionManager;

    private final TransactionTemplate txTemplate;
    private final MemberRepositoryV3 memberRepository;

    public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
        this.txTemplate = new TransactionTemplate(transactionManager);
        this.memberRepository = memberRepository;
    }

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

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private static void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}

트랜잭션 문제 해결 - 트랜잭션 AOP 이해

  • 트랜잭션을 편리하게 처리하기 위해서 트랜잭션 추상화도 도입하고, 추가로 반복적인 트랜잭션 로직을 해결하기 위해 트랜잭션 템플릿도 도입하였다.
  • 하지만 서비스 계층에 순수한 비즈니스 로직만 남긴다는 목표는 아직 달성하지 못함!
  • 이럴 때 스프링 AOP 를 통해 프록시를 도입하면 문제를 해결할 수 있다.

→ “@Transactional” 을 사용하면 스프링이 AOP 를 사용해서 트랜잭션을 편리하게 처리해준다 정도로 이해해도 괜찮다!

프록시 도입

프록시를 사용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.

프록시 도입 이후

스프링이 제공하는 트랜잭션 AOP

  • 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.
  • 개발자는 트랜잭션 처리가 필요한 곳에 “@Transactional” 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.

트랜잭션 문제 해결 - 트랜잭션 AOP 적용

트랜잭션 AOP를 사용하는 새로운 서비스 클래스를 만든다.

MemberServiceV3_3

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.sql.SQLException;

/**
 * 트랜잭션 - @Transaction AOP
 */

@Slf4j
public class MemberServiceV3_3 {
    private final MemberRepositoryV3 memberRepository;

    public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Transactional //성공하면 Commit, 실패하면 Rollback -> 메서드에 붙여도 되고, 클래스에 붙여도 된다.
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(fromId, toId, money);
    }

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private static void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}

MemberServiceV3_3Test

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * 트랜잭션 - 트랜잭션 매니저
 */

@Slf4j
@SpringBootTest
class MemberServiceV3_3Test {

    @Autowired
    private MemberRepositoryV3 memberRepository;
    @Autowired
    private MemberServiceV3_3 memberService;

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    @TestConfiguration
    static class TestConfig{
        @Bean
        DataSource dataSource() {
            return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        }

        @Bean
        PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }

        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource());
        }

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }

    @AfterEach
    void after() {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() throws SQLException {
        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        //when
        log.info("START TX");
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(12000);

    }

    @Test
    @DisplayName("비정상 이체 - 이체 중 예외 발생")
    void accountTransferEx() throws SQLException {
        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        //when

        assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000)).isInstanceOf(IllegalStateException.class);

        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberEx = memberRepository.findById(memberEx.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(10000);
        assertThat(findMemberEx.getMoney()).isEqualTo(10000);

    }
}
  • 스프링 AOP 를 적용하려면 스프링 컨테이너가 필요하다. 이 애노테이션이 있으면 테스트 시 스프링 부트를 통해 스프링 컨테이너를 생성하고 테스트에서 @Autowired 를 통해 스프링 컨테이너가 관리하는 빈들을 사용할 수 있다.
  • @TestConfiguration : 테스트 안에서 내부 설정 클래스를 만들어서 사용하면서 이 애노테이션을 붙이면, 스프링 부트가 자동으로 만들어주는 빈들에 추가로 필요한 스프링 빈들을 등록하고 테스트를 수행할 수 있다.

트랜잭션 문제 해결 - 트랜잭션 AOP 정리

트랜잭션 AOP 적용한 전체 흐름

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

데이터 소스와 트랜잭션 매니저를 스프링 빈으로 직접 등록하는 과정

@TestConfiguration
    static class TestConfig{
        @Bean
        DataSource dataSource() {
            return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        }

        @Bean
        PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }

        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource());
        }

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }

→ 이를 자동 등록해보자!

데이터 소스 - 자동 등록

  • 스프링 부트는 데이터소스를 스프링 빈에 자동으로 등록한다.
  • 자동으로 등록되는 스프링 빈 이름 : dataSource

이때 스프링 부트는 다음과 같이 application.properties 에 있는 속성을 사용해서 DataSource 를 생성한다.

application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • 스프링 부트가 기본으로 생성하는 데이터소스는 커넥션 풀을 제공하는 HikariDataSource 이다.

트랜잭션 매니저 - 자동 등록

  • 스프링 부트는 적절한 트랜잭션 매니저를 자동으로 스프링 빈에 등록한다.

MemberServiceV3_4Test

@Bean
DataSource dataSource() {
    return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}

@Bean
PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
}

→ 해당 부분이 빠졌다.

  • 데이터 소스와 트랜잭션 매니저를 스프링 빈으로 등록하는 코드가 생략되었다.

0개의 댓글