본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
12장 : 스레드 로컬·템플릿·프록시

ProxyFactory 구성

Spring ProxyFactory가 대상과 어드바이저에서 인터셉터 체인을 만드는 흐름을 실행하고, 클래스 필터·메서드 매처·어드바이스 순서·동적 matching·multiple 어드바이저의 적용 기준을 설계합니다.

JDK 핸들러에 메서드 선택과 부가 기능을 같이 쓰면 재사용이 어렵습니다. Spring AOP는 “어디에 적용할지”를 Pointcut, “무엇을 할지”를 Advice로 분리하고 둘을 어드바이저로 묶습니다. ProxyFactory는 대상의 인터페이스와 클래스 조건을 보고 프록시를 만들며 호출 때 어드바이저 체인을 실행합니다.


포인트컷 교집합

클래스 필터가 거절하면 메서드 매처를 볼 필요가 없습니다. 정적 매처는 Method와 대상 클래스만 사용해 프록시 생성 단계에서 체인을 캐시하기 좋습니다. 인자 값까지 보는 런타임 매처는 호출마다 비용이 들고 정책이 데이터에 의존합니다.

메서드 이름 접두사만으로 save를 선택하면 다른 제한된 컨텍스트의 메서드에도 적용될 수 있습니다. 패키지, 애노테이션, 타입, 시그니처를 조합해 의도를 좁힙니다. 업무 파라미터 값으로 인가를 구현하는 것은 별도 보안 인터셉터가 더 명시적입니다.

src/main/java/board/advisor/BoardPointcut.java
package board.advisor;

import java.lang.reflect.Method;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;

public final class BoardPointcut implements Pointcut {
    @Override
    public ClassFilter getClassFilter() {
        return type -> type.getPackageName().startsWith("board");
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new MethodMatcher() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                return method.getName().startsWith("register");
            }

            @Override
            public boolean isRuntime() {
                return false;
            }

            @Override
            public boolean matches(
                    Method method,
                    Class<?> targetClass,
                    Object[] arguments
            ) {
                return matches(method, targetClass);
            }
        };
    }
}

포인트컷 객체는 무상태하게 만들면 여러 어드바이저에서 안전하게 재사용할 수 있습니다. 패키지 이동과 메서드 이름 변경은 적용 범위를 바꾸므로 포인트컷 테스트를 아키텍처 테스트처럼 유지합니다.

MethodInterceptor 흐름

주변 어드바이스는 MethodInvocation.proceed()를 호출해야 다음 인터셉터 또는 대상으로 이동합니다. 호출하지 않으면 접근 거부나 캐시 적중처럼 체인을 종료합니다. 두 번 호출하면 대상도 두 번 실행될 수 있어 재시도 의도가 아니라면 금지합니다.

src/main/java/board/advisor/AdvisorDemo.java
package board.advisor;

import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public final class AdvisorDemo {
    public interface Command {
        long register(String title);
        long find(long id);
    }

    static final class Target implements Command {
        @Override
        public long register(String title) {
            return 41L;
        }

        @Override
        public long find(long id) {
            return id;
        }
    }

    public static void main(String[] args) {
        MethodInterceptor advice = invocation -> {
            System.out.println("advised=" + invocation.getMethod().getName());
            return invocation.proceed();
        };
        var advisor = new DefaultPointcutAdvisor(new BoardPointcut(), advice);
        var factory = new ProxyFactory(new Target());
        factory.addAdvisor(advisor);
        Command proxy = (Command) factory.getProxy();

        System.out.println("created=" + proxy.register("advisor"));
        System.out.println("found=" + proxy.find(7L));
    }
}
AdvisorDemo 실행
advised=register
created=41
found=7

find는 대상에 도달하지만 포인트컷과 맞지 않아 어드바이스 메시지가 없습니다. 프록시 자체는 빈의 모든 호출을 받되 메서드별 체인이 달라집니다.

관찰 결과는 register에서 어드바이스 1회·대상 1회, find에서 어드바이스 0회·대상 1회입니다. 이 개수가 Pointcut 적용 범위를 실행 수준에서 증명하며, 프록시 생성 여부만 확인하는 테스트보다 잘못된 메서드 일치를 직접 드러냅니다.

복수 어드바이저 순서

트랜잭션, 인가, 재시도, 메트릭 어드바이저가 함께 적용되면 순서가 동작을 바꿉니다. 재시도가 트랜잭션 바깥이면 시도마다 새 트랜잭션을 만들 수 있고, 안쪽이면 롤백 전용 트랜잭션을 반복할 수 있습니다. 인가는 비용이 큰 작업보다 먼저 실행되어야 합니다.

Ordered 또는 어드바이저 등록 순서를 명시하고 체인 테스트를 둡니다. 순서 숫자를 여러 모듈에서 임의로 정하면 충돌하므로 의미 있는 상수 범위를 관리합니다. 동일 순서의 상대 순서에 의존하지 않습니다.

이전, 정상 반환 이후, 예외 이후 어드바이스는 표현이 간결하지만 둘레 어드바이스가 가장 일반적입니다. 반환과 예외를 모두 다루는 메트릭에는 try/catch/finally가 명시적인 MethodInterceptor가 적합합니다.

ProxyFactory 선택

인터페이스가 있으면 기본적으로 JDK 프록시가 가능하고 proxyTargetClass로 클래스 프록시를 강제할 수 있습니다. optimize, opaque, exposeProxy, 고정 구성 같은 옵션은 필요한 이유가 없으면 기본값을 유지합니다. exposeProxy는 내부 호출 우회를 가능하게 하지만 애플리케이션 코드를 AOP API에 묶습니다.

대상 소스를 바꾸는 고급 프록시는 요청별 대상이나 풀링을 구현할 수 있지만 싱글톤 생명주기, 상태, 스레드 안전성이 복잡해집니다. 일반 서비스는 고정 싱글톤 대상을 사용합니다.

프록시를 직렬화하거나 구체 구현 클래스로 찾지 않습니다. 진단할 때는 어드바이저 목록과 대상 클래스를 Spring API로 확인합니다.

포인트컷·동작 테스트

포인트컷 테스트는 대표 Method 객체를 가져와 register는 참, find와 다른 패키지 클래스는 거짓인지 빠르게 검사합니다. 동작 테스트는 프록시를 실제 호출해 어드바이스 수, 대상 수, 예외 동일성, 순서를 확인합니다.

메서드 추가 때 포인트컷이 너무 넓게 일치하는 것도 회귀입니다. 부정 픽스처를 충분히 둡니다. 생성된 브리지 메서드와 합성 메서드가 보이면 가장 구체적인 메서드 해결을 Spring 유틸리티에 맡깁니다.

연습 문제

@Measured가 붙은 공개 애플리케이션 메서드만 선택하는 포인트컷과 타이머 어드바이스를 만드세요. 객체 메서드와 애노테이션 없는 메서드를 제외하고, 실패에서도 지속 시간이 한 번 기록되며 원본 예외가 유지되는 테스트를 작성하세요.

해설 보기

애노테이션 탐색은 인터페이스 메서드와 구현 메서드 위치를 고려해야 합니다. 간단한 구체 클래스 기준 예시는 다음과 같습니다.

package board.advisor;

import java.lang.reflect.Method;

import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.annotation.AnnotatedElementUtils;

public final class MeasuredPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        try {
            Method specific = targetClass.getMethod(
                    method.getName(), method.getParameterTypes());
            return AnnotatedElementUtils.hasAnnotation(specific, Measured.class);
        } catch (NoSuchMethodException failure) {
            return false;
        }
    }

    public @interface Measured { }
}

실제 애노테이션에는 @Retention(RUNTIME)@Target(METHOD)를 선언해야 합니다.