본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

Step5. 자동 프록시 생성기와 포인트컷 표현식

by 민휘 2023. 4. 20.

개선 목표

스프링 팩토리 빈을 사용하여 타깃과 부가기능의 조합마다 Handler 클래스가 만들어지던 문제를 해결했다. 하지만 빈 설정 정보에는 타깃과 어드바이저 조합마다 설정이 중복된다. 어드바이저 등록 설정은 똑같은데 타깃만 달라지는 설정 정보 중복이 발생하는 것이다. 설정 중복을 제거하기 위해 팩토리 빈을 스프링 빈으로 등록하는 방법이 아닌 자동 프록시 생성기를 만드는 방법을 선택한다.

<!-- 설정 정보의 중복 -->
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="userServiceImpl" />
		<property name="interceptorNames">
			<list>
				<value>transactionAdvisor</value>
			</list>
	</property>
</bean>

 

 

 

빈 후처리기

빈 후처리기는 스프링에 제공하는 확장 포인트다. 빈 후처리기를 빈으로 동록하여 사용한다. 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다. 프록시에 적용하려면 빈 오브젝트가 생성될 때마다 빈 후처리기가 프록시 적용 여부를 판단하고, 프록시를 생성하여 컨테이너에 등록하고 어드바이저에 연결한다.

 

포인트컷

프록시 자동생성 방식에서 사용하는 포인트컷은 클래스 필터와 메소드 매처를 사용하여 부가기능을 적용할 메소드를 판단할 때 클래스 이름과 메소드 이름을 모두 조건 검사한다. 즉 빈 후처리기에 등록하는 포인트컷은 클래스와 메소드 선정 알고리즘을 모두 갖고 있어야 한다.

아래 예시처럼 NameMatchMethodPointcut에 ClassFilter와 메소드 패턴을 추가하여 포인트컷을 만들 수 있다.

public class ReflectionTest {

		@Test
    public void classNamePointcutAdvisor() {

        // 포인트컷 준비
        NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
            public ClassFilter getClassFilter() {
                return new ClassFilter() {
                    @Override
                    public boolean matches(Class<?> clazz) {
                        return clazz.getSimpleName().startsWith("HelloT"); // 클래스 이름이 HelloT로 시작하는 것만 선정
                    }
                };
            }
        };
        classMethodPointcut.setMappedName("sayH*");

        // 테스트
        checkAdviced(new HelloTarget(), classMethodPointcut, true);
        checkAdviced(new HelloWorld(), classMethodPointcut, true);
        checkAdviced(new HelloToby(), classMethodPointcut, true);

    }

}

 

 

 

트랜잭션 포인트컷 작성과 자동 프록시 생성기 등록

NameMatchMethodPointcut의 클래스 필터는 모든 클래스를 허용한다. 그래서 프로퍼티로 받은 클래스 이름을 이용해 필터를 만들어 덮어 씌우도록 한다.

public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {

    public void setMappedClassName(String mappedClassName) {
        this.setClassFilter(new SimpleClassFilter(mappedClassName)); // 클래스 필터 추가
    }

    static class SimpleClassFilter implements ClassFilter {
        String mappedName;

        private SimpleClassFilter(String mappedName) {
            this.mappedName = mappedName;
        }

        public boolean matches(Class<?> clazz) {
            return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName());
        }
    }

}

 

포인트컷은 다음과 같이 등록한다. 참고로 어드바이스와 어드바이저 설정은 바뀌지 않는다. 다만 이전처럼 어드바이저를 명시적으로 주입 받는 빈(팩토리 빈)은 존재하지 않는다. 자동 등록되기 때문이다.

<bean id="transactionPointcut" class="springbook.user.service.NameMatchClassMethodPointcut">
	<property name="mappedClassName" value="*ServiceImpl"/>
	<property name="mappedName" value="upgrade*"/>
</bean>

 

자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator를 등록한다. 빈에서 참조되지 않거나 코드에서 빈 이름으로 조회될 필요가 없다면 아이디로 등록하지 않아도 된다.

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

 

그리고 스프링 팩토리 빈을 등록했던 코드는 이제 더이상 필요하지 않으므로 제거한다. 아이디가 userService였는데, 팩토리 빈 대신 실제 타깃을 등록한다. 이제 타깃 빈이 만들어질 때마다 자동 프록시 빈 생성기으로 전달되어 포인트컷을 거쳐 프록시가 등록될 것이다.

<bean id="userService" class="springbook.user.service.UserServiceImpl">
	<property name="userDao" ref="userDao"/>
	<property name="mailSender" ref="mailSender"/>
</bean>

 

등록한 서비스 빈이 프록시인지 확인하는 테스트를 통과한다. 자동 프록시 빈 생성기가 타깃을 프록시로 바꿔서 등록해준다는 사실을 알 수 있다.

@Test
public void advisorAutoProxyCreator() {
		assertThat(userService, is(java.lang.reflect.Proxy.class));
}

 

 

포인트컷 표현식

 

단순히 이름 비교하는 것 말고 더 복잡하고 세밀한 기준을 이용해 클래스나 메소드를 선정해보자. 리플렉션으로 메타 정보를 사용할 수 있지만, 리플렉션 API를 직접 사용하면 조건이 달라질 때마다 구현 코드를 변경해야하므로 번거롭다. 대신 AspectJ 포인트컷 표현식을 사용해 클래스와 메소드 선정 알고리즘을 포인트컷 표현식으로 한번에 지정하게 할 수 있다.

 

의존성 추가

implementation 'org.aspectj:com.springsource.org.aspectj.tools:1.6.6.RELEASE'

포인트컷 지시자

  • execution(포인트컷 표현식 - 리플렉션 메소드 시그니처)
  • bean(*Service) : 아이디가 Service로 끝나는 빈
  • @annotation(애노테이션 풀 패키지 경로) : 경로의 애노테이션이 붙은 메소드를 선정하는 포인트컷

execution 표현식 문법

메소드의 풀 시그니처를 사용한다. 여기서 접근제한자, 패키지 경로, 예외 패턴은 생략할 수 있다. 리턴값의 타입, 메소드 이름, 매개변수 패턴은 반드시 사용해야한다. 다음과 같은 패턴을 사용한다.

execution([접근제한자 패턴] 리턴값패턴 [패키지경로패턴.]메소드이름패턴 (파라미터 타입 패턴*) [throws 예외 패턴]

입력하기 귀찮다면 리플렉션으로 메소드 메타 정보를 출력해 가져와서 수정할 수 있다.

System.out.println(Target.class.getMethod("minus", int.class, int.class));
// public int learningtest.pointcut.Target.minus(int,int) throws java.lang.RuntimeException

AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public int learningtest.pointcut.Target.minus(int,int) throws java.lang.RuntimeException)");

모든 타입을 허용하고 싶다면 와일드 카드 *를, 파라미터 개수와 타입을 무시하려면 (..)를 사용한다. 패키지 경로에 ..를 사용하면 모든 하위 패키지를 포함한다.

아까 NameMatchclassMethodPointcut를 다음과 같이 등록했는데, 이제 AspectJExpressionPointcut를 사용해보자. 이제 직접 만든 포인트컷 구현 클래스를 사용하는 일은 없을 것이다.

<!-- before -->
<property name="mappedClassName" value="*ServiceImpl"/>
<property name="mappedName" value="upgrade*"/>

<!-- after -->
<bean id="transactionPointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
	<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..)))"/>
</bean>

참고로 표현식에 사용하는 타입 패턴은 클래스 이름 뿐만 아니라 슈퍼클래스, 인터페이스까지 모두 포함한다. 그래서 TestServiceImpl 클래스가 ServiceImpl 클래스를 상속 받고 있다면 여기에 걸린다.

표현식 문법은 복잡한 설정을 간단하게 표현할 수 있다는 장점이 있지만, 문자열이므로 런타임 이전에는 문법 검증이나 기능 확인이 불가능하다는 단점이 있으므로 조심해서 사용해야 한다.

 

트랜잭션 포인트컷 표현식 전략

일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는 것이 바람직하다. 따라서 트랜잭션 포인트컷 표현식에 메소드나 파라미터, 예외에 대한 패턴을 정의하는 것은 바람직하지 않다. 대신 트랜잭션 경계로 삼을 클래스나 인터페이스를 포인트컷 표현식에 사용하는 것이 좋다.