-
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 적용 대상이 되지 않게 하는 방법도 있지만, 상당히 복잡해짐
- PROPRAGATION_REQUIRED
- 트랜잭션 매니저를 통해 트랜잭션을 시작하려고 할 때, getTransaction() 메소드를 사용하는 이유가 트랜잭션 전파 속성이 있기 때문
- getTransaction() 메소드가 항상 트랜잭션을 새로 시작하는 것이 아닌, 전파 속성에 따라 결정
- 격리수준
- 모든 DB 트랜잭션은 격리수준을 가지고 있음
- DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT
- DataSource의 Default 격리수준을 따름
- 제한시간
- 트랜잭션을 수행하는 제한시간
- DefaultTransactionDefinition의 기본 설정은 제한시간이 없음
- 트랜잭션을 직접 시작할 수 있는 PROPRAGATION_REQUIRED, PROPRAGATION_REQUIRES_NEW와 함께 사용해야 유의미
- 읽기전용
- read only로 설정
- 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있음
- 데이터 엑세스 기술에 따라 성능 향상 기대
- read only로 설정
- TransactionDefinition 타입의 오브젝트를 사용하면, 네 가지 속성을 이용해 트랜잭션의 동작방식을 제어할 수 있음
- TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 가진 TransactionAdvice
- 따라서, 트랜잭션 정의를 바꾸고 싶다면 DefaultTransactionDefinition을 사용하는 대신, 외부에서 정의된 TransactionDefinition 오브젝트를 DI 받아서 사용하도록 설정
- TransactionDefinition 타입의 빈을 프로퍼티를 통해 원하는 속성을 지정할 수 있지만, TrasactionAdvice를 사용하는 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 존재
- TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 가진 TransactionAdvice
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>
- Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렉션
6.6.3 포인트컷과 트랜잭션 속성의 전용 전략
- 포인트컷 표현식과 트랜잭션 속성 정의에 좋은 전략들
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다
- 비즈니스 로직을 담고 있는 클래스는 메소드 단위까지 세밀하게 포인트컷을 정의해줄 필요 없음
- UserService로 예를 들면, add() 메소드도 트랜잭션 적용 대상
- 사용자 정보를 DB에 추가하는 것 외에도 DB 정보를 다루는 작업이 추가될 가능성 존재
- 단순한 조회 작업의 경우에도 모두 트랜잭션을 적용하는 것이 좋음
- 성능 향상 기대, 복잡한 조회의 경우는 제한시간 지정, 격리 수준에 따른 조회도 반드시 트랜잭션 안에서 진행해야할 필요존재
- 트랜잭션용 포인트컷 표현식에는 메소드나 파라미터, 예외에 대한 패턴을 정의하지 않는 것이 좋음
- 클래스들이 모여있는 패키지를 통째로 선택하거나 클래스 이름 패턴으로 표현식을 만드는 것이 좋음
- ex)
execution(**..*ServiceImpl.*(..))
- ex)
- 클래스보다는 인터페이스 타입을 기준으로 타입패턴 적용
- 변경 빈도가 적고 일정한 패턴을 유지하기 쉬움
- 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 적용
- 설정에 불편함이 뒤따르기 때문에 꼭 필요한 경우에만 사용
- 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤, 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제
- 프록시 방식의 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메소드에 읽기전용 트랜잭션이 걸려있지 않다면 성공할 것이기 때문
- 일시적이라는 의미는 재시도하면 성공할 가능성이 있음을 의미
- 스프링의 DataAccessResourceException의 한 종류로 일시적인 예외상황을 만났을 때 발생하는 예외
- TransactionDataAccessResourceException 예외가 발생
-
- 위에서 설정한 <tx:attributes>로 지정한 트랜잭션 속성을 보면, get으로 시작하는 메소드에 read-only 옵션을 true로 설정했는데, 쓰기 작업이 허용되지 않는지 테스트
정리
- 트랜잭션 격리수준
- 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번째로 시작한 트랜잭션부터는 수정이 불가능하다.
- 첫 트랜잭션에서 수정작업이 일어나면 다른 트랜잭션에서 읽기도 불가능해짐
- 다른 트랜잭션이 끝날때 까지 대기함
- 필요한 상황이 거의 없다.
- READ UNCOMMITTED
- 테스트 하기 위해서는 서비스 계층과 DAO를 분리해야함
- 아키텍처를 단순하게 가져가면 서비스 계층과 DAO가 통합될 수 있지만 순수한 비즈니스 로직을 테스트할 수 없어진다
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
7장) 7.3 서비스 추상화 적용 (0) 2021.08.08 7장) 7.1 SQL과 DAO의 분리 ~ 7.2 인터페이스의 분리와 자기참조 빈 (0) 2021.08.06 6장) 6.5 스프링 AOP (0) 2021.07.23 6장) 6.4 스프링의 프록시 팩토리 빈 (0) 2021.07.16 6장) 6.3 다이내믹 프록시와 팩토리 빈 (0) 2021.07.16