ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장) 5.2 트랜잭션 서비스 추상화
    Java & Spring/토비의 스프링 3.1 2021. 6. 26. 18:12
    반응형

    5장 서비스 추상화

    5.2 트랜잭션 서비스 추상화

    • 트랜잭션

      • 더 이상 나눌 수 없는 단위 작업
      • 트랜잭션 커밋
        • 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시키는 작업
        • 변경 내용이 DB에 반영되도록 설정하는 작업
      • 트랜잭션 롤백
        • SQL 수행 작업 중 뒤 차례의 수행에 문제가 발생한 경우에 앞에서 처리한 SQL 수행 작업도 취소시키는 작업
        • DB에 변경 내용을 변경 이전으로 되돌리는 작업
    • 트랜잭션 경계 설정

      • 트랜잭션 경계

        • 트랜잭션이 시작되고 끝나는 위치
        • transaction 시작 선언 이후, commit() or rollback() 으로 트랜잭션을 종료하는 작업
      • JDBC 트랜잭션의 트랜잭션 경계 설정

        • 하나의 Connection을 사용하다가 닫는 사이에 일어남
        • 트랜잭션의 시작과 종료가 Connection 오브젝트를 통해 이루어짐
        • 자동 커밋 옵션을 false로 만들어주어 트랜잭션을 시작
        • 로컬 트랜잭션
          • 하나의 DB 커넥션 안에서 만들어지는 트랜잭션
      • UserService와 UserDao의 트랜잭션 문제

        • 일반적으로 트랜잭션은 커넥션보다 라이프 사이클이 짧음
        • JdbcTemplate 메소드를 사용하는 UserDao는 메소드마다 하나씩 독립적인 트랜잭션으로 실행됨
        • 예시
          • upgradeLevels() 메소드에서 세 번에 걸쳐 UserDao의 update()를 호출
          • 매번 새로운 DB 커넥션과 트랜잭션을 만들어 사용
          • 첫 번째 수행이 성공하고, 두 번째 호출 시점에서 오류가 발생해도, 첫 번째 커밋한 트랜잭션의 결과가 DB에 반영됨
        • 데이터 엑세스 코드를 DAO로 만들어 분리해놓은 경우, DAO 메소드를 호출할 때마다 하나의 새로운 트랜잭션이 만들어지는 구조가 됨
      • 비즈니스 로직 내의 트랜잭션 경계설정

        • DAO 메소드 안으로 upgradeLevels() 메소드 내용을 옮기면 하나의 트랜잭션으로 관리가 됨

          • 하지만, 비즈니스 로직과 데이터 로직을 한데 묶는 좋지 않은 결과 초래
        • 트랜잭션의 경계설정 작업을 UserService쪽으로 위임하는 방법으로 해결

          • UserService에 트랜잭션 시작과 종료를 담당하는 최소한의 코드만 가져오게 만들면, 책임이 다른 코드를 분리해둔 채로 트랜잭션 문제를 해결할 수 있음
        • public void upgradeLevels() throws Exception {
              (1) DB Connection 생성
              (2) 트랜잭션 시작
               try {
                   (3) DAO 메소드 호출
                   (4) 트랜잭션 커밋
               } catch (Exception exception) {
                   (5) 트랜잭션 롤백
                   throw exception;
               } finally {
                   (6) DB Connection 종료
               }
          }
          • Connection 오브젝트를 가지고 데이터 엑세스 작업을 진행하는 코드가 UserDao의 update 메소드 안에 있어야함
          • DAO 메소드 호출마다 Connection 오브젝트를 마라메터로 전달해줘야함
      • UserService 트랜잭션 경계설정의 문제점

        • 위와 같이 코드를 수정하면 아래와 같은 문제가 발생
          • 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없음
          • DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라메터가 추가돼야함
          • Connection 파라메터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 액세스 기술에 독립적일 수 없음
            • UserDao 인터페이스가 바뀌고, 그에 따라 UserService도 함께 수정돼야함
          • DAO 메소드에 Connection 파라메터를 받게 하면 테스트 코드에도 영향
    • 트랜잭션 동기화

      • 스프링이 제공하는 기능을 사용

        • 트랜잭션 동기화(Transaction Synchronization)
      • Connection 파라메터 제거

        • upgradeLevels 메소드가 트랜잭션 경계 설정을 해야하기 때문에, 트랜잭션 시작과 종료를 관리
        • 트랜잭션 동기화를 사용
          • UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다 사용하게 하는 방식
          • UserService에서 Connection 생성 -> 트래잭션 시작 -> update 메소드 호출 -> JdbcTemplate 메소드에서 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션 Connection 오브젝트 존재 확인 후 가져옴 -> 로직 진행 및 반복 -> 모든 작업이 정상적으로 종료되면 commit으로 트랜잭션 완료 / 예외 상황이라면 rollback으로 트랜잭션을 종료
          • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리
            • 멀티 쓰레드 환경에서 충돌 우려 X
      • 트랜잭션 동기화 적용

        • //UserService
          private DataSource dataSource;
          
          public void setDataSource(DataSource dataSource) {
              this.dataSource = dataSource;
          }
          
          public void upgradeLevels() throws Exception {
              TransactionSynchronizationManager.initSynchronization(); // 동기화
              Connection c = DataSourceUtils.getConnection(dataSource); // connection 생성 및 동기화
              c.setAutoCommit(false); // connection 생성 및 동기화
          
              try {
                  userDao.getAll()
                      .filter(user -> canUpgradeLevel(user))
                      .forEach(user -> upgradeLevel(user));
                  c.commit();
              } catch (Exception exception) {
                  c.rollback();
                  throw exception;
              } finally {
                  DataSourceUtils.releaseConnection(c, dataSource); // connection close
                  TransactionSynchronizationManager.unbindResource(this.dataSource); // 동기화 작업 종료
                  TranscationSynchronizationManager.clearSynchronization(); // 동기화 정리
              }
          }
          • UserService에서 DB 커넥션을 직접 다룰 때 DataSource가 필요 ~> DI
          • 스프링 제공 트랜잭션 동기화 관리 클래스 ~> TransactionSynchronizationManager
          • DataSourceUtils의 getConnection 메소드 ~> Connection 객체 생성 및 트랜잭션 동기화에 사용하도록 저장소에 바인딩
      • JdbcTemplate 트랜잭션 동기화

        • JdbcTemplate은 Connection에 대해 영리하게 동작
          • 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없으면 직접 커넥션을 생성하고 트랜잭션을 시작 및 작업 진행
          • 트랜잭션 동기화 저장소에 등록되어 있으면 커넥션을 가져와서 사용
    • 트랜잭션 서비스 추상화

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

        • DB 종류를 여러 개 이용하는 경우 트랜잭션 처리 코드를 담은 UserService에서 문제가 발생

          • 글로벌 트랜잭션을 사용해서 트랜잭션을 관리

          • 자바는 JTA(JavaTransactionAPI)를 제공

            • DB와 메시징 서버를 제어하고 관리하는 각 리소스 매니저와 XA 프로토콜을 통해 연결
          • InitialContext context = new InitialContext();
            UserTransaction transaction = (UserTransaction)context.lookup(USER_TX_JNDI_NAME);
            
            trasaction.begin();
            Connection connection = dataSource.getConnection();
            
            try {
                // Data Access Code
                taransaction.commit();
            } catch (Exception exception) {
                transaction.rollback();
                throw exception;
            } finally {
                connection.close();
            }
          • 트랜잭션 처리 방법은 별로 달라진게 없지만, JDBC 로컬 트랜잭션을 JTA 글로벌 트랜잭션으로 바꾸는 경우 UserService를 수정해야하는 문제점이 존재

            • DB 종류마다 트랜잭션 관리 코드가 다르기 때문에 계속해서 변경해야함
      • 트랜잭션 API의 의존관계 문제와 해결책

        • UserDao가 DAO 패턴을 사용해 구현 데이터 엑세스 기술을 유연하게 바꿔 사용하도록 했지만, UserService에 트랜잭션 경계 설정을 하게되면 다시 특정 데이터 엑세스 기술에 종속되는 구조가 존재
        • 여러 트랜잭션 경계 설정 기술 사용 방법에 공통점으로 추상화
      • 스프링의 트랜잭션 서비스 추상화

        • public void upgradeLevels() {
              PlatformTranscationManager transactionManager = new DataSourceTransactionManager(dataSOurce);
              TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          
              try {
                  userDao.getAll()
                      .filter(user -> canUpgradeLevel(user))
                      .forEach(user -> upgradeLevel(user));
              } catch (RuntimeException e) {
                  transactionManager.rollback(status);
                  throw e;
              }
          }
          • 스프링이 제공하는 트랜잭션 경계설정 추상 인터페이스 PlatformTransactionManger를 이용
          • 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행
          • 트랜잭션은 TransactionStatus 타입 변수에 저장되어 조작이 필요한 경우, PlatformTransactionManger 메소드의 파라미터로 전달
      • 트랜잭션 기술 설정의 분리

        • JTA를 이용하는 글로벌 트랜잭션으로 변경

          • PlatformTransactionManger 구현 클래스를 DataSourceTransactionManager -> JTATransactionManger로 바꿈
        • 어떤 트랜잭션 매니저 구현 클래스를 사용할지 UserService 코드가 알고있는 것은 DI 원칙에 위배

          • 외부에서 스프링 DI를 통해 제공받도록 수정

          • 스프링에서 제공하는 PlatformTransactionManager는 싱글톤으로 사용이 가능

          • @AllArgsContructor
            public class UserService {
                ...
                private PlatformTransactionManager transactionManger;
            
                public void upgradeLevels() {
                    TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
            
                    try {
                        userDao.getAll()
                            .filter(user -> canUpgradeLevel(user))
                            .forEach(user -> upgradeLevel(user));
                        this.transactionManager.commit(status);
                    } catch (RuntimeException e) {
                        this.transactionManager.rollback(status);
                        throw e;
                    }
                }
            }
            • PlatformTransactionManager의 구현 클래스에 따라 주입을 다르게 해주면서 사용

    정리

    • 트랜잭션과 관련해서 spring boot에서는 @Transactional을 제공해서, 위의 과정을 쉽게 이용 가능
    반응형

    댓글

Designed by Tistory.