목차
- OOP vs AOP
- AOP 핵심 개념
- AOP 설정 방식
- AspectJ 문법
- AOP 구현 방법 ( JDK Dynamic Proxy vs CGLIB.jar )
Aspect Oriented Programming
관점 지향 프로그래밍
결제 / 회원인증과 같은 비즈니스 로직 (Core Concerns)에 대해 공통적으로 들어가야하는 모듈 (Cross-Cutting Concerns) 이 있다.
단순히 비즈니스 로직 하나를 구현하기 위해 DB Connection, 인증, DB Close 등의 부가 작업을 수행한 코드를 앞 뒤로 붙여줘야 합니다.
이걸 Util로 빼더라도 개발자가 해당 메서드 호출 자체를 해야하는 것은 있고, 사람이기에 호출을 실수할 수도 있습니다.
AOP를 도입해서, 내가 원하는 것(비즈니스 로직)에만 집중하고, 프레임워크에게 공통 부분을 맡길 수 있습니다.
공통의 관심사항 (횡단 관심사)을 (= DB Conn, DB Close) 분리하여,
비즈니스 로직에서의 의존관계의 복잡성이나 코드 중복을 제거할 수 있습니다.
비즈니스 로직의 앞 뒤에 해야할 일들을 공통 Function으로 들어낼 수 있습니다.
비즈니스 로직에 대해서는 개발자 나름대로 설정하는 것이 편하고, 이를 어노테이션화하여 분리할 수 있습니다.
Util로 뺐던 공통관심사의 호출 책임을 Spring Container에게 맡기고, 개발자는 비즈니스 로직에만 집중할 수 있게 해준다. ( IoC의 예시 중 하나 )
핵심 관심 모듈 ( 비즈니스 로직 )을 Core Concern이라 하고, 해당 로직을 포함하는 클래스 단위를 Target
, 클래스 내의 메서드를 JoinPoint
, 이러한 메서드들에 조건을 걸어 추출한 메서드 집합 단위를 Pointcut
이라 합니다.
횡단 관심 모듈 ( 공통 관심 사항 )을 Cross-Cutting Concern이라 하고, 이러한 로직의 메서드들을 Advice
라 합니다. Aspect
는 Advice
를 Pointcut
에 언제 (Before
, After
, Around
) 적용할지 설정해놓은 클래스나 설정이라 생각하면 됩니다.
Aspect
를 외울 때, WWW
를 생각하면 편합니다.
Aspect는 What
( Advice )을 Where
( Pointcut )에 When
( Before, After, AfterThrowing, AfterReturning, Around) 적용할 지 설정하는 것입니다.
뒤에 더 이야기하겠지만, 어노테이션 기반으로 AOP를 설정하면 Aspect
는 클래스가 될 것이고, XML 기반으로 AOP를 설정하면 Aspect
는 XML ( 정확히는 servlet-context.xml ) 내의 설정 코드 부분이 될 것입니다.
Weaving
은 영어로 '(천 따위를) 짜다' 라는 의미를 가지고 있습니다.
즉, 공통 관심 모듈인 Aspect
를 Pointcut
에 적용하는 절차를 의미합니다.
이 때, Target 클래스는 해당 횡단 모듈이 적용된 Proxy
로 새로 생성되게 됩니다.
Weaving
은 Aspect
를 Target의 Pointcut
에 적용하여, Proxy
를 만드는 과정이라 할 수 있습니다.공통을 핵심에 적용하는 절차
입니다.AOP
는 기존 OOP
에서 공통 관심사항 관련 중복 코드가 발생하는 상황에 대해 스프링 컨테이너에게 해당 작업을 하도록 위임함으로써 개발 편의성과 중복을 제거하여 유지보수성에 유리한 개발을 할 수 있도록 도와줍니다.
Proxy
는, 대리자 혹은 위임받는 대상을 의미합니다.
Proxy
는 Target
을 기반으로 새로 생성되는 객체이며 Target
의 JoinPoint
메서드 호출 시, 중간에 해당 메서드를 가로채서 실행을 대신하고 ( 이 때 공통 메서드 실행 ) 결과를 Target
에게 돌려줍니다.
Proxy
를 통해 Spring Container가 Target 클래스를 제어할 수 있는 것입니다.
사용자 (개발자)는 Target
클래스가 동작하는 줄 알지만, 사실은 Aspect
가 적용된 Proxy
가 동작하고 있는 것입니다.
- Advice : What (공통기능)
- Target : 어드바이스가 적용될 객체 (핵심기능)
- JoinPoint : N개 이상(객체 안의 적용될 메서드)
- PointCut : Where
- Proxy : 어드바이스를 타겟 객체에 적용하면 생성되는 객체 (= 중계자, 대행자)
- Aspect : Advice(What) + PointCut(Where) + When
- When[5]
- 1) 앞 Before : 메서드가 실행되기 전
- 2) 뒤 After-Finally : 메서드가 실행된 후 무조건
- 2-1) After-Throwing (Exception 발생 시)
- 2-2) After-Returning (Try)
- 3) Around (메서드 앞 뒤) : ex. 수행시간 재기
- Weaving
- 공통을 핵심에 적용하는 절차
- Target에 Aspect를 적용해서 Proxy 객체를 생성하는 절차
<aop:config>
<aop:aspect>
<aop:pointcut>
<aop:around>
<aop:before>
<aop:after-reurning>
<aop:after-throwing>
<aop:after>
<aop:around>
<!-- lec06-servlet-context.xml -->
<beans xmlns:xsi ~~~>
<bean id="MY_ASPECT" class="com.lec06.aop.CommonAspect" />
<aop:config>
<aop:aspect id="MY_What_Where_When" ref="MY_ASPECT">
<aop:pointcut id="MY_CUT" expression="execution(public * com.lec06..*Impl.*(..))"/>
<aop:before pointcut-ref="MY_CUT" method="beforeAdvice" />
<aop:after pointcut-ref="MY_CUT" method="afterAdvice" />
</aop:aspect>
</aop:config>
<bean id="MY_SVC" class="com.lec06.aop.AOPServiceImpl">
<property name="aOPDAO" ref="MY_DAO"/>
</bean>
<bean id="MY_DAO" class="com.lec06.aop.AOPDAO"/>
<!-- ** Aspect또한 Spring Bean으로 등록해주어야 합니다. ** -->
<bean id="MY_ASPECT" class="com.lec06.aop.CommonAspect" />
</beans>
이는 servlet-context.xml
의 일부입니다.
CommonAspect
라는 횡단 관심사 모듈이 "MY_ASPECT"
의 id로 Spring Bean으로 등록되고, "MY_CUT"
이라는 이름의 포인트컷으로 잘려나온 JoinPoint들의 실행 전(aop:before
), 후 (aop:after
) 로 지정된 Aspect
클래스 내의 Advice
가 실행됩니다.
이 때, <aop:pointcut>
요소에 expression
속성을 주의해서 보아야합니다. 조인 포인트들의 포인트컷을 뽑아낼 때, 특별한 스크립트가 이용되고 이 스크립트 언어를 AspectJ
라 합니다.
접근자 리턴타입 패키지.클래스.메소드명(매개변수)
포인트컷을 잡아내는 데에 사용하는 문법이기에 '메소드'를 정의하는 형식과 매우 비슷합니다.
조금 다른 건 메소드 선언 부분이 패키지, 클래스까지 포함한다는 점입니다.
이러한 정의문을 통해 Aspect를 적용할 메소드의 대상(Pointcut)을 잡아낼 수 있습니다.
AspectJ
를 적용해서 나타내보기public int com.lec06.aop.boardDAO.boardInsert(BoardVO)
public int com.lec06.aop.boardDAO.replyInsert(ReplyVO)
=> public int com.lec06.aop.boardDAO.*Insert(*VO)
public
, 반환 타입은 int
, 패키지.클래스명은 com.lec06.aop.boardDAO
, 적용되는 메서드는 Insert
로 끝나는 모든 메서드 라는 의미의 *Insert
, 매개변수는 VO
로 끝나는 이름의 매개변수 타입AspectJ
를 적용해서 나타내보기public int com.lec06.aop.boardDAO.boardInsert(BoardVO)
public int com.lec06.aop.boardDAO.replyInsert(ReplyVO)
public BoardVO com.lec06.aop.boardDAO.selectOne(int)
public List com.lec06.aop.boardDAO.select()
=> public * com.lec06.aop.boardDAO.*(..)
public
, 반환 타입은 모든 타입(*
), 패키지.클래스 명은 com.lec06.aop.boardDAO
, 메서드 명은 모든 것을 포함(*
) 및 존재해야 함, 매개변수 타입은 0개 ~ N개를 나타내는 ..
표기 ( 없어도 되거나 1개 이상 )AspectJ
를 적용해서 나타내보기public int com.lec06.aop.boardDAO.boardInsert(BoardVO)
public int com.lec06.aop.boardDAO.replyInsert(ReplyVO)
public BoardVO com.lec06.aop.boardDAO.selectOne(int, int)
=> public * com.lec06.aop.boardDAO.*(*,..)
public
, 반환 타입은 모든 타입 (*
), 패키지.클래스 명은 com.lec06.aop.boardDAO
, 메서드 명은 모든 것을 포함 (*
)하며 존재해야 함. 매개변수 타입은 1개는 타입에 상관없이 꼭 있어야 하고, 나머지는 있거나 없어도 됨. 즉 1개 이상의 매개변수가 있어야 하며, 처음 매개변수의 타입은 아무거나 상관이 없다는 의미AspectJ
를 적용해서 나타내보기public int com.lec06.aop.boardDAO.boardInsert(BoardVO)
public int com.lec06.aop.boardDAO.replyInsert(ReplyVO)
public BoardVO com.lec06.aop.boardDAO.selectOne(int, int)
public List com.lec06.aop.boardDAO.select()
=> public * com.lec06.aop.boardDAO.*(..)
public
, 반환 타입은 모든 타입(*
), 패키지.클래스 명은 com.lec06.aop.boardDAO
, 메서드 명은 모든 것을 포함 (*
)하며 존재해야 함. 매개변수 타입은 0개 이상 있거나 없거나 상관없음.AspectJ
를 적용해서 나타내보기public int com.lec06.aop.BoardDAO.boardInsert(BoardVO)
public int com.lec06.aop.BoardDAO.replyInsert(ReplyVO)
public BoardVO com.lec06.aop.BoardDAO.selectOne(int, int)
public List com.lec06.aop.BoardDAO.select()
public void com.kosta.test.DAOCallTest.myprint()
=> public * com.*.*.*DAO*.*(..)
public
, 반환 타입은 모든 타입(*
), 패키지.클래스 명은 com
으로 시작 및 두 depth는 무엇이 되었든 꼭 존재해야함. (com.*.*
) 메서드 이름은 DAO를 포함 앞 뒤에 무언가 포함된 형태의 메서드 (*DAO*
). 메서드 명은 모든 것을 포함 (*
)하며 존재해야 함. 매개변수 타입은 0개 이상 있거나 없거나 상관없음.AspectJ
를 적용해서 나타내보기public int com.lec06.aop.BoardDAO.boardInsert(BoardVO)
public int com.lec06.aop.BoardDAO.replyInsert(ReplyVO)
public BoardVO com.lec06.aop.BoardDAO.selectOne(int, int)
public List com.lec06.aop.BoardDAO.select()
public void com.lec01.sample.DAOCallTest.myprint()
public void com.kosta.CallTest.myprint()
=> public * com..*.*(..)
public
, 반환 타입은 모든 타입(*
), 패키지는 com
으로 시작하며 depth가 상관이 없음. ( com..
) 클래스 명은 아무거나 상관이 없음 (*
) 메서드 명 또한 아무거나 상관이 없음 (*
). 매개변수는 0개 혹은 그 이상. (..
)AspectJ
해석 정리 예) execution(public Integer com.edu.aop.*.*(*))
접근제어자 : public
메서드리턴타입 : Integer
패키지: com.edu.aop
클래스: 모든클래스
메서드 : 모든 메서드
파라미터 : 1개
예) execution( * com.edu..*.get*(..))
접근제어자 : public
메서드리턴타입 : 모든타입
패키지: com.edu. 뎁스무관
클래스: 모든클래스
메서드 : get____()
파라미터 : 0개~N개
예) execution( * com.edu.aop..*Service.*(..))
접근제어자 : public
메서드리턴타입 : 모든타입
패키지: com.edu.aop. 뎁스무관
클래스: ___Service
메서드 : 모든메서드
파라미터 : 0개~N개
예) execution( * com.edu.aop.BoardService.*(..))
접근제어자 : public
메서드리턴타입 : 모든타입
패키지: com.edu.aop만
클래스: BoardService만
메서드 : 모든메서드
파라미터 : 0개~N개
예) execution( * some*(*, *))
접근제어자 : public
메서드리턴타입 : 모든타입
패키지: 없음
클래스: 없음
메서드 : some_____()
파라미터 : 2개
*
는 인정되지 않음), 기본 default 값으로 public
이 지정된다.JDK 동적 프록시를 이용
CGLIB 프록시를 이용
인터페이스
기반인지 아닌지의 차이이다.인터페이스가 있다.
조립기 ( Spring Container )는 인터페이스에만 의존한다.
인터페이스가 앞단에 존재하는 클래스에 대해서만 AOP를 적용할 수 있다.
핵심 기능을 구현한 클래스 (Target
) 의 I/F를 상속받아 기능을 확장한 프록시를 생성하는 방식으로 동작한다.
인터페이스가 없어도 클래스 객체에 AOP를 적용시킬 수 있는 방식이다.
핵심 기능을 구현한 클래스 (Target
)자체를 상속받아 기능을 확장한 자식 클래스 프록시를 생성하는 방식으로 동작한다.
java.lang.reflect.Proxy
클래스를 사용한다. ( JDK 기반 )
reflect
의 뜻?
반영
. Java Reflection API 를 통해 런타임
때 JVM Method Area의 Class 메타 데이터를 참조하여 해당 클래스에 대한 작업 ( 인스턴스 생성 및 조작 등 )을 수행할 수 있다.런타임
때 I/F를 구현하는 프록시 객체를 "동적으로 생성"한다.프록시 객체는 대상 객체(Target)와 동일한 인터페이스를 구현한다. 메서드 호출을 가로채어 추가기능(Advice)
을 수행한다.
빈을 주입받을 때, 실제 빈 대신 프록시 객체를 주입받는다.
1) AOP 설정
@Configuration
@EnableAspectJAutoProxy // AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기(Bean PostProcessor)가 이를 관리함.
public class AppConfig {
}
2) 빈 후처리기 등록 ( @EnableAspectJAutoProxy
)
: 스프링 컨텍스트는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 통해
빈을 스캔하여 AOP 관련 설정을 적용합니다.
3) 프록시 생성
a) 초기화 후 처리 : 빈이 초기화 후, 해당 빈이 AOP 대상인지 검사합니다.
b) 해당 빈이 AOP 대상인 경우 다음과 같이 프록시를 생성합니다.
import java.lang.reflect.Proxy;
MyService proxy = (MyService) Proxy.newProxyInstance(
MyService.class.getClassLoader(), // 인터페이스 로드
new Class<?>[] { MyService.class }, // 동일한 객체 동적 생성
new MyInvocationHandler(target) // 메서드 가로채기 invoke() 오버라이딩해서 어드바이스 로직 적용
4) 이 때, MyInvocationHandler를 통해 어드바이스 로직이 적용되기에, 이를 구현해서 메서드 실행 방식을 수정합니다.
public class MyInvocationHandler implements InvocationHandler {
private final Object target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Before Target method
// Invoke actual method on target
Object result = method.invoke(target, args);
// After Target Method
}
}
5) 프록시 객체 반환
프록시 객체는 스프링 컨텍스트에 의해 관리됩니다.
클라이언트 코드가 빈을 주입받을 때, 실제 빈 대신 프록시 객체가 반환됩니다.
이 프록시 객체는 메서드 호출을 가로채어 InvocationHandler를 통해 어드바이스를 적용합니다.
//---------------------------------------------------------------------------
// 스프링 AOP는 JDK 동적 프록시를 사용하여 인터페이스 기반 프록시를 생성하고, 이를 통해 AOP 기능을 제공
// MyServiceImpl 객체 대신 프록시 객체를 생성하여 빈으로 등록
// 이 프록시 객체는 performTask 메서드 호출을 가로채어 어드바이스 적용
//---------------------------------------------------------------------------
public interface MyService {
void performTask();
}
public class MyServiceImpl implements MyService {
@Override
public void performTask() {
System.out.println("Executing task...");
}
}
@Aspect
public class MyAspect {
@Before("execution(* com.example.MyService.performTask(..))")
public void beforeTask(JoinPoint joinPoint) {
System.out.println("Before task: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.MyService.performTask(..))")
public void afterTask(JoinPoint joinPoint) {
System.out.println("After task: " + joinPoint.getSignature().getName());
}
}
바이트코드를 조작하여 런타임에 새로운 클래스를 생성할 수 있게 해줌.
인터페이스를 구현하지 않은 클래스에도 프록시를 적용한다. ( 타겟 클래스를 상속받아 동작한다 )
JDK 동적 프록시보다 약간 더 높은 성능을 가진다.
추가 의존성으로 CGLIB.jar를 추가해주어야 한다. ( 대부분의 스프링 배포판에는 CGLIB가 포함되어있다. )
상속 기반 : 상속을 기반으로 프록시를 생성하므로, final로 선언된 클래스는 프록시화 시킬 수 없다.
대상 클래스의 서브클래스를 생성하고, 메서드 호출을 가로채어 추가 기능(Advice)을 수행한다.
Target은 곧 프록시의 부모, 마음대로 오버라이딩 해서 새로운 기능을 추가해서 넣는다.
1) AOP 설정
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
@Bean
public MyService myService() {
return new MyService();
}
}
2) 빈 후처리기 등록
@EnableAspectJAutoProxy(proxyTargetClass = true)
-> 빈을 생성 후, 스캔하여 AOP 관련 설정을 적용한다.
3) 프록시 생성
: 빈 후처리기는 빈이 초기화될 때, CGLIB를 사용하여 클래스 기반 프록시를 생성한다.
★★★★★ CGLIB는 대상 클래스를 상속받아 (서브클래스를 동적으로 생성) ★★★★★★
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); ---★★★★★★★★★--- (부모Service클래스 상속받아 차일드클래스 동적 생성)
enhancer.setCallback(new MyMethodInterceptor(target)); ------- 메서드 호출을 가로채고 어드바이스 로직 적용
MyService proxy = (MyService) enhancer.create();
4) MethodInterceptor 구현 ( = JDK 동적 프록시에서 InvocationHandler의 역할 )
public class MyMethodInterceptor implements MethodInterceptor {
private final Object target;
public MyMethodInterceptor(Object target) {
this.target = target;
}
@Override
//public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// Before advice
System.out.println("Before method: " + method.getName());
// Invoke actual method on target
// Object result = method.invoke(target, args);
Object result = proxy.invoke (target, args); ---★★★★★★★★★---
// After advice
System.out.println("After method: " + method.getName());
return result;
}
}
5) 프록시 객체를 반환합니다.
//---------------------------------------------------------------------------
// MyService 클래스는 인터페이스를 구현하지 않습니다.
// MyAspect 클래스는 해당 서비스 메서드의 전후에 실행될 어드바이스를 정
// 스프링은 MyService 객체 대신 CGLIB를 사용하여 프록시 객체를 생성하고, 이를 빈으로 등록합니다.
// 이 프록시 객체는 performTask 메서드 호출을 가로채어 어드바이스를 적용
//---------------------------------------------------------------------------
//public interface MyService {
// void performTask();
//}
//public class MyServiceImpl implements MyService {
// @Override
public class MyService {
public void performTask() {
System.out.println("Executing task...");
}
}
@Aspect
public class MyAspect {
@Before("execution(* com.example.MyService.performTask(..))")
public void beforeTask(JoinPoint joinPoint) {
System.out.println("Before task: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.MyService.performTask(..))")
public void afterTask(JoinPoint joinPoint) {
System.out.println("After task: " + joinPoint.getSignature().getName());
}
}
public class AOPController extends MultiActionController {
//property 방식으로 의존성 주입
private AOPService aOPService;
public void setAOPService(AOPService svc) {
this.aOPService = svc;
}
public void ctlDelete(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("1.___AOPController.ctlDelete() 실행");
aOPService.svcDelete();
//return new ModelAndView("test"); // / test .jsp
}
}
public interface AOPService {
public void svcDelete() throws Exception;
}
public class AOPServiceImpl implements AOPService {
private AOPDAO aOPDAO;
public void setAOPDAO(AOPDAO aOPDAO) {
this.aOPDAO = aOPDAO;
}
@Override
public void svcDelete() throws Exception {
System.out.println("2.___AOPServiceImpl.svcDelete() 실행");
aOPDAO.delete();
// 강제 에러 발생 ( Aspect - afterThrowing 을 위해 )
// throw new Exception();
}
}
public class AOPDAO {
public void delete() {
System.out.println("3.____AOPDAO.delete() 실행");
}
}
public class CommonAspect {
public void beforeAdvice() {
System.out.println("\t 실행 전 :: CommonAspect.beforeAdvice()");
}
public void afterAdvice() {
System.out.println("\t 실행 후 무조건 :: CommonAspect.afterAdvice()");
}
public void afterThrowingAdvice(Exception exception) {
System.out.println("\t 실행 후 에러시 :: CommonAspect.afterThrowingAdvice() :: " + exception.getMessage());
}
public void afterReturningAdvice(Object res) {
System.out.println("\t 실행 후 정상시 :: CommonAspect.afterReturningAdvice() :: " + res);
}
public void aroundAdvice(ProceedingJoinPoint jp) {
try {
System.out.println("\t 앞-CommonAspecct.aroundAdvice()");
System.out.println("\t :: " + jp.getSignature());
jp.proceed();
System.out.println("\t 뒤-CommonAspect.aroundAdvice()");
} catch (Throwable e) {
e.printStackTrace();
}
}
}
XML 기반 설정이라 Java 소스 코드가 깔끔(?)한 것을 알 수 있습니다.
이제, XML 기반으로 AOP를 설정해줍니다.
servlet-context.xml
은 Tomcat이 우리 서비스의 /WEB-INF/web.xml
을 보며 우리 웹 어플리케이션의 초기 설정 시, DispatcherServlet
을 초기화(init()
)시킬 때 설정 정보로 넘겨주는 XML 설정 파일입니다.<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd"
>
<!-- 로그출력 : log4j -->
<mvc:resources mapping="/resources/**" location="/resources/"/>
<bean name="/ctlDelete" class="com.lec06.aop.AOPController">
<property name="aOPService" ref="MY_SVC"/>
</bean>
<bean id="MY_SVC" class="com.lec06.aop.AOPServiceImpl">
<property name="aOPDAO" ref="MY_DAO"/>
</bean>
<bean id="MY_DAO" class="com.lec06.aop.AOPDAO"/>
<bean id="MY_ASPECT" class="com.lec06.aop.CommonAspect" />
<!-- AOP 설정 -->
<aop:config>
<aop:aspect id="MY_What_Where_When" ref="MY_ASPECT">
<aop:pointcut id="MY_CUT" expression="execution(public * com.lec06..*Impl.*(..))"/>
<aop:before pointcut-ref="MY_CUT" method="beforeAdvice" />
<aop:after pointcut-ref="MY_CUT" method="afterAdvice" />
</aop:aspect>
<aop:aspect id="MY_What_Where_When" ref="MY_ASPECT">
<aop:pointcut id="MY_CUT" expression="execution(public * com.lec06..*Impl.*(..))"/>
<aop:after-throwing pointcut-ref="MY_CUT" method="afterThrowingAdvice" throwing="exception" />
</aop:aspect>
<aop:aspect id="MY_What_Where_When" ref="MY_ASPECT">
<aop:pointcut id="MY_CUT" expression="execution(public * com.lec06..*Impl.*(..))"/>
<aop:after-returning pointcut-ref="MY_CUT" method="afterReturningAdvice" returning="res" />
</aop:aspect
<aop:aspect id="MY_What_Where_When" ref="MY_ASPECT">
<aop:pointcut id="MY_CUT" expression="execution(public * com.lec06..*Impl.*(..))"/>
<aop:around pointcut-ref="MY_CUT" method="aroundAdvice"/>
</aop:aspect>
<!-- advisor 설정을 통해 Aspect Class와 Pointcut을 한번에 지정해줄 수도 있습니다 -->
<aop:advisor advice-ref="MY_ASPECT" pointcut-ref="MY_CUT">
<aop:before method="beforeAdvice" />
</aop:advisor>
</aop:config>
<!--
<mvc:annotation-driven />
적용 대상 패키지
<context:component-scan base-package="com.lec05.rest" /> -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
@Controller
public class AOPController {
@Autowired
private AOPService aOPService;
@RequestMapping(value = "/ano_aop_ctl", method = RequestMethod.GET)
public void ctlDelete() {
System.out.println("1.___AOPController.ctlDelete() 실행");
aOPService.svcDelete();
//return new ModelAndView("test"); // / test .jsp
}
}
MultiActionController
상속에서 @Controller
+ @RequestMapping
으로 수정되었다. 또한, 의존성 주입 또한 @Autowired
어노테이션을 통해 주입되었다. ( 필드 주입 )public interface AOPService {
public void svcDelete();
}
Service 인터페이스에 대해서는 @Service
어노테이션을 생성하지 않습니다.
추후 리플랙션을 통해 인스턴스를 생성해 빈으로 등록해야하는데, 인터페이스는 인스턴스화할 수 없기 때문입니다.
@Service
public class AOPServiceImpl implements AOPService {
@Autowired
private AOPDAO AOPDAO;
@Override
public void svcDelete() {
System.out.println("2.___AOPServiceImpl.svcDelete() 실행");
AOPDAO.delete();
// 강제 에러 발생 ( Aspect - afterThrowing 을 위해 )
// throw new Exception();
}
}
@Override
는 스프링 컨테이너에게 지시하는 어노테이션이 아닌, javac
이 보게 되는 어노테이션입니다.@Repository
public class AOPDAO {
public void delete() {
System.out.println("3.____AOPDAO.delete() 실행");
}
}
@Repository
스테레오타입 어노테이션으로 Spring Context의 빈으로 등록합니다.@Component // 인스턴스 초기화
@Aspect // 공통기능 :: AnnotationAwareAspectJAutoProxyCreator가 현재 클래스를 프록시 대상으로 설정한다.
public class CommonAspect {
@Pointcut("execution(public * com.lec07..*DAO.*(..))")
public void dummyDAOCut() {}
// I/F가 없는 DAO에도 AOP가 적용된다..?
@Pointcut("execution(public * com.lec07..*Impl.*(..))")
public void dummyImplCut() {}
@Before("dummyDAOCut()")
public void beforeAdvice() {
System.out.println("\t 실행 전 :: CommonAspect.beforeAdvice()");
}
@After("dummyDAOCut()")
public void afterAdvice() {
System.out.println("\t 실행 후 무조건 :: CommonAspect.afterAdvice()");
}
@AfterThrowing(pointcut = "dummyDAOCut()", throwing="exception")
public void afterThrowingAdvice(Exception exception) {
System.out.println("\t 실행 후 에러시 :: CommonAspect.afterThrowingAdvice() :: " + exception.getMessage());
}
@AfterReturning(pointcut = "dummyDAOCut()", returning="res")
public void afterReturningAdvice(Object res) {
System.out.println("\t 실행 후 정상시 :: CommonAspect.afterReturningAdvice() :: " + res);
}
@Around("dummyDAOCut()")
public void aroundAdvice(ProceedingJoinPoint jp) {
try {
System.out.println("\t 앞-CommonAspecct.aroundAdvice()");
System.out.println("\t :: " + jp.getSignature());
jp.proceed();
System.out.println("\t 뒤-CommonAspect.aroundAdvice()");
} catch (Throwable e) {
e.printStackTrace();
}
}
}
Aspect
클래스는 해당 Aspect를 필요로 하는 Target 클래스 (= 이는 Spring Bean에 속함)에 적용되기 위해서 자신 또한 Spring Bean으로 등록되어 관리되어야 합니다.
@Component
를 통해 빈 전처리기에게 자신을 빈으로 등록하라는 지시를 내립니다.
@Aspect
를 통해 빈 후처리기에게 자신을 Aspect
로 바라보고 Pointcut
에 속하는 메서드에 자신을 적용하여 Proxy를 생성해야함을 알립니다.
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd"
>
<!-- 로그출력 : log4j -->
<mvc:resources mapping="/resources/**" location="/resources/"/>
<mvc:annotation-driven/>
<context:component-scan base-package="com.lec07.aop" />
<!-- AOP 어노테이션 기반 설정 (JDK Proxy기반 = 인터페이스 구현 객체 ( 현재 상황에서는 Service )만 AOP 적용) -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!--
CGLIB프록시 방식 :: 인터페이스 없는 AOPDAO 클래스에도 AOP 적용이 가능.
-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- ===============================[어노테이션 기반으로 동작]================================== -->
<!--
<mvc:annotation-driven />
적용 대상 패키지
<context:component-scan base-package="com.lec05.rest" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
-->
</beans>
beans ~~~>
<!-- 공통기능 들어간 클래스를 Bean으로 등록 = 인스턴스 초기화 -->
<bean id="MY_ADVICE_WHAT_공통" class="com.lec04.di.board.MyOracleConnection" />
<!-- 핵심기능 들어간 클래스를 Bean으로 등록 = 인스턴스 초기화 -->
<bean id="boardDAO" class="com.lec04.di.board.BoardDAO" />
<!-- <aop:aspectj-autoproxy /> --> // 얘는 어노테이션 기반 방식
<aop:config>
<aop:aspect id="loggingAspect" ref="MY_ADVICE_WHAT_공통"> -- 공통, WHAT, 뭘 해야하는지
-- WHERE : 핵심로직의 어디에 적용할지
<aop:pointcut id="MY_핵심로직_DAO메서드" expression="execution(public * com.lec04..*DAO.*(..))" /> -- WHERE, 핵심 로직 어디에 적용?
-- WHEN : execution(public * com.lec04..*DAO.*(..)) : 실행시 앞? 뒤? 앞뒤?언제 실행할지 )
<aop:before pointcut-ref="MY_핵심로직_DAO메서드" method="oracleConn" />
<aop:after pointcut-ref="MY_핵심로직_DAO메서드" method="oracleClose" />
</aop:aspect>
</aop:config>
</beans>
<aop:aspectj-autoproxy/>
는 <mvc:annotation-driven/>
과 마찬가지로, AOP를 어노테이션 기반으로 설정한다는 의미 ( = XML 방식 설정에 해당하지 않는다 )
<aop:config>
요소 안에 AOP 관련 설정을 넣어줄 수 있다.
xmlns
로 AOP 관련 XSD를 링크해주어야 한다.<aop:aspect>
로 Aspect ( Advice Method(s) + Pointcut(s) + 언제 Advice를 Pointcut에 적용할지를 지정 여부 )를 정의할 수 있다.
이 때, <aop:aspect>
또한 Spring Context에서 관리하는 Bean으로 등록되어야 한다.
<aop:aspect>
안에 <aop:pointcut>
을 지정하고, <aop:before>
, <aop:after>
과 같은 메서드로 pointcut 지정 및 적용할 advice 메서드 명을 지정해줄 수 있다.
이 때, pointcut을 지정할 때 AspectJ 문법을 사용한다.
<aop:aspectj-autoproxy proxy-target-class="true"/>
Spring에서 AOP를 어노테이션 기반으로 사용하겠다는 XML 설정
다음의 순서로 동작한다.
@Aspect
찾아내기 ( Component-Scan 대상인 Base-Package 부터 시작한다 )
@Aspect
를 기반으로 하는 자동 프록시 활성화
@Aspect
가 붙은 클래스를 다 찾아서, Bean
으로 등록한다. ( Target 클래스를 @Aspect로 감싸서 복제를 뜬다 )
실제 Spring Container에서 Bean을 생성 및 초기화 시, @Component / @Configuration + @Bean으로 빈으로써 생성된 클래스에 대해 @Aspect 내의 @Pointcut 지정이 되어있다면, 해당 Bean 및 Bean을 감싼 Proxy Bean 이렇게 두 개의 빈이 스프링 컨테이너에 의해 관리된다 할 수 있다.
@Aspect
: Aspect를 정의하는 클래스를 표시함.
@Pointcut
: AspectJ를 사용하여 Pointcut을 나타내서 바인딩할 메서드를 표시
@Advice
: 공통 관심 기능 메서드를 표시
@Before
: 가로챌 메서드의 이전에 실행할 Advice
@After
: 가로챌 메서드 실행 이후에 항상
실행해줄 Advice
@Around
: 가로챌 메서드 실행 이전, 이후에 실행해줄 Advice
@AfterThrowing
: 가로챌 메서드 실행 중 예외 발생 시 실행해줄 Advice
@AfterReturning
: 가로챌 메서드 정상 실행 시 실행해줄 Advice
@Before
~ @AfterReturning
어노테이션에 대해서는 해당 메서드를 실행해줄 pointcut을 꼭 지정해주어야 한다.
proxy-target-class
가 "true" 라는 것은 Proxy의 Target이 Class 인 것을 허용한다는 것, 즉 인터페이스 기반이 아닌, 클래스 상속 기반의 CGLIB
프록시를 사용하는 것.
CGLIB