티스토리 뷰

트랜잭션 서비스 추상화

트랜잭션


웹 커뮤니티에서는 조건에 맞는 모든 사용자의 등급을 업그레이드 시키는 것과 같은 작업이 있다. 만약 사용자의 등급을 업그레이드 시키는 과정에서 예외가 발생하여, 업그레이드가 완료되지 않은 사용자가 존재한다면 불만을 표출할 수 있을 것이다.

따라서 커뮤니티 사용자들의 등급을 업그레이드 시키는 것은 논리적인 하나의 작업단위로 보고, 전체가 다 성공하던지 하나라도 실패하면 모두 실패하던지 해야한다. 즉 사용자들의 등급 업그레이드는 나눌 수 없는 원자적인 작업이라는 것이다. 이러한 더이상 나눌 수 없는 논리적인 작업 단위를 트랜잭션이라한다.

트랜잭션의 경계


DB는 그 자체로 완벽한 트랜잭션을 지원한다. SQL을 통해 다수 레코드의 수정 및 삭제를 위한 요청을 했을 때, 일부만 수정되거나 삭제되는 경우 없이 모두 성공하거나 모두 실패한다. 하지만 문제는 여러 SQL을 통해 데이터베이스의 상태를 변경할 때 발생한다.

만약 계좌 이체를 위해 A 계좌에서 돈을 이체해 B 계좌에 이체된 금액을 기입하는 과정에서 예외가 발생한다면, A 계좌에서는 금액이 감소했지만 B 계좌에서는 금액이 증가하지 않을 수 있다. 이로 인해 데이터 베이스의 일관성이 무너지게된다. 따라서 두 가지 작업을 하나의 트랜잭션으로 보고, 한 작업이라도 실패한 경우 모든 작업을 취소해야 하는데 이를 롤백(Rollback)이라 한다. 그리고 트랜잭션의 모든 작업이 성공했을 때 이를 DB에 반영해야 하는데 이를 커밋(Commit)이라 한다.

JDBC 트랜잭션의 경계 설정

JDBC의 트랜색션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문이다. JDBC의 트랜잭션 경계 설정 방법은 아래와 같다.

Connection c = dataSource.getConnection();

c.setAutoCommit(false); // 트랜잭션 경계 시작
try {
    // 데이터 베이스의 상태를 변경하는 코드 1
    // 데이터 베이스의 상태를 변경하는 코드 2
    // 데이터 베이스의 상태를 변경하는 코드 3
    ...
    c.commit() // 트랜잭션 성공 후 커밋
} catch (Exception e) {
    c.rollback(); // 트랜잭션 실패 후 롤백
}
c.close();

이렇게 setAutoCommit()을 통해 트랜잭션 경계를 설정하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다. 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 것도 기억하자. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고 한다.

트랜잭션 동기화

순수한 JDBC API를 통해 트랜잭션의 경계를 설정하려면, 비즈니스 로직이나 DAO에 커넥션 오브젝트를 인자로 전달 할 수 밖에 없고 더이상 JdbcTemplate를 사용할 수 없게된다. 따라서 스프링은 트랜잭션 동기화를 제안한다. 트랜잭션 동기화는 비즈니스 로직에서 트랜잭션을 시작하기 위해 만들어진 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서 저장된 Connection 객체를 가져다 사용하게 하는 것이다. 그리고 트랜잭션이 모두 종료되면 동기화를 마친다.

  1. UserService는 Connections을 생성한다
  2. 생성한 Connection을 저장소(TransactionSynchronizations)에 저장한 후, setAutoCommit(false)를 호출해 트랜잭션을 시작한 후 DAO의 기능을 수행한다
  3. 첫 번째 update() 메소드가 실행되고
  4. 가장 먼저 저장소의 Connection 오브젝트가 존재하는 지 확인한다.
  5. Connection을 사용해 PrepareStatement를 만들어 수정하는 SQL을 실행한다. Connection을 닫지 않은 채로 작업을 마무리한다
  6. 7, 8, 9, 10, 11 모두 동일한 작업을 수행한다
  7. Connection의 commit()을 호출해서 작업 결과를 DB에 반영한다
  8. 트랜잭션 저장소가 더 이상 Connection 오브젝트를 저장하지 않도록 제거한다

트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 멀티스레드 환경에서도 충돌이 날 염려가 없다.

public void upgradeLevels() throws Exception {
        TransactionSynchronizationManager.initSynchronization();
        Connection connection = DataSourceUtils.getConnection(dataSource);
        connection.setAutoCommit(false);
        try {
            List<User> users = userDao.getAll();

            for (User user : users) {
                if (canUpgradeLevel(user))
                    upgradeLevel(user);
            }
            connection.commit(); // 정상적으로 작업 마치면 커밋
        } catch (Exception e) {
            connection.rollback(); // 작업 실패하면 롤백
            throw e;
        } finally {
            DataSourceUtils.releaseConnection(connection, dataSource); // DB 커넥션을 안전하게 닫는다
            TransactionSynchronizationManager.unbindResource(this.dataSource); // 동기화 작업 종료 및
            TransactionSynchronizationManager.clearSynchronization(); // 저장소 정리
        }
    }

기술과 환경에 종속되는 트랜잭션 경계설정 코드

JDBC의 Connections을 활용한 트랜잭션 경계설정은 DB에 종속적이다. 이러한 트랜잭션 방식을 로컬 트랜잭션이라한다. 따라서 다양한 DB를 사용할 경우에는 적용할 수 없다. 이럴 경우 Connection을 통한 트랜잭션 관리가 아닌 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식을 사용해야한다.

글로벌 트랜잭션 방식에는 다양한 기술이 있지만, 결과적으로 UserService가 UserDao의 구현체에 의존하는 결과를 가져온다. 따라서 트랜잭션 관리 방식의 추상화를 통해 결합도를 낮춰야한다. 스프링은 트랜잭션 경계 설정을 위한 추상 인터페이스로 PlatformTransactionManager를 제공한다.

public void upgradeLevels() {
    PlatformtransactionManager transactionManager = new DataSourceTransactionManager(datasource);

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

    try {
        List<User> users = userDao.findAll();

        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeLevel(user);
            }
        }

        transactionManger.commit(status); // 트랜잭션 커밋
    } catch (RuntimeException e) {
        transactionManager.rollback(status);
        throw e;
    }
}
  • DefaultTransactionDefinition : 트랜잭션의 속성을 담고있다.

하지만 코드는 아직도 JDBC에 종속적이다. PlatformTransactionManager의 구현체를 직접 생성하고 있기 때문이다. 객체지향의 원칙을 지키기 위해 외부에서 DI를 해 줄 필요가 있다.

서비스 추상화와 단일 책임 원칙


서비스 추상화

추상화 기법을 이용하면 특정 기술 환경에 종속되지 않는 포터블한 코드를 만들 수 있다. 추상화를 통해 서로 불필요한 영향을 주지 않으면서 독자적으로 확장이 가능해지는 것이다. 같은 애플리케이션의 로직을 담은 코드지만 내용에 따라 분리하는 것을 같은 계층에서 분리 했다 하여 수평적 분리라고 한다. 반면에, 트랜잭션 추상화는 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리했는데, 이를 수직적 분리라고한다.

수직적, 수평적 분리를 통해 코드를 추상화하여 서로 결합도를 낮추며 관련성이 높은 코드를 모아 응집도를 높인다. 이렇게 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는데는 스프링의 DI가 중요한 역할을 한다. DI의 가치는 이렇게 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는데 있다.

단일 책임 원칙

적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중 하나인 단일 책임 원칙으로 설명할 수 있다. 단일 책임 원칙은 하나의 모듈은 한 가지 책임을 가져야한다는 의미이며, 하나의 모듈이 바뀌는 이유는 한 가지여야 한다고 설명할 수 있다. 단일 책임 원칙을 따르면 어떤 변경이 발생했을 때 수정 사항이 명확해진다.

객체지향 설계와 프로그래밍의 원칙은 서로 긴밀하게 연결되어있다. 단일 책임 원칙을 잘 지키는 코드를 만드려면 인터페이스를 도입하고 이를 DI로 연결해야 하며, 그 결과로 단일 책임 원칙뿐 아니라 개방 폐쇄 원칙도 잘 지키고, 모듈 간에 결합도가 낮아서 서로의 변경이 영향을 주지 않고, 같은 이유로 단일 책임에 응집도 높은 코드가 나온다. 그리고 그 과정에서 다양한 디자인 패턴이 적용되고 테스트가 쉬워진다.

하지만 패턴이나 원칙은 달달 외운다고 적용되는 것이 아니다. 좋은 코드를 만들기 위한 고민과 시행착오를 통해 만들어진 것이 패턴과 원칙이다

728x90

'Backend' 카테고리의 다른 글

MySQL과 시간차 발생 해결  (0) 2021.09.24
[Spring] Spring MVC 구조  (0) 2021.09.14
[Spring] Spring MVC - Servlet  (0) 2021.09.07
[Spring] Spring MVC - HTTP, Web Server, WAS, Servlet  (0) 2021.09.02
[토비의 스프링 3.1] 4장 예외  (0) 2021.08.31
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함