스프링 DB1 - 스프링 트랜잭션

2023. 1. 27. 17:45spring/db

1/27

 

* 기본 애플리케이션 구조

- 프레젠테이션 계층 (UI 관련, 웹 요청과 응답)

- 서비스 계층(비즈니스 로직)

- 데이터 접근 계층 

 

* 서비스 계층은 순수 자바코드로 작성되어야 한다.

-> 컨트롤러나 레포지토리가 변경되도 서비스는 상관없어야한다. (유지보수)

 

-  기존 JDBC 문제점

이와 같은 패턴을 트랜잭션 사용 로직의 개수에 따라서 반복해야 한다.

throws SQLException 은 JDBC 전용 예외이다. 따라서 JPA 등으로 변경시 예외 코드도 모두 변경해줘야 한다.

 

* 스프링의 해결

 

 

 

* 레포지토리 부분 수정

JdbcUtils.closeConnection(con);
DataSourceUtils.releaseConnection(con, dataSource);

- 트랜잭션 사용시 DataSourceUtils.releaseConnections은 바로 닫지 않는다.

 

* 서비스 부분 수정

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

- 트랜잭션 연결 부분이 모두 빠지고 commit, rollback만 할 수 있게끔 바뀌었다.

 

private final PlatformTransactionManager transactionManager;

 서비스에서 트랜잭션 매니저를 주입 받는다. (JDBC, JPA 등)

 

* 트랜잭션 동작 순서

 

* 트랜잭션 시작

DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV3(dataSource);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberService = new MemberServiceV3_1(transactionManager, memberRepository);
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 500);

 

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

transaction.getTransaction() -> 커넥션을 얻기 위해 데이터 소스를 사용해서 커넥션을 생성

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

 

* 비즈니스 로직 시작

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

각각 리포지토리의 함수에서 데이터베이스와 연결하는 로직이 포함되어 있다.

리포지토리에서 데이터베이스와 연결하는 함수를 호출할 때 < DataSourceUtils.getConnection() >

트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.

 

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();
private Connection getConnection() throws SQLException {
    //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
    Connection con = DataSourceUtils.getConnection(dataSource);
    log.info("get connection={}, class={}", con, con.getClass());
    return con;
}

 

* 트랜잭션 종료

bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋
} catch(Exception e){
    transactionManager.rollback(status); //실패시 롤백

비즈니스 로직이 끝나고 트랜잭션이 커밋하거나 롤백을 호출하며 종료한다.

그러면 동기화 매니저를 통해 동기화된 커넥션을 획득한다.

획득한 커넥션을 통해 실제 데이터베이스에 커밋하거나 롤백한다.

 

DataSourceUtils.releaseConnection(con, dataSource);

커넥션을 자동 커밋으로 변경하고, 커넥션을 종료한다.

 

==============================================================================

 

트랜잭션 매니저를 사용함에 불구하고 비즈니스 로직을 사용할때마다 성공하면 commit 실패하면 rollback 이라는

반복적인 코드를 제거하지 못했다.

반복적인 코드를 제거하기 위해서 트랜잭션 탬플릿을 알아보자.

 

* 트랜잭션 매니저 -> 트랜잭션 탬플릿 (탬플릿 콜백 패턴)

@RequiredArgsConstructor
public class MemberServiceV3_1 {

//    private final DataSource dataSource;
    private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;
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;
    }

TransactionTemplate 는 생성자로 (PlatformTransactionManager) 를 받는다.

 

* 비즈니스 로직 변경

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

}

- 트랜잭션 탬플릿은 비즈니스 로직이 정상 수행되면 자동 커밋한다.

- 언체크 예외가 발생하면 롤백한다. (체크 예외는 커밋한다.)

 

==============================================================================

 

반복적인 코드 문제는 해결했다.

하지만 서비스 계층의 클래스에서 비즈니스 로직만 남기기 위해서 스프링 AOP를 통해 프록시를 도입해야 한다.

 

* @Transactional -> 스프링 AOP

 

* 트랜잭션 등록 방법 변경

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

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

비즈니스 로직에서 트랜잭션과 관련된 로직을 전부 지우고 스프링이 제공하는

@Transactional을 등록한다. 

 

하지만 이것만 바꿔서는 테스트가 정상적으로 실행되지 않는다.

해당 클래스는 @Transactional 이라는 어노테이션을 썻는지 모르기 때문에

 

* SpringBootTest 등록 (스프링 컨테이너 생성)

@SpringBootTest
class MemberServiceV3_3Test {
@SpringBootTest 를 등록하면 @Autowired를 통해 스프링 컨테이너가 관리하는 빈을 사용할 수 있다.

 

* @TestConfiguration(Test 내부 설정 클래스)

@TestConfiguration
static class TestConfig{
    @Bean
    DataSource dataSource(){
        return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }
    @Bean
    MemberRepositoryV3 memberRepositoryV3(){
        return new MemberRepositoryV3(dataSource());
    }
    @Bean
    PlatformTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dataSource());
    }
    @Bean
    MemberServiceV3_3 memberServiceV3_3(){
        return new MemberServiceV3_3(memberRepositoryV3());
    }
}

 

-  기본으로 사용할 DataSource (DriverManagerDataSource) 등록

- 트랜잭션을 관리해줄 PlatformTransactionManager (DataSourceTransactionManager) 등록

- 사용할 리포지토리, 서비스 등록

 

* 스프링 부트는 자동으로 DataSource와 TransactionManager 를 등록해준다.

(application.properties에 속성 추가 )

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

 

* 트랜잭션 전체 구조