2020-5-28 토비의 스프링 6장 AOP (3)
애스펙트 지향 프로그래밍
- 핵심기능을 담당하는 코드를 독립적인 모듈로 만들기 위해 DI, 데코레이터패턴, 다이나믹 프록시, 오브젝트 생성 후처리, 자동프록시생성, 포인트컷과 같은 방식들을 도입했었다.
- 부가기능 모듈을 객체지향 기술에서 오브젝트와는 다르게 애스펙트라고 한다.
- 애스팩트 : 핵심기능을 담고 있지는 않지만 핵심기능에 부가되어있는 특별한 모듈. 부가기능이 정으된 어드바이스와 어디에 적용될지 결정하는 포인트컷을 함께담고있다.
- 핵심기능에서 부가기능을 분리해서 모듈로 만들어서 개발하는 방법을 AOP(Aspect Oriented Programming)이라 한다.
- AOP는 OOP를 돕는 보조적인 개념이다.
AOP를 적용하는 방법
- 프록시를 이용
- IoC/DI 컨테이너, 다이나믹 프록시, 데코레이터패턴 등 다양한 기술을 활용해서 AOP를 지원한다. 핵심은 프록시를 사용하는것이다.
- 프록시로 만들어서 DI로 연결된 빈 사이에 적용해 타깃의 메서드 호출과정에 참여해서 부가 기능을 제공
- 스프링 AOP는 JDK와 스프링컨테이너외에 필요한게 없다.
- 어드바이스가 적용되는 대상은 오브젝트의 메서드. 프록시 방식을 사용했기때문에 메서드 호출과정에 참여해서 부가기능을 제공해주게 되어있다.
- 바이트코드 생성과 조작을 통한 AOP
- 프록시를 사용하지 않고도 AOP를 쓸 수 있다. AspectJ가 대표적.
- AspectJ는 타깃오브젝트를 뜯어서 기능을 직접 넣는 직접적인 방식을 사용. 부가기능을 위해 소스코드를 수정할 수 없으니 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 방법을 사용한다.
- 왜 바이트코드같은 복잡한 방법을 쓰나?
- DI컨테이너의 도움을 받아서 자동 프록시 방식을 안쓰고 AOP를 쓸 수 있다. 스프링컨테이너가 사용되지 않는 환경에서도 적용가능
- 프록시방식보다 훨씬 유연. 프록시를 쓰면 부가기능 대상이 메서드로 제한된다. 바이트코드를 직접 조작하면 오브젝트의 생성, 필드값의 조회와 조작 등 다양한 기능을 부여할 수 있다.
- AspectJ를 쓰기 위해 JVM실행옵션을 조작하고 별도의 바이트코드 컴파일러. 클래스로더를 사용해야되서 권장하진 않는다.
- 스프링 AOP를 적용하기 위해 최소한 4가지 빈을 등록해야한다.
- 자동프록시생성기
- DefaultAdvisorAutoProxyCreator클래스를 빈으로 등록.
- 다른빈들을 DI하지 않고 자신도 DI되지 않고 독립적으로 존재
- ApplicationContext가 빈을 생성하는 과정에서 빈후처리기로 참여
- 어드바이스 : 부가기능을 구현한 클래스를 빈으로 등록
- 포인트컷 : AspectJExpressionPointcut을 빈으로 등록하고 expression프로퍼티에 포인트컷 표현식을 넣어라.
- 어드바이저 : DefaultPointcutAdvisor클래스를 빈으로 등록해라. 어드바이스와 포인트컷을 참조하는것 말고 기능이 없다. 자동프록시생성기에 의해 자동검색되어 사용된다.
- 부가기능을 제공하는 어드바이스외에 나머지 3개는 스프링이 제공하는 빈이다.
- 자동프록시생성기
- AOP네임스페이스
- <aop:config>, <aop:pointcut>, <aop:advisor> 태그를 활용해서 등록할수도 있다.
용어
- 타깃 : 부가기능을 사용할 대상.
- 어드바이스
- 타깃에게 제공할 부가기능을 담은 모듈. 어드바이스는 오브젝트로 정의하기도 하지만 메서드로 정의할수도 있다.
- 조인포인트 : 어드바이스가 적용될 수 있는 위치
- 스프링 프록시 AOP에서는 조인포인트가 메서드의 실행 한가지 뿐이다.
- 포인트컷 : 어드바이스를 적용할 조인포인트를 선별하는 작업.
- 프록시 : 클라이언트와 타깃사이에 부가기능을 제공하는 오브젝트.
- DI를 통해 타깃 대신 클라이언트에 주입.
- 클라이언트의 메서드 호출을 대신받아서 타깃에 위임해주고 그 과정에서 부가기능을 제공.
- 어드바이저 : 포인트컷과 어드바이스를 하나씩 가지고 있는 오브젝트. 어떤 부가기능을 어디에 적용할지 알고있다. AOP의 가장 기본모듈
- 스프링은 자동프록시 생성기가 어드바이저를 AOP정보로 활용한다.
- 스프링 AOP에서만 사용되는 용어다.
- 애스펙트 : AOP의 기본 모듈. 한개이상의 포인트컷과 어드바이스의 조합이로 만들어진다.
- 보통 싱글턴 오브젝트로 존재.
트랜잭션
- 트랜잭션은 최소 단위의 작업. 트랜잭션 경계안에서 commit()을 통해 모두 성공하거나 rollback()을 통해 모두 취소돼야 한다. 이 외에도 트랜잭션 격리레벨, 전파 등을 통해 제어할 수 있다.
- 트랜잭션전파(Propagation) : 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있거나 없을때 어떻게 동작하는지 결정하는 방식이다.
- 예시 : A에서 트랜잭션이 진행중인데, 중간에 B.method()를 호출한 상황에서 B의 코드는 어느 트랜잭션에서 동작할까??? 새로운 트랜잭션을 만들지 않고 A트랜잭션에 참여할지 아니면 A트랜잭션과 별도로 B트랜잭션을 따로 만들지.
- 속성
- PROPAGATION_REQUIRED : 가장많이사용. 진행중인 트랜잭션이 없으면 새로 생성. 이미 시작된 트랜잭션이 있으면 여기에 참여. 결합방식을 이용해서 하나의 트랜잭션으로 구성한다.
- PROPAGTAION_REQUIRES_NEW : 항상 새로운 트랜잭션. 독립적인 트랜잭션이 보장된다.
- PROPAGATION_NOT_SUPPORTED : 트랜잭션 없이 동작할 수 있다. 진행중인 트랜잭션이 있어도 그냥 무시. 무시하는데는 이유가 있다. 보통 경계설정은 AOP를 이용해 한번에 많은 메서드를 동시에 적용하는데, 특별한 메서드만 제외시키기 위해 이렇게 설정한다. 특정한 메서만 제외할때 포인트컷을 만들 수 있지만 포인트컷이 복잡해진다.
- 격리수준 : 서버에서 여러개의 트랜잭션이 동시에 진행될 수 있어서 적절하게 격리수준을 설정해야한다. 기본적인 DB에 설정되있지만 JDBC, JPA, Datasource에서 재설정이 가능.
- 제한시간 : 트랜잭션을 수행하는 제한시간을 설정. PROPAGATION_REQUIRED, PROPAGATION_REQUIRE_NEW와 함께 쓴다.
- 읽기전용 : 트랜잭션내에서 데이터 조작이 안된다.
트랜잭션 인터셉터와 속성
- 메서드별로 다른 트랜잭션 정의를 적용하려면 어드바이스 기능을 확장해야한다. 스프링의 TransactionInterceptor를 사용하자.
- TransactionInterceptor를 통해 트랜잭션 정의를 메서드 이름 패턴을 이용해서 다르게 지정할 수 있다. PlatformTransactionManager, Properties타입을 속성으로 가지고 있다.
- Properties는 트랜잭션 속성을 정의한것이다.
- TransactionAdvice는 런타임예외에서만 롤백시킨다. 런타임예외가 아니면 제대로 처리되지 않고 메서드를 빠져나가게 되있다. 체크예외를 던지는 곳에서 문제될 수 있다.
- TransactionInterceptor에는 2가지 예외처리 방식이 있다. 런타임예외가 발생되면 롤백. 체크 예외가 발생하면 예외상황으로 해석하지 않고 트랜잭션을 커밋한다. 따라서 TransactionAttribute는 rollbackOn()을 통해 기본원칙과 다른 예외처리를 가능하게 해준다.
- TransactionInterceptor는 TransactionAttribute를 Properties라는 맵타입 오브젝트로 전달을 받는다.
- Properties타입의 transactionAttributes프로퍼티는 메서드패턴과 트랜잭션 속성을 키-값으로 갖는다. 전파속성만 필수가 나머지는 생략가능하다. 생략하면 DefaultTransactionDefinition 기본값이 그대로 적용된다.
주의사항
프록시 방식 AOP는 같은 타깃 오브젝트 내의 메서드를 호출할때 트랜잭션이 적용되지 않는다. 프록시를 통해 메서드A,B를 호출하면 트랜잭션이 적용되지만, 타깃객체 안에서 A,B를 호출하면 트랜잭션이 적용되지 않는다. 타깃객체에서 프록시를 거치지 않고 직접호출하기 때문이다.
- 해결법
- 스프링API를 활용해 프로젝트에 대한 레퍼런스를 가져온 뒤 오브젝트의 메서드 호출도 프록시를 통해 강제. 추천하는 방법은 아니다.
- AspectJ와 같은 타깃 오브젝트의 바이트코드를 직접조작
트랜잭션 어노테이션
- @Transactional은 메서드, 클래스, 인터페이스에 사용가능
- TransactionInterceptor는 @Transactional 엘리먼트에서 트랜잭션 속성을 가져오는 AnnotaionTransactionAttributeSource를 사용한다. 이를 통해 유연한 트랜잭션을 쓸 수 있게해준다. 포인트컷도 @Transactional을 통한 트랜잭션 속성정보를 참조하게 만든다.
- 위 방식을 통해 포인트컷과 트랜잭션 속성을 어노테이션 하나로 지정할 수 있다. 이를통해 세밀한 트랜잭션 조정이 가능하다.
대체정책
- 스프링은 @Trnasactional을 적용할때 4단계 대체정책을 이용한다. 메서드의 속성을 확인할 때 타깃메서드, 타깃클래스, 선언메서드, 선언타입(클래스,인터페이스) 순서로 @Transactional이 적용됬는지 확인하고 가장 먼저 발견된 속성정보를 활용한다.
// [1]
public interface Service {
// [2]
void method1();
// [3]
void method2();
}
// [4]
public class ServiceImpl implements Service {
// [5]
public void method1();
// [6]
public void method2();
}
-
타깃오브젝트의 메서드인 5,6번 위치부터 확인. 여기가 첫번째 후보. 여기서 발견되면 바로 트랜잭션 속성을 가져다가 사용
-
5,6에 없으면 타깃오브젝트인 4번위치에서 확인. 클래스에 부여되면 해당 클래스의 모든 메서드에 공통적으로 적용되는 속성. 메서드가 여러개면 클래스에 부여해라
-
타깃클래스에도 @Transactional이 없으면 인터페이스가 선언된 2,3번 위치로 간다. 여기서 확인. 없으면 1번 위치에 있는지 확인한다.
-
어노테이션을 최소한만 써라.
-
@Transactional은 인터페이스가 정의한 메서드라서 타깃클래스보다 인터페이스에 적용하는게 바람직하지만, 프록시 방식의 AOP가 아닌 방식으로 트랜잭션을 적용하면 인터페이스에 정의한 @Transactional이 무시된다. 따라서 타깃클래스에 두는게 좋다.
-
<tx:annotation-driven /> // 이것만 선언하면 사용가능
-
@Transactional public interface UserService { void add(User user); @Transactional(readOnly = true) User get(String id); @Transactional(readOnly = true) List<User> getAll(); void deleteAll(); void update(User user); void upgradeLevels(); } // 고려해야될 상황 @Transactional public class UserServiceImpl implements UserService {}
-
인터페이스에도 어노테이션이 붙어있고, UserServiceImpl에도 붙어있으면 정책에 따라 타깃클래스의 트랜잭션이 채택된다. 위의 코드에서는 아무런 속성이 없기때문에 default속성이 적용되고, 인터페이스에서 정의한 readOnly=true는 적용되지 않는다.
- AOP를 이용해 코드 외부에서 트랜잭션 기능을 부여해주고 속성을 지칭하는걸 선언적트랜잭션이라한다. TransactionTemplate이나 트랜잭션API를 통해 코드에서 직접사용하는건 프로그램에 의한 트랜잭션이라 한다. 선언적 트랜잭션을 권장한다.
트랜잭션 동기화
-
AOP덕분에 트랜잭션의 자유로운 전파와 유연한 개발이 된다. 트랜잭션 추상화를 통해 부기기능을 간단하게 설정할 수 있다.
-
추상화의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.
- PlatfromTransactionManger인터페이스의 구현체를 통해 구체적인 트랜잭션 기술의 종류와 상관없이 일관된 트랜잭션 제어가 가능
- 트랜잭션 동기화 때문에 정보를 저장소에 보고나했다가 DAO에서 공유가능
-
@Test public void transactionSync() { userService.deleteAll();; userService.add(users.get(0)); userService.add(users.get(1)); } @Test public void transactionSync() { DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(txDefinition); userService.deleteAll(); userService.add(users.get(0)); userService.add(users.get(1)); transactionManager.commit(status); } //트랜잭션 예외로 실패 @Test public void transactionSync() { DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); txDefinition.setReadOnly(true); TransactionStatus status = transactionManager.getTransaction(txDefinition); userService.deleteAll(); userService.add(users.get(0)); userService.add(users.get(1)); transactionManager.commit(status); } // 트랜잭션 롤백테스트 @Test public void transactionSync() { userService.deleteAll(); assertThat(userDao.getCount(), is(0)); DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(txDefinition); userService.add(users.get(0)); userService.add(users.get(1)); assertThat(userDao.getCount(), is(2)); transactionManager.rollback(status); // 강제롤백 assertThat(userDao.getCount(), is(0)); }
- UserService의 모든 메서드에 트랜잭션을 적용해서 여기서 트랜잭션은 3개다. 하나로 어떻게 합칠까? UserService메서드 호출하기전에 트랜잭션을 미리 시작하면 된다. 테스트 아래코드처럼 UserService메서드를 호출하기전에 트랜잭션을 시작하면 하나로 합칠 수 있다.
- 트랜잭션 결과나 상태를 조작하면 하이버네이트의 detached엔티티의 상태를 확인할때 좋다.
-
테스트에서도 @Transactional을 사용할 수 있다. 테스트메서드에서 트랜잭션 경계가 설정된다. 테스트프레임워크에 의해 부여된다.
-
@Rollback : 테스트가 끝나면 자동으로 롤백된다. 테스트에 @Transactional은 강제 롤백시키도록 되어있다.
- @TransactionalConfiguration : 테스트클래스의 모든 메서드에 트랜잭션을 적용하면서 롤백되지 않고 커밋하기 위해서는 @Rollback(false)를 쓸수도 있지만 @TransactionalConfiguration을 쓸수도 있다. 여기서 defaultRollback=false를 하면된다.