토비의 스프링 Chapter 6.5.1 ~ 6.5.3 정리

종명·2021년 5월 8일
0

스프링 AOP

자동 프록시 생성

투명한 부가기능을 적용하는 과정에서 발견됐던 거의 대부분의 문제는 제거했다.

하지만, 아직 해결할 과제가 남아 있다. 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해 주는 부분이다.

중복 문제의 접근 방법

JDK의 다이내믹 프록시는 특정 인터페이스를 구현한 오브젝트에 대해서 프록시 역할을 해주는 클래스를 런타임 시 내부적으로 만들어준다. 런타임 시에 만들어져 사용되기 때문에 클래스 소스가 따로 남지 않을 뿐이지 타깃 인터페이스의 모든 메소드를 구현하는 클래스가 분명히 만들어진다. 그 덕분에 개발자가 일일이 인터페이스 메소드를 구현한 프록시 클래스를 만들어서 위임과 부가기능의 코드를 중복해서 넣어주지 않아도 되게 해줬다.

변하지 않는 타깃으로의 위임과 부가기능 적용 여부 판단이라는 부분은 코드 생성기법을 이용하는 다이내믹 프록시 기술에 맡기고, 변하는 부가기능 코드는 별도로 만들어서 다이내믹 프록시 생성 팩토리에 DI로 제공하는 방법을 사용한 것이다.

의미있는 부가기능 로직인 트랜잭션 경계설정은 코드로 만들게 하고, 기계적인 코드인 타깃 인터페이스 구현과 위임, 부가기능 연동 부분은 자동생성하게 한 것이다.

반복적인 프록시의 메소드 구현은 코드 자동생성 기법을 이용해 해결했다면 반복적인 ProxyFactoryBean 설정 문제는 설정 자동등록 기법으로 해결할 수 없을까?

빈 후처리기를 이용한 자동 프록시 생성기

스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다.

그 중에서 관심을 가질 만한 확장 포인트는 BeanPostProcessor 인터페이스를 구현해서 만든 빈 후처리기다. 빈 후처리기는 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다.

여기서는 빈 후처리기 중의 하나인 DefaultAdvisorAutoProxyCreator를 살펴보겠다. DefaultAdvisorAutoProxyCreator 는 어드바이저를 이용한 자동 프록시 생성기다.

빈 후처리기를 스프링에 적용하는 방법은 간단하다. 빈 후처리기 자체를 빈으로 등록하는 것이다. 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

빈 후처리기는 빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고 별도의 초기화 작업을 수행할 수도 있다. 심지어는 만들어진 빈오브젝트 자체를 바꿔치기할 수도 있다. 따라서 스프링이 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키는 것이 가능하다.

이를 잘 활용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 바로 이것이 자동 프록시 생성 빈 후처리기다.

빈 후처리기를 이용한 자동 프록시 생성 방법

  1. DefaultAdvisorAutoProxyCreator 빈 후처리기가 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에 빈을 보낸다.
  2. DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 확인하여 적용 대상인지 확인한다.
  3. 적용 대상이면 내장된 프록시 생성기에게 현재 빈에대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결해준다.
  4. 빈 후처리기는 프록시가 생성되면 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에게 돌려준다.
  5. 컨테이너는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용한다.

적용할 빈을 선정하는 로직이 추가된 포인트컷이 담긴 어드바이저를 등록하고 빈 후처리기를 사용하면 일일이 ProxyFactoryBean 빈을 등록하지 않아도 타깃 오브젝트에 자동으로 프록시가 적용되게 할 수 있다.

확장된 포인트 컷

포인트컷은 클래스 필터와 메소드 매처 두 가지를 돌려주는 메소드를 갖고 있다. 실제 포인트컷의 선별 로직은 이 두가지 타입의 오브젝트에 담겨 있다.

public interface Pointcut {
    ClassFilter getClassFilter(); // 프록시를 적용할 클래스인지 확인해준다.
    MethodMatcher getMethodMatcher(); // 어드바이스를 적용할 메소드인지 확인해준다.
}

만약 Pointcut 선정 기능을 모두 적용한다면 먼저 프록시를 적용할 클래스인지 판단하고 나서, 적용 대상 클래스인 경우에는 어드바이스를 적용할 메소드인지 확인하는 식으로 동작한다.

ProxyFactoryBean에서는 굳이 클래스 레벨의 필터는 필요 없었지만, 모든 빈에 대해 프록시 자동 적용 대상을 선별해야 하는 빈 후처리기인 DefaultAdvisorAutoProxyCreator는 클래스와 메소드 선정 알고리즘을 모두 갖고 있는 포인트컷이 필요하다. 정확히는 그런 포인트컷과 어드바이스가 결합되어 있는 어드바이저가 등록되어 있어야 한다.

포인트컷 테스트

    @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*"); // sayH로 시작하는 메소드 이름을 가진 메소드만 선정한다.
        // 적용 클래스다.
        checkAdviced(new HelloTarget(), classMethodPointcut, true);

        class HelloWorld extends HelloTarget {};
        // 적용 클래스가 아니다.
        checkAdviced(new HelloWorld(), classMethodPointcut, false);

        class HelloToby extends HelloTarget {};
        // 적용 클래스다.
        checkAdviced(new HelloToby(), classMethodPointcut, true);
    }

    private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(target);
        proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

        Hello proxyHello = (Hello)proxyFactoryBean.getObject();

        if (adviced) {
            assertThat(proxyHello.sayHello("Toby"), is("HELLO TOBY")); // 메소드 선정 방식을 통해 어드 바이스 적용
            assertThat(proxyHello.sayHi("Toby"), is("HI TOBY")); // 메소드 선정 방식을 통해 어드 바이스 적용
            assertThat(proxyHello.sayThankYou("Toby"), is("Thank You Toby"));
        } else { //어드바이스 적용대상 후보에서 아예 탈락
            assertThat(proxyHello.sayHello("Toby"), is("Hello Toby"));
            assertThat(proxyHello.sayHi("Toby"), is("Hi Toby"));
            assertThat(proxyHello.sayThankYou("Toby"), is("Thank You Toby"));
        }
    }

DefaultAdvisorAutoProxyCreator의 적용

클래스 필터를 적용할 포인트컷 작성

메소드 이름만 비교하던 포인트컷인 NameMatchMethodPointcut을 상속해서 프로퍼티로 주어진 이름 패턴을 가지고 클래스 이름을 비교하는 ClassFilter를 추가하도록 만든다.

//...
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) {
            // simpleMatch: 와일드카드(*)가 들어간 문자열 비교를 지원하는 스프링의 유틸리티 메소드다. *name, name*, *name* 세 가지 방식을 모두 지원한다.
            return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName());
        }
    }
}

어드바이저를 이용하는 자동 프록시 생성기 등록

다음은 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator 동작 과정이다.
1. 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾는다.
2. 생성되는 모든 빈에 대해 어드바이저의 포인트컷을 적용해보면서 프록시 적용 대상을 선정한다.
3. 빈 클래스가 프록시 선정 대상이라면 프록시를 만들어서 원래 빈 오브젝트와 바꿔치기한다.
4. 원래 빈 오브젝트는 프록시 뒤에 연결돼서 프록시를 통해서만 접근 가능하게 바뀌며, 타깃 빈에 의존한다고 정의한 다른 빈들은 등록시 프록시 오브젝트를 대신 DI 받게 된다.

DefaultAdvisorAutoProxyCreator 등록은 다음 한 줄과 같다.

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

포인트 컷 등록

기존의 포인트컷 설정을 삭제하고 새로 만들 클래스 필터 지원 포인트컷을 빈으로 등록한다. ServiceImpl로 이름이 끝나는 클래스와 upgrade로 시작하는 메소드를 선정해주는 포인트 컷이다.

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

어드바이스와 어드바이저

어드바이스인 transactionAdvice 빈의 설정은 수정할게 없다. 어드바이저인 transactionAdvisor 빈도 수정할 필요는 없다. 하지만 어드바이저로 사용되는 방법이 바뀌었다는 사실은 기억해두자.

이제는 transactionAdvisor를 명시적으로 DI하는 빈은 존재하지 않는다. 대신 어드바이저를 이용하는 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator에 의해 자동 수집되고, 프록시 대상 선정 과정에 참여하며, 자동 생성된 프록시에 다이내믹하게 DI돼서 동작하는 어드바이저가 된다.

ProxyFactoryBean 제거와 서비스 빈의 원상복구

프록시를 도입했던 때부터 아이디를 바꾸고 프록시에 DI돼서 간접적으로 사용돼야 됐던 userServiceImpl 빈의 아이디를 다시 userService로 되돌려놓을 수 있다. 더 이상 명시적인 프록시 팩토리 빈을 등록하지 않기 때문이다.

포인트컷 표현식을 이용한 포인트컷

스프링은 아주 간단하고 효과적인 방법으로 포인트컷의 클래스와 메소드를 선정하는 알고리즘을 작성할 수 있는 방법을 제공한다. 정규식이나 JSP의 EL과 비슷한 일종의 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법이다. 그래서 이것을 포인트컷 표현식이라고 부른다.

포인트컷 표현식

포인트컷 표현식을 지원하는 포인트컷을 적용하려면 AspectJExpressionPointcut 클래스를 사용하면 된다. Pointcut 인터페이스를 구현해야 하는 스프링의 포인트컷은 클래스 선정을 위한 클래스 필터와 메소드 선정을 위한 메소드 매처 두 가지를 각각 제공해야 한다.

포인트컷 표현식 문법

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | ”..”, ...) [throws 예외패턴])

  • [접근제한자 패턴]: public, private 등이 올 수있다. (생략 가능)
  • 타입패턴: 리턴 값의 타입을 나타내는 패턴이다. (필수 항목)
  • [타입패턴.]: 패키지와 타입의 이름을 포함한 클래스 타입 패턴이다. (생략 가능, *를 사용할 수 있고 ..를 사용하면 한번에 여러 패키지를 선택할 수 있다.)
  • 이름패턴: 메소드 이름 패턴 (필수 항목)
  • (타입패턴 | ”..”, ...) : 파라미터의 타입 패턴을 순서대로 넣을 수 있다. 와일드카드를 이용해 파라미터 개수에 상관없는 패턴을 만들 수 있다. (필수 항목)
  • [throws 예외 패턴]: 예외 이름 패턴 (생략 가능)

포인트컷 표현식 적용

포인트컷 표현식은 메소드의 시그니처를 비교하는 방식인 execution() 외에도 몇 가지 표현식 스타일을 갖고 있다. 대표적으로 스프링에서 사용될 때 빈의 이름으로 비교하는 bean()이 있다.

또, 특정 애노테이션이 타입, 메소드, 파라미터에 적용되어 있는 것을 보고 메소드를 선정하게 하는 포인트컷도 만들 수 있다. 애노테이션만 부여해놓고, 포인트컷을 통해 자동으로 선정해서, 부가기능을 제공하게 해주는 방식은 스프링 내에서도 애용되는 편리한 방법이다. 아래와 같이 쓰면 @Transaction이라는 애노테이션이 적용된 메소드를 선정하게 해준다.

@annotation(org.springframework.transcation.annotation.Transactional)

클래스 이름은 ServiceImpl로 끝나고 메소드 이름은 upgrade로 시작하는 모든 클래스에 적용되도록 하는 표현식을 만들고 이를 적용한 빈 설정은 다음과 같다.

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

포인트컷 표현식을 사용하면 로직이 짧은 문자열이 담기기 때문에 클래스나 코드를 추가할 필요가 없어서 코드와 설정이 모두 단순해진다. 반면에 문자열로 된 표현식이므로 런타임 시점까지 문법 검증이나 기능 확인이 되지 않는다는 단점도 있다.

타입 패턴과 클래스 이름 패턴

클래스 이름 패턴과 포인트컷 표현식에서 사용하는 타입 패턴은 중요한 차이점이 있다. 이를 확인하기 위해서 TetstUserServiceImpl이라고 변경했던 테스트용 클래스의 이름은 다시 TestUserService라고 바꿔보자.
테스트를 실행 해보면 결과는 성공이다. 포인트 컷이 execution(* *..*ServiceImpl.upgrade*(..)) 로 되어 있는데 어떻게 TestUserService 클래스로 등록된 빈이 선정 됐을까?

그 이유는 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입 패턴이기 때문이다.
TestUserService의 클래스 이름은 TestUserService 이지만, 타입을 따져보면 TestUserService 클래스 이고, 슈퍼클래스인 UserServiceImpl, 구현 인터페이스인 UserService 세 가지가 모두 적용된다.

0개의 댓글