[ KOSTA 교육 39일차 ] OOP와 AOP | Advice, JoinPoint, Pointcut, Aspect, @Before, @After, @AfterThrowing, @AfterReturning, @Around, Weaving, Proxy | AspectJ | JDK 동적 프록시 vs CGLIB 프록시 | AOP 설정 방법 (XML, 어노테이션 )

junjun·2024년 6월 20일
1

KOSTA

목록 보기
37/48

목차

  1. OOP vs AOP
  2. AOP 핵심 개념
  3. AOP 설정 방식
  4. AspectJ 문법
  5. AOP 구현 방법 ( JDK Dynamic Proxy vs CGLIB.jar )

OOP

  • Object Oriented Programming
  • 객관적 사실에 의해 실제로 표현할 수 있는 대상을 메서드와 변수로 표현하려는 프로그래밍 기법입니다. 이렇게 도출된 대상을 객체라고 하며, 이런 객체에 대해 책임을 부여하고 다른 객체와 메시지를 기반으로 통신하며 서비스를 구성할 수 있습니다.

AOP

  • Aspect Oriented Programming

  • 관점 지향 프로그래밍

  • 결제 / 회원인증과 같은 비즈니스 로직 (Core Concerns)에 대해 공통적으로 들어가야하는 모듈 (Cross-Cutting Concerns) 이 있다.

    • DB Connection (공통)
    • 인증 (공통)
    • DB Close (공통)
  • 단순히 비즈니스 로직 하나를 구현하기 위해 DB Connection, 인증, DB Close 등의 부가 작업을 수행한 코드를 앞 뒤로 붙여줘야 합니다.

  • 이걸 Util로 빼더라도 개발자가 해당 메서드 호출 자체를 해야하는 것은 있고, 사람이기에 호출을 실수할 수도 있습니다.

  • AOP를 도입해서, 내가 원하는 것(비즈니스 로직)에만 집중하고, 프레임워크에게 공통 부분을 맡길 수 있습니다.

    • 어떤 메서드에 앞 뒤로 어떤 작업을 시키기 위해서는 '특정 설정'이 필요합니다.
    • 이러한 설정은 XML 기반으로 할 수도 있고, 어노테이션 기반으로 설정할 수도 있습니다.
      • XML 장점 / 단점
        • 코드에 어노테이션이 없어도, AOP가 돌아갑니다. ( 중앙관리형 설정 )
      • 어노테이션 장점 / 단점
        • 각자의 코드에 어노테이션이 붙어있음.
        • 중앙관리형 설정이 아닌, 개인주의형 설정
        • 각자만의 설정이 필요할 때는 그렇게 해도, 공통 부분에 대해서 공통 설정으로 빼는게 낫다. => AOP
    • Spring Web MVC에서는 어노테이션 기반이 좋습니다.
      • 비즈니스 로직 같은 경우는 변화가 잦고, 메서드가 계속 바뀌기 때문입니다.
      • 이것을 XML기반으로 설정하면 매우 비효율적입니다.
      • 이건 각자 개발자들이 어노테이션화하여 개발자들이 편안하게 사용할 수 있도록 합니다.
    • AOP는 모든 개발자가 공유해야하는 설정을 해야할 때 도입하기 적합합니다.
      - 시스템의 전반적인 사항을 XML로 빼는 것입니다.
      • ex) 로깅, 트랜잭션 관리, DB연결
      • 한 회사 내에서 모두가 공통으로 하는 부분은 설정으로 빼는 것이 유리합니다.
      • 중앙 관리의 단점이라면 설정 (중앙 관리)를 잘못하면 해당 설정을 공유하는 개발자들의 코드 전부가 산으로 갈 수도 있다는 것입니다.
      • 설정을 관리하는 사람을 보통 아키 ( 아키텍처 )라 부릅니다.
      • 아키텍처는 장비뿐이 아니라, 시스템 설정하는 것을 의미하기도 합니다.
      • 공통 설정의 예시로 로깅, 트랜잭션 관리, DB 연결이 있습니다.
      • 설정이 꽃을 발하는 부분이 AOP, 관점 지향 프로그래밍입니다. ( = 개발자가 바라보는 관점만 코딩 )
      • 개개인이 해야할 작업은 어노테이션으로 뺍니다.

AOP

  • 공통의 관심사항 (횡단 관심사)을 (= DB Conn, DB Close) 분리하여,
    비즈니스 로직에서의 의존관계의 복잡성이나 코드 중복을 제거할 수 있습니다.

  • 비즈니스 로직의 앞 뒤에 해야할 일들을 공통 Function으로 들어낼 수 있습니다.

  • 비즈니스 로직에 대해서는 개발자 나름대로 설정하는 것이 편하고, 이를 어노테이션화하여 분리할 수 있습니다.

  • Util로 뺐던 공통관심사의 호출 책임을 Spring Container에게 맡기고, 개발자는 비즈니스 로직에만 집중할 수 있게 해준다. ( IoC의 예시 중 하나 )

AOP Weaving

  • 공통 기능 로직 ( Aspect )를 Target의 Pointcut에 적용하여 Proxy를 만드는 절차를 의미합니다.
  • 직관적으로 말하면, 나의 비즈니스 로직 앞 뒤에 공통 작업을 추가하는 것입니다.

그림으로 이해하는 Core Concern vs Cross-Cutting Concerns

  • 핵심 관심 모듈 ( 비즈니스 로직 )을 Core Concern이라 하고, 해당 로직을 포함하는 클래스 단위를 Target, 클래스 내의 메서드를 JoinPoint, 이러한 메서드들에 조건을 걸어 추출한 메서드 집합 단위를 Pointcut이라 합니다.

  • 횡단 관심 모듈 ( 공통 관심 사항 )을 Cross-Cutting Concern이라 하고, 이러한 로직의 메서드들을 Advice 라 합니다. AspectAdvicePointcut에 언제 (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은 영어로 '(천 따위를) 짜다' 라는 의미를 가지고 있습니다.
    즉, 공통 관심 모듈인 AspectPointcut에 적용하는 절차를 의미합니다.
    이 때, Target 클래스는 해당 횡단 모듈이 적용된 Proxy로 새로 생성되게 됩니다.

    • 즉, WeavingAspect를 Target의 Pointcut에 적용하여, Proxy를 만드는 과정이라 할 수 있습니다.
    • 좀 더 간단하게만 이야기하면, 공통을 핵심에 적용하는 절차입니다.
  • AOP는 기존 OOP에서 공통 관심사항 관련 중복 코드가 발생하는 상황에 대해 스프링 컨테이너에게 해당 작업을 하도록 위임함으로써 개발 편의성과 중복을 제거하여 유지보수성에 유리한 개발을 할 수 있도록 도와줍니다.

    • 직관적인 표현으로, AOP의 목적은 '공통부'는 Spring Container에게 맡기고, 개발자는 핵심 비즈니스 로직에만 신경쓰게 하는 것입니다.

AOP 위빙의 결과 : Proxy

  • Proxy는, 대리자 혹은 위임받는 대상을 의미합니다.

  • ProxyTarget을 기반으로 새로 생성되는 객체이며 TargetJoinPoint 메서드 호출 시, 중간에 해당 메서드를 가로채서 실행을 대신하고 ( 이 때 공통 메서드 실행 ) 결과를 Target에게 돌려줍니다.

  • Proxy를 통해 Spring Container가 Target 클래스를 제어할 수 있는 것입니다.

  • 사용자 (개발자)는 Target 클래스가 동작하는 줄 알지만, 사실은 Aspect가 적용된 Proxy가 동작하고 있는 것입니다.

[ 한번만 더 AOP 관련 개념 정리 ]
- 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 객체를 생성하는 절차 
[ Spring AOP란? ]
  • 공통부는 컨테이너에게 맡기고, 개발자는 핵심 비즈니스 로직에 대해서만 집중하게 하는 프로그래밍 기법입니다.

Spring AOP 설정 방법

  1. POJO Class를 이용한 AOP 설정
  2. XML 스키마를 이용한 AOP 설정
  3. 어노테이션(Annotation)을 이용한 AOP 구현

XML 스키마를 이용한 AOP 설정

  • AOP 설정 정보임을 나타내는 태그 : <aop:config>
  • Aspect 설정 : <aop:aspect>
  • Pointcut 설정 : <aop:pointcut>
  • Advice 설정 : <aop:around>
    • 메서드 실행 전 : <aop:before>
    • 정상적으로 실행된 후 : <aop:after-reurning>
    • 예외를 발생시킬 때 : <aop:after-throwing>
    • (예외 발생에 상관없이) 종료된 경우 : <aop:after>
    • 모든 시점에 적용 가능 : <aop:around>

XML 스키마를 이용한 AOP 설정

<!-- 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라 합니다.

AspectJ 실습

  1. 기본적으로 AspectJ 문법 구문은 다음과 같습니다.

접근자 리턴타입 패키지.클래스.메소드명(매개변수)

  • 포인트컷을 잡아내는 데에 사용하는 문법이기에 '메소드'를 정의하는 형식과 매우 비슷합니다.

  • 조금 다른 건 메소드 선언 부분이 패키지, 클래스까지 포함한다는 점입니다.

  • 이러한 정의문을 통해 Aspect를 적용할 메소드의 대상(Pointcut)을 잡아낼 수 있습니다.

  1. 다음 두 메서드를 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로 끝나는 이름의 매개변수 타입
  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)
public 	List	com.lec06.aop.boardDAO.select()

=> public	*	com.lec06.aop.boardDAO.*(..)
  • 접근 제어자는 public, 반환 타입은 모든 타입(*), 패키지.클래스 명은 com.lec06.aop.boardDAO, 메서드 명은 모든 것을 포함(*) 및 존재해야 함, 매개변수 타입은 0개 ~ N개를 나타내는 .. 표기 ( 없어도 되거나 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 *		com.lec06.aop.boardDAO.*(*,..)
  • 접근 제어자는 public, 반환 타입은 모든 타입 (*), 패키지.클래스 명은 com.lec06.aop.boardDAO, 메서드 명은 모든 것을 포함 (*)하며 존재해야 함. 매개변수 타입은 1개는 타입에 상관없이 꼭 있어야 하고, 나머지는 있거나 없어도 됨. 즉 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개 이상 있거나 없거나 상관없음.
  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	void	com.kosta.test.DAOCallTest.myprint()

=> public	*	com.*.*.*DAO*.*(..)
  • 접근 제어자는 public, 반환 타입은 모든 타입(*), 패키지.클래스 명은 com으로 시작 및 두 depth는 무엇이 되었든 꼭 존재해야함. (com.*.*) 메서드 이름은 DAO를 포함 앞 뒤에 무언가 포함된 형태의 메서드 (*DAO*). 메서드 명은 모든 것을 포함 (*)하며 존재해야 함. 매개변수 타입은 0개 이상 있거나 없거나 상관없음.
  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	void	com.lec01.sample.DAOCallTest.myprint()
public	void	com.kosta.CallTest.myprint()

=> public	*	com..*.*(..)
  • 접근 제어자는 public, 반환 타입은 모든 타입(*), 패키지는 com으로 시작하며 depth가 상관이 없음. ( com.. ) 클래스 명은 아무거나 상관이 없음 (*) 메서드 명 또한 아무거나 상관이 없음 (*). 매개변수는 0개 혹은 그 이상. (..)
  1. 그 외 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이 지정된다.

AOP 구현 방식

  • 두가지 방법이 있다.
  1. JDK 동적 프록시를 이용

  2. CGLIB 프록시를 이용

  • 두 방법의 가장 큰 차이는 인터페이스 기반인지 아닌지의 차이이다.

AOP by JDK Dynamic Proxy

  • 인터페이스가 있다.

  • 조립기 ( Spring Container )는 인터페이스에만 의존한다.

  • 인터페이스가 앞단에 존재하는 클래스에 대해서만 AOP를 적용할 수 있다.

  • 핵심 기능을 구현한 클래스 (Target) 의 I/F를 상속받아 기능을 확장한 프록시를 생성하는 방식으로 동작한다.

AOP by CGLIB Proxy

  • 인터페이스가 없어도 클래스 객체에 AOP를 적용시킬 수 있는 방식이다.

  • 핵심 기능을 구현한 클래스 (Target)자체를 상속받아 기능을 확장한 자식 클래스 프록시를 생성하는 방식으로 동작한다.

JDK Dynamic Proxy 동작 방식

  • java.lang.reflect.Proxy 클래스를 사용한다. ( JDK 기반 )

  • reflect의 뜻?

    • 반영. Java Reflection API 를 통해 런타임 때 JVM Method Area의 Class 메타 데이터를 참조하여 해당 클래스에 대한 작업 ( 인스턴스 생성 및 조작 등 )을 수행할 수 있다.
    • 런타임 때 I/F를 구현하는 프록시 객체를 "동적으로 생성"한다.
  • 프록시 객체는 대상 객체(Target)와 동일한 인터페이스를 구현한다. 메서드 호출을 가로채어 추가기능(Advice)을 수행한다.

  • 빈을 주입받을 때, 실제 빈 대신 프록시 객체를 주입받는다.

    • 프록시가 Impl(Target)의 메서드 호출을 가로채어 Advice를 적용한다.
    • 프록시는 사라지고, 제어권을 타겟에게 돌려준다.
    • 타겟 입장에서는 잠시 기절해있다가 눈떴더니 집인 상황이라 표현하심.

1) AOP 설정

  • 애스팩트(Aspect : what, where, when : www) /
    어드바이스 (Advice : what) + 포인트컷 (where) + when ( before, after, around )
@Configuration
@EnableAspectJAutoProxy // AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기(Bean PostProcessor)가 이를 관리함.
public class AppConfig {


}

2) 빈 후처리기 등록 ( @EnableAspectJAutoProxy )

  • 메서드(Pointcut으로 특정된 JoinPoint)가 불려지는 시점에 프록시 복제가 시작됨.
  • 빈을 스캔하여 AOP 관련 설정을 적용한다.
  • cf. @Autowired => 빈 전처리기

: 스프링 컨텍스트는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 통해
빈을 스캔하여 AOP 관련 설정을 적용합니다.

3) 프록시 생성

  • 빈 후처리기는 빈이 초기화될 때, 인터페이스 기반 프록시를 생성합니다. (JDK 동적 프록시)

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());
		    }
		}
MyService.class.getClassLoader()를 통해 해당하는 클래스를 로드하고, 이를 상속받은 Proxy 클래스를 실제 Heap 메모리에 올림. 이를 통해 Method를 실행한다.

AOP by CGLIB lib

  • 바이트코드를 조작하여 런타임에 새로운 클래스를 생성할 수 있게 해줌.

  • 인터페이스를 구현하지 않은 클래스에도 프록시를 적용한다. ( 타겟 클래스를 상속받아 동작한다 )

  • 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를 사용하여 클래스 기반 프록시를 생성한다.

  • a) 빈이 초기화 후, AnnotationAwareAspectJAutoProxyCreator는 해당 빈이 AOP 대상인지 검사
  • b) 프록시 생성 : 대상 빈이 AOP 대상인 경우, GLIB를 사용하여 클래스 기반 프록시 객체를 생성
★★★★★  CGLIB는 대상 클래스를 상속받아 (서브클래스를 동적으로 생성) ★★★★★★
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(MyService.class);      ---★★★★★★★★★--- (부모Service클래스 상속받아 차일드클래스 동적 생성)
		enhancer.setCallback(new MyMethodInterceptor(target));   ------- 메서드 호출을 가로채고 어드바이스 로직 적용
		MyService proxy = (MyService) enhancer.create();

4) MethodInterceptor 구현 ( = JDK 동적 프록시에서 InvocationHandler의 역할 )

  • CGLIB 프록시 객체는 메서드 호출을 가로채기 위해 MethodInterceptor을 사용한다.
  • intercept 메서드를 구현하여 실제 대상 객체의 메서드를 호출하기 전에 어드바이스 로직을 수행한다.
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());
		    }
		}	  	 

AOP 설정 방식 : XML 설정 방식 vs 어노테이션 기반 방식 ( 예시 -> 이론 순서로 설명 )

1. XML 기반 AOP 설정 적용 코드 예시

lec06 - AOP : XML 설정, lec07 - AOP : Annotation 설정입니다.

lec06 - AOPController ( Spring Context & MVC 또한 XML 설정 )
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
	}
}
lec06 - AOPService ( I/F를 사용하니, JDK 동적 프록시 기반으로 AOP를 내부 구성하겠다 생각하시면 됩니다. )
public interface AOPService {

	public void svcDelete() throws Exception;
	
}
lec06 - AOPServiceImpl
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();
	}
}
lec06 - AOPDAO
public class AOPDAO {

	public void delete() {
		System.out.println("3.____AOPDAO.delete() 실행");
	}
	
}
lec06 - CommonAspect
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를 설정해줍니다.

lec06-servlet-context.xml
  • 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>

2. Annotation 기반 AOP 설정 적용 예시

Annotation 기반 AOPController ( Spring Context & MVC 또한 Annotation 기반 설정 )

@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 어노테이션을 통해 주입되었다. ( 필드 주입 )

Annotation 기반 AOPService

public interface AOPService {

	public void svcDelete();
	
}
  • Service 인터페이스에 대해서는 @Service 어노테이션을 생성하지 않습니다.

  • 추후 리플랙션을 통해 인스턴스를 생성해 빈으로 등록해야하는데, 인터페이스는 인스턴스화할 수 없기 때문입니다.

Annotation 기반 AOPServiceImpl

@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이 보게 되는 어노테이션입니다.

Annotation 기반 AOPDAO

@Repository
public class AOPDAO {

	public void delete() {
		System.out.println("3.____AOPDAO.delete() 실행");
	}
	
}
  • DAO를 @Repository 스테레오타입 어노테이션으로 Spring Context의 빈으로 등록합니다.
Annotation 기반 CommonAspect
@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를 생성해야함을 알립니다.

lec07-servlet-context.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/"/>

<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>

3. XML 기반 AOP 설정

AOP를 XML 기반으로 설정하는 예시

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 문법을 사용한다.

4. AOP를 어노테이션 기반으로 설정

<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 프록시를 사용하는 것.

    • 메이븐을 사용한다면 사전에 pom.xml에 CGLIB.jar 라이브러리가 의존성으로 명시되어있어야 하낟.
  • CGLIB
    • 코드 생성 라이브러리 ( Code Generation LIBrary )
    • 자바 클래스의 프록시를 생성해주는 기능 제공
    • 클래스의 바이트 코드를 조작해서 새로운 클래스를 생성할 수 있게 해주는 라이브러리

0개의 댓글