rheeri/toby-spring-study

6장 AOP(2)

Opened this issue · 0 comments

6.6 트랜잭션 속성

6.6.1 트랜잭션 정의

  • 트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아님
  • commit, rollback 외에도 트랜잭션의 동작 방식을 제어할 수 있는 몇 가지 조건이 있음

트랜잭션 전파

트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정

A의 트랜잭션이 시작되고 아직 끝나지 않은 시점에서 B를 호출했다면, B의 코드는 어떤 트랜잭션 안에서 동작해야 할까?
스크린샷 2024-09-16 19 46 44

  1. B의 코드는 A에서 이미 시작한 트랜잭션에 참여한다.

    → (2)의 코드를 진행하는 중에 예외가 발생하면 A, B코드에서 진행됐던 DB작업이 모두 취소됨

  2. A와 무관하게 B를 독립적인 트랜잭션으로 만든다.

    → (2)의 코드를 진행하는 중에 예외가 발생하더라도 B의 코드는 영향을 받지 않음

  • PROPAGATION_REQUIRED

    • 가장 많이 사용되는 트랜잭션 전파 속성

    • 진행중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있다면 이에 참여한다.

    • 해당 속성을 갖는 코드는 다양하게 결합해서 하나의 트랜잭션으로 구성하기 쉽다.

      ex) A, B, A→B, B→A

  • PROPAGATION_REQUIRES_NEW

    • 항상 새로운 트랜잭션을 시작
  • PROPAGATION_NOT_SUPORTED

    • 트랜잭션 없이 동작하도록 만들고, 진행중인 트랜잭션이 있어도 무시
    • 이럴거면 트랜잭션 경계설정을 아예 안하면 되는거 아닌가?
      • 트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용하기 때문에 이런 경우에 특별한 메소드만 트랜잭션에서 제외하기 위해 설정
      • 물론 포인트컷을 잘 만들어서 사용하면 되지만 상당히 복잡해질 수 있기 때문에 차라리 모든 메소드에 트랜잭션 AOP를 설정하고 특정 메소드에만 해당 속성을 사용하는게 낫다고 함

격리수준

  • 모든 DB 트랜잭션은 격리수준을 갖고 있어야 함
  • 서버환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있음
    • 모든 트랜잭션이 순차적으로 진행되면 성능이 크게 떨어지므로, 격리수준을 조정해 가능한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게끔 제어한다.
  • 기본적으로는 DB, DataSource에 설정된 디폴트 격리수준을 따르지만 필요에 따라 메소드별로 독자적인 격리수준을 지정하기도 한다.

제한시간

  • 트랜잭션을 수행하는 제한시간을 설정할 수 있음
  • 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW 속성과 같이 사용해야 의미가 있음

읽기전용

  • 트랜잭션 내에서 데이터를 조작하는 시도를 막아줌

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

💡 원하는 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 있는 방법이 없을까?

  • TransactionDefinition 네 가지 속성을 이용해 트랜잭션의 동작방식을 제어

  • TransactionAdvice TransactionDefinition 오브젝트를 생성하고 사용, 트랜잭션 경계설정 기능을 가짐


TransactionInterceptor

  • 트랜잭션 경계설정 어드바이스로 사용 가능

  • 트랜잭션 정의를 메소드 이름 패턴을 사용해 지정할 수 있는 방법을 제공해줌

  • TransactionInterceptor의 두 가지 프로퍼티

    1. PlatformTransactionManager
    2. Properties - transactionAttributes
      • TransactionDefinition의 네 가지 기본 트랜잭션 속성 + rollbackOn() 메소드를 갖는 TransactionAttribute 인터페이스
      • 트랜잭션 부가기능의 동작방식을 모두 제어할 수 있음
  • TransactionInterceptor의 예외 처리 방식

    1. 런타임 예외가 발생하면 트랜잭션을 롤백시킨다.

    2. 타깃 메소드가 런타임 예외가 아닌 체크 예외를 던지는 경우에는 트랜잭션을 커밋한다.

      → 체크 예외를 일종의 비즈니스 로직에 따른 의미 있는 리턴 방식의 한 가지로 인식

  • TransactionAttribute는 rollbackOn() 속성을 둬 이런 기본적인 예외 처리 방식을 따르지 않아도 되게끔 해준다.


트랜잭션 속성 정의 방식

  1. TransactionInterceptor 타입의 빈 정의
  2. tx 스키마의 전용 태그를 이용해 정의

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

트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다

  • 일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는 것이 바람직하다.
    • 비즈니스 로직을 담고 있는 클래스라면 메소드 단위까지 세밀하게 포인트컷을 정의해줄 필요는 없음
  • 쓰기 작업이 없는 단순한 조회 작업 메소드에도 모두 트랜잭션을 적용하는게 좋다.
  • 트랜잭션 경계로 삼을 클래스들이 모여 있는 패키지 혹은 비즈니스 로직 서비스를 담당하는 클래스명의 패턴을 찾아서 표현식으로 만든다.
    • 메소드 시그니처를 이용한 execution() 방식의 포인트컷 표현식
      • Service, ServiceImpl로 끝나는 경우 execution(**..ServiceImpl.(..))
    • 스프링의 빈 이름을 이용하는 bean() 표현식
      • bean(*Service)

공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다

  • 기준이 되는 몇 가지 트랜잭션 속성을 정의하고, 그에 따라 적절한 메소드 명명 규칙을 만들어두면 하나의 어드바이스만으로 애플리케이션 모든 서비스 빈에 트랜잭션 속성을 지정할 수 있다.
    • get으로 시작하는 메소드에 대해서는 읽기 전용 속성을 부여할 수 있음

      <tx:advice id="transactionAdvice">
      	<tx:attributes>
      		<tx:method name="get*" read-only="true" />
      		<tx:method name="*" />
      	</tx:attributes>
      </tx:attributes>

프록시 방식 AOP는 같은 타킷 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다

  • 프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용은 클라이언트로부터 호출이 일어날 때만 가능하다.
스크린샷 2024-09-16 20 57 49
  • (2)를 통해 update() 메소드가 호출될 때, 전파속성이 REQUIRES_NEW로 설정되어 있더라도 프록시의 delete()메소드에서 시작된 트랜잭션에 참여하게 된다.

    • 즉 타깃 오브젝트 안에서 메소드 호출이 일어나는 경우에는 부가기능 적용이 되지 않음
  • 타깃 안에서의 호출에 프록시가 적용되지 않는 문제를 해결할 수 있는 방법

    • AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 기술 적용

트랜잭션 속성 테스트

스크린샷 2024-09-16 21 04 40 image

TransientDataAccessResourceException 예외가 발생한다. 스프링의 DataAccessException의 한 종류로 일시적인 예외상황을 만났을 때 발생하는 예외다.



6.7 애노테이션 트랜잭션 속성과 포인트컷

트랜잭션 속성을 비즈니스 로직을 담당하는 패키지, 클래스 등에 일괄적으로 적용하는 방식은 대부분의 상황에 잘 들어맞는다. 하지만 세밀한 제어가 필요한 경우도 종종 있는데, 이때는 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정한다.

6.7.1 트랜잭션 애노테이션

@transactional

//Transactional.class

@Inherited
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
    TxType value() default Transactional.TxType.REQUIRED;

    @Nonbinding
    Class[] rollbackOn() default {};

    @Nonbinding
    Class[] dontRollbackOn() default {};

    public static enum TxType {
        REQUIRED,
        REQUIRES_NEW,
        MANDATORY,
        SUPPORTS,
        NOT_SUPPORTED,
        NEVER;

        private TxType() {
        }
    }
}
  • @target
    • 메소드와 타입이 타깃이므로 메소드, 클래스, 인터페이스에 @transactional 애노테이션을 사용할 수 있음
  • 스프링은 @transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다.
    • 이때 TransactionAttributeSorcePointcut 포인트컷을 사용하는데 표현식과 같은 선정기준을 갖는 것이 아니라, 해당 애노테이션이 붙은 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.

트랜잭션 속성을 이용하는 포인트컷

@transactional 애노테이션을 사용했을 때 어드바이저의 동작방식
스크린샷 2024-09-16 21 13 52

  • 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정 가능
  • 메소드 단위로 지정할 수 있으므로 세밀한 제어가 가능
  • 애노테이션이 메소드마다 반복적으로 부여되어 코드가 지저분해질 수 있음

대체 정책

스프링은 @transactional을 적용할 때 4단계의 대체정책을 이용한다.

  • 메소드의 속성을 확인할 때 다음의 순서로 트랜잭션이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성을 사용하게 하는 방법

    • 타깃 메소드 → 타깃 클래스 → 선언 메소드 → 선언 타입
  • 예시) @transactional을 부여할 수 있는 위치는 총 6개

스크린샷 2024-09-16 21 18 34
  • 스프링은 5 → 6 → 4 → 2 → 3 → 1의 순서로 애노테이션을 찾는다.

  • 대체정책을 잘 활용하면 애노테이션을 최소한으로 사용하면서 세밀하게 제어할 수 있음

    1. 타입 레벨에 정의할 수 있는지 본다.
    2. 공통 속성을 따르지 않는 메소드에 대해서만 메소드 레벨에 애노테이션을 부여한다.

6.7.2 트랜잭션 애노테이션 적용

애노테이션을 이용할 때는 단순하게 트랜잭션이 필요한 타입 또는 메소드에 직접 애노테이션을 부여한다.
스크린샷 2024-09-16 21 23 44



6.8 트랜잭션 지원 테스트

6.8.1 선언적 트랜잭션과 트랜잭션 전파 속성

  • 사용자 등록 로직을 담당하는 add() 메소드가 트랜잭션 전파 방식을 사용할 수 없어서 매번 새로운 트랜잭션을 시작하도록 만들어졌다면, 다른 메소드에서 호출하기가 꺼려질 것이다.

    • 결국 add() 메소드를 복사해서 하나의 메소드 안에 넣게 되는 중복코드가 계속 발생할 것이다.
  • 스프링의 선언적 트랜잭션을 사용하면 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있다

스크린샷 2024-09-16 21 26 32
  • 또한 스프링은 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 프로그램에 의한 트랜잭션도 지원한다.

6.8.2 트랜잭션 동기화와 테스트

트랜잭션 매니저와 트랜잭션 동기화

  • 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화
    • 트랜잭션 매니저 구체적인 트랜잭션 기술의 종류에 상관없이 일관적인 제어 가능
    • 트랜잭션 동기화 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유
      • 이 기술 덕분에 트랜잭션 전파 속성에 따라 진행 중인 트랜잭션이 있는지 확인하고 이에 참여할 수 있는 것

트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어

스크린샷 2024-09-16 21 36 04
  • 메소드를 추가하지 않고도, UserService의 메소드를 호출하기 전에 트랜잭션을 미리 시작시켜서 UserService의 세 개의 메소드가 동일한 트랜잭션 내에 참여하게끔 유도할 수 있음

트랜잭션 동기화 검증

스크린샷 2024-09-16 21 37 14
  • 테스트에서 미리 트랜잭션을 시작함으로써 테스트코드를 하나의 트랜잭션 단위로 묶는 것이 가능 (@transactional 애노테이션 적용과 같은 용도로..)
    • DB 작업이 포함되는 테스트를 원하는 대로 제어하는 것이 가능해졌다.
    • 하이버네이트 등 ORM에서 세션에서 분리된 엔티티 동작을 확인할 때도 유용하다.

롤백 테스트

스크린샷 2024-09-16 21 40 24
  • DB에 엑세스하는 테스트를 할 때마다 테스트 데이터를 초기화하는 작업이 반복되는데, 이럴 때 롤백 테스트가 유용하다.
  • 테스트에서 트랜잭션을 제어할 수 있기 때문에 얻을 수 있는 가장 큰 유익이 있다면 롤백 테스트다.
    • MySQL에서는 동일한 작업을 수행한 뒤에 롤백하는게 커밋보다 더 빠르기 때문에 성능이 향상되기도 함

6.8.3 테스트를 위한 트랜잭션 애노테이션

@transactional

  • 테스트에서 사용하는 이 애노테이션은 AOP를 위한 것은 아님

@Rollback

  • 테스트용 @transactional 애노테이션은 테스트가 끝나면 자동으로 롤백된다.
  • 강제 롤백을 원하지 않는 경우에는 @Rollback(false) 애노테이션을 사용하자

@TransactionConfiguration

  • @transactional은 테스트 클래스에 적용가능하지만, @Rollback은 메소드 레벨에만 적용 가능하다.

  • 테스트 클래스의 모든 메소드에 트랜잭션을 적용하되 모든 트랜잭션을 롤백시키지 않고 커밋하려면 이 애노테이션을 사용해보자.

    @TransactionConfiguration(defaultRollback=false)

NotTransactional과 Propagation.NEVER

  • 필요하지도 않은 트랜잭션이 만들어지는 것이 싫은 경우에 적용한다.

    @Transactional(propagation=Propagation.NEVER)

효과적인 DB 테스트

  • 일반적으로 의존, 협력 오브젝트를 사용하지 않는 단위 테스트와, DB 같은 외부의 리소스나 여러 계층 클래스가 참여하는 통합 테스트는 아예 구분해서 만드는 게 좋다.
  • 테스트는 어떤 경우에도 서로 의존하면 안 된다.


6.9 정리

  • 목 오브젝트를 활용하면 의존관계에 있는 오브젝트로 쉽게 고립된 테스트로 만들 수 있다.
  • DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다.
  • AOP는 OOP만으로 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.
  • AOP를 이용해 트랜잭션 속성을 지정할 때, 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 @Transactioanl 애노테이션을 사용하는 방법이 있다.