6.6.1 트랜잭션 속성
- 트랜잭션 추상화를 할 때 그냥 넘어간 것이 한 가지 있음
- 트랜잭션 속성을 담당하는 DefaultTransactionDefinition 오브젝트임
- 트랜잭션 경계는 트랜잭션 매니저에서 트랜잭션을 가져오는 것과 commit(), rollback() 중 하나를 호출하는 것으로 설정됨
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status =
this.transactionManager.getTransaction(
new DefaultTransactionDefinition());
try{
Object ret = invocation.proceed();
this.transactionManager.commit(status);
return ret;
}catch (RuntimeException e){
this.transactionManager.rollback(status);
throw e;
}
}
6.6.2 트랜잭션
- 트랜잭션은 더 이상 쪼갤 수 없는 최소 단위의 작업임
- 트랜잭션 경계 안의 작업은 commit()을 만나 성공 하거나, rollback()을 만나 취소가 되야 함
- DefaultTransactionDefinition가 구현하는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 주는 네 가지 속성을 지님
- 트랜잭션 전파(Transaction Propagation)
- 격리수준(Isolation Level)
- 제한시간(Timeout)
- 읽기전용(Read Only)
6.6.3 트랜잭션 전파
- 트랜잭션 전파란, 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가 결정
- 각각 독립적인 트랜잭션 경계를 가진 A, B가 존재한다고 가정
- 그림과 같이 A가 종료되기 전에 B를 호출했을 때, B가 정상적으로 종료되고 (2)에서 예외 발생 시 동작을 결정하는 것이 트랜잭션 전파임
- A의 트랜잭션이 시작되서 진행중이고, B는 새로운 트랜잭션을 만들지 않고 A의 트랜잭션에 참여하면 (2)에서 예외 발생시 A, B의 작업이 하나로 묶여있기 때문에 모두 취소됨
- A, B가 독립적인 트랜잭션으로 동작을 하면 (2)에서 예외 발생시 B는 커밋되고 A는 롤백됨
- 즉, 트랜잭션 전파 속성은 이미 진행중인 트랜잭션이 어떻게 영향을 미칠 수 있는가를 정의하는 것임
- 트랜잭션 전파 속성 세 가지
- PROPAGATION_REQUIRED
- 진행중인 트랜잭션이 없으면새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여함
- DefaultTransactionDefinition 전파 속성은 PROPAGATION_REQUIRED임
- PROPAGATION_REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작함
- 모든 트랜잭션은 독립적으로 동작함
- 독립적인 트랜잭션이 보장돼야 하는 코드에 적용할 수 있음
- PROPAGATION_NOT_SUPPORTED
- 트랜잭션 없이 동작하도록 할 수 있음
- 트랜잭션 경계설정은 AOP를 이용해 한 번에 많은 메소드를 동시에 적용하는 방법 사용
- 특정 메소드만 제외하는 포인트컷을 만드는 것은 복잡함
- 모든 메소드에 트랜잭션 AOP가 적용되게 하고 특정 메소드의 전파 속성만 PROPAGATION_NOT_SUPPORTED를 사용하여 트랜잭션을 제외 시키는 것이 좋음
- PROPAGATION_REQUIRED
- 트랜잭션 매니저가 getTransaction()를 사용하는 트랜잭션 전파 속성이 있기 때문임
6.6.4 격리수준
- 모든 트랜잭션은 격리수준을 갖고 있어야 함
- 적절한 격리수준 조정으로 가능한 많은 트랜잭션을 동시에 동작시키면서 문제가 발생하지 않도록 해야 성능이 증가함
- 필요시 트랜잭션 단위로 격리수준을 조정할 수 있음
- DefaultTransactionDefinition의 격리 수준은 ISOLATION_DEFAULT임
- 💡 DataSource에 설정되어 있는 디폴트 격리수준을 따른다는 의미임
- 기본적으로 디폴트를 따르는 것이 좋지만, 특별한 작업을 수행하는 경우 독자적 격리수준을 지정할 필요가 있음
6.6.5 제한시간
- DefaultTransactionDefinition는 제한시간이 없음
- 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED나 PROPAGATION_REQUIRES_NEW와 함께 사용해야 의미가 있음
6.6.6 읽기전용
- 읽기전용 설정시 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있음
6.6.7 디폴트 외 속성 적용
- TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 지닌 TransactionAdvice임
- 트랜잭션 정의를 바꾸고 싶으면 디폴트인 DefaultTransactionDefinition대신 외부에서 정의 된 TransactionDefinition 오브젝트를 DI 받으면 됨
- TransactionDefinition 타입의 빈을 정의해두면 프로퍼티를 통해 원하는 속성을 지정할 수 있음
- 하지만, 이 방법으로 트랜잭션 속성을 변경하려면 TransactionAdvice를 사용하는 모든 트랜잭션의 속성이 변경됨
- 일부 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 없음
6.6.8 TransactionInterceptor
- 메소드별로 다른 트랜잭션 정의를 적용하기 위해 어드바이스의 기능을 확장해야 함
- 메소드 이름 패턴에 따라 트랜잭션 정의가 적용되도록 함
- 기존의 TransactionAdvise 대신 TransactionInterceptor 사용
- TransactionInterceptor는 TransactionAdvise와 비슷하지만,트랜잭션 정의를 메소드 이름 패턴을 이용해 다르게 지정할 수 있는 방법 추가로 제공
- TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있음
- Properties 타입의 프로퍼티 이름은 transactionAttribute로, 트랜잭션 속성을 정의한 프로퍼티임
- TransactionAttribute 인터페이스는 TransactionDefinition의 네 가지 기본 항목에 rollbackOn()가 추가로 존재함
- rollbackOn()은 예외 발생 시 롤백을 할 것인지 결정하는 메소드임
- TransactionAdvise는 RuntimeException이 발생하는 경우에만 롤백함
- 때문에, 체크 예외는 트랜잭션이 정상적으로 처리되지 않고 메소드를 빠져나감
- TransactionInterceptor는 런타임 예외가 발생하면 트랜잭션은 롤백됨
- 반면에 체크 예외는 스프링의 기본적 원칙에 따라 비즈니스 로직에 따른 의미가 있다고 판단하여 트랜잭션을 커밋함
- 스프링의 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외 상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 예외는 런타임 예외로 포장해서 던짐
- 하지만, TransactionInterceptor는 예외처리 기본 원칙을 따르지 않은 경우가 있음
- TransactionAttribute의 rollbackOn()은 기본 원칙과 다른 예외 처리를 가능하게 함
- 특정 체크 예외의 경우 롤백을 시키고, 특정 런타임 예외는 커밋하게 할 수 있음
- TransactionInterceptor는 이런 TransactionAttribute를 Properties라는 일종의 Map 타입 오브젝트로 전달받음
- 컬렉션을 사용하는 이유는 메소드 패턴에 따라 각기 다른 트랜잭션 속성을 부여하기 위함임
6.6.9 메소드 이름 패턴을 이용한 트랜잭션 속성 지정
- Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키(Key)와 값(Value)으로 갖는 컬렉션임
- 트랜잭션 속성은 밑과 같은 문자열로 정의할 수 있음
/*
PROPAGATION_NAME : 트랜잭션 전파 방식. 필수
ISOLATION_NAME : 격리수준. 생략시 디폴트
readOnly : 읽기전용. 생략가능(디폴트는 읽기전용 아님)
timeout_NNNN : 제한시간. 생략가능
-Exception1 : 체크 예외 중 롤백 대상으로 추가할 것을 넣음
+Exception2 : 런타임 예외 중 롤백하지 않을 예외를 넣음
*/
PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
- 트랜잭션 전파 항목만 필수이고 나머지 생략 가능
- 생략시 DefaultTransactionDefinition에 설정된 디폴트 속성이 부여됨
- “+”, “-”는 기본 원칙을 따르지 않은 예외를 정의할 때 사용
- 예시
<bean id="transactionAdvice"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager" />
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly,timeout_30</prop>
<prop key="upgrade*">PROPAGATION_REQUIRES_NEW,ISOLATION_SERIALIZABLE</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
/*
get으로 시작하는 메소드는 PROPAGATION_REQUIRED, 읽기전용, 제한시간 30초
upgrade로 시작하는 메소드는 PROPAGATION_REQUIRES_NEW, 완벽한 고립
나머지 메소드는 PROPAGATION_REQUIRED
*/
- 문자열로 표현한 이유는 트랜잭션 속성을 메소드 패턴에 따라 일일이 중첩된 태그와 프로퍼티로 설정하게 만들면 번거롭기 때문임
- 메소드가 하나 이상의 패턴과 일치하면 가장 정확히 일치하는 것에 적용됨
6.6.10 tx 네임스페이스를 이용한 방법
- TransactionInterceptor 타입의 어드바이스 빈과 TransactionAttribute 타입의 속성 정보도 tx 스키마의 전용 태그를 이용해 정의할 수 있음
- <bean> 태그에 비해 작성이 편해 tx 스키마를 사용해 어드바이스를 등록하도록 권장함
- 예시
<beans
...
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="...
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!--transaction-manager가 transactionManager일 경우 생략 가능-->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30"/>
<tx:method name="upgrade*" propagation="REQUIRES_NEW" isolation="SERIALIZABLE"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
</beans>
6.6.11 포인트컷과 트랜잭션 속성의 전략
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용함
- 일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는것이 바람직 함
- 타깃 클래스의 각 메소드는 필요하는 트랜잭션 TransactionAttribute 속성 값이 다를 수 있음
- 때문에, 트랜잭션용 포인트컷의 경우 비즈니스로직을 담은 클래스에 메소드 단위까지 세밀하게 포인트컷을 정의하면 안됨
- 트랜잭션 경계로 삼을 클래스들이 설정됐다면, 패키지를 통채로 선택하거나 클래스 이름에서 일정 패턴을 포인트컷 표현식으로 만들면 됨
- 관례적으로 비즈니스 로직 서비스를 담당하는 클래스 이름은 Service, ServiceImpl로 끝나는 경우가 많음
- 이의 경우 “execution(**.. Service*.*(..))”로 포인트컷 표현식을 만들면 됨
- 가능하면 클래스 보다 인터페이스 타입을 기준으로 타입 패턴을 적용하는 것이 좋음
- 메소드의 시그니처를 이용한 execution() 방식의 포인트컷 표현식 대신 스프링 빈 이름을 이용하는 bean() 표현식을 사용해도 됨
- 빈 이름을 기준으로 선정하기 때문에 클래스나 인터페이스 이름에 일정한 규칙을 만들기 어려운 경우 유용함
- 빈의 아이디가 Service로 끝나는 경우 bean(*Service)라 하면 됨
- 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의함
- 몇 가지 트랜잭션 속성을 정의하고 그에 따라 적정한 메소드 명명 규칙을 만들어 사용하는 것이 유용함
- 예외적인 경우에는 트랜잭션 어드바이스와 포인트컷을 새롭게 추가할 필요성이 있음
- 디폴트 트랜잭션 속성 부여 예시
<tx:advice id="transactionAdvice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
- 메소드 이름 패턴을 접두어에 붙여 공통된 규칙을 갖게 해야 함
- 두 가지 트랜잭션 속성 패턴을 사용한 예
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
<aop:advisor advice-ref="batchTxAdvice" pointcut="execution(a.b.*BatchJob.*.(..))"/>
</aop:config>
<tx:advice id="batchTxAdvice">
...
</tx:advice>
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
...
</tx:advice>
- 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때 적용되지 않음
- 전략이라기 보다는 주의사항임
- 프록시의 부가기능 적용은 클라이언트로부터 호출이 일어날 때만 가능함
- 즉, 타킷 메소드 내부에서의 호출은 부가기능 적용안됨
- PROPAGATION_REQUIRES_NEW를 사용하더라도 1, 3번에만 적용이 되지, 2번은 적용 안됨
6.6.12 트랜잭션 경계설정의 일원화
- 부가기능을 여러 계층에서 중구난방으로 적용하는 것은 옳지 않음
- 일반적으로 특정 계층의 경계와 트랜잭션 경계와 일치시키는 것이 바람직 함
- 비즈니스 로직을 담고 있는 서비스 계층 경계와 트랜잭션 경계와 일치하는 것이 적절함
- 서비스 계층을 트랜잭션이 시작되고 종료되는 경계로 설정 됐으면, 테스트와 같이 특별한 이유가 아니고서는 다른 계층이나 모듈에서 직접적으로 DAO에 접근하는 것을 차단해야 함
- 트랜잭션을 통해 모든 DAO 접근 작업이 완료되거나 취소 되어야하기 때문에, 다른 계층이 DAO에 접근하기 위해서는 서비스 계층을 거쳐서 DAO에 접근해야 함
- 예를 들어, 데이터를 추가하는 insert 작업일 경우 직접적으로 UserDao에 접근하지 않고, UserService의 add()함수를 거쳐야 함
- UserService는 UserDao에 작업을 위임함
- 이를 위해, UserDao 메소드 중에서 이미 서비스 계층에 부가적 로직을 담은 add()함수를 제외한 나머지를 UserService에 추가해야 함
public interface UserService {
//이미 서비스 계층에 부가적 로직을 담아서 추가한 메서드
void add(User user);
//새로 추가한 메서드
User get(String id);
List<User> getAll();
void deleteAll();
void update(User user1);
//UserService에서 만든 메소드
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
@Override
public User get(String id) {
return userDao.get(id);
}
@Override
public List<User> getAll() {
return userDao.getAll();
}
@Override
public void deleteAll() {
userDao.deleteAll();
}
@Override
public void update(User user1) {
userDao.update(user1);
}
...
}
6.6.13 서비스 빈에 적용되는 포인트컷 표현식 등록
- 기존 포인트컷 표현식을 모든 비즈니스 로직의 서비스 빈에 등록되도록 수정
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
</aop:config>
6.6.14 트랜잭션 속성을 가진 트랜잭션 어드바이스 등록
- tx 스키마 사용
<!--transaction-manager가 transactionManager일 경우 생략 가능-->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--propagation이 "REQUIRED"일 경우 생략 가능-->
<tx:method name="get*" read-only="true"/>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
6.6.15 트랜잭션 속성 학습 테스트
- tx:attributes의 “get”으로 시작하는 메소드가 읽기전용으로 셋팅됨
- 이에 대한 학습 테스트 생성
- 예외 상황을 만들기 위해 롤백 테스트를 위한 TestUserService 이용
- 예외는 TransientDataAccessResourceException임
public class UserServiceImpl implements UserService {
public static class TestUserService extends UserServiceImpl {
...
public List<User> getAll(){
for (User user:super.getAll()){
super.update(user); //읽기전용 속성으로 인해 예외 발생
}
return null;
}
}
}
public class UserServiceTest {
...
@Test(expected = TransientDataAccessResourceException.class)
public void readOnlyTransactionAttribute(){
testUserService.getAll();
}
}
'토비의 스프링 정리' 카테고리의 다른 글
토비의 스프링 - 6.8 트랜잭션 지원 테스트 (0) | 2022.10.20 |
---|---|
토비의 스프링 - 6.7 애노테이션 트랜잭션 속성과 포인트컷 (0) | 2022.10.20 |
토비의 스프링 - 6.5 스프링의 AOP (0) | 2022.10.20 |
토비의 스프링 - 6.4 스프링의 프록시 팩토리 빈 (0) | 2022.10.07 |
토비의 스프링 - 6.3 다이내믹 프록시와 팩토리 빈 (0) | 2022.10.07 |