트랜잭션 변천사 - JDBC부터 Spring AOP까지

전홍영·2024년 12월 18일
0

Spring

목록 보기
26/26

트랜잭션에 관한 개념이나 특징 그와 관련된 내용은 작성했던 포스트에 정리해본 적이 있다. 트랜잭션에 대한 개념을 바탕으로 어떻게 트랜잭션이 작동하고 과거 JDBC부터 현재 Spring @Transactional까지 어떻게 변화되어져 왔고 왜 변화가 필요했는지에 대해 알아보자.

트랜잭션은 일단 하나의 흐름이라고 생각하면 된다. 때문에 애플리케이션 서버와 DB 간의 커넥션을 맺고 그 커넥션을 유지하는 것이 트랜잭션의 핵심이라고 생각된다. 예를 들어보자. 계좌이체를 하는 기능을 개발한다고 했을때 트랜잭션 없이 비즈니스 로직만 구현해보자.

public void accountTransfer(int fromAccountId, int toAccountId, int money) {
    Account fromAccount = accountRepository.findById(fromAccountId);
    Account toAccount = accountRepository.findById(toAccountId);

    accountRepository.update(fromAccountId, toAccount.getMoney() - money);
    if (fromAccount.getMoney() - money < 0) {
        throw new InsufficientBalanceException();
    }
    accountRepository.update(toAccountId, toAccount.getMoney() + money);
}

이렇게 서비스 계층 로직을 구현했다. 그리고 DB와 연동되는 레포지토리 계층을 구현했다. 아직 트랜잭션을 적용하지는 않고 JDBC를 이용한 커넥션 연결 SQL 전달 및 응답 결과를 받도록 구현하였다.

public class AccountRepositoryJDBC {
    private final DataSource dataSource;

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

    public Account save(Account account) {
        String sql = "insert into account(account_id, money) values (?,?)";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, account.getId());
            preparedStatement.setInt(2, account.getMoney());
            preparedStatement.executeUpdate();
            return account;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public Account findById(int accountId) {
        String sql = "select * from account where account_id = ?";

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

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, accountId);
            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Account account = new Account();
                account.setId(resultSet.getInt("account_id"));
                account.setMoney(resultSet.getInt("money"));
                return account;
            } else {
                throw new NotFoundAccountException();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, preparedStatement, resultSet);
        }
    }

    public void update(int accountId, int money) {
        String sql = "update account set money = ? where account_id = ?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setInt(2, accountId);
            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public void truncate() {
        String sql = "truncate table account";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, preparedStatement, null);
        }
    }

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

    private Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

이 코드에 대해서는 포스트를 참고하면 좋다. 이렇게 JDBC를 활용하여 코드를 구현했다. 그럼 테스트를 통해 잘 작동하는지 검증해보자.

@Test
void account_transfer_success() {
    //given
    accountRepository.save(ACCOUNT1);
    accountRepository.save(ACCOUNT2);
    //when
    accountTransferService.accountTransfer(ACCOUNT1.getId(), ACCOUNT2.getId(), 5000);
    //then
    Account fromAccount = accountRepository.findById(ACCOUNT1.getId());
    Account toAccount = accountRepository.findById(ACCOUNT2.getId());

    assertThat(fromAccount.getMoney()).isEqualTo(5000);
    assertThat(toAccount.getMoney()).isEqualTo(15000);
}

새로운 계좌를 두 개 생성하고 계좌1에서 계좌2로 돈을 이체하고 이체가 성공적으로 이루어졌는지 검증했다. 만약 이체하는 과정에서 오류가 발생하면 어떻게 될까? 그러면 어떤 SQL은 성공하고 어떤 SQL은 실패되어 데이터의 정합성에 문제가 발생할 수 있고 이는 곧 서비스에 큰 장애가 발생한다는 의미이다.

@Test
void account_transfer_fail() {
    //given
    accountRepository.save(ACCOUNT1);
    accountRepository.save(ACCOUNT2);
    //when
    assertThatThrownBy(() -> accountTransferService.accountTransfer(ACCOUNT1.getId(), ACCOUNT2.getId(), 50000))
            .isInstanceOf(InsufficientBalanceException.class);

    assertThat(accountRepository.findById(ACCOUNT1.getId()).getMoney()).isEqualTo(-40000);
    assertThat(accountRepository.findById(ACCOUNT2.getId()).getMoney()).isEqualTo(10000);
}

이 테스트 코드를 보면 계좌 이체도중에 오류가 발생하였지만 계좌1에서는 돈이 빠져나갔는데 계좌2에는 돈이 입금이 안되었다. 이렇게 데이터 정합성이 깨지는 일이 생기는 것이다.

이러한 일을 방지하는 것이 바로 트랜잭션이다. 트랜잭션의 특징 중 하나가 원자성인데 원자성은 모두 성공하거나 모두 실패한다는 뜻이다. 즉, 어떤 SQL이 실패하면 모든 SQL은 실패하도록 만들어 다시 원상태로 되돌리거나 모든 SQL을 성공시켜 저장하도록하는 것이 트랜잭션이다. 트랜잭션을 이용해서 위의 문제점을 해결할 수 있다.

그럼 트랜잭션은 어떻게 이루어지는지 알아보자.

트랜잭션 흐름

위의 문제점을 해결하기 위해서는 트랜잭션이 필요하다고 했다. 왜 필요하냐면 계좌 이체하는 과정에서 오류가 발생하면 어떤 계좌에서는 돈이 빠져나가고 어떤 계좌에서는 돈이 빠져나가지 않는 것은 굉장히 큰 오류이다. 따라서 오류 발생시 모두 실패하여 원상태로 되돌려야 할 것이다. 이 역할이 트랜잭션의 역할이다.

따라서 클라이언트가 서버에 요청을 하면 비즈니스 로직이 존재하는 서비스 계층에서 DB와 커넥션을 생성하고 트랜잭션을 시작해야 한다. 비즈니스 로직을 커넥션을 유지한 채로 수행을 하고 성공적으로 비즈니스 로직을 처리했으면 커밋을 하고 실패했으면 롤백을 실행하여 원상태로 복귀시켜야 한다. 이렇게 커밋이나 롤백을 수행한후 트랜잭션을 종료하고 커넥션을 반납해야 한다. 이때 중요한 포인트가 비즈니스 로직은 한 트랜잭션 안에서 이루어져야 한다는 점이다. 즉, 트랜잭션을 사용하는 동안에는 같은 커넥션을 유지한다는 의미이다.

코드를 통해 트랜잭션이 어떻게 이루어지는지 보자.

public class AccountTransferServiceWithTransaction {
    private final AccountRepositoryWithTransaction accountRepository;
    private final DataSource dataSource;


    public AccountTransferServiceWithTransaction(AccountRepositoryWithTransaction accountRepository, DataSource dataSource) {
        this.accountRepository = accountRepository;
        this.dataSource = dataSource;
    }

    public void accountTransfer(int fromAccountId, int toAccountId, int money) throws SQLException {
    (1) Connection connection = dataSource.getConnection();
        try {
        (2) connection.setAutoCommit(false);

        (3) accountTransferLogic(fromAccountId, toAccountId, money, connection);

        (4) connection.commit();
        } catch (Exception e) {
        (5) connection.rollback();
            throw e;
        } finally {
        (6) release(connection);
        }
    }

    private void accountTransferLogic(int fromAccountId, int toAccountId, int money, Connection connection) {
        Account fromAccount = accountRepository.findById(connection, fromAccountId);
        Account toAccount = accountRepository.findById(connection, toAccountId);


        if (fromAccount.getMoney() - money < 0) {
            throw new InsufficientBalanceException();
        }

        accountRepository.update(connection, fromAccountId, toAccount.getMoney() - money);
        accountRepository.update(connection, toAccountId, toAccount.getMoney() + money);
    }

    private void release(Connection connection) {
        if (connection != null) {
            try {
                connection.setAutoCommit(true);
                connection.close();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
    }
}

이 코드는 서비스 계층의 비즈니스 로직이다. (1)을 보면 비즈니스 로직을 처리하는 메서드 accountTranfer() 안에서 커넥션을 생성하는 것을 볼 수 있다. 서비스 계층에서 커넥션을 생성하는 이유는 비즈니스 로직에는 수 많은 데이터 관련 메서드가 실행될 수 있고 이를 트랜잭션으로 묶기 위해서는 하나의 커넥션을 공유해야 한다. 따라서 서비스 계층에서 생성한 커넥션을 계속 유지하기 위해서 레포지토리 계층에 커넥션을 전달하여 사용할 수 있게하기 위해서는 서비스 계층에서 커넥션을 생성하여야 한다.

(2)을 보면 커넥션의 autoCommit을 false로 설정해준다. 커넥션은 디폴트로 autoCommit이 켜져있다. 이러면 하나의 SQL을 성공하면 바로 DB에 적용시키기 때문에 후에 롤백을 시킬 수 없어진다. 따라서 autoCommit을 끄고 DB에 적용시키지 않은채로 비즈니스로직을 처리해야 한다.

(3)은 핵심 비즈니스 로직이다. 이 로직 안에서는 많은 SQL 문을 전달하고 응답 결과를 받아와서 조립한다. 이 메서드의 코드를 보면 레포지토리 계층 메서드에 Connection을 매개변수로 전달하는 것을 볼 수 있다. 앞서 말했듯이 하나의 트랜잭션을 유지하기 위해 하나의 커넥션을 공유하는 것으로 커넥션을 매개변수로 전달하여 처리할 수 있도록 되어있다. 여기서 중요한 부분이 레포지토리 계층 메서드에서는 매개변수로 받은 커넥션을 close()해서는 안된다. 매개변수로 받은 커넥션은 종료시켜 반환하게 되면 해당 트랜잭션이 종료되어 원자성이 깨지게 된다.

이렇게 비즈니스 로직을 성공적으로 실행하게 되면 (4)을 보면 알 수 있듯이 커밋을 수행하여 DB에 적용시킨다. 만약, 실패하거나 오류가 발생하면 (5)처럼 롤백시켜 원상태로 복귀시키다. 커밋을 하거나 롤백을 하면 해당 트랜잭션을 끝내고 다시 autoCommit을 활성화한 후 커넥션을 반납한다. 간단히 다시보면 클라이언트가 요청하면 커넥션을 생성하고 트랜잭션을 시작한다. 그 후 비즈니스 로직을 수행하면 커밋을 하고 비즈니스 로직 수행에 오류가 발생하면 롤백을 하고 커넥션을 반납하는 트랜잭션의 흐름을 알 수 있다.

@Test
void account_transfer_fail_with_transaction() {
    //given
    accountRepository.save(ACCOUNT1);
    accountRepository.save(ACCOUNT2);
    //when
    assertThatThrownBy(() -> accountTransferService.accountTransfer(ACCOUNT1.getId(), ACCOUNT2.getId(), 50000))
            .isInstanceOf(InsufficientBalanceException.class);
    //then
    Account fromAccount = accountRepository.findById(ACCOUNT1.getId());
    Account toAccount = accountRepository.findById(ACCOUNT2.getId());

    assertThat(fromAccount.getMoney()).isEqualTo(10000);
    assertThat(toAccount.getMoney()).isEqualTo(10000);
}

이렇게 트랜잭션을 활용하면 오류가 발생해도 롤백이 되기 때문에 모든 SQL이 실패하게 된다. 그러나 이러한 코드들에는 문제가 크게 3가지 존재한다.

첫번째는 트랜잭션을 적용하면서 생기는 문제점 들이다. JDBC 구현 기술이 서비스 계층의 코드에 등장함으로써 서비스 계층이 JDBC 기술에 의존하게 된다. 위의 서비스 계층 코드를 보면 커넥션을 생성할때 DataSource에서 커넥션을 획득하는데 이때 JDBC의 구현기술이 사용된다. 서비스 계층 코드는 순수한 코드로 남아있는 것이 좋다. 왜냐하면 레포지토리 계층 구현 기술이 변경되어도 서비스 계층에는 영향을 주지 않아야 유지보수하기에도 좋고 변화에 대응하기가 좋다. 기존에는 레포지토리 계층에 JDBC 관련 코드를 작성했는데 트랜잭션을 적용하면서 JDBC 구현 기술의 누수가 발생했다. 또한 트랜잭션을 유지하기 위해서 커넥션을 파라미터로 넘기는데 굳이 트랜잭션을 유지하지 않아도 되는 기능도 있을 수 있고 반복되는 트랜잭션 코드들도 많이 존재한다.

두번째는 JDBC의 구현기술의 예외가 서비스 계층으로 전파된다. SQLException 체크 예외이기 때문에 레포지토리 계층을 호출한 서비스 계층에서 해당 예외를 처리해주거나 throws를 통해 다시 밖으로 던져야 한다. SQLException은 JDBC 구현기술의 예외이기 때문에 JDBC를 외의 다른 기술로 변경할 때 서비스 계층 코드도 수정해주어야 한다는 문제점이 생긴다.

세번째는 반복되는 JDBC 코드이다. 레포지토리 계층의 JDBC 코드를 보면 같은 코드가 반복되는 것을 볼 수 있다. 커넥션 생성, try-catch-finally, close가 모든 메서드마다 반복된다. 이러한 반복되는 코드의 양이 많아질 수록 유지보수하기가 까다로워지고 오류가 발생할 가능성이 높아진다.

이렇게 트랜잭션을 적용하면서 생기는 문제점을 해결해 보자.

TransactionManager를 이용하여 트랜잭션 추상화

트랜잭션을 적용하면서 가장 큰 문제는 서비스 계층 코드가 특정 기술에 의존한다는 문제점이다. 트랜잭션 생각해보면 단순한 흐름이다. 트랜잭션을 시작하고, 비즈니스 로직을 수행하고 성공하면 커밋, 실패하면 롤백하는 것이다. 모든 트랜잭션을 구현하고 있는 기술이라면 해당 기능을 다 구현하고 있을것이다. 그럼 트랜잭션을 추상화한다면 서비스 계층이 이 추상화에 의존하여 DI를 통해 서비스 코드를 수정하지 않고, 트랜잭션 기술을 사용할 수 있을 것이다.

서비스 계층은 트랜잭션 추상화 인터페이스에 의존하고 원하는 구현체만 DI를 통해 주입하여 사용하기만 하면된다. Spring은 이러한 기능을 제공해준다. 트랜잭션 추상화 인터페이스인 PlatformTransactionManager에 의존하면 원하는 기술의 TransactionManager만 주입하여 사용하면 된다.

또한 이 TransactionManager는 트랜잭션 동기화 매니저를 통해서 커넥션을 유지시켜 트랜잭션을 수행할 수 있게 해준다. 기존에는 커넥션을 서비스 계층에서 생성해서 레포지토리 계층에 넘겨주었는데 TransactionManager는 커넥션을 생성한 후 트랜잭션이 시작하면 해당 커넥션을 트랜잭션 동기화 매니저에 보관한다. 레포지토리는 이렇게 저장되어 있는 커넥션을 동기화 매니저에서 꺼내서 사용하게된다. 이로인해 커넥션을 파라미터로 전달해주지 않아도 커넥션이 유지되도록 해준다. 트랜잭션이 종료되면 TransactionManager가 동기화 매니저에 보관중인 커넥션을 닫는다.

코드를 보자.

public class AccountTransferServiceWithTransactionManager {
    private final PlatformTransactionManager transactionManager;
    private final AccountRepositoryWithDataSourceUtils accountRepository;

    public AccountTransferServiceWithTransactionManager(PlatformTransactionManager transactionManager, AccountRepositoryWithDataSourceUtils accountRepository) {
        this.transactionManager = transactionManager;
        this.accountRepository = accountRepository;
    }

    public void accountTransfer(int fromAccountId, int toAccountId, int money) throws SQLException {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            accountTransferLogic(fromAccountId, toAccountId, money);

            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }

    private void accountTransferLogic(int fromAccountId, int toAccountId, int money) {
        Account fromAccount = accountRepository.findById(fromAccountId);
        Account toAccount = accountRepository.findById(toAccountId);

        if (fromAccount.getMoney() - money < 0) {
            throw new InsufficientBalanceException();
        }

        accountRepository.update(fromAccountId, toAccount.getMoney() - money);
        accountRepository.update(toAccountId, toAccount.getMoney() + money);
    }
}

기존의 커넥션을 생성하여 커넥션을 파라미터로 넘겨주면서 커넥션을 유지했다. 하지만 TransactionManager를 주입받아 커넥션을 유지하도록 구현했다. accountTransfer()을 보면 transactionManager가 getTransaction()을 호출하여 트랜잭션을 시작했다. 그리고 비즈니스 로직을 처리하였다. 이때 파라미터로 더이상 커넥션을 넘기지 않아도 트랜잭션 매니저가 동기화 매니저에 커넥션을 보관해두고 이를 레포지토리 계층에서 꺼내와 사용하도록 하였다. 레포지토리 계층 코드를 보자.

public class AccountRepositoryWithDataSourceUtils {
    private final DataSource dataSource;

    public AccountRepositoryWithDataSourceUtils(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public Account findById(int accountId) {
        String sql = "select * from account where account_id = ?";

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

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, accountId);
            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Account account = new Account();
                account.setId(resultSet.getInt("account_id"));
                account.setMoney(resultSet.getInt("money"));
                return account;
            } else {
                throw new NotFoundAccountException();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, preparedStatement, resultSet);
        }
    }

    /**
    
    생략
    
    **/

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

    private Connection getConnection() throws SQLException {
        return DataSourceUtils.getConnection(dataSource);
    }
}

여기서 getConnection()을 보면 DataSourceUtils에서 커넥션을 꺼내온다. 이는 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환하고 없으면 새로운 커넥션을 반환한다. 그래서 반환된 커넥션을 활용하고 이 커넥션을 다시 동기화 매니저에 반납해야 하는데 이 역할을 하는 것이 close() 메서드의 DataSourceUtils.releaseConnection()이다. 이는 트랜잭션을 사용하기 위해 동기화된 커넥션은 닫지 않고 유지시켜주고 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.

이렇게 비즈니스 로직이 끝나면 트랜잭션은 커밋이나 롤백으로 트랜잭션을 종료한다. 이때 동기화 매니저에서 동기화된 커넥션을 획득하여 DB에 트랜잭션을 커밋하거나 롤백한 후 리소스를 정리하게 된다.

TransactionManager를 통해 서비스 계층 코드가 JDBC 구현기술에 의존하지 않고 추상화에 의존하면 JDBC가 아닌 다른 기술로 변경하더라도 서비스 코드는 그대로 유지할 수 있다. 하지만 아직 반복되는 try-catch는 남아있다. 이를 해결하기 위해서 등장한 것이 TransactionTemplate이다.

TransactionTemplate으로 반복되는 코드 생략

Spring에서 제공하는 TransactionTempalte을 이용하면 try-catch-finally, commit, rollback을 생략할 수 있다. TransactionTempalte은 템플릿 콜백 패턴을 활용하여 중복되는 코드를 생략하는 방식인데 이는 포스트에 작성되어 있으니 참고바란다.

바로 코드를 통해서 어떻게 TransacitonTemplate이 적용됬는지 알아보자ㅏ.

//TransactionTemplate 코드
public void accountTransfer(int fromAccountId, int toAccountId, int money) {
    transactionTemplate.executeWithoutResult((transactionStatus ->
    try{
    	accountTransferLogic(fromAccountId, toAccountId, money)));
    } catch (SQLException e) {
    	throw new IllegalStateException(e);
    }
}

//transactionManager 코드
public void accountTransfer(int fromAccountId, int toAccountId, int money) throws SQLException {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

    try {
        accountTransferLogic(fromAccountId, toAccountId, money);

        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}

이렇게 TransactionTemplate을 이용하여 훨씬 간단하게 구현이 가능해진다. TransactionTempalte 클래스를 사용하기 위해서는 TransactionManager를 필요하기 때문에 트랜잭션 매니저와 동기화 매니저가 동작하는 것은 같고 트랜잭션 시작, 커밋, 롤백 코드가 모두 제거 되었다.

트랜잭션 템플릿은 비즈니스 로직이 정상 수행되면 커밋되고 언체크 예외가 발생되면 롤백된다. 이때 SQLException은 체크 예외이기 때문에 람다에서는 체크 예외를 던질 수 없어 try-catch로 묶어서 체크 예외를 언체크 예외로 변경하여 던지도록 하였다. 이로 인해 반복되는 코드를 제거하기는 했으나 여전히 핵심 비즈니스 로직외에 트랜잭션 코드가 남아있다. 이는 SRP 원칙을 깨는 것으로 두 관심사를 하나의 클래스에서 처리하게 된다. 이를 해결할 방법은 Spring AOP를 이용하는 것이다.

Spring AOP - @Transactional

지금까지 트랜잭션을 편리하게 처리하기 위해서 TrnasacitonManager, TransactionTemplate을 도입했지만 아직도 순수한 비즈니스 로직만을 남겨둘 수는 없었다. 하지만 AOP를 이용한다면 비즈니스 로직만 남길 수 있게된다. AOP가 무엇인지는 포스트를 참고바란다.

AOP는 프록시를 이용해서 공통 기능을 추가하는 역할이라고 생각하면 된다. 직접 트랜잭션에 처리용 AOP를 구현한다면 좋겠지만 Spring에서는 이미 트랜잭션 처리용 AOP를 제공한다. 개발자는 이 @Transactional 애테이션만 메서드나 클래스에 붙여준다면 Spring이 해당 애노테이션을 인식해서 트랜잭션 프록시를 적용시켜준다.

//@Transactional 코드
@Transactional
public void accountTransfer(int fromAccountId, int toAccountId, int money) throws Exception {
    accountTransferLogic(fromAccountId, toAccountId, money);
}

//TransactionTemplate 코드
public void accountTransfer(int fromAccountId, int toAccountId, int money) {
    transactionTemplate.executeWithoutResult((transactionStatus ->
    try{
    	accountTransferLogic(fromAccountId, toAccountId, money)));
    } catch (SQLException e) {
    	throw new IllegalStateException(e);
    }
}

이렇게 @Transactional만 붙이면 순수한 핵심 비즈니스 로직만 남고 트랜잭션에 관련된 코드가 전부 제거되는 것을 볼 수 있다. 그러면 어떻게 작동하는 걸까? 그림을 보고 설명하자.

사실 위의 TransactionManager와 AOP를 이해했다면 간단하다. 트랜잭션이라는 공통 기능을 핵심 기능과 분리해서 프록시를 통해 실행한것이다. 클라이언트가 요청을 하면 프록시가 호출되어 트랜잭션이 시작될 것이다. 그러면 트랜잭션 매니저가 커넥션을 생성하고 트랜잭션 동기화 매니저에 보관하게된다. 그후 실제 비즈니스 로직을 호출하면서 트랜잭션 동기화 매니저에 보관중이 커넥션을 통해서 DB에 접근한다. 비즈니스 로직을 성공적으로 처리하면 커밋을 하고 트랜잭션 매니저는 동기화 매니저에 보관중인 커넥션을 종료하고 트랜잭션을 종료한다. 이렇게 @Transactional을 붙임으로써 TransactionManager가 하는 역할을 알아서 Spring이 처리하도록 만들어 서비스 계층에는 핵심 비즈니스 로직만 남도록 하였다.

이렇게 JDBC에서 트랜잭션을 어떻게 적용하는지 부터 TransactionManager, TransactionTemplate, Spring AOP까지 문제점을 하나하나 해결해나가면서 어떻게 트랜잭션이 동작하는지 알아보았다. 사실 @Transactional을 붙여서 사용하기만했지 어떻게 트랜잭션이 동작하고 관리되어 지는 알지 못했다. 이렇게 트랜잭션이 어떻게 동작하는지 알아보니깐 트랜잭션에 관련된 내용도 좀 더 이해하기 쉬워질 수도 있겠다는 생각이 든다.

참고

profile
Don't watch the clock; do what it does. Keep going.

0개의 댓글