설명AOP 적용 방법(Weaving)Spring AOPJDK Proxy vs CGLib ProxyProxy 동작방식에 따른, Proxy 이용시 주의점(*중요*)AOP 주요 용어에스펙트(Aspect)조인포인트(Join Point)타겟어드바이스포인트 컷위빙(Weaving)코드로 보는 주요 용어 정리@Aspectpointcut expressionAdvice에 포인트컷 적용하는 방법JoinPoint Argument는 어디에 쓰는 것인가?여러 execution을 사용하고 싶을 때(getter 와 setter를 둘다 match하고 싶은 경우)AOP를 활용 케이스AOP를 활용한 파라미터 출력AOP를 활용한 시간 측정Aop를 활용한 변수 변경Annotation 붙인 클래스의 모든 public 메서드를 pointcut 하는 방법
설명
- 여러 클래스에 걸쳐 특정한 관심사를 수행하는 코드를 분산하는 대신에 관심사를 다루는 로직을 하나의 애스펙트에 넣음
관심사 예시
- Auditing
- Security
- API의 성능 로깅
- Transaction Management

class 계좌이체서비스 { method 이체() { AAA 비즈니스 로직 BBB } method 계좌확인() { AAA 비즈니스 로직 BBB } }
- 위와 같이 중간의 비즈니스 로직이 있고 앞 뒤로 부가 기능을 덧붙이고 싶을 때에 AOP를 이용함
AOP 적용 방법(Weaving)
- AOP에서 위빙은 다른 필수 객체와 애스펙트를 연결해 어드바이스 객체를 생성하는 프로세스임
- 컴파일 시점 — aspectJ 같은 프레임워크에서 사용하는 방법
- 클래스 로딩 시점
- 런타임 시점 — Spring 에서 사용하는 방법

- build.gradle dependency
implementation “org.springframework.boot:spring-boot-starter-aop
Spring AOP
- 스프링 AOP에서 프레임워크는 런타임 동안 애스펙트 계약을 이행하기 위해 @Apsect가 붙은 객체에 대한 프락시 객체를 생성함
- 기본적으로 스프링 AOP는 표준 JDK Dynamic Proxy로 AOP 프락시를 생성함
JDK Proxy vs CGLib Proxy
Java Dynamic Proxy (JDK Proxy 🆚 CGLib)
- JDK가 제공해 주는 Proxy와 CGLib이라고 library에서 제공해주는 Proxy가 존재함. 하나는 interface기반이고 하나는 class 기반임
- JDK Proxy는 TargetObject interface(Calculator)에 대해 해당 interface를 구현하는 객체(CalculatorImpl)를 내부적으로 구현하여 해당 객체의 Proxy 객체를 만들어 줌. 그렇게 해서 접근 제어를 가능케 함 — Dynamic Proxy(https://www.baeldung.com/java-dynamic-proxies)
- CGLib Proxy는 TargetObject라는 클래스가 있으면 해당 클래스를 상속하는 클래스를 구현하여 Proxy안에 가지고 있고 그 객체에 대한 호출에 접근제어를 가능케 해줌

- Spring Aop에서는 Bean으로 만들어진 객체에 대해서만 Proxy객체를 만들 수 있어 Aop 적용이 해당 객체에 대해서만 가능함
- Spring Aop는 public 메서드에 대해서 밖에 안됨. interface기반이기 때문에
- 아래 예시는 JDK Proxy를 이용한 방법
package org.prgms; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; class CalculatorImpl implements Calculator { @Override public int add(int a, int b){ return a + b; } } interface Calculator { int add(int a, int b); } class LoggingInvocationHandler implements InvocationHandler { private static final Logger log = LoggerFactory.getLogger(LoggingInvocationHandler.class); private final Object target; LoggingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log.info("{} executed in {}", method.getName(), target.getClass().getName()); return method.invoke(target, args); } } public class JdkProxyTest { private static final Logger log = LoggerFactory.getLogger(JdkProxyTest.class); public static void main(String[] args) { var calculator = new CalculatorImpl(); Calculator proxyInstance = (Calculator) Proxy.newProxyInstance( LoggingInvocationHandler.class.getClassLoader(), new Class[]{Calculator.class}, new LoggingInvocationHandler(calculator)); System.out.println(proxyInstance.getClass().getName()); // -> org.prgms.$Proxy0 var result = proxyInstance.add(1, 2); log.info("Add -> {}", result); } }
Proxy 동작방식에 따른, Proxy 이용시 주의점(*중요*)
- Proxy가 Bean의 호출에 대해서 가로채서 특정 기능을 부가적으로 수행해 주기에, 아래 조건들이 충족되어야 함
- Spring bean의
public method
들만 proxy가 가능함 - public method에 대한 call이 Spring bean의 외부에서 와야 함
public class Service { public URI test(String host) { return baseUrl(host); } @Cacheable(cacheNames = "baseUrl") public URI baseUrl(String channelServerHost) { return URI.create(String.format("http://%s:%d", channelServerHost, gameServerPort)); } }
AOP 주요 용어

에스펙트(Aspect)
- 관심사를 모듈화 한 것. 관심사를 다루는 로직을 하나의 애스펙트 안에 넣음
- 에스펙트 = 어드바이스 + 포인트 컷
- 부가기능(Advice)의 셋을 하나의 에스펙트로 묶는 것
- Spring에서는
@Aspect
어노테이션을 이용하여 일반 클래스에 에스팩트를 구현할 수 있음
조인포인트(Join Point)
- 특정 프로그램이 실행되는 지점
- 스프링 AOP에서 조인 포인트는 항상 메소드 호출을 나타냄
- AspectJ와 같은 AOP의 다른 구현체는 필드 접근과 예외 발생에 대한 조인 포인트도 지원함
- 어드바이스가 적용될 수 있는 위치
- 타겟 객체가 구현한 인터페이스의 모든 메서드
타겟
- 핵심 기능을 담고 있는 모듈로서 부가기능(관심사)을 부여(적용)할 대상
어드바이스
- 특정 관심사를 처리하는 행동
- 어드바이스는 타겟의 특정 조인포인트에 제공할 부가 기능
포인트 컷
- 일치하는 여러 조인 포인트를 결합한 것
- 어드바이스를 적용할 타겟의 메서드(조인 포인트)를 선별하는 정규 표현식
- 포인트컷 표현식은 execution으로 시작하고 메서드의 Signature를 비교하는 방법을 주로 이용함
위빙(Weaving)
- 타겟의 조인 포인트에 어드바이스를 적용하는 과정
코드로 보는 주요 용어 정리
@Aspect @Component public class UserRepoMonitor { @Before("com.ps.aspects.PointcutContainer.serviceUpdate()") public void beforeServiceUpdate(JoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); String text = (String)args[1]; String className = joinPoint.getSignature().getDeclaringTypeName(); String methodName = joinPoint.getSignature().getName(); if (StringUtils.indexOfAny(text, new String[]{"$", "#", "&", "%"}) != -1) { throw new IllegalArgumentException("Text contains weird characters!"); } } }
- 에스펙트 : UserRepoMonitor class
- 포인트 컷 :
"com.ps.aspects.PointcutContainer.serviceUpdate()"
— 해당 serviceUpdate() 부분에 @PointCut 어노테이션이 붙어있는 것
- 어드바이스(애스펙트 클래스의 메서드) :
@Before public void beforeServiceUpdate(JoinPoint joinPoint){}
- 조인포인트 : beforeServiceUpdate에 들어가는 파라미터 — joinPoint (즉, 해당 어드바이스가 적용되는 메서드)
@Aspect
- @Aspect를 사용하는 이유는 pointcut expression의 중복을 줄이기 위하여. @Pointcut annotation은 재사용 가능한 포인트 컷을 @Aspect로 선언된 클래스 안에서 생성할 수 있음
- @EnableAspectAutoProxy annotation을 configuration class(@Configuration)에 붙임으로 auto-proxying을 할 수 있음
- SpringBoot에서는 @EnableAspectAutoProxy 필요 없음. 알아서 해줌
pointcut expression

- The scope of the methods could either be
public
,protected
, orprivate
.
- The
[ReturnType]
is mandatory
- The
[Modifers]
is not mandatory and if not specified defaults to public
- The
[MethodName]
is not mandatory, meaning no exception will be thrown at boot time
- The
[Arguments]
is mandatory. To bypass the Arguments filtering, you can specify two dots..
Advice에 포인트컷 적용하는 방법
// 1. execution을 이용 @Around("execution(public * org.prgms..*.*(..))") public Object log(ProceedingJoinPoint joinPoint) throws Throwable { log.info("Before method called. {}", joinPoint.getSignature()); var result = joinPoint.proceed(); log.info("After method called. {}", result); return result; } //위와 같이 할수도 있고 poitCut method 만들어두고 @Around 어노테이션 이용할 수도 있음 // 2.아래와 같이 Pointcut 메서드를 만들어두고 이에 대한 참조 @Pointcut("execution(public * org.prgms.kdt..*Service.*(..)))") public void servicePublicMethod(){ } @Around("org.prgms.~~~.servicePublicMethod()") // 같은 클래스라면 함수이름만 써도 됨 // 3. 특정 어노테이션이 붙은 곳에만 적용. stereotype에 대해서도 적용가능 @Around("@annotation(org.prgms.aop.TrackTime)") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TrackTime { }
JoinPoint Argument는 어디에 쓰는 것인가?
- joinPoint.getTarget() → target object를 얻을 수 있음
- joinPoint.getSignature() → method signature
- joinPoint.getArgs() → method argument
여러 execution을 사용하고 싶을 때(getter 와 setter를 둘다 match하고 싶은 경우)

AOP를 활용 케이스
AOP를 활용한 파라미터 출력
@Aspect @Component public class ParameterApp { // com.example.aop.controller 패키지 아래에 있는 모든 함수들에 대해 적용 @Pointcut("execution(* com.example.aop.controller..*.*(..))") private void cut(){} @Before("cut()") public void before(JoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); for(Object obj : args){ System.out.println("type : " + obj.getClass().getSimpleName()); System.out.println("value : " + obj); } } @AfterReturning(value= "cut()", returning="returnObj") public void afterReturn(JoinPoint joinPoint, Object returnObj){ System.out.println("return obj"); System.out.println(returnObj); } }
AOP를 활용한 시간 측정
// Timer.java package com.example.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Timer { } //TimerAop.java package com.example.demo.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @Aspect @Component public class TimerAop { @Pointcut("@annotation(com.example.demo.annotation.Timer)") private void enableTimer(){} @Around("enableTimer()") public void around(ProceedingJoinPoint joinPoint) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object result = joinPoint.proceed(); // 이게 해당 method 실행 부분임 stopWatch.stop(); System.out.println("total time : " + stopWatch.getTotalTimeSeconds()); } }
Aop를 활용한 변수 변경
//Deocde.java package com.example.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Decode { } //DecodeAop.java package com.example.demo.aop; import com.example.demo.dto.PutRequestDto; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Base64; // method에 들어가는 parameter를 변경하는 클래스 @Aspect @Component public class DecodeAop { @Pointcut("@annotation(com.example.demo.annotation.Decode)") private void enableDecode(){} @Before("enableDecode()") public void before(JoinPoint joinPoint) throws UnsupportedEncodingException { Object[] args = joinPoint.getArgs(); for(Object arg : args){ if (arg instanceof PutRequestDto){ PutRequestDto putRequestDto = (PutRequestDto) arg; String base64name = putRequestDto.getName(); String name = new String(Base64.getDecoder().decode(base64name), "UTF-8"); putRequestDto.setName(name); } } } @AfterReturning(value = "enableDecode()", returning = "returnObj") public void afterReturn(JoinPoint joinPoint, Object returnObj){ if(returnObj instanceof PutRequestDto){ PutRequestDto putRequestDto = (PutRequestDto) returnObj; String name = putRequestDto.getName(); String base64Name = Base64.getEncoder().encodeToString(name.getBytes(StandardCharsets.UTF_8)); putRequestDto.setName(base64Name); } } }
Annotation 붙인 클래스의 모든 public 메서드를 pointcut 하는 방법
public class LoggingAop { @Pointcut("within(@com.uplus.virtualoffice.logging.Logging *)") private void annotatedClassWithLogging() { } @Pointcut("execution(public * *(..))") private void publicMethod() {} @Around("publicMethod() && annotatedClassWithLogging()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { // ... } }