Spring

[인프런/스프링 DB 1편] 4. 스프링과 문제 해결 - 트랜잭션

주니어주니 2023. 6. 21. 13:51

 

 

1. 트랜잭션 문제점

 

 

1) 문제점

 

서비스 계층은 순수하게 유지해야 함 

서비스 계층 : 핵심 비즈니스 로직 -> 특정 기술에 종속 X -> 최대한 순수 자바 코드로 유지 -> 변경 최소화

 

 

 

예) MemberServiceV1 - 특정 기술에 종속 X, 순수한 비즈니스 로직으로만 존재

 

 

예) MemberServiceV2 - 트랜잭션 사용하기 위해 DataSource, Connection, SQLException같은 JDBC 기술에 의존

 

 

* 지금까지 개발한 애플리케이션의 문제점 3가지

  1. 트랜잭션 문제
    • JDBC 구현 기술이 서비스 계층에 누수되는 문제 
      • 서비스 계층을 순수하게 유지하기 위해 데이터 접근 계층(Repository)에 JDBC 코드를 몰았는데,
        트랜잭션을 적용하기 위해 JDBC 구현 기술이 서비스 계층에 누수
    • 트랜잭션 동기화 문제 
      • 같은 트랜잭션을 유지하기 위해 커넥션을 파라미터로 넘김
        -> 트랜잭션용 기능, 트랜잭션 유지하지 않아도 되는 기능으로 분리해야 함
    • 트랜잭션 적용 반복 문제
      • try (트랜잭션 시작, 비즈니스 로직, 커밋), catch (롤백), finally (릴리즈)
  2. 예외 누수
    • 데이터 접근 계층의 JDBC 구현 기술 예외가 서비스 계층으로 전파 
    • SQLException은 체크 예외 -> 데이터 접근 계층에서 서비스 계층으로 예외를 던지거나 잡아서 처리해야 함
    • SQLException은 JDBC 전용 기술 -> JPA나 다른 데이터 접근 기술 사용하면 다른 예외로 변경 -> 서비스 코드도 수정해야 함 
  3. JDBC 반복 문제
    • 유사한 코드의 반복
    • try, catch, finally
    • 커넥션, PreparedStatement 사용, 결과 매핑, 실행, 커넥션 리소스 정리, ...

 

 

 

2) 해결방법 - 트랜잭션 추상화 

 

- JDBC, JPA 등 데이터 접근 기술마다 트랜잭션을 사용하는 코드가 다름

-> 데이터 접근 기술을 JDBC에서 JPA로 변경하게 되면 -> 서비스 계층의 트랜잭션 처리 코드도 모두 변경해야 함

 

 

-> 스프링의 트랜잭션 추상화 

 

  • 추상화된 인터페이스 : PlatformTransactionManager 인터페이스
  • 데이터 접근 기술에 따른 트랜잭션 구현체도 대부분 생성되어 있음
  • 서비스 계층은 이제 트랜잭션 기술에 직접 의존 X, 추상화된 인터페이스에만 의존
    -> 구현체를 주입받음
    -> 서비스 코드를 변경하지 않고, 트랜잭션 기술을 변경할 수 있음

 

* PlatformTransactionManager 인터페이스 

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() : 트랜잭션 롤백

 

 

-> 인터페이스 + 구현체 = 트랜잭션 매니저 

 

 

 

3) 해결방법 - 트랜잭션 동기화 

 

원래 커넥션을 동기화하기 위해 파라미터로 전달했음 -> 코드 복잡 

 

 

  • 트랜잭션 매니저(인터페이스 + 구현체)의 역할
    • 트랜잭션 추상화
    • 리소스 동기화 : 같은 커넥션을 동기화
  • 트랜잭션 동기화 매니저 
    • 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션 동기화 
    • 쓰레드 로컬 - 멀티쓰레드 상황에서도 안전하게 커넥션 동기화
      (각각의 쓰레드마다 별도의 저장소 부여 -> 해당 쓰레드만 해당 데이터에 접근 가능)
  • 동작 방식
    1. 트랜잭션을 시작하려면 커넥션 필요
      -> 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고, 트랜잭션 시작
    2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션트랜잭션 동기화 매니저에게 전달
    3. 리포지토리트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용
      -> 파라미터로 커넥션을 전달하지 않아도 됨 
    4. 트랜잭션이 종료되면 트랜잭션 매니저트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션 종료, 커넥션도 닫음

 

 


2. 트랜잭션 문제 해결

 

2-1. 트랜잭션 문제 해결 - 트랜잭션 매니저 적용

 

1) MemberRepositoryV3 - 커넥션을 통해 데이터베이스에 접근

- 커넥션 직접 획득 X, 트랜잭션 동기화 매니저에서 가져옴

- 커넥션 획득하는 부분, 커넥션 닫는 부분 주의 ! 

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 con = null;                      	// finally에서도 호출해야 하기 때문에 밖에 따로 선언
        PreparedStatement pstmt = null;

        try {
            con = getConnection();                  	// 커넥션 획득 -> 메소드로 추출
            pstmt = con.prepareStatement(sql);      	// sql에 파라미터 바인딩
            pstmt.setString(1, member.getMemberId());   // sql의 첫번째 ?에 문자 저장
            pstmt.setInt(2, member.getMoney());         // sql의 두번째 ?에 숫자 저장
            pstmt.executeUpdate();                  	// 실행 (sql을 커넥션을 통해 데이터베이스에 전달)
            return member;
        } catch (SQLException e) {
            log.error("db error", e);              	 	// 로그를 찍기 위해 예외 잡음
            throw e;	
        } finally {
            close(con, pstmt, null);
        }
    }

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

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery();

            if (rs.next()) {
                Member member = new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.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(con, pstmt, rs);
        }
    }

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

        Connection con = null;
        PreparedStatement pstmt = null;

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

    public void delete(String memberId) throws SQLException {
        String sql = "delete member where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        // 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        DataSourceUtils.releaseConnection(con, dataSource);
    }

    private Connection getConnection() throws SQLException {
        // 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. (트랜잭션 동기화 매니저에서 꺼냄)
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info("get connection={}, class{}", con, con.getClass());
        return con;
    }
}
  • 커넥션을 파라미터로 전달하는 부분 모두 제거
    • 트랜잭션 동기화 매니저 사용 -> 커넥션을 파라미터로 전달하지 않음 

  • DataSourceUtils.getConnection(dataSource) 
    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션 반환 
    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없으면 새로운 커넥션 생성해서 반환
  • DataSourceUtils.releaseConnection(con, dataSource)
    • 트랜잭션을 사용하기 위해 동기화된 커넥션은 닫지 않고 그대로 유지
    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음

 

 

2) MemberServiceV3_1 - 트랜잭션 매니저 사용

- 트랜잭션 매니저 인터페이스 사용해서 트랜잭션 시작, 커밋, 롤백 

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
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 final PlatformTransactionManager
    • 트랜잭션 매니저를 주입받음 (실제 동작 구현할 때)
    • 지금은 JDBC 기술 사용하기 때문에 DataSourceTransactionManager 구현체 주입 

  • transantionManager.getTransaction(new DefaultTransactionDefinition())
    • 트랜잭션 시작
    • TransactionStatus 를 반환 (현재 트랜잭션의 상태 정보)
  • new DefaultTransactionDefinition()
    • 트랜잭션과 관련된 옵션 지정

  • transactionManager.commit(status)
    • 트랜잭션 성공시 커밋

  • transactionManager.rollback(status)
    • 트랜잭션 실패시 롤백

 

 

3) MemberServiceV3_1 Test

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import hello.jdbc.repository.MemberRepositoryV3;
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;

/**
 * 트랜잭션 - 트랜잭션 매니저
 */
class MemberServiceV3_1Test {

    private MemberRepositoryV3 memberRepository;
    private MemberServiceV3_1 memberService;

    @BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV3(dataSource);
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); // JDBC 사용
        memberService = new MemberServiceV3_1(transactionManager, memberRepository);
    }

	...
}

 

  • PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
    • JDBC용 트랜잭션 매니저 - DataSourceTransactionManager 주입
    • 트랜잭션 매니저데이터소스를 통해 커넥션 생성하므로 DataSource 필요

 

 

4) 동작 흐름

 

(1) 트랜잭션 시작

 

1. 서비스 계층에서 transactionManager.getTransaction() 호출 -> 트랜잭션 시작

 

2. 트랜잭션 시작하려면 데이터베이스 커넥션 필요 -> 트랜잭션 매니저가 내부에서 데이터소스를 사용해 커넥션 생성

 

3. 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션 시작

4. 커넥션을 트랜잭션 동기화 매니저에 보관

5. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션 보관 -> 멀티 쓰레드 환경에서 안전하게 커넥션 보관

 

 

(2) 로직 실행

6. 서비스는 비즈니스 로직 실행하면서 리포지토리의 메소드 호출 -> 커넥션을 파라미터로 전달 X 

 

7. 리포지토리에서는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용 -> 같은 커넥션 사용, 트랜잭션 유지

 

8. 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행

9. 리포지토리에서 각 메소드 실행하고 난 뒤에는 트랜잭션 동기화 매니저에 커넥션 다시 반환 

 

 

(3) 트랜잭션 종료 

10. 비즈니스 로직 끝나고 트랜잭션 종료 (커밋, 롤백)

 

11. 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 동기화된 커넥션 획득

12. 획득한 커넥션을 통해 데이터베이스에 트랜잭션 커밋/롤백 

13. 전체 리소스 정리 (트랜잭션 동기화 매니저 정리, 자동커밋으로 되돌림, close 호출로 커넥션 종료 - 커넥션 풀에 반납)

 

 

 

 

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

 

트랜잭션 코드 반복되는 문제 

(트랜잭션 시작 -> 비즈니스 로직 -> 커밋/롤백)

 

 

1) 트랜잭션 템플릿 

public class TransactionTemplate {

    private PlatformTransactionManager transactionManager;
    
    public <T> T execute(TransactionCallback<T> action){..}		// 응답 값이 있을 때
    void executeWithoutResult(Consumer<TransactionStatus> action){..}	// 응답 값이 없을 때
}

 

 

2) 원래 반복되던 부분

// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

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

 

 

3) MemberServiceV3_2 - 반복되는 부분 제거 

/**
 * 트랜잭션 - 트랜잭션 템플릿
 */
@Slf4j
public class MemberServiceV3_2 {

    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) throws SQLException {

	// 메소드의 반환값이 void니까 executeWithoutResult 사용
        txTemplate.executeWithoutResult((status) -> {
            // 비즈니스 로직
            try {
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }
	... 
}

 

  • 트랜잭션 템플릿을 사용하려면 트랜잭션 매니저 필요 (템플릿 안에서 트랜잭션 시작, 커밋/롤백 해야되니까)
    -> 생성자에서 트랜잭션 매니저를 주입받으면서 트랜잭션 템플릿 생성 
  • 트랜잭션 템플릿 -> 트랜잭션 시작, 커밋/롤백 코드 모두 제거
  • 트랜잭션 템플릿의 기본 동작
    • 비즈니스 로직이 정상 수행 -> 커밋 
    • 언체크 예외 발생 -> 롤백 (체크 예외는 커밋)

  • bizLogic() 호출하면 SQLException 체크 예외를 넘겨줌
    -> 해당 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크(런타임) 예외로 바꿔서 던지도록 예외 전환

 

 

4) 테스트 - 기존과 같음

/**
 * 트랜잭션 - 트랜잭션 템플릿
 */
class MemberServiceV3_2Test {

    private MemberRepositoryV3 memberRepository;
    private MemberServiceV3_2 memberService;

    @BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV3(dataSource);
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); // JDBC 사용
        memberService = new MemberServiceV3_2(transactionManager, memberRepository);
    }
	... 
}

 

 

-> 트랜잭션 반복 코드를 줄였지만

서비스 로직에서 트랜잭션 처리 로직이 포함되어 있음

 

 


 

2-3. 트랜잭션 문제 해결 - 트랜잭션 AOP 

 

서비스 로직에 트랜잭션 처리 로직이 포함되어 있는 문제 

 

AOP (관점 지향 프로그래밍) : 핵심 기능부가 기능 분리 -> 부가 기능을 모듈화하여 재사용할 수 있게 지원

 

1) 프록시 

 

프록시 도입 전

서비스 로직에서 트랜잭션 직접 시작

//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

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

 

 

프록시 도입 후 

트랜잭션 처리 로직, 서비스 로직, 데이터 접근 로직의 분리 

 

- 트랜잭션 처리 로직 (프록시에서 처리)

public class TransactionProxy {

    private MemberService target;
    
    public void logic() {
        //트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(..);
        try {
        	//실제 대상 호출
        	target.logic();
        	transactionManager.commit(status); //성공시 커밋
        } catch (Exception e) {
        	transactionManager.rollback(status); //실패시 롤백
        	throw new IllegalStateException(e);
        }
    }
}

- 서비스 로직

public class Service {
    public void logic() {
    	//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
    	bizLogic(fromId, toId, money);
    }
}

 

 

 

2) 트랜잭션 AOP 적용 

 

(1) MemberServiceV3_3 - 비즈니스 로직

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.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.sql.SQLException;

/**
 * 트랜잭션 - @Transactional AOP
 */
@Slf4j
public class MemberServiceV3_3 {

    private final MemberRepositoryV3 memberRepository;

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

    @Transactional
    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 {
        ...
    }
   ...
}
  • 순수 비즈니스 로직만 남기고, 트랜잭션 관련 코드는 모두 제거 
  • @Transactional 
    • 스프링이 제공하는 트랜잭션 AOP 적용
    • 이 어노테이션을 붙이면 스프링에서 트랜잭션 AOP를 적용하는 대상으로 인식 

 

 

(2) 테스트 

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
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.aop.support.AopUtils;
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;

/**
 * 트랜잭션 - @Transactional AOP
 */
@Slf4j
@SpringBootTest
class MemberServiceV3_3Test {

    // 상수 설정
    ...

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

    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_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());
        }
    }

    @Test
    void AopCheck() {
        log.info("memberService class={}", memberService.getClass());
        log.info("memberRepository class={}", memberRepository.getClass());
        Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }

    @Test
	...

}
  • @SpringBootTest
    • 스프링 AOP를 적용하려면 스프링 컨테이너 필요
      (위에서는 스프링 컨테이너 없이 그냥 바로 객체 생성해서 사용했던 것임)
    • 이 애노테이션을 붙이면 테스트 시 스프링 부트를 통해 스프링 컨테이너 생성
    • @Autowired 등을 통해 스프링 컨테이너가 관리하는 빈들을 사용할 수 있음 

  • @TestConfiguration
    • 테스트 안에서 내부 설정 클래스를 만들어 이 애노테이션을 붙이면
      스프링 부트가 자동으로 만들어주는 빈들에 추가로 필요한 스프링 빈들을 등록, 테스트 수행
    • @Transactional이 적용되려면 스프링 빈이어야 함
    • DataSource : 스프링에서 기본으로 사용할 데이터소스를 스프링 빈으로 등록
    • DataSourceTransactionManager : 트랜잭션 매니저를 스프링 빈으로 등록
      -> 스프링이 제공하는 트랜잭션 AOP스프링 빈에 등록된 트랜잭션 매니저를 찾아 사용하기 때문에 DataSource, TransactionManaver가 스프링 빈으로 등록되어 있어야 함

 

 

* AOP 프록시 적용 확인 

 

  • memberServlce : 프록시(CGLIB) 적용 -> 실제 서비스 클래스가 아니라 프록시 -> 여기서 실제 서비스 로직 호출
  • memberRepository : AOP를 적용하지 않았기 때문에 프록시 적용 X (@Transactional 적용 X)

 

 

(3) 트랜잭션 AOP 정리 

 

0. 계좌이체 로직 호출 

1. 실제 서비스 로직이 아니라 프록시 호출 

2. 스프링 컨테이너를 통해 트랜잭션 매니저 획득

3. 트랜잭션 시작 호출 

4. 데이터소스를 통해 커넥션 획득 

5. 수동 커밋 설정 -> 트랜잭션 시작 

6. 트랜잭션 동기화 매니저에 커넥션 보관 

7. 트랜잭션 처리 로직에서 실제 서비스 호출

8. 서비스 로직 실행 -> 데이터 접근 로직 호출

9. 트랜잭션 동기화 매니저에 보관되어 있는 커넥션 획득해서 실행

10. 트랜잭션 종료(커밋/롤백)

 

-> @Transactional을 붙이면 1 ~ 7 까지를 스프링 컨테이너가 자동으로 실행

 

 

선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리 

- 선언적 트랜잭션 관리 
: @Transactional 애노테이션 하나만 선언해서 트랜잭션 적용 -> 대부분 이렇게 사용 

- 프로그래밍 방식 트랜잭션 관리 
: 트랜잭션 매니저 or 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드 직접 작성
스프링 컨테이너나 스프링 AOP 기술 없이 간단히 사용하지만, 대부분 스프링 컨테이너를 사용하기 때문에 이 방식은 사용되지 않음

 

 

 

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

 

(1) 기존 테스트 코드 - 데이터소스, 트랜잭션 매니저를 스프링 빈으로 직접 등록 

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

 

 

 

(2) 스프링 부트 자동 등록

 

데이터 소스 자동 등록

  • 스프링 부트는 데이터소스(DataSource)를 스프링 빈에 자동으로 등록
  • 자동으로 등록되는 스프링 빈 이름 : dataSource
  • application.properties 에 있는 속성을 사용해서 DataSource 생성 -> 스프링 빈에 등록
  • 기존 코드처럼 개발자가 직접 데이터소스를 빈으로 등록하면 스프링 부트는 데이터소스를 자동으로 등록하지 않음
  • 스프링 부트가 기본으로 생성하는 데이터소스는 커넥션 풀을 제공하는 HikariDataSource

* application.properties 

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

 

 

트랜잭션 매니저 자동 등록 

  • 스프링 부트는 적절한 트랜잭션 매니저(PlatformTransantionManager)를 자동으로 스프링 빈에 등록
  • 자동으로 등록되는 스프링 빈 이름 : transactionManager
  • 개발자가 직접 트랜잭션 매니저를 빈으로 등록하면 스프링 부트는 트랜잭션 매니저를 자동으로 등록하지 않음
  • 어떤 트랜잭션 매니저를 선택하냐 -> 현재 등록된 라이브러리를 보고 판단
    -> JDBC 기술 사용 -> DataSourceTransactionManager 를 빈으로 등록,
    -> JPA 기술 사용 -> JpaTransactionManager 를 빈으로 등록
    -> 둘다 사용 -> JpaTransactionManager (제공 기능 범위가 더 넓음)

 

package hello.jdbc.service;

/**
 * 트랜잭션 - DataSource, TransactionManager 자동 등록
 */
@Slf4j
@SpringBootTest
class MemberServiceV3_4Test {

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

    @TestConfiguration
    static class TestConfig {

        private final DataSource dataSource;

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

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

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }
	... 
}
  • 데이터 소스, 트랜잭션 매니저를 스프링 빈으로 직접 등록하는 코드 삭제
  • 스프링 부트가 application.properties에 지정된 속성, 현재 등록된 라이브러리를 참고 -> 데이터소스, 트랜잭션 매니저 자동 등록
  • MemberRepository의 매개변수로 dataSource를 넘겨줘야 함
    -> 위 코드처럼 생성자를 통해 스프링 부트가 만들어준 데이터소스 빈 주입받음
    -> 또는, @Autowired private DataSource dataSource;