본문 바로가기

Spring

Toby's Spring - Chap 6 - AOP

6. 트랜잭션 속성

6-1 트랜잭션 정의

트랜잭션이라고 모두 같은 방식으로 동작하지 않습니다.

하지만 트랜잭션의 기본 개념인 더 이상 쪼갤 수 없는 최소 단위의 작업이라는 개념은 유효합니다.

DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있습니다.

트랜잭션 전파

트랜잭션 전파(transaction propagation)란 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식입니다.

그림 6-22

A 트랜잭션 중에 B 트랜잭션을 진행하다 B 에서 에러가 나면 B 만 롤백 할 것인지 A 를 다 롤백할 것인지,

(2) 에서 에러가 나면 A 만 롤백할 것인지 B 도 같이 롤백할 것인지에 대한 설정을 말합니다.

 

- PROPAGATION_REQUIRED

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

 

- PROPAGATION_REQUIRED_NEW

앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작하게 합니다.

 

- PROPAGATION_NOT_SUPPORTED

트랜잭션 없이 동작하도록 합니다.

 

특정 메소드가 AOP 적용 대상이 되지 않도록 하려면?

모든 메소드에 트랜잭션 AOP가 적용되게 하고, 특정 메소드의 트랜잭션 전파 속성만 PROPAGATION_NOT_SUPPORTED 로 설정해서 

트랜잭션 없이 동작하게 만드는 방법이 있습니다.

 

격리수준

모든 DB 트랜잭션은 격리수준(Isonlation level)을 갖고 있어야 합니다.

적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 하는 제어가 필요합니다.

기본적으로 DB나 DataSource에 설정된 디폴트 격리수준을 따르는 편이 좋지만, 특별한 작업을 수행하는 메소드의 경우는 독자적인 격리수준을 지정할 필요가 있습니다.

제한시간

트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있습니다.

 

읽기전용

읽기전용(read only)로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있습니다.

 

6-2 트랜잭션 인터셉터와 트랜잭션 속성

TransactionInterceptor

TransactionInterceptor 어드바이스는 TranscationAdvice와 다르지 않지만 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공해줍니다.

그리고 rollbackOn() 메소드를 가지고 있는데 어떤 예외가 발생하면 롤백을 할 것인가를 결정하는 메소드입니다.

 

TransactionInterceptor에는 두 가지 종류의 예외 처리 방식이 있습니다.

 

1. 런타임 예외가 발생하면 트랜잭션은 롤백된다.

2. 체크 예외를 던지는 경우에는 이것을 예외상황으로 보지 않고 트랜잭션을 커밋한다.

 

하지만 특정 체크 예외 상황에서 롤백을 해야 된다면 rollbackOn() 속성을 둬서 특정 예외는 롤백 시키고 특정 런타임 예외에서는 

트랜잭션을 커밋시킬 수 있습니다.

 

트랜잭션 설정 적용 범위

만약 데이터를 수정하는 로직 안에서 readOnly 속성이 있는 메소드를 호출하면 에러가 날까?

트랜잭션 속성 중 readOnly나 timeout 등은 트랜잭션이 처음 시작될 때가 아니라면 적용되지 않습니다.

 

6-3 포인트컷과 트랜잭션 속성의 적용 전략

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

1-1 타입 패턴

만약 서비스 담당 클래스라면 excution(**..*ServiceImpl.*(..)) 과 같이 사용합니다.

 

1-2 빈 이름 패턴

bean() 표현식은 빈 이름을 기준으로 선정하기 때문에 클래스나 인터페이스 이름에 일정한 규칙을 만들기가 어려운 경우에 유용합니다.

bean(*Service) 와 같이 사용합니다.

 

2. 공통된 메소드 이름 규칙을 통해 최소한의 어드바이스와 속성을 정의한다.

실제로 하나의 애플리케이션에서 사용할 트랜잭션 속성의 종류는 그다지 다양하지 않습니다.

너무 다양하게 트랜잭션 속성을 부여하면 관리만 힘들어질 뿐입니다.

 

하지만 개발 중 특정 메소드에 다른 설정을 사용해야할 경우가 생긴다면 일단 모든 메소드에 기본 설정을 걸어두고

개발에 진행 됨에 따라 단계적으로 속성을 추가해 줍니다.

 

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

프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용은 클라이언트로 부터 호출이 일어날 때만 가능합니다.

여기서 클라이언트란 타깃 오브젝트를 사용하는 다른 모든 오브젝트를 말합니다.

6-23

[2] 과정을 보면 delete() 에서 update()를 바로 호출하게 됩니다.

이렇게 같은 타깃 오브젝트 안에서 메소드 호출이 일어날 경우에는 프록시 AOP를 통해 부여해준 부가기능이 적용되지 않는 점을 주의해야 합니다.

 

타깃 안에서의 호출에서 프록시가 적용되지 않는 문제를 해결 할 두가지 방법이 있습니다.

 

1. 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져와서 프록시를 이용하도록 강제한다.

- 부가적인 코드가 생겨나서 비추천

 

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

- 14장에서 자세히 설정

 

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

가끔 클래스나 메소드에 따라 제각각 속성이 다른, 세밀하게 튜닝된 트랜잭션 속성을 적용해야 하는 경우도 있습니다.

이런 세밀한 트랜잭션 속성의 제어가 필요한 경우를 위해 스프링이 제공하는 다른 방법이 있습니다.

설정파일을 사용하는 대신 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법입니다.

 

7-1 트랜잭션 애노테이션

@Transactional 애노테이션

@Transcational 애노테이션의 타깃은 메소드와 타입입니다.

따라서 메소드, 클래스, 인터페이스에 사용할 수 있습니다.

@Transcational 은 기본적으로 트랜잭션 속성을 정의하는 것이지만, 동시에 포인트컷의 자동등록에도 사용됩니다.,

 

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

@Transcational 은 메소드마다 트랜잭션 속성을 다르게 설정할 수 있으므로 매우 유연한 트랜잭션 속성 설정이 가능합니다.

따라서 메소드마다 @Transcational을 부여하고 속성을 지정할 수 있습니다.

이렇게 하면 유연한 속성 제어는 가능하지만 코드가 지저분해지고 애노테이션을 반복적으로 메소드마다 부여해주는 바람직하지 못한 결과를 가져올 수 있습니다.

대체 정책

스프링은 @Transcational을 적용할 때 4단계의 대체(fallback) 정책을 이용하게 해줍니다.

메소드의 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스)의 순서에 따라서 @Transcational이 적용됐는지 차례로 확인하고 가장 먼저 발견되는 속성정보를 사용합니다.

[1]
public interface Service {
	
    [2]
    void method1();
    
    [3]
    void method2();
}
 
[4]
public class ServiceImpl implements Service {
	
    [5]
    public void method1() {
    }
    
    [6]
    public void method2(){
    }
 }

 

메소드가 여러 개라면 클래스 레벨에 @Transactional을 부여하는 것이 편리합니다.

특정 메소드만 공통 속성을 따르지 않는다면 해당 메소드에만 추가로 @Transactional 을 부여해주면 됩니다.

 

SimpleJpaRepository - 1

 

 

 

7-2 트랜잭션 애노테이션 적용

장점

- 트랜잭션 설정이 직관적이고 간단하다.

- IDE의 자동완성 기능을 활용할 수 있다.

- 속성을 잘못 지정한 경우 컴파일 에러가 발생해서 손쉽게 확인 할 수 있다.

단점

- 트랜잭션 적용 대상을 손쉽게 파악할 수 없다.

- 사용 정책을 잘 만들어두지 않으면 무분별하게 사용되거나 자칫 빼먹을 위험이 있다.

- 트랜잭션이 적용되지 않았다는 사실을 파악하기 쉽지 않다.

 

8. 트랜잭션 지원 테스트

8-1 선언적 트랜잭션과 트랜잭션 전파 속성

트랜잭션을 정의할 때 지정할 수 있는 트랜잭션 전파 속성은 매우 유용한 개념입니다.

REQUIRED 전파 속성을 가진 메소드를 결합해서 다양한 크기의 트랜잭션 작업을 만들 수 있습니다.

트랜잭션 적용 때문에 불필요하게 코드를 중복하는 것도 피할 수 있으며, 애플리케이션을 작은 단위로 쪼개서 개발 할 수 있습니다.

 

- 선언적 트랜잭션(declarative transaction) : AOP를 이용해 코드 외부에서 트랜잭션 기능을 부여해주고 속성을 지정하는 방법

- 프로그램에 의한 트랜잭션(programmatic transaction) : TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법

 

스프링은 두 가지 방법을 모두 지원하지만 특별한 경우가 아니라면 선언적 방식의 트랜잭션을 사용하는 것이 바람직합니다,

 

8-2 트랜잭션 동기화와 테스트

트랜잭션 기술에 상관없이 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶어서 추상 레벨에서 관리하게 해주는 트랜잭션 추상화가 없었다면 AOP를 통한 선언적 트랜잭션이나 트랜잭션 전파 등은 불가능했을 것입니다.

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

트랜잭션 추상화의 핵심기능은 트랜잭션 매니저와 트랜잭션 동기화 입니다.

 

- 트랜잭션 매니저 : 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어

- 트랜잭션 동기화 :

1. 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유

2. 진행 중인 트랜잭션이 있는지 확인하고 트랜잭션 전파 속성에 따라서 이에 참여하게 만들어 줌

 

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

@Test
public void transactionSync() {
	userService.deleteAll();

	userService.add(users.get(0));
	userService.add(users.get(1));
}

세 메소드의 트랜잭션을 통합하려면 테스트 메소드에서 UserService의 메소드를 호출하기 전에 트랜잭션을 미리 시작해주면 됩니다.

트랜잭션 전파는 트랜잭션 매니저를 통해 트랜잭션 동기화 방식이 적용되기 때문에 가능합니다.

 

@Test
public void transactionSync() {
	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
	TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);

	userService.deleteAll();

	userService.add(users.get(0));
	userService.add(users.get(1));

	transactionManager.commit(txStatus);
}

 

롤백 테스트

롤백 테스트는 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트를 말합니다.

@Test
public void transactionSync() {
	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
	TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
	
	try {
		userService.deleteAll();

		userService.add(users.get(0));
		userService.add(users.get(1));
	}
	finally {
		transactionManager.rollback(txStatus);
	}

}

롤백 테스트는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 시작하기 전 상태로 만들어줍니다.

적절한 격리수준만 보장해주면 동시에 여러 개의 테스트가 진행돼도 상관없고 DB에 따라서 성공적인 작업이라도 트랜잭션을 롤백하면 커밋할 때보다 성능이 더 향상되기도 합니다.

 

8-3 테스트를 위한 트랜잭션 애노테이션

@Transcational

테스트에도 @Transactional을 적용할 수 있습니다. 테스트에서 사용하는 @Transactional은 AOP를 위한 것은 아닙니다.

단지 컨텍스트 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 쓰일 뿐입니다.

 

@Rollback

테스트 메소드나 클래스에 사용하는 @Transactional은 애플리케이션의 클래스에 적용할 때와 디폴트 속성은 동일합니다.

하지만 중요한 차이점이 있는데, 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백됩니다.

만약 트랜잭션은 적용되지만 롤백을 원치 않는다면 @Rollback(false)라고 해줘야 합니다.

 

@TranscationConfiguration

클래스 레벨에서 롤백에 대한 공통 속성을 지정할 수 있습니다.

롤백을 원치 않는 메소드가 많다면 디폴트 롤백 속성을 false로 해두고, 롤백을 원하는 일부 메소드만 @Rollback을 부여해주면 됩니다.

 

@Transactional
@TransactionConfiguration(defaultRollback=false)
public class UserServiceTest {
	
	@Test
	@Rollback
	public void add() {
		...
	}
}

 

NotTransactional 과 Propagation.NEVER

클래스 레벨에서 트랜잭션이 설정되어 있고 일부 메소드는 트랜잭션을 사용하지 않으려면 @NotTransactional 을 테스트 메소드에 부여하면 됩니다.

하지만 스프링 3.0 에서 제거 대상이 되었기 때문에 

@Transactional(propagation=Propagation.NEVER)

위와 같이 지정해주면 트랜잭션이 시작되지 않습니다.

 

효과적인 DB 테스트

일반적으로 고립된 상태에서 테스트를 진행하는 단위 테스트와 DB 같은 외부의 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는게 좋습니다.

테스트는 어떤 경우에도 서로 의존하면 안되고 테스트가 진행되는 순서나 앞의 테스트의 성공 여부에 따라서 다음 테스트의 결과가 달라지는 테스트를 만들면 안됩니다.

트랜잭션을 지원하는 롤백 테스트는 테스트 작성에 있어서 유용한 도구가 되어 줄 것입니다.