ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6장) 6.6 트랜잭션 속성
    Java & Spring/토비의 스프링 3.1 2021. 7. 26. 14:27
    반응형

    6장 AOP

    6.6 트랜잭션 속성

    • 앞서 학습했던 PlatformTransactionManager로 대표되는 스프링의 트랜잭션 추상화 적용 중, 트랜잭션 매니저에서 트랜잭션을 가져올 때 사용한 DefaultTransactionDefinition 오브젝트의 용도에 대해 알아보자

    6.6.1 트랜잭션 정의

    • 더 이상 쪼갤 수 없는 최소 단위의 작업
    • 트랜잭션 동작 방식
      • commit()
      • rollback()
    • 이 밖에도 트랜잭션 동작방식을 제어할 수 있는 조건이 존재
    • DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션 동작 방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있음
      • 트랜잭션 전파, 격리수준, 제한시간, 읽기전용
    • 트랜잭션 전파
      • 이미 진행 중인 트랜잭션이 있을 때 or 없을 때 어떻게 동작할 것인가를 결정하는 방식
      • 트랜잭션 안의 트랜잭션
        • 독자적인 트랜잭션 경계를 가진 코드에 대해 진행중인 트랜잭션이 어떻게 미칠 수 있는가를 정의하는 것이 트랜잭션 전파
      • 속성
        • PROPRAGATION_REQUIRED
          • 가장 많이 사용되는 트랜잭션 전파 속성
          • 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 해당 트랜잭션에 참여
          • DefaultTransactionDefinition의 트랜잭션 전파 속성
        • PROPRAGATION_REQUIRES_NEW
          • 항상 새로운 트랜잭션 시작
          • 독자적으로 동작해서, 독립적인 트랜잭션이 보장돼야하는 코드에 적용
        • PROPRAGATION_NOT_SUPPORTED
          • 트랜잭션 없이 동작
          • 진행 중인 트랜잭션이 있어도 무시
            • 특별한 메소드만 트랜젝션 적용에서 제외하는 경우, 해당 메소드에 이 트랜잭션 전파 속성을 적용해서 트랜잭션 없이 작동하도록 설정
            • 포인트컷을 잘 만들어서 특정 메소드에 AOP 적용 대상이 되지 않게 하는 방법도 있지만, 상당히 복잡해짐
      • 트랜잭션 매니저를 통해 트랜잭션을 시작하려고 할 때, getTransaction() 메소드를 사용하는 이유가 트랜잭션 전파 속성이 있기 때문
        • getTransaction() 메소드가 항상 트랜잭션을 새로 시작하는 것이 아닌, 전파 속성에 따라 결정
    • 격리수준
      • 모든 DB 트랜잭션은 격리수준을 가지고 있음
      • DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT
        • DataSource의 Default 격리수준을 따름
    • 제한시간
      • 트랜잭션을 수행하는 제한시간
      • DefaultTransactionDefinition의 기본 설정은 제한시간이 없음
      • 트랜잭션을 직접 시작할 수 있는 PROPRAGATION_REQUIRED, PROPRAGATION_REQUIRES_NEW와 함께 사용해야 유의미
    • 읽기전용
      • read only로 설정
        • 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있음
        • 데이터 엑세스 기술에 따라 성능 향상 기대
    • TransactionDefinition 타입의 오브젝트를 사용하면, 네 가지 속성을 이용해 트랜잭션의 동작방식을 제어할 수 있음
      • TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 가진 TransactionAdvice
        • 따라서, 트랜잭션 정의를 바꾸고 싶다면 DefaultTransactionDefinition을 사용하는 대신, 외부에서 정의된 TransactionDefinition 오브젝트를 DI 받아서 사용하도록 설정
        • TransactionDefinition 타입의 빈을 프로퍼티를 통해 원하는 속성을 지정할 수 있지만, TrasactionAdvice를 사용하는 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 존재

    6.6.2 트랜잭션 인터셉터와 트랜잭션 속성

    • 메소드별로 다른 트랜잭션을 정의하려면 어드바이스 기능을 확장해야함
      • 메소드 이름 패턴에 따라 다른 트랜잭션 정의가 적용되도록 설정
    • TransactionInterceptor
      • 기존에 만들었던 TransactionAdvice를 다시 설계할 필요 없이, 스프링에서 트랜잭션 경계설정 어드바이스로 제공하는 TransactionInterceptor 사용
      • 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정
      • TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 가지고 있음
        • Properties 타입은 transactionAttributes로, 트랜잭션 속성을 정의한 프로퍼티
        • 트랜잭션 속성은 TransactionDefinition의 네 가지 기본 항목에 rollback() 메소드를 하나 더 가지고 있는 TranscationAttribute 인터페이스로 정의됨
        • rollback() 메소드는 어떤 예외가 발생하면 롤백을 할지 결정하는 메소드
        • // 기존 TransactionAdvice 설정 코드
          public Object invoke(MethodInvocation invocation) throws Throwable {
              TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); // 트랜잭션 정의를 통한 네가지 조건
              try {
                  // ...
              } catch (RuntimeException e) { // 롤백 대상인 예외 종류
                  this.transactionManager.rollback(status);
                  throw e;
              }
          }
        • 트랜잭션 조건과 롤백 항목을 결합해서 트랜잭션 부가기능의 행동을 결정하는 TransactionAttribute 속성이 됨
      • 모든 종류의 예외에 대해 트랜잭션을 롤백해서는 안됨
        • 비즈니스 로직상 예외의 Checked Exception을 던지는 경우에는 DB 트랜잭션은 커밋해야함
      • TransactionInterceptor가 제공하는 예외 처리 방식 두 가지
        • 런타임 예외가 발생하면 트랜잭션 롤백
        • 체크 예외를 던지는 경우에는 예외상황으로 해석하지 않고, 일종의 비즈니스 로직에 따른 리턴으로 인식하여 트랜잭션 커밋
      • 위의 예외처리 기본 원칙을 따르지 않는 경우
        • TransactionAttribute의 rollbackOn()이라는 속성은 기본 원칙과 다른 예외처리가 가능하도록 해줌
        • TransactionInterceptor는 TransactionAttribute를 Properties라는 일종의 맵 타입 오브젝트로 전달받음
          • 컬렉션을 사용하는 이유 ~> 메소드 패턴에 따라서 각기 다른 트랜잭션 속성을 부여하기 위함
    • 메소드 이름 패턴을 이용한 트랜잭션 속성 지정
      • Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렉션
        • PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
        • 트랜잭션 속성은 위와 같은 문자열로 정의
        • 트랜잭션 전파 항목인 PROPAGATION_NAME만 필수, 나머지는 생략 가능
          • 생략 시, DefaultTransactionDefinition에 설정된 default 속성이 부여
          • +-로 시작하는 Exception은 기본 원칙을 따르지 않는 예외를 정의
        • <beans xmlns="http://www.springfamework.org/schema/beans"
                 ...
                 xmlns:tx="http://www.springframework.org/schema/tx"
                 xsi:schemaLocation="http://springframework.org/schema/beans
                                     http://springframework.org/schema/tx
                                     http://springframework.org/schema/tx/spring-tx-2.5.xsd"
                 ...
          
              <tx:advice id="transcationAdvice" transaction-manager="transactionManager">
                  <tx:attributes>
                      <tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30" />
                      <tx:method name="upgrade*" propagation="REQUIRED_NEW" isolation="SERIALIZABLE" />
                      <tx:method name="*" propagation="REQUIRED"/> <!-- default 값이 스키마에 정의되어 있어서, propagation이 REQUIRED라면 생략 가능 -->
                  </tx:attributes>
              </tx:advice>
          </beans>

    6.6.3 포인트컷과 트랜잭션 속성의 전용 전략

    • 포인트컷 표현식과 트랜잭션 속성 정의에 좋은 전략들
      • 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다
        • 비즈니스 로직을 담고 있는 클래스는 메소드 단위까지 세밀하게 포인트컷을 정의해줄 필요 없음
        • UserService로 예를 들면, add() 메소드도 트랜잭션 적용 대상
          • 사용자 정보를 DB에 추가하는 것 외에도 DB 정보를 다루는 작업이 추가될 가능성 존재
        • 단순한 조회 작업의 경우에도 모두 트랜잭션을 적용하는 것이 좋음
          • 성능 향상 기대, 복잡한 조회의 경우는 제한시간 지정, 격리 수준에 따른 조회도 반드시 트랜잭션 안에서 진행해야할 필요존재
        • 트랜잭션용 포인트컷 표현식에는 메소드나 파라미터, 예외에 대한 패턴을 정의하지 않는 것이 좋음
          • 클래스들이 모여있는 패키지를 통째로 선택하거나 클래스 이름 패턴으로 표현식을 만드는 것이 좋음
            • ex) execution(**..*ServiceImpl.*(..))
          • 클래스보다는 인터페이스 타입을 기준으로 타입패턴 적용
            • 변경 빈도가 적고 일정한 패턴을 유지하기 쉬움
            • ex) execution(**..*Service.*(..))
        • 메소드 시그니처를 사용한 execution() 방식의 포인트컷 표현식 대신 스프링의 bean() 표현식을 사용하는 방법도 존재
          • 클래스나 인터페이스 이름에 일정한 규칙을 만들기 어려운 경우에 유용
          • 포인트컷 표현식 자체가 간단해서 읽기 편함
      • 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다
        • 다양한 트랜잭션 속성 부여는 관리가 힘들다
          • 몇 가지 트랜잭션 속성을 정의하고 적절한 메소드 명명 규칙을 만들어 하나의 어드바이스만으로 애플리케이션의 모든 서비스 빈에 트랜잭션 속성을 지정
        • 위의 일반적인 경우와 크게 다른 오브젝트가 존재하는 경우
          • 트랜잭션 어드바이스와 포인트컷을 새롭게 추가해야함
          • default 속성으로 설정했다가, 개발이 진행됨에 따라 단계적으로 속성을 추가하면서 개발
            • 간단한 메소드 이름의 패턴 적용
              • <tx:advice id="transactionAdvice">
                    <tx:attributes>
                        <tx:method name="get*" read-only="true" />
                        <tx:method name="*"/>
                    </tx:attributes>
                </tx:advice>
            • 일반화하기 어려운 트랜잭션 속성이 필요한 타깃 오브젝트에는 별도의 어드바이스와 포인트컷 표현식을 사용
              • 트랜잭션 어드바이스를 이용한 예시
              • <aop:config>
                    <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
                    <aop:advisor advice-ref="batchAdvice" pointcut="execution(a.b.*BatchJob.*.(..))" />
                </aop:config>
                
                <tx:advice id="transactionAdvice">
                    <tx:attributes>...</tx:attributes>
                </tx:advice>
                <tx:advice id="batchAdvice">
                    <tx:attributes>...</tx:attributes>
                </tx:advice>
      • 프록시 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다
        • 프록시 방식의 AOP에서는 프록시를 통한 부가기능 적용은 클라이언트로부터 호출이 일어날 때만 가능
          • 인터페이스를 통해 타깃 오브젝트를 사용하는 다른 모든 오브젝트에서의 호출
        • 타깃 오브젝트가 자신의 메소드를 호출할 때는 프록시를 통한 부가기능 적용이 불가
          • 클라이언트로부터 메소드가 호출되면, 트랜잭션 프록시를 통해 타깃 메소드로 호출이 전달되면서 트랜잭션 경계설정 부가기능이 부여되기 때문
        • 타깃 안에서 호출할 때, 프록시가 적용되지 않는 문제 해결 방법
          • 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤, 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제
            • 순수한 비즈니스 로직에 스프링 API와 프록시 호출 코드가 공존하기 때문에 바람직하지 않음
          • AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 적용
            • 설정에 불편함이 뒤따르기 때문에 꼭 필요한 경우에만 사용

    6.6.4 트랜잭션 속성 적용

    • 트랜잭션 속성과 전략을 UserService에 적용하기
    • 트랜잭션 경계설정의 일원화
      • 트랜잭션 경계설정 부가기능은 일반적으로 특정 계층의 경계를 트랜잭션 경계와 일치하는 것이 바람직
        • 비즈니스 로직을 담은 서비스 계층에 트랜잭션 경계를 부여하는 것이 가장 적절
        • 테스트와 같은 특별한 경우가 아닌 경우, 다른 계층에서 DAO에 직접 접근하는 것은 차단하는 것이 바람직
        • 트랜잭션은 보통 서비스 계층의 메소드 조합을 통해 만들어짐
          • DAO가 제공하는 주요 기능을 서비스 계층에 위임 메소드를 만들어야함
          • DAO에 접근할 때는서비스 계층을 거치도록 설정
    • 서비스 빈에 적용되는 포인트컷 표현식 등록
      • upgradeLevels()에만 트랜잭션이 적용되게 했던 기존 포인트컷 표현식을 모든 비즈니스 로직 서비스 빈에 적용되도록 수정
      • <aop:config>
            <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
        </aop:config>
    • 트랜잭션 속성 테스트
      • 위에서 설정한 <tx:attributes>로 지정한 트랜잭션 속성을 보면, get으로 시작하는 메소드에 read-only 옵션을 true로 설정했는데, 쓰기 작업이 허용되지 않는지 테스트
        • static class TestUserService extends UserServiceImpl {
              ...
              public List<User> getAll() {
                  super.getAll().forEach(user -> super.update(user));
                  return null;
              }
          }
          • TransactionDataAccessResourceException 예외가 발생
            • 스프링의 DataAccessResourceException의 한 종류로 일시적인 예외상황을 만났을 때 발생하는 예외
              • 일시적이라는 의미는 재시도하면 성공할 가능성이 있음을 의미
                • update()에 의해 일어나는 DB 쓰기 작업은 원래 정상적으로 처리돼야하지만, 일시적인 제약조건 때문에 예외가 발생
                • get메소드에 읽기전용 트랜잭션이 걸려있지 않다면 성공할 것이기 때문

    정리

    • 트랜잭션 격리수준
      • READ UNCOMMITTED
        • 어떤 트랜잭션의 변경내용이 COMMIT이나 ROLLBACK과 상관없이 다른 트랜잭션에서 보여진다.
        • DIRTY READ PROBLEM
          • 한 트랜잭션이 특정 데이터 값을 변경하고 커밋하지 않았을 때, 다른 트랜잭션이 해당 데이터를 Read하는 경우, 뒤늦게 참여한 트랜잭션이 READ한 데이터 값은 앞선 트랜잭션의 변경 이후의 값
          • 하지만, commit을 하지 않는 경우, 두 트랜잭션의 데이터는 꼬이게 됨
      • READ COMMITTED
        • Oracle의 default 격리수준
        • 어떤 트랜잭션의 변경 내용이 COMMIT 되어야만 다른 트랜잭션에서 조회할 수 있다.
        • NON-REPETABLE READ 부정합 문제 발생
          • 항상 같은 값을 반환하지 않는다. (commit 전과 후에 read하면 값이 다름)
      • REPEATABLE READ
        • MySQL의 default 격리수준
        • 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준
          • read에 부정합이 없음
          • 하지만 update 부정합과 Phantom READ(첫 쿼리에서 없던 레코드가 발견)이 있음
            • 없는 값을 읽었다.(잘못된 값을 읽었다)
            • 부캠 루카스 수업 자료를 참고하자
            • 하지만, 극히 드물다 라고 하셨음 (mysql에서는 확인이 불가)
      • SERIALIZEABLE
        • 가장 단순하고 가장 엄격한 격리 수준
        • 읽기 작업에서도 공유 잠금을 하는데, 성능 저하가 발생
          • 다른 트랜잭션이 끝날때 까지 대기함
            • 30초 ? 1분? 정도 지나면 알아서 취소됨
          • 읽기는 모든 트랜잭션 다 되지만 2번째로 시작한 트랜잭션부터는 수정이 불가능하다.
            • 첫 트랜잭션에서 수정작업이 일어나면 다른 트랜잭션에서 읽기도 불가능해짐
        • 필요한 상황이 거의 없다.
    • 테스트 하기 위해서는 서비스 계층과 DAO를 분리해야함
      • 아키텍처를 단순하게 가져가면 서비스 계층과 DAO가 통합될 수 있지만 순수한 비즈니스 로직을 테스트할 수 없어진다
    반응형

    댓글

Designed by Tistory.