← Documents

디자인 패턴 & Spring AOP

🧩

한 줄 소개 — "모든 메서드에 로그·트레이스 를 남기고 싶은데, 원본 코드는 건드리고 싶지 않다" 는 문제를 GoF 디자인 패턴으로 풀어 나가는 여정이다. 템플릿 메서드 · 전략 · 프록시 · 데코레이터 패턴에서 출발해 JDK 동적 프록시 → Spring ProxyFactory → BeanPostProcessor → @Aspect AOP 로 추상화가 올라가는 과정을 보면, Spring AOP 가 결국 프록시 패턴의 자동화 라는 게 보인다. (김영한님 스프링 고급편의 GoF 부분 + 직접 구현)

1 · 문제 — 횡단 관심사의 중복

모든 컨트롤러·서비스·리포지토리 메서드에 "시작 로그 → 실행 → 종료 로그(+예외 로그)" 를 넣고 싶다. 그런데 이 로깅·트레이스 는 비즈니스 로직과 무관한 횡단 관심사(cross-cutting concern) 다. 메서드마다 같은 코드를 복붙하면 — 핵심 로직이 부가 코드에 묻히고, 수정 한 번에 수백 곳을 고쳐야 한다.

🎯

목표원본(핵심 로직)을 전혀 건드리지 않고 부가 기능(로그)을 입힌다. 이 한 문장이 아래 모든 패턴의 동기다.

먼저 "변하는 부분(핵심 로직)"과 "변하지 않는 부분(로그 골격)"을 분리하는 패턴들이 있다 — 템플릿 메서드(상속으로 변하는 부분만 override), 전략 패턴(변하는 부분을 인터페이스로 주입), 템플릿 콜백(전략을 람다로 그때그때 전달). 하지만 이들은 원본 코드를 수정해야 적용된다는 한계가 있다. 그래서 프록시 로 넘어간다.

2 · 프록시 & 데코레이터 패턴

프록시(Proxy) 는 원본(Real Subject)과 같은 인터페이스 를 구현한 대리자다. 클라이언트는 프록시를 원본인 줄 알고 호출하고, 프록시는 부가 기능(로그)을 수행한 뒤 원본에 위임한다. 부가 기능에 초점을 맞추면 데코레이터(Decorator) 패턴이라 부른다 — 구조는 같다.

프록시 패턴 — 원본을 안 건드리고 부가 기능 Client Proxy로그 begin/end Real Subject핵심 로직 (원본) 같은 인터페이스 위임
클라이언트는 프록시를 원본으로 알고 호출 → 프록시가 부가 기능 후 원본에 위임 (원본 코드 불변)

하지만 이렇게 손으로 만들면 — 대상 클래스마다 프록시 클래스를 하나씩 만들어야 한다(인터페이스 기반 프록시, 구체 클래스 기반 프록시 모두). 클래스가 수십 개면 프록시도 수십 개. 그래서 동적 프록시 가 등장한다.

3 · JDK 동적 프록시 (Dynamic Proxy)

동적 프록시 는 프록시 클래스를 개발자가 만들지 않고 런타임에 자동 생성한다. 부가 기능 로직은 InvocationHandler 하나에만 작성하면, 어떤 인터페이스든 그 핸들러를 공유하는 프록시가 만들어진다. (인터페이스가 없으면 CGLIB 로 구체 클래스를 상속한 프록시를 만든다.)

// 부가 기능(로그)을 InvocationHandler 한 곳에만 작성 → 모든 프록시가 공유 public class LogTraceBasicHandler implements InvocationHandler { private final Object target; // 원본 private final LogTrace logTrace; public LogTraceBasicHandler(Object target, LogTrace logTrace) { this.target = target; this.logTrace = logTrace; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { TraceStatus status = null; try { // 메서드 메타정보로 "OrderController.request()" 메시지 생성 String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); // 부가 기능 (전) Object result = method.invoke(target, args); // 원본 로직 위임 logTrace.end(status); // 부가 기능 (후) return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } // Proxy.newProxyInstance(loader, interfaces, new LogTraceBasicHandler(target, logTrace))

4 · Spring ProxyFactory + Advisor

스프링은 "인터페이스면 JDK 동적 프록시, 아니면 CGLIB" 를 ProxyFactory 가 알아서 선택하게 추상화했다. 그리고 부가 기능과 적용 대상을 Advisor 로 묶는다.

  • Advice — 부가 기능 그 자체(로그 begin/end). InvocationHandler 와 비슷하지만 더 추상화됨.
  • Pointcut — 부가 기능을 어디에 적용할지(어떤 클래스·메서드).
  • Advisor = Advice + Pointcut (하나의 부가기능 + 적용 대상 묶음).
Advisor = Advice + Pointcut Advisor Advice Pointcut 무엇을(부가기능) 어디에(적용 대상)
Advice(부가 기능) + Pointcut(적용 대상) = Advisor — ProxyFactory 가 이걸로 프록시를 만든다

5 · 자동 프록시 — BeanPostProcessor

아직 남은 문제 — 설정 클래스에서 빈마다 ProxyFactory 로 프록시를 만들어 등록 하는 코드가 반복된다. BeanPostProcessor 는 스프링 컨테이너가 빈을 등록하기 직전에 가로채, 원본 빈을 프록시로 바꿔치기 할 수 있게 해 준다. 스프링이 제공하는 자동 프록시 생성기(AutoProxyCreator) 가 바로 이 빈 후처리기로, 등록된 Advisor 들을 보고 Pointcut 에 걸리는 빈을 자동으로 프록시화한다.

BeanPostProcessor — 빈을 프록시로 자동 교체 원본 빈 (Order…) BeanPostProcessorAdvisor 로 프록시 생성 프록시 빈 등록 컨테이너 등록 직전 가로채기 → 반복되던 프록시 등록 코드가 사라진다
빈 후처리기가 등록 직전의 원본 빈을 프록시로 바꿔 끼운다 — 자동 프록시 생성기의 원리

6 · @Aspect — Spring AOP

마지막 추상화. @Aspect 로 "부가 기능(@Around 안의 로직)과 적용 대상(execution(...) 포인트컷)" 을 한 클래스에 선언하면, 스프링이 이를 Advisor 로 변환 하고 자동 프록시 생성기가 알아서 프록시를 적용한다. 개발자는 더 이상 프록시·핸들러·팩토리를 신경 쓰지 않는다.

@Aspect // 스프링이 이 클래스를 보고 Advisor 로 변환해 등록 public class LogTraceAspect { private final LogTrace logTrace; public LogTraceAspect(LogTrace logTrace) { this.logTrace = logTrace; } @Around("execution(* design.pattern.proxy.app..*(..))") // Pointcut — 어디에 public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { // Advice — 무엇을 TraceStatus status = null; try { String message = joinPoint.getSignature().toShortString(); status = logTrace.begin(message); // 부가 기능 (전) Object result = joinPoint.proceed(); // 원본 로직 실행 logTrace.end(status); // 부가 기능 (후) return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } }
추상화의 진화 — 같은 목표, 점점 적은 코드 수동 프록시클래스마다 1개 JDK 동적Handler 1개 ProxyFactoryAdvisor BeanPostProcessor자동 프록시 @Aspect (AOP)선언만
수동 프록시 → JDK 동적 프록시 → ProxyFactory(Advisor) → BeanPostProcessor(자동) → @Aspect AOP. Spring AOP 는 프록시 패턴의 자동화다

7 · 보너스 — 책임 연쇄 패턴 (Chain of Responsibility)

요청을 여러 처리기(필터)가 사슬처럼 이어서 처리하고, 각자 "내가 처리할지 / 다음으로 넘길지" 결정하는 패턴. 서블릿 필터 · 스프링 인터셉터 · API 게이트웨이 필터 체인 이 정확히 이 구조다(앞서 정리한 게이트웨이의 Pre/Post 필터도 같은 원리).

public interface ChainFilter { void handle(Request req, ChainFilter next); // 처리 후 next 로 위임 } public class Filter1 implements ChainFilter { public void handle(Request req, ChainFilter next) { // ... 인증/로깅 등 처리 ... next.handle(req, /* 다음 필터 */); // 다음 책임자에게 전달 } } // Filter1 → Filter2 → Filter3 … 사슬로 연결

📓 2022–2023년 제가 김영한님의 스프링 고급편(GoF 디자인 패턴 부분)을 공부하며 직접 구현하고 정리한 노트입니다 · 구현 GitHub · DesignPattern ↗ · 강의 코드 SpringAdvanced ↗