스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 6장. AOP (6.7 ~ 6.8)

ttoance 2025. 5. 11. 10:00
반응형

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

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

  • 이런 경우라면 메소드 이름 패턴을 이용해서 일괄적으로 트랜잭션 속성을 부여하는 방식은 적합하지 않다. 
  • 기본 속성과 다른 경우가 있을때마다 일일이 포인트컷과 어드바이스를 새로 추가해줘야 한다. 

 

6.7.1 트랜잭션 애노테이션 

@Transactional 

  • 타깃은 메소드, 클래스, 인터페이스에 사용할 수 있다. 
  • 이때 사용되는 포인트컷은 TransactionAttributeSourcePointcut이다. 
/*
 * Copyright 2002-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.TransactionDefinition;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {

	/**
	 * Alias for {@link #transactionManager}.
	 * @see #transactionManager
	 */
	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	Propagation propagation() default Propagation.REQUIRED;
	Isolation isolation() default Isolation.DEFAULT;
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
	String timeoutString() default "";
	boolean readOnly() default false;
	Class<? extends Throwable>[] rollbackFor() default {};
	String[] rollbackForClassName() default {};
	Class<? extends Throwable>[] noRollbackFor() default {};
	String[] noRollbackForClassName() default {};

}

 

 

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

  • TransactionInterceptor는 메소드 이름 패턴을 통해 부여되는 일괄적인 트랜잭션 속성정보 대신 @Transactional 애노테이션의 앨리먼트에서 트랜잭션 속성을 가져오는 AnnotaionTransactionAttributeSource를 사용한다. 
    • @Transactional은 메소드마다 다르게 설정할 수 있어서 유연한 트랜잭션 속성 설정이 가능해진다. 
  • 트랜잭션 속성은 타입 레벨에 일괄적으로 부여 가능하고, 메소드 단위로 세분화해서 트랜잭션 속성을 다르게 지정할 수 있다. 

 

대체 정책 failback 정책 

  • 스프링은 @Transactional을 적용할 때 4개의 대체 failback 정책을 이용하게 해준다. 
    • 메소드의 속성 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스)의 순서에 따라 @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 한다. 
    • @Transactional이 부여되어 있다면 이를 속성으로 사용하고, 만약 없으면 다음 대체 후보인 타깃 클래스에 부여된 @Transactional 애노테이션을 찾는다.
  • 기본적으로 @Transactional 적용 대상은 클라이언트가 사용하는 인터페이스가 정의한 메소드이므로 @Transactional 적용 대상은 클라이언트가 사용하는 인터페이스가 정의한 메소드이므로 @Transactional도 타깃 클래스보다는 인터페이스에 두는게 바람직하다. 
    • 하지만 인터페이스를 사용하는 프록시 방식의 AOP가 아닌 방식으로 트랜잭션 적용하면 인터페이스에 정의한 @Transactional은 무시되기 때문에 안전하게 타깃 클래스에 @Transactional 두는 방법을 권장한다. 
    • 인터페이스에 @Transactional을 두면 구현 클래스가 바뀌더라도 트랜잭션 속성을 유지할 수 있다는 장점이 있다. 

 

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

  • @Transactional 애노테이션은 UserServiceImpl 클래스 대신 UserService 인터페이스에 적용한다. 
    • 그래야 UserServiceImpl과 TestUserService 양쪽에 트랜잭션이 적용될 수 있다. 

 

6.8 트랜잭션 지원 테스트 

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

  • add() 메소드에 REQUIRED 방식의 트랜잭션 전파 속성을 지정할 때 
    • 앞에서 진행 중인 트랜잭션이 있으면 참여하고 없으면 자동으로 새로운 트랜잭션을 시작한다. 
  • AOP 이용해 코드 외부에서 트랜잭션 기능을 부여해주고 속성 지정할 수 있게 하는 방법을 선언적 트랜잭션 declarative transaction 이라고 한다. 
  • 반대로 TransactionTemplate 이나 개별 데이터 기술의 트랜잭션 API 사용해 직접 코드 안에서 사용하는 방법은 프로그램에 의한 트랜잭션 progammatic transaction 이라고 한다. 
  • 스프링은 이 두 가지 방법을 모두 지원한다. 

 

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

  • 트랜잭션의 자유로운 전파와 그로 인한 유연한 개발이 가능할 수 있었던 기술적인 배경에는 AOP와 스프링의 트랜잭션 추상화가 있다. 
    • AOP 덕분에 프록시 이용한 트랜잭션 부가기능을 간단하게 애플리케이션 전반에 적용할 수 있다. 
    • 트랜잭션 추상화 덕분에 데이터 액세스/트랜잭션 기술에 상관없이 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶어서 추상 레벨에서 관리해준다. 

 

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

  • 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.
    • PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저 통해 구체적인 트랜잭션 기술의 종류와 상관없이 일관된 트랜잭션 제어가 가능하다.
    • 트랜잭션 동기화 기술이 있어서 시작된 트랜잭션 정보를 저장소에 보관했다가 DAO에도 공유할 수 있다. 

 

트랜잭션 동기화 검증 

 

 

롤백 테스트

  • 롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다. 
  • DB를 엑세스하는 테스트를 위해서는 테스트를 할 때마다 테스트 데이터를 초기화하는 번거로운 작업이 필요해진다. 
  • 롤백 테스트는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 시작하기 전 상태로 만들어준다. 
  • DB에 따라서 성공적인 작업이라도 트랜잭션을 롤백하면 커밋할 때보다 성능이 더 향상되기도 한다. 
    • 예를 들어, MySQL에서는 동일한 작업을 수행한 뒤에 롤백하는 게 커밋하는 것보다 더 빠르다. 
    • 하지만 DB의 트랜잭션 처리 방법에 따로 롤백이 커밋보다 더 많은 부하를 주는 경우도 있으니 단지 성능 때문에 롤백 테스트가 낫다고는 볼 수 없다. 

 

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

  • 스프링의 컨텍스트 테스트 프레임워크는 애노테이션을 이용해 테스트를 편리하게 만들 수 있는 여러 가지 기능을 추가해준다. 
  • @ContextConfiguration을 클래스에 부여하면 테스트를 실행하기 전에 스프링 컨테이너를 초기화하고 
  • @Autowired 애노테이션 붙은 필드를 통해 테스트에 필요한 빈에 자유롭게 접근할 수 있다. 

 

@Transactional 

  • 테스트 클래스 또는 메소드에 @Transactional 애노테이션을 부여해주면 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다. 
  • 테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶어줄 수 있다. 
  • @Transactional은 테스트 클래스 레벨에 부여할 수 있다. 그러면 테스트 클래스 내의 모든 메소드에 트랜잭션이 적용된다. 

 

@Rollback 

  • @Rollback의 기본 값은 true다. 트랜잭션 적용되지만 롤백을 원치 않는다면 @Rollback(false)라고 해줘야 한다. 

 

@TransactionConfiguration 

  • @Transactional은 테스트 클래스네 얺어서 모든 테스트 메소드에 일괄 적용할 수 있지만, @Rollback 애노테이션은 메소드 레벨에만 적용할 수 있다. 
  • 테스트 클래스의 모든 메소드에 적용하려면 @TransactionConfiguration 애노테이션을 사용하면 편리하다. 
    • 디폴트 롤백 속성은 false로 해두고, 테스트 메소드 중에서 일부만 적용하고 싶다면 @Rollback을 부여해주면 된다. 

 

 

NotTransactional과 Propagation.NEVER 

  • 테스트 클래스 안에서 일부 메소드에만 트랜잭션이 필요하다면 메소드 레벨의 @Transactional을 적용하면 된다. 
  • 반면에 대부분의 메소드에서 트랜잭션이 필요하다면 테스트 클래스에 @Transactional을 지정하는 것이 편리하다. 
  • @NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션 설정을 시작하지 않은 채로 테스트를 진행한다. 
    • @NotTransactional은 스프링 3.0에서는 제거되었다. 
  • @NotTransactional 대신 @Transactional의 트랜잭션 전파 속성을 사용하는 방법이 있다. 
    • @Transactional(propagation=Propagion.NEVER) 전파 속성을 사용해두면 트랜잭션이 사직되지 않는다. 

 

효과적인 DB 테스트 

  • 일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트 진행하는 단위 테스트와,
  • DB같은 외부 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는 것이 좋다. 
    • DB가 사용되는 통합 테스트를 별도의 클래스로 만들어준다면 기본적으로 클래스 레벨에 @Transactional을 부여해준다. 
    • DB가 사용되는 통합 테스트는 가능한 한 롤백 테스트로 만드는 게 좋다. 
      • 애플리케이션의 모든 테스르를 한꺼번에 실행하는 빌드 스크립트 통해서 공통적으로 이용할 수 있는 테스트DB 셋업하고 각 테스트는 자신이 필요한 테스트 데이터를 보충해서 테스트 진행하게 만든다. 
      • 테스트가 기본적으로 롤백 테스트로 되어 있다면 테스트 사이에 서로 영향을 주지 않으므로 독립적이고 자동화된 테스트로 만들기가 매우 편하다. 
  • 테스트는 어떤 경우에도 서로 의존하면 안 된다. 
    • 테스트가 진행되는 순서나 앞의 테스트의 성공 여부에 따라서 다음 테스트의 결과가 달라지는 테스트를 만들면 안 된다. 
    • 코드가 바뀌지 않는 한 어떤 순서로 진행되더라도 테스트는 일정한 결과를 내야 한다. 

 

6.9 정리 

  • 트랜잭션 경계설정 코드를 분리해서 별도의 클래스로 만들고 비즈니스 로직 클래스와 동일한 인터페이스를 구현하면 DI의 확장 기능을 이용해 클라이언트의 변경 없이도 깔끔하게 분리된 트랜잭션 부가기능을 만들 수 있다. 
  • 트랜잭션처럼 환경과 외부 리소스에 영향을 받는 코드를 분리하면 비즈니스 로직에만 충실한 테스트를 만들 수 있다. 
  • 목 오브젝트를 활용하면 의존관계 속에 있는 오브젝트도 손쉽게 고립된 테스트로 만들 수 있다. 
  • DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다. 
  • 번거로운 프록시 클래스 작성은 JDK의 다이내믹 프록시를 사용하면 간단하게 만들 수 있다. 
  • 다이내믹 프록시는 스태틱 팩토리 메소드를 사용하기 때문에 빈으로 등록하기 번거롭다. 따라서 팩토리 빈으로 만들어야 한다. 스프링은 자동 프록시 생성 기술에 대한 추상화 서비스를 제공하는 프록시 팩토리 빈을 제공한다. 
  • 프록시 팩토리 빈의 설정이 반복되는 문제를 해결하기 위해 자동 프록시 생성기와 포인트컷을 활용할 수 있다. 자동 프록시 생성기는 부가기능이 담긴 어드바이스를 제공하는 프록시를 스프링 컨테이너 초기화 시점에 자동으로 만들어준다. 
  • 포인트컷은 AspectJ 포인트컷 표현식을 사용해서 작성하면 편리하다. 
  • AOP는 OOP만으로는 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다. 
  • 스프링은 자주 사용되는 AOP 설정과 트랜잭션 속성을 지정하는 데 사용할 수 있는 전용 태그를 제공한다. 
  • AOP를 이용해 트랜잭션 속성을 지정하는 방법에는 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 @Transactional 애노테이션을 사용하는 방법이 있다. 
  • @Transactional을 이용한 트랜잭션 속성을 테스트에 적용하면 손쉽게 DB를 사용하는 코드와 테스트를 만들 수 있다. 

 

 

 

토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main

토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358

반응형