[LG U+ 유레카 3기]Spring Proxy | 프록시 기반 AOP 원리 이해

2025. 11. 3. 13:41Java/Spring

❶ 프록시란?

프록시(Proxy)는 ‘대리인’이라는 뜻으로, 실제 객체(Real Object)를 감싸서 대신 메서드를 실행하고, 그 과정에서 공통 기능(전처리, 후처리 등)을 삽입하는 역할을 한다. 스프링은 트랜잭션, 캐시, 보안 같은 부가기능을 삽입할 때 이 프록시 방식을 사용한다.

 

❷ 코드 구조 요약


package com.mycom.myapp.proxy;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        MyIF myIF = new MyIFImpl();

        String param1 = "abc";
        String param2 = null;

        MyIF proxy = (MyIF) Proxy.newProxyInstance(
            myIF.getClass().getClassLoader(),
            myIF.getClass().getInterfaces(),
            new CheckNotNullInvocationHandler(myIF)
        );

        proxy.m(param1, param2);
        proxy.m2(param1, param2);
    }
}

여기서 핵심은 Proxy.newProxyInstance()이다. 이 메서드가 런타임에 새로운 프록시 객체를 생성하고, 호출이 일어날 때마다 InvocationHandler가 가로채서 제어한다.

---

❸ InvocationHandler의 핵심 동작


public class CheckNotNullInvocationHandler implements InvocationHandler {
    private Object target;

    public CheckNotNullInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());

        if (targetMethod.isAnnotationPresent(CheckNotNull.class)) {
            return handleCheckNotNull(targetMethod, args);
        }

        return method.invoke(target, args); // 원본 호출
    }

    private Object handleCheckNotNull(Method method, Object[] args) throws Throwable {
        CheckNotNull annotation = method.getAnnotation(CheckNotNull.class);
        String[] parameterNames = annotation.parameterNames();

        for (int i = 0; i < args.length; i++) {
            if (args[i] == null) {
                throw new IllegalArgumentException(
                    "Parameter " + parameterNames[i] + " is null (should be notnull)");
            }
        }
        return method.invoke(target, args);
    }
}

이 클래스는 스프링의 AOP 어드바이스(Advice) 역할을 한다. 즉, 메서드 실행 전에 공통 로직(여기서는 null 검사)을 수행하고, 문제 없으면 실제 target 메서드를 호출한다.

---

❹ 인터페이스와 구현 클래스


public interface MyIF {
    void m(String param1, String param2);
    void m2(String param1, String param2);
}

public class MyIFImpl implements MyIF {
    @Override
    @CheckNotNull(parameterNames = {"param1","param2"})
    public void m(String param1, String param2) {
        System.out.println("m() : " + param1 + ", " + param2);
    }

    @Override
    public void m2(String param1, String param2) {
        System.out.println("m2() : " + param1 + ", " + param2);
    }
}

`m()`은 @CheckNotNull이 붙어 있기 때문에 프록시가 null 검사를 수행하지만, `m2()`는 애노테이션이 없어서 그냥 바로 실행된다.

---

❺ @CheckNotNull 애노테이션 정의


@Retention(RUNTIME)
@Target(METHOD)
public @interface CheckNotNull {
    String[] parameterNames();
}

- @Retention(RUNTIME) : 런타임에도 유지되어 리플렉션으로 접근 가능 - @Target(METHOD) : 메서드에만 적용 가능 → 결국 이 애노테이션이 부가기능의 ‘트리거’ 역할을 한다.

---

❻ 실행 흐름

  1. Test에서 MyIFImpl 객체 생성
  2. Proxy.newProxyInstance()로 프록시 객체 생성
  3. proxy.m("abc", null) 호출 → InvocationHandler로 이동
  4. @CheckNotNull 확인 → null 발견 → 예외 발생
  5. proxy.m2() 호출 → 애노테이션 없음 → 원본 메서드 직접 실행

---

❼ 스프링 AOP와의 관계

스프링은 내부적으로 다음 두 가지 프록시 방식을 사용한다:

  • JDK 동적 프록시: 인터페이스가 존재할 때 (→ 현재 예제)
  • CGLIB 프록시: 인터페이스 없이 클래스 상속으로 구현

스프링에서 @Transactional, @Cacheable, @Async 같은 기능들이 바로 이런 프록시 객체를 통해 동작한다. 즉, 우리가 만든 CheckNotNull 예제는 스프링 AOP의 축소판이다.

---

❽ 주의할 점

  • 파라미터 이름 배열과 실제 인자 수가 다르면 오류 발생 가능
  • Self-invocation(자기 내부 메서드 호출) 시 AOP 적용 안 됨
  • final 클래스/메서드는 CGLIB 프록시로도 가로채기 불가

---

❾ 정리

개념 역할
Proxy 객체 원본 객체를 감싸 호출을 가로챔
InvocationHandler 실제 호출 시 부가기능 수행
Annotation 적용 대상을 선언적으로 지정
리플렉션(Reflection) 런타임에 메서드·필드 접근
AOP 공통 로직을 필요한 지점에 삽입

---

🔑 핵심 요약

✅ 프록시는 런타임에 생성되어 대리로 메서드를 실행한다.
✅ InvocationHandler가 전·후처리 로직을 수행한다.
✅ 애노테이션은 부가기능을 선언적으로 적용하게 한다.
✅ 스프링의 AOP(@Transactional 등)는 동일 원리로 동작한다.

---

🧭 마무리

이 예제는 스프링의 프록시 기반 AOP를 이해하기 위한 완벽한 축소판이다.
단순히 코드 트릭이 아니라, 스프링이 애노테이션 하나로 다양한 기능을 삽입할 수 있는 근본 원리를 보여준다.

프록시의 작동 원리를 정확히 이해하면, 트랜잭션 경계, 예외 처리, DI 컨테이너 내부 구조까지도 자연스럽게 연결된다.