2025. 6. 20. 20:45ㆍ스프링
모든 내용은 공부 후 정리해서 적어둔 내용입니다. 틀린 내용이 있다면 댓글로 말씀해 주세요.
Spring 공식 문서의 내용에는 아래와 같은 내용이 있습니다.
Spring AOP는 현재 메서드 실행 JoinPoint만 지원합니다.
(= Spring Bean의 메서드 수행에 대해서만 Advice를 적용할 수 있다.)
필드 접근(Field Interception)은 구현되어 있지 않으며, 필드 접근에 대한 지원을 추가하는 것이 Spring AOP의 핵심 API를 깨뜨리지는 않지만, 현재로서는 제공되고 있지 않습니다.
만약 필드 접근이나 필드 변경에 대해서도 Advice를 적용하고 싶다면 AspectJ를 사용하는 것을 고려하세요.
이 외에도 AspectJ에 대한 언급이 중간중간 있습니다.
그래서 우선 Spring AOP를 알아보기 전에 또다른 AOP 프레임워크인 AspectJ에 대해서 간략하게 알아보겠습니다.
AspectJ
AspectJ는 프록시를 사용하지 않고, 클래스의 바이트코드를 직접 조작하는 방식을 사용합니다.
즉, 타겟이 되는 객체의 바이트코드를 수정하여 부가적인 로직을 직접 삽입하게 됩니다.
반면, Spring AOP는 프록시 객체를 생성하여 대상 객체를 감싸는 방식을 사용합니다.
따라서 AOP를 적용하려면 대상 객체가 반드시 Spring Bean으로 등록되어야 하며, 간접 호출 방식이기 때문에 약간의 오버헤드가 발생할 수 있습니다.
또한, AspectJ는 바이트코드를 직접 조작하기 때문에 Spring AOP보다 훨씬 다양한 JoinPoint를 지원합니다.
예를 들어, 메서드 실행뿐 아니라 필드 접근, 생성자 호출, static 초기화 블록 등에도 Advice를 적용할 수 있습니다.
이러한 이유로 기능적으로만 보면 AspectJ는 Spring AOP보다 훨씬 강력하고 유연한 프레임워크입니다.
그럼 "무조건 AspectJ를 사용하는게 맞냐"라고 하면 그렇지 않습니다.
Spring AOP는 Spring 애플리케이션에 맞게 설계되어 있어 설정이 간단하고 직관적입니다.
또한, 실제 서비스에서 필요한 대부분의 AOP 기능(예: 트랜잭션 처리, 로깅, 인증/인가 등)은 메서드 실행 시점만 intercept 하면 충분하기 때문에, 굳이 AspectJ의 모든 기능이 필요하지 않은 경우가 많습니다.
스프링의 공식문서에도 나와있듯이 두 프레임워크 모두 가치가 있으며 경쟁관계가 아닌 상호보완적인 관계이므로,
서비스의 요구사항에 따라 Spring AOP와 AspectJ를 적절히 선택하면 될 것 같습니다.
Spring AOP
Spring AOP는 기본적으로 프록시 방식으로 동작합니다.
이 프록시는 타겟 객체를 감싸는 껍질처럼 작동하며, 부가로직(Advice)을 타겟 객체의 메서드 실행 전/후 로직을 제어합니다.
프록시 방식으로 AOP를 직접 구현하면 아래와 같습니다.
기본구조
public interface Pojo {
void foo();
}
@Service
public class SimplePojo implements Pojo {
@Override
public void foo() {
...
}
}
프록시 수동 적용
public class SimplePojoProxy implements Pojo {
private final Pojo target;
public SimplePojoProxy(Pojo target) {
this.target = target;
}
@Override
public void foo() {
System.out.println("[프록시] 실행 시간 측정 시작");
long start = System.currentTimeMillis();
target.foo();
long end = System.currentTimeMillis();
System.out.println("[프록시] 걸린 시간: " + (end - start));
}
}
이런 방식으로 프록시 객체를 이용해서 메서드 호출 전/후에 필요한 부가 로직을 추가함으로써 AOP를 구현할 수 있습니다.
하지만, Pojo타입의 빈이 불필요하게 2개 등록되고, 의존성 주입시 어떤 빈을 주입할지 모호해져서 문제가 생길 수 있으므로 추가로 지시자등을 설정해주어야 합니다.
Spring의 JDK Dynamic Proxy
Spring은 위의 불편함을 해결하기 위해서 JDK 동적 프록시를 제공합니다.
동작 방식
- 프록시 객체(SimplePojoProxy)를 Bean으로 등록하고 실제 Bean(SimpleProxy)대신 사용합니다.
- 프록시는 실제 객체와 같은 인터페이스를 구현하고 있기 때문에 의존성 주입이 매끄럽게 작동합니다.
class JdkDynamicAopProxy {
...
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
Boolean var8;
try {
if (this.equalsDefined || !AopUtils.isEqualsMethod(method)) {
...
// 실제 객체를 가져옴
target = targetSource.getTarget();
Class<?> targetClass = target != null ? target.getClass() : null;
// 적용할 Advice 리스트
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty()) {
// Advice가 없으면 메서드 직접 호출
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
} else {
// Advice가 있으면 순차적으로 실행
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}
...
}
...
}
...
}
하지만, 아래와 같이 인터페이스가 아닌 구현 클래스에 의존하는 경우가 생긴다면 문제가 발생할 수 있습니다.
@Service
@RequiredArgsConstructor
public class SamplePojo {
private final SimplePojo simplePojo;
}
SimplePojoProxy는 Pojo를 구현한 클래스이지 SimplePojo를 상속받은 클래스가 아니기 때문에 SimplePojo 타입의 빈을 찾을 수 없어서 에러가 발생합니다.
그래서 JDK 동적 프록시는 "반드시 인터페이스를 생성해야한다.", "구체 클래스를 주입받을 수 없다" 라는 제약이 있습니다.
Spring의 CGLIB Proxy
이를 해결하기 위해서 Spring은 CGLIB 프록시를 제공합니다.
CGLIB 프록시는 바이트 조작 라이브러리(spring-core에 포함)를 통해서 클래스 상속으로 프록시를 구현합니다.
public class SimplePojoProxy extends SimplePojo {
@Override
public void foo() {
...
}
}
CGLIB 프록시 사용 시 주의사항
- final 클래스는 상속이 불가능하기 때문에 프록시 생성이 안 됩니다.
- final 메서드는 오버라이드가 불가능하므로 Advice 적용이 안 됩니다.
- private 메서드도 오버라이드할 수 없어서 Advice 적용이 불가능합니다.
- 다른 패키지에 있는 부모 클래스의 package-private 메서드는 외부에서 접근 불가이므로 프록시에서 Advice 적용이 안 됩니다.
- CGLIB 프록시는 생성자를 통해 객체를 생성하지 않고 Objenesis를 사용해 인스턴스를 만들기 때문에 일반적으로 생성자가 두 번 호출되지 않습니다.
단, JVM이 constructor bypassing을 허용하지 않으면 생성자가 두 번 호출되는 것처럼 보일 수 있습니다. - Java Module System을 사용하는 경우에는 제약이 있을 수 있습니다.
예: java.lang 패키지의 클래스를 프록시하려 하면 --add-opens JVM 옵션을 주어야 하는데, 모듈 기반 환경에서는 제약이 많습니다.
적용 방법 예시
AOP 설정
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
...
}
우선 @Aspect 어노테이션 기반의 프록시 AOP를 활성화하기 위해서 @EnableAspectJAutoProxy를 설정해줍니다.
(SpringBoot가 아닌데 CGLIB를 이용할 것이라면 proxyTargetClass 속성을 true 직접 명시해주세요. - 내용 출처의 MangKyu님 블로그글 내용 참고)
Aspect 클래스 정의
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("[LogAspect] 메서드 실행 전 로그");
}
}
이렇게 공통 관심사를 생성해 적용하면 됩니다.
마지막으로 Advice 어노테이션들에 대해서 적으며 마무리 하겠습니다.
@Before | 메서드 실행 전에 |
@After | 메서드 실행 후 (성공, 예외 상관 없음) |
@AfterReturning | 메서드 정상 종료 후 |
@AfterThrowing | 메서드 실행 중 예외 발생 시 |
@Around | 메서드 실행 전/후 모두 제어 |
https://docs.spring.io/spring-framework/reference/core/aop/introduction-spring-defn.html
https://mangkyu.tistory.com/175
'스프링' 카테고리의 다른 글
AOP(Aspect Oriented Programming, 관점지향 프로그래밍)에 대해서 알아보기 (1) | 2025.06.19 |
---|---|
@Transactional 알아보기 (0) | 2025.06.19 |
에러 해결: Content-Type 'multipart/form-data' is not supported (0) | 2025.04.20 |
에러 처리 - Filter에서 발생한 에러 처리하기 (0) | 2025.02.19 |
에러 처리 - 커스텀 Exception (0) | 2025.02.05 |