Aspect Oriented Programming 의 약자로 관점 지향 프로그래밍을 의미합니다. 이를 좀 더 풀어서 이야기 해보면 어떤 기능을 구현할 때 주요 핵심 기능과 핵심 기능 구현을 위한 부가적인 기능 구현을 분리하여 각각의 관점별로 묶어서 개발하는 방식을 말합니다.
여기서 부가적인 기능이라고 하면 비지니스 기능을 구현시 필요한 기타 작업(로깅 작업, 데이터 베이스 연결 및 트랜잭션 처리, 파일 입출력)과 같은 작업들을 말합니다. 이런한 부가 기능들은 핵심 기능에 부가되어 의미를 갖는 특별한 모듈이기 때문에 독립적으로 존재할 수 없습니다.
이때 부가적인 기능들이 여러 비지니스 로직에서 반복적으로 사용되는 패턴을 보이게 되는데 이를 하나의 공통된 로직에서 처리할 수 있도록 모듈화하여 개발하는 것이 AOP 를 하는 주요 목적이라고 볼 수 있습니다.
스프링 프레임워크에서도 위에 설명드린 관점 지향 프로그래밍을 할 수 있도록 내부적으로 지원하고 있습니다. 스프링 AOP 의 경우 프록시 기반의 AOP 를 지원하고 있는데, 프록시 기반이라는건 스프링 내부적으로 주요기능을 구현한 비지니스 로직인 Target 클래스 빈에 대한 프록시 빈을 만드는 형태를 의미합니다. 런타임 시점(정확히는 스프링 컨테이너 로딩 시점)에 자동 프록시 생성기에서 주요 Target 클래스 빈에 대해 부가 기능을 같이 실행해 줄 프록시 빈을 만들어줍니다.
스프링의 자동 프록시 생성기(DefaultAdvisorAutoProxyCreator)는 JDK 의 다이나믹 프록시를 좀 더 스프링 프레임워크에 맞게 추상화하여 개발된 프록시 생성기 입니다. 자동 프록시 생성기는 타겟 객체의 인터페이스 타입을 기준으로 프록시 객체를 생성하도록 되어 있습니다. 이는 스프링 AOP 적용에 최적화 된 제약 조건이지만 어플리케이션 내부 빈들의 의존성 주입 상태에 따라 오류를 불러일으키기도 합니다.
자동 프록시 생성기는 스프링 컨테이너 로딩 시점에 아래와 같은 작업을 수행합니다.
한 가지 주의할 점은 스프링 컨테이너에서 관리하지 않는 빈에 대해서는 프록시 빈 자동 생성 및 타깃 빈과의 바꿔치기가 불가능하기 때문에 스프링 AOP 에서는 원본 타겟 객체가 스프링 컨테이너 내부 빈으로 관리되어야 합니다.
프록시 빈을 원래 타겟 빈으로 바꿔치기 할 경우, 스프링 DI 원칙을 거스르고 인터페이스 참조가 아닌 클래스 직접 참조를 하고 있는 객체에 대해서는 스프링 AOP 적용시 타겟 객체 참조가 안되는 이슈가 발생합니다. 이유는 자동 프록시 생성기는 인터페이스 기반으로 프록시 빈을 생성하고 이를 원본 타겟 빈인 것처럼 등록해주는데, 이때 원본 타겟 빈의 타입은 원본 타겟 빈의 클래스가 아닌 인터페이스로 정의됩니다. 그래서 @Autowired 와 같이 타입에 의한 선언적 참조로 DI 해오려고 할 경우 원본 타겟 클래스 타입과 일치하는 빈 객체를 찾지 못해 에러가 발생하게 됩니다.
<!-- 각종 빈 및 어드바이스 빈 참조 -->
<context:component-scan base-package="packageName"></context:component-scan>
<!-- advice 빈은 component-scan에 의해 자동 등록된 상태, Spring AOP 관련 인터페이스 구현 필요 -->
<aop:config>
<aop:advisor advice-ref="advice"
pointcut="excution(returnType packageName.ClassName.methodName(..))" />
</aop:config>
<aop:aspectj-autoproxy/>
@Aspect
public class Advice {
// . . .
// AspectJ 라이브러리에서 지원하는 어노테이션 사용 가능
}
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
// . . .
}
스프링에서 가장 이상적인 프록시 기반 AOP 적용의 전제 조건은 타겟 객체가 인터페이스를 구현하고 있고, 해당 객체를 참조하는 다른 객체들은 인터페이스 타입 기준으로 객체를 참조하고 있어야 정상적인 적용이 가능합니다. 그런데 스프링에서는 인터페이스와 클래스가 아닌 클래스와 클래스간의 강한 의존 관계가 있을 경우에도 프록시 기반 AOP 를 적용할 수 있도록 지원하고 있습니다.
타겟 클래스가 인터페이스를 구현하고 있지 않을 경우 혹은 인터페이스가 아닌 클래스 직접 참조를 하는 경우 CGLib 라는 바이트코드 생성 라이브러리를 추가하여 인터페이스 기반이 아닌 타겟 클래스를 기반으로 프록시를 생성할 수 있습니다. 이는 레거시 소스에서 스프링 AOP 적용이 필요한 케이스 이외에는 가급적 지양해야하는 프록시 생성 방식 입니다.
<!-- proxy-target-class 속성은 클래스 기반 프록시 생성을 강제하는 설정 -->
<aop:config proxy-target-class="true"></aop:config>
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AppConfig {
// . . .
}
클래스 프록시 생성시 주의해야 할 점으로는 클래스 상속을 통해 프록시 클래스를 만들어 생성하기 때문에 AOP 를 적용할 클래스가 final 로 선언되어 있으면 클래스 프록시 생성이 불가능 합니다. 그리고 상속 구조로 인해 상위 클래스 생성자 호출 후 하위 클래스(프록시 클래스) 생성자가 한번 더 호출되기 때문에 생성자 내부에 리소스를 할당한다거나 하는 주요 작업은 피하는게 좋습니다.
참조로 얻은 내용들이 깔끔하게 관련 내용들을 모두 담고 있어 저의 글이 아래 글들과 포맷과 내용이 너무나 유사하게 느껴지신다면 그건 기분탓이 아니고 느끼신 그대로가 맞습니다. 저만의 언어로 정리하는 연습을 하고 있는 과정에 너무나 큰 도움이 되고 있습니다.