1cm

자바 프로그래밍_Day_105_Spring AOP 본문

국비지원_Java/Java Programming_2

자바 프로그래밍_Day_105_Spring AOP

dev_1cm 2022. 2. 2. 05:08
반응형

2022. 01. 14

 

Spring AOP 구조

 

* AOP를 적용하면 실제 타겟 클래스, 비즈니스 로직 등만 작성하면 되고, 실제 런타임 시 aspect를 통해 부가 기능이 Weaving되는 형태로 처리가 된다고 보면 된다. -> 부가적인 기능에 대해서는 실제 작성하는 코드에서는 신경쓸 필요가 없음

 

 

 Spring AOP 특징과 구현 방식

- XML 기반의 aop 네임스페이스를 통한 AOP 구현

1. 부가기능을 제공하는 Advice클래스 작성

2. XML 설정 파일에 <aop:config>를 이용해서 Aspect를 설정(즉, 어드바이스와 포인트컷 설정)

 

- @Aspect 어노테이션 기반의 AOP 구현

1. @Aspect 어노테이션을 이용해서 부가기능을 제공하는 Aspect 클래스를 작성한다.

(이 때, Aspect 클래스는 어드바이스를 구현하는 메소드와 포인트컷을 포함한다.)

2. XML 설정 파일에 <aop:aspect-autoproxy />를 설정

 

 

- Spring AOP 구현 방식 - 태그와 어노테이션

태그 <aop:before> 메소드 실행 전에 적용되는 어드바이스를 정의
<aop:around> 메소드 호출 이전, 이후, 예외 발생 등 모든 시점에 적용 가능한 어드바이스를 정의
<aop:after> 메소드가 정상적으로 실행되는지 또는 예외를 발생시키는지 여부에 상관없는 어드바이스를 정의
<aop:after-returning> 메소드가 정상적으로 실행된 후에 적용되는 어드바이스를 정의
<aop:after-throwing> 메소드가 예외를 발생시킬 때 적용되는 어드바이스를 정의
(try-catch 블록에서 catch블록과 유사)
어노테이션 @Before("pointcut") - 타겟 객체의 메소드가 실행되기 전에 호출되는 어드바이스
- JoinPoint를 통해 파라미터 정보를 참조할 수 있다.
@Around("pointcut") - 타겟 객체의 메소드 호출 전과 후에 실행 될 코드를 구현할 어드바이스
@After("pointcut") - 타겟 객체의 메소드가 정상 종료 됐을 때와 예외가 발생했을 때 모두 호출되는 어드바이스로, 반환 값을 받을 수 없다.
@AfterReturning(Pointcut="", Returning="") - 타겟 객체의 메소드가 정상적으로 실행을 마친 후에 호출되는 어드바이스
- 리턴 값을 참조할 때는 returning 속성에 리턴 값을 저장할 변수 이름을 지정해야 된다.
@AfterThrowing(Pointcut="", throwing="") - 타겟 객체의 메소드에서 예외가 발생하면 호출되는 어드바이스
- 발생된 예외를 참조할 때는 throwing 속성에 발생한 예외를 저장할 변수이름을 지정해야 한다.

 

 

 

 

Advice 작성하기

Advice의 종류

Before Advice 타겟의 메소드가 실행되기 이전(before) 시점에 처리해야 할 필요가 있는 부가기능 정의
-> Joinpoint 앞에서 실행되는 Advice
Around Advice 타겟의 메소드가 호출되기 이전(before) 시점과 이후(after) 시점에 모두 처리해야 할 필요가 있는 부가기능 정의
-> Joinpoint 앞과 뒤에서 실행되는 Advice
After Returning Advice 타겟의 메소드가 정상적으로 실행된 이후 (after) 시점에 처리해야 할 필요가 있는 부가기능 정의
-> Joinpoint 메소드 호출이 정상적으로 종료된 뒤에 실행되는 Advice
After Throwing Advice 타겟의 메소드에 예외가 발생된 이후(after) 시점에 처리해야 할 필요가 있는 부가기능 정의
-> 예외가 던져질 때 실행되는 Advice

 

 

 

JoinPoint

JoinPoint Interface

- JoinPoint는 스프링 AOP 혹은 AspectJ에서 AOP의 부가기능을 지닌 코드가 적용되는 지점을 뜻하며, 모든 어드바이스는 org.aspectj.lang.JoinPoint 타입의 파라미터를 어드바이스 메소드의 첫 번째 매개변수로 선언해야 함.

단, Around 어드바이스는 JoinPoint의 하위 클래스인 ProcedingJoinPoint 타입의 파라미터를 필수적으로 선언해야 함

 

JoinPoint Interface 메소드

getArgs() 메소드의 매개 변수를 반환한다.
getThis() 현재 사용 중인 프록시 객체를 반환한다.
getTarget() 대상 객체를 반환한다.
getSignature() 대상 객체 메소드의 설명(메소드 명, 리턴 타입 등)을 반환한다.
toString() 대상 객체 메소드의 정보 출력

 

 

 

 

실습

 

 > CharacterTest.java

/*
 * AOP 용어 정리
 * 1. Aspect
 * 		- AOP 횡단 관심사(한 애플리케이션의 여러 부분에 공통적으로 사용하고 있는 기능)를 Aspect라는 특별한 클래스로 모듈화해서 관리한다.
 * 		- Aspect는 Advice와 PointCut을 합친 것이다.
 * 
 * 2. JoinPoint
 * 		- Advice를 적용할 수 있는 모든 지점을 JoinPoint라고 한다.
 * 		- 즉, JoinPoint는 애플리케이션 실행에 공통적인 기능을 끼워 넣을 수 있는 지점(Point)을 말한다.
 * 			(메소드 호출 지점이나 예외 발생 등)
 * 
 * 3. Advice
 * 		- Aspect가 해야 하는 작업(공통적으로 사용하고 있는 기능)과 언제 그 작업을 수행해야 하는지 정의하는 것을 AOP 용어로 Advice라고 한다.
 * 		- 스프링 AOP에서는 5 종류의 Advice를 제공한다.
 * 			Before Advice : 어드바이스 대상 메소드가 호출되기 전에 어드바이스 기능을 수행한다.
 * 			After Advice : 결과와 상관없이 어드바이스 대상 메소드가 완료된 후에 어드바이스 기능을 수행한다.
 * 			After-Returning Advice : 어드바이스 대상 메소드가 성공적으로 완료된 후에 어드바이스 기능을 수행한다.
 * 			After-Throwing Advice : 어드바이스 대상 메소드가 예외를 던진 후에 어드바이스 기능을 수행한다.
 * 			Around Advice : 어드바이스 대상 메소드 호출 전과 후에 어드바이스 기능을 수행한다.
 * 
 * 4. PointCut
 * 		- Advice가 적용될 조인 포인트의 영역을 좁히는 일을 한다.
 * 		- Advice는 Aspect가 해야하는 '작업'과 '언제' 그 작업을 수행해야 하는지 정의하는 것이라면 PointCut은 어디에 어드바이스를 적용할지 정의하는 것이다.
 * 		- PointCut을 지정하기 위해서는 AspectJ 포인트 커트 표현식을 통해서 지정해 줄 수 있다.
 * 
 */

 

 

 > root-context.xml 의 aop:config 부분

<!-- bean을 aspect로 선언하여 사용 -->
<aop:config>
    <aop:aspect ref="characterAspect">
        <aop:before
            pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
            method="beforeQuest"/>
    </aop:aspect>
</aop:config>

execution : 메소드 실행에 대한 joinpoint 시점 지정(아래 method) -> 괄호 내 -> 매개값의 (..) : 어떤 매개타입이든 상관 없다는 뜻. 한정적으로 주고 싶다면 String, int 등 타입을 지정해줄 수 있다.

 

 

AfterAdvice 생성해주기

	<!-- bean을 aspect로 선언하여 사용 -->
	<aop:config>
		<aop:aspect ref="characterAspect">
			<aop:before
				pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
				method="beforeQuest"/>
			<aop:after method="afterQuest"/>
		</aop:aspect>
	</aop:config>

afterQuest 메소드 생성

 

 

퀘스트 진행 중에 오류가 발생했는데 퀘스트 완료가 된 상황 발생 -> 상황별로 정상적으로 성공 시, 실패 시 각각의 처리가 달라야 하는 경우 -> After-Returning Advice 혹은 After-Throwing Advice를 사용해주면 된다.

 

 

 > CharacterTest.java

 

 > root-context.xml

 

 > CharacterAspect.java

 

 > Character.java

 

 

그리고 테스트 실행 시 예외가 발생하면 questFail이 실행된 것을 확인할 수 있다.

 

 

 

중복되어있는 pointcut 정리해주기 (root-context.xml)

 

<aop:config>
    <aop:aspect ref="characterAspect">
        <aop:pointcut 
            id="questPointCut"
            expression="execution(* com.kh.aop.character.Character.quest(..))"/>

        <aop:before
            pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
            method="beforeQuest"/>
        <!-- 
        <aop:after
            pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
            method="afterQuest"/>
         -->

         <aop:after-returning 
            pointcut-ref="questPointCut" 
            method="questSuccess"/>

         <aop:after-throwing
            pointcut-ref="questPointCut" 
            method="questFail"/>
    </aop:aspect>
</aop:config>

 

 

aop:around

- root-context.xml

<aop:around 
    pointcut-ref="questPointCut"
    method="questAdvice"/>

 

- CharacterAspect.java

//	ProceedingJoinPoint 매개값은 필수
	public void questAdvice(ProceedingJoinPoint jp) {
		
		try {
			// before 어드바이스에 대한 기능을 수행
			
			// 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
			jp.proceed();
			
			// after-returning 어드바이스에 대한 기능을 수행
			
		} catch (Throwable e) {
			// after-throwing 어드바이스에 대한 기능을 수행
			e.printStackTrace();
		}
	}

-> 앞에서 세 가지 어드바이스를 적용 했던걸 around로 한번에 처리해줄 수가 있다.

 

//	ProceedingJoinPoint 매개값은 필수
	public void questAdvice(ProceedingJoinPoint jp) {
		
		try {
			// before 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 준비중...");
			
			// 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
			jp.proceed();
			
			// after-returning 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 수행 완료..");
		} catch (Throwable e) {
			// after-throwing 어드바이스에 대한 기능을 수행
			System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
		}
	}

 

 

테스트가 실패하는 이유

-> questAdvice에서 반환하는 값이 없기 때문에 테스트에서 null이라고 나옴

 

//	ProceedingJoinPoint 매개값은 필수
	public String questAdvice(ProceedingJoinPoint jp) {
		String result = null;
		
		try {
			// before 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 준비중...");
			
			// 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
//			jp.proceed();
			
			// 타겟 객체의 메소드에 리턴값이 있을 경우
			result = (String) jp.proceed();
			
			// after-returning 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 수행 완료..");
		} catch (Throwable e) {
			// after-throwing 어드바이스에 대한 기능을 수행
			System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
		}
		
		return result;
	}

String으로 반환값을 만들어줘서, 타겟 메소드에서 리턴값이 있을 경우 result에 값을 담아서 반환해줄 수 있도록 해준다.-> test메소드에도 전달됨

그러면 테스트도 통과된다.

 

 

만약 파라미터로 전달되는 값을 quest에 전달해주기 전에 advice에서도 가져오고, 값 변경 후 다시 전달해주고 싶을 때

//	ProceedingJoinPoint 매개값은 필수
	public String questAdvice(ProceedingJoinPoint jp) {
		String result = null;
		String questName = "[" + (String) jp.getArgs()[0] + "]";
		
//		실제 타겟 메소드의 매개값을 가져올 수 있다.
//		System.out.println(jp.getArgs()[0]);
		
		try {
			// before 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 준비중...");
			
			// 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
//			jp.proceed();
			
			// 타겟 객체의 메소드에 리턴값이 있을 경우
//			result = (String) jp.proceed();
			
			// 타겟 객체의 메소드에 파라미터 값을 변경해서 전달해줄 경우
			result = (String) jp.proceed(new Object[] {questName});
			
			// after-returning 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 수행 완료..");
		} catch (Throwable e) {
			// after-throwing 어드바이스에 대한 기능을 수행
			System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
		}
		
		return result;
	}

 

 

그리고 Character.java 살짝 변경해서 테스트 실행시 통과하는 것을 확인할 수 있다.

 

 

CharacterTest.java에서 [강철검]과 같은 값이 있는지에 대한 테스트도 통과하는 것을 확인할 수 있다.

 

 

 

Aspect를 참고해서 Character를 감싸고 있는 객체를 만들어서(Proxy 객체) 처리하게 된다.

프록시 패턴 -> 인터페이스의 역할을 한다고 생각하면 됨

aspect의 내용을 참고해서 그 내용이 적용된 proxy 객체를 만듦-> proxy내에서 Character를 처리하고 그 값을 리턴해준다. (오른쪽 그림 참고)

 

 

 

 

 

- Spring AOP 구현을 어노테이션 기반으로 동작방법 바꿔보기

 

@Aspect // 일반적인 자바 클래스가 아니라 Aspect임을 나타낸다.
public class CharacterAspect {
	
	@Before("execution(* com.kh.aop.character.Character.quest(..))")
	// quest가 수행되기 전 찍을 내용
	public void beforeQuest(JoinPoint jp) {
		// 실제 JoinPoint, 대상이 되는 메소드의 선언부분도 가져올 수 있음 (이름)
		System.out.println(jp.getSignature().getName());
		// 타겟 객체에 대한 클래스 타입
		System.out.println(jp.getSignature().getDeclaringTypeName());
		
		// 필요로 하면 JoinPoint도 주입해줄 수 있고, 매개값도 받아 올 수 있다.
		System.out.println(jp.getArgs()[0] + "퀘스트 준비중...");
	}

 

 

 

 

매개값을 가져오는 방법 - 표현식 args(파라미터)을 사용해서 값을 가져올수도 있다. (AspectJ)

 

	@After("execution(* com.kh.aop.character.Character.quest(..)) && args(questName)")
	// afterQuest 생성
	public void afterQuest(String questName) {
//		퀘스트 수행 결과에 상관없이 수행 후에 필요한 부가 작업을 수행한다.
		System.out.println("퀘스트 수행 완료..");
	}

 

 

AspectJ 포인트커트 표현식

 * * AspectJ 포인트커트 표현식
 * 		- 스프링 AOP에서 포인트커트는 AspectJ의 포인트 커트 표현식을 이용해서 정의한다.
 * 		- 스프링 AOP에서 지원되는 AspectJ의 포인트 커트 표현식
 * 			execution([접근지정자] 리턴타입 클래스이름.메소드명(파라미터) : 메소드 실행에 대한 조인포인트를 지정한다.(메소드명에는 실제 실행되는 메소드명)
 * 				접근지정자 : public, private, .. 값을 적어준다.(생략 가능)
 * 				리턴 타입 : 메소드의 반환값을 의미한다.
 * 				클래스 이름 : 클래스의 풀 패키지명이 포함된 이름을 적어둔다.
 * 				" * " : 모든 값을 표현한다.
 * 				" .. " : 0개 이상을 의미한다.
 * 			args(파라미터) : 타겟 메소드에 전달되는 파라미터 값을 어드바이스에 전달하기 위한 파라미터를 지정한다.
 * 				(이 표현식을 통해 타겟 메소드에 전달되는 파라미터를 advice에 전달해줄 수 있다. 이 때 파라미터와 advice의 이름이 같아야 한다.)

 

 

@AfterReturning 적용(테스트 성공)

@AfterReturning(
        value = "execution(* com.kh.aop.character.Character.quest(..)) && args(questName)",
        returning = "result"
)

// 퀘스트 성공 시
public void questSuccess(String questName, String result) {
//		퀘스트 수행 완료 후에 필요한 부가 작업을 수행한다.
    System.out.println(result);
    System.out.println(questName + " 퀘스트 수행 완료..");
}

 

 

 

@AfterThrowing 적용(테스트 실패)

@AfterThrowing(
        value = "execution(* com.kh.aop.character.Character.quest(..)) && args(questName)",
        throwing = "exception"
)
// 퀘스트 실패 시
public void questFail(String questName, Exception exception) {
//		퀘스트 수행 중에 예외를 던졌을 때 필요한 부가 작업을 수행한다.
    System.out.println(exception.getMessage());
    System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
}

그리고 나서 Character.java에 예외 처리 구문을 살려서 테스트를 진행하면,

 

 

퀘스트 실패 구문을 출력해준다.

 

 

중복되는 pointcut 정리

	// 중복되는 포인트컷 정리
	@Pointcut("execution(* com.kh.aop.character.Character.quest(..)) && args(questName)")
	public void questPointCut(String questName) {
		
	}

메소드에 @Pointcut 어노테이션을 붙여줬다.

메소드의 몸체는 비어있음 -> 메소드는 마커역할만 한다고 보면 됨.

 

 

메소드 이름을 호출하듯이 받아올 매개값과 함께 적어주면 된다.

 

 

 

Around Advice 처리

@Around("execution(* com.kh.aop.character.Character.quest(..))")
//	ProceedingJoinPoint 매개값은 필수
public String questAdvice(ProceedingJoinPoint jp) {
    String result = null;
    String questName = "[" + (String) jp.getArgs()[0] + "]";

//		실제 타겟 메소드의 매개값을 가져올 수 있다.
//		System.out.println(jp.getArgs()[0]);

    try {
        // before 어드바이스에 대한 기능을 수행
        System.out.println("퀘스트 준비중...");

        // 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
//			jp.proceed();

        // 타겟 객체의 메소드에 리턴값이 있을 경우
//			result = (String) jp.proceed();

        // 타겟 객체의 메소드에 파라미터 값을 변경해서 전달해줄 경우
        result = (String) jp.proceed(new Object[] { questName });

        // after-returning 어드바이스에 대한 기능을 수행
        System.out.println("퀘스트 수행 완료..");
    } catch (Throwable e) {
        // after-throwing 어드바이스에 대한 기능을 수행 (예외 발생 시)
        System.out.println(e.getMessage());
        System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
    }

    return result;
}

 

 

 

 

 

- 실습문제

 

 

 > root-context.xml

<!-- 
    AspectJ 어노테이션을 사용한 Aspect 적용을 위한 프록시 설정 -> 어노테이션 기반으로 AOP를 적용할 수 있음
 -->
<aop:aspectj-autoproxy proxy-target-class="true" />

 

 

 > CharacterTest.java

@Test
void attackTest() {
    System.out.println(character.getWeapon().attack());
}

 

 

 > CharacterAspect.java

// 실습 문제
// Swoard, Bow의 attack 메소드 실행 시
// @Around 어드바이스를 사용하여 코드작성 하세요.
// 1. attack 메소드 정상 동작
//	공격을 준비 중 입니다.
//	"검을 휘두른다." or "활을 쏜다. 슉!" 출력
//	공격을 성공했습니다.
// 2. attack 메소드 실행중에 예외 발생 시
//	공격을 준비 중 입니다.
//	에러가 발생했습니다.

 

1차적으로 CharacterAspect.java

@Around("execution(* com.kh.aop.weapon.Sword.attack())")
public String attackAdvice(ProceedingJoinPoint jp) {
    String result = null;

    try {
        System.out.println("공격을 준비중 입니다.");

        result = (String) jp.proceed();

        System.out.println(result);

        System.out.println("공격을 성공했습니다.");
    } catch (Throwable e) {

        System.out.println("에러가 발생했습니다.");
    }

    return result;
}

 

CharacterTest.java

@Test
void attackTest() {
    assertThat(character.getWeapon().attack()).isNotNull();
}

 

 

sword 적용은 끝났고, bow도 같이 볼 수 있도록

root-context에 bow bean도 추가해줬고,

@Around에서

weapon 패키지 내에 존재하는 모든 클래스에 적용할 수 있도록 *을 붙여줬다.

-> 모든 클래스의 attack 메소드를 advice의 포인트 컷으로 설정 -> attack 메소드 실행 시 advice가 weaving된다.

 

 

 

 

 

실습 전체 코드

 > CharacterAspect.java

package com.kh.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect // 일반적인 자바 클래스가 아니라 Aspect임을 나타낸다.
public class CharacterAspect {
	// 포인트 커트를 한 번만 정의하고 필요할 때마다 참조할 수 있는 @Pointcut 어노테이션을 제공한다.
	// 중복되는 포인트컷 정리
	@Pointcut("execution(* com.kh.aop.character.Character.quest(..)) && args(questName)")
	public void questPointCut(String questName) {
	}
	
//	@Before("execution(* com.kh.aop.character.Character.quest(..))")
	// quest가 수행되기 전 찍을 내용
	public void beforeQuest(JoinPoint jp) {
		// 실제 JoinPoint, 대상이 되는 메소드의 선언부분도 가져올 수 있음 (이름)
		System.out.println(jp.getSignature().getName());
		// 타겟 객체에 대한 클래스 타입
		System.out.println(jp.getSignature().getDeclaringTypeName());
		
		// 필요로 하면 JoinPoint도 주입해줄 수 있고, 매개값도 받아 올 수 있다.
		System.out.println(jp.getArgs()[0] + " 퀘스트 준비중...");
	}
	
//	@After("execution(* com.kh.aop.character.Character.quest(..)) && args(questName)")
	// afterQuest 생성
	public void afterQuest(String questName) {
//		퀘스트 수행 결과에 상관없이 수행 후에 필요한 부가 작업을 수행한다.
		System.out.println(" 퀘스트 수행 완료..");
	}
	
//	@AfterReturning(
//			value = "questPointCut(questName)",
//			returning = "result"
//	)
	// 퀘스트 성공 시
	public void questSuccess(String questName, String result) {
//		퀘스트 수행 완료 후에 필요한 부가 작업을 수행한다.
		System.out.println(result);
		System.out.println(questName + " 퀘스트 수행 완료..");
	}
	
//	@AfterThrowing(
//			value = "questPointCut(questName)",
//			throwing = "exception"
//	)
	// 퀘스트 실패 시
	public void questFail(String questName, Exception exception) {
//		퀘스트 수행 중에 예외를 던졌을 때 필요한 부가 작업을 수행한다.
		System.out.println(exception.getMessage());
		System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
	}
	
	
	@Around("execution(* com.kh.aop.character.Character.quest(..))")
//	ProceedingJoinPoint 매개값은 필수
	public String questAdvice(ProceedingJoinPoint jp) {
		String result = null;
		String questName = "[" + (String) jp.getArgs()[0] + "]";
		
//		실제 타겟 메소드의 매개값을 가져올 수 있다.
//		System.out.println(jp.getArgs()[0]);
		
		try {
			// before 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 준비중...");
			
			// 타겟 객체(Character)의 메소드(quest)를 실행 시킨다.
//			jp.proceed();
			
			// 타겟 객체의 메소드에 리턴값이 있을 경우
//			result = (String) jp.proceed();
			
			// 타겟 객체의 메소드에 파라미터 값을 변경해서 전달해줄 경우
			result = (String) jp.proceed(new Object[] { questName });
			
			// after-returning 어드바이스에 대한 기능을 수행
			System.out.println("퀘스트 수행 완료..");
		} catch (Throwable e) {
			// after-throwing 어드바이스에 대한 기능을 수행 (예외 발생 시)
			System.out.println(e.getMessage());
			System.out.println("에러가 발생하였습니다. 다시 시도해 주세요.");
		}
		
		return result;
	}
	
	
	
	// 실습 문제
	// Swoard, Bow의 attack 메소드 실행 시
	// @Around 어드바이스를 사용하여 코드작성 하세요.
	// 1. attack 메소드 정상 동작
	//	공격을 준비 중 입니다.
	//	"검을 휘두른다." or "활을 쏜다. 슉!" 출력
	//	공격을 성공했습니다.
	// 2. attack 메소드 실행중에 예외 발생 시
	//	공격을 준비 중 입니다.
	//	에러가 발생했습니다.
	
	@Around("execution(* com.kh.aop.weapon.*.attack())")
	public String attackAdvice(ProceedingJoinPoint jp) {
		String result = null;
		
		try {
			System.out.println("공격을 준비중 입니다.");
			
			result = (String) jp.proceed();
			
			System.out.println(result);
			
			System.out.println("공격을 성공했습니다.");
		} catch (Throwable e) {
			
			System.out.println("에러가 발생했습니다.");
		}
		
		return result;
	}
	
	
}

 

 

 

 

 > Character.java

package com.kh.aop.character;

import com.kh.aop.weapon.Weapon;

import lombok.Data;

@Data
public class Character {
	private String name;
	
	private int level;
	
	private Weapon weapon;
 
	public String quest(String questName) throws Exception {
		
		if(true) {
			throw new Exception("Quest 처리 중 예외 발생");
		}
		
		System.out.println(questName + " 퀘스트 진행 중..");

		return questName + " 퀘스트 진행 중";
	}
}

 

 

 > Bow.java

package com.kh.aop.weapon;

import lombok.Data;

@Data
public class Bow implements Weapon {

	private String name;
	
	@Override
	public String attack() throws Exception {
		
//		if (true) {
//		throw new Exception();
//		}
		
		return "활을 쏜다. 슉!";
	}
}

 

 

 > Sword.java

package com.kh.aop.weapon;

import lombok.Data;

@Data
public class Sword implements Weapon {
	
	private String name;

	@Override
	public String attack() throws Exception {
		
//		if (true) {
//			throw new Exception();
//		}
		
		return "검을 휘두른다.";
	}

}

 

 

> Weapon.java

package com.kh.aop.weapon;

public interface Weapon {
	// 무기마다 공격 방식이 다르기 때문에 어떻게 공격할 것인지에 대한 설정 - 추상 메소드
	public String attack() throws Exception;
}

 

 

> root-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	<!-- 
		AspectJ 어노테이션을 사용한 Aspect 적용을 위한 프록시 설정 -> 어노테이션 기반으로 AOP를 적용할 수 있음
	 -->
	<aop:aspectj-autoproxy proxy-target-class="true" />
	
	<bean id="character" class="com.kh.aop.character.Character" p:name="동동이" p:level="100" p:weapon-ref="bow"/>
	
	<bean id="sword" class="com.kh.aop.weapon.Sword" p:name="강철검"/>
	
	<bean id="bow" class="com.kh.aop.weapon.Bow" p:name="최종병기 활"/>
	
	<!-- 
		Aspect로 지정할 객체는 반드시 빈으로 선언 되어야 한다.
		(xml, annotation으로 설정 시에도 모두 빈으로 선언이 되어야 한다.)
	 -->
	<bean id="characterAspect" class="com.kh.aop.aspect.CharacterAspect"/>
	
	<!-- bean을 aspect로 선언하여 사용 -->
	<!-- 	
	<aop:config>
		<aop:aspect ref="characterAspect">
			<aop:pointcut 
				id="questPointCut"
				expression="execution(* com.kh.aop.character.Character.quest(..))"/>
			<aop:before
				pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
				method="beforeQuest"/>

			<aop:after
				pointcut="execution(* com.kh.aop.character.Character.quest(..))" 
				method="afterQuest"/>
			 
			 
			<aop:after-returning 
			 	pointcut-ref="questPointCut" 
			 	method="questSuccess"/>
			 
		    <aop:after-throwing
			 	pointcut-ref="questPointCut" 
			 	method="questFail"/>
			<aop:around 
				pointcut-ref="questPointCut"
				method="questAdvice"/>
		</aop:aspect>
	</aop:config>
	-->

</beans>

 

 

 > CharacterTest.java

package com.kh.aop.character;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"classpath:root-context.xml"})
class CharacterTest {
	/*
	 * AOP 용어 정리
	 * 1. Aspect
	 * 		- AOP 횡단 관심사(한 애플리케이션의 여러 부분에 공통적으로 사용하고 있는 기능)를 Aspect라는 특별한 클래스로 모듈화해서 관리한다.
	 * 		- Aspect는 Advice와 PointCut을 합친 것이다.
	 * 
	 * 2. JoinPoint
	 * 		- Advice를 적용할 수 있는 모든 지점을 JoinPoint라고 한다.
	 * 		- 즉, JoinPoint는 애플리케이션 실행에 공통적인 기능을 끼워 넣을 수 있는 지점(Point)을 말한다.
	 * 			(메소드 호출 지점이나 예외 발생 등)
	 * 
	 * 3. Advice
	 * 		- Aspect가 해야 하는 작업(공통적으로 사용하고 있는 기능)과 언제 그 작업을 수행해야 하는지 정의하는 것을 AOP 용어로 Advice라고 한다.
	 * 		- 스프링 AOP에서는 5 종류의 Advice를 제공한다.
	 * 			Before Advice : 어드바이스 대상 메소드가 호출되기 전에 어드바이스 기능을 수행한다.
	 * 			After Advice : 결과와 상관없이 어드바이스 대상 메소드가 완료된 후에 어드바이스 기능을 수행한다.
	 * 			After-Returning Advice : 어드바이스 대상 메소드가 성공적으로 완료된 후에 어드바이스 기능을 수행한다.
	 * 			After-Throwing Advice : 어드바이스 대상 메소드가 예외를 던진 후에 어드바이스 기능을 수행한다.
	 * 			Around Advice : 어드바이스 대상 메소드 호출 전과 후에 어드바이스 기능을 수행한다.
	 * 
	 * 4. PointCut
	 * 		- Advice가 적용될 조인 포인트의 영역을 좁히는 일을 한다.
	 * 		- Advice는 Aspect가 해야하는 '작업'과 '언제' 그 작업을 수행해야 하는지 정의하는 것이라면 PointCut은 어디에 어드바이스를 적용할지 정의하는 것이다.
	 * 		- PointCut을 지정하기 위해서는 AspectJ 포인트 커트 표현식을 통해서 지정해 줄 수 있다.
	 * 
	 * 
	 * * AspectJ 포인트커트 표현식
	 * 		- 스프링 AOP에서 포인트커트는 AspectJ의 포인트 커트 표현식을 이용해서 정의한다.
	 * 		- 스프링 AOP에서 지원되는 AspectJ의 포인트 커트 표현식
	 * 			execution([접근지정자] 리턴타입 클래스이름.메소드명(파라미터) : 메소드 실행에 대한 조인포인트를 지정한다.(메소드명에는 실제 실행되는 메소드명)
	 * 				접근지정자 : public, private, .. 값을 적어준다.(생략 가능)
	 * 				리턴 타입 : 메소드의 반환값을 의미한다.
	 * 				클래스 이름 : 클래스의 풀 패키지명이 포함된 이름을 적어둔다.
	 * 				" * " : 모든 값을 표현한다.
	 * 				" .. " : 0개 이상을 의미한다.
	 * 			args(파라미터) : 타겟 메소드에 전달되는 파라미터 값을 어드바이스에 전달하기 위한 파라미터를 지정한다.
	 * 				(이 표현식을 통해 타겟 메소드에 전달되는 파라미터를 advice에 전달해줄 수 있다. 이 때 파라미터와 advice의 이름이 같아야 한다.)
	 * 
	 */
	@Autowired
	private Character character;
	
	@Test
	void test() {
	}
	
	@Test
	void create() {
		System.out.println(character);
		
		assertThat(character).isNotNull();
		assertThat(character.getWeapon()).isNotNull();
	}
	
	@Test
	void questTest() throws Exception {
		
		assertThat(character.quest("강철검")).contains("[강철검]");
	}
	
	@Test
	void attackTest() throws Exception {
		
		assertThat(character.getWeapon().attack()).isNotNull();
	}
}
반응형
Comments