본문 바로가기
Java . Spring . Web . SQL

[AOP] JoinPoint 와 바인드 변수

by heidish 2020. 10. 18.
반응형

 

 

 

 

 

 

 

횡단 관심에 해당하는 어드바이스 메소드를 의미있게 구현하기 위해서는

클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다.

 

 

예를들어 After Throwing 어드바이스 메소드를 구현할 때,

예외발생 메소드 이름이 무엇이며, 해당 메소드가 속한 클래스, 패키지 정보를 무엇인지 등등을 알아야

정확한 예외처리 로직을 구현할 수 있게 된다.

 

스프링에서는 이런 다양한 정보들을 이용할 수 있도록 하기 위해서

JoinPoing 인터페이스를 제공한다.

 

 

 


 

 

스프링 JoinPoint 에서 제공하는 메소드들.

 

 

Signature getSignature()

-  클라이언트가 호출한 메소드의 시그니처 (리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체를 리턴

 

Object getTarget()

-  클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체를 리턴

    ( 해당 클래스 객체를 리턴)

 

Object[] getArgs()

-  클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴

 

 

 

 


 

 

이 전 글에서 Around 어드바이스 메소드를 구현할 때 사용한 ProceedingJoinPoint 인터페이스는

JoinPoint를 상속받은 클래스이다.

 

 

따라서 JoinPoint가 갖고있는 모든 메소드를 지원하며, 거기에 추가적으로 proceed() 메소드가 더해진셈이다.

heidish.tistory.com/79?category=889082

 

[AOP] Advice(어드바이스) 동작 시점

Advice (어드바이스) - before, after, after-returning, after-throwing, around 5가지의 동작 시점이 존재 어드바이스는 횡단 관심에 해당하는 공통 기능의 코드를 의미하며, 독립된 클래스의 메소드로 작성된다.

heidish.tistory.com

( 위 링크는 이전의 글.... )

 

 

 

주의할점은,

Before, After Returning, After Throwing, After 어드바이스는 JoinPoint 를 매개변수로 사용하고,

Around 어드바이스에서만 ProceedingJoinPoint 를 매개변수로 사용한다는 점!

 

이는 Around 어드바이스에서만 proceed() 메소드가 필요하기 때문이다.

 

 

 


 

 

그리고 스프링의 JoinPoint 가 제공하는 메소드들 중에서,

getSignature() 메소드가 리턴하는 Signature 객체를 사용하면 호출되는 메소드에 대한 다양한 정보를 얻을 수 있는데

Signature 객체가 제공하는 메소드들 은 다음과 같다.

 

 

 

String getName()

-  클라이언트가 호출한 메소드 이름 리턴

 

String toLongString()

-  클라이언트가 호출한 메소드의 리턴타입, 이름, 매개변수패키지 경로까지 포함해서 리턴

 

String toShortString()

-  클라이언트가 호출한 메소드의 시그니처를 축약한 문자열로 리턴

 

 

 


 

 

( LogAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.JoinPoint;

public class LogAdvice {

	public LogAdvice() {
	}
	
	public void printLog(JoinPoint jp) {
		System.out.println("[공통로그] 비즈니스 로직 수행 전 동작");
	}
}

JoinPoint 객체를 사용하기 위해서는 메소드 매개변수로 JoinPoint를 선언해주면 되는데,

 

그러면 클라이언트가 비즈니스 메소드를 호출할 때,

스프링 컨테이너가 JoinPoint 객체를 생성하고,

메소드 호출과 관련된 모든 정보를 이 객체에 저장해서,

어드바이스 메소드를 호출할 때 인자로 넘겨준다.

 

 

 

 

JoinPoint 들은 어드바이스 종류가 뭐냐에 따라서 사용법이 조금씩 다른데,

각 어드바이스에서 JoinPoint를 어떻게 사용하는지는 아래의 예제들에서 구체적으로 보자.

 

 

 

 

 


 

1.  Before 어드바이스

 

 

 

 

( BeforeAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.JoinPoint;

public class BeforeAdvice {

	public BeforeAdvice() {
	}
	
	public void beforeLog(JoinPoint jp) {
		String method = jp.getSignature().getName();
		Object[] args = jp.getArgs();
		
		System.out.println("[사전 처리] " + method + "() 메소드 ARGS 정보 : " + args[0].toString());
	}
}

 

 

( applicationContext.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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.heidi.biz"></context:component-scan>
	
	<bean id="before" class="com.heidi.biz.board.common.BeforeAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* com.heidi.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref="before">
			<aop:before method="beforeLog" pointcut-ref="allPointcut" />
		</aop:aspect>
	</aop:config>

</beans>

 

 

( UserServiceClient.java )

package com.heidi.biz.user;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class UserServiceClient {

	public static void main(String[] args) {
		// 1. Spring 컨테이너를 구동한다.
		AbstractApplicationContext container = new GenericXmlApplicationContext("applicationContext.xml");
				
		// 2. Spring 컨테이너로부터 UserServiceImpl 객체를 Lookup 한다.
		UserService userService = (UserService) container.getBean("userService");
		
		// 3. 로그인 기능 테스트
		UserVO vo = new UserVO();
		vo.setId("test");
		vo.setPassword("test1234");
		
		UserVO user = userService.getUser(vo);
		if (user != null) {
			System.out.println(user.getName() + "님 환영합니다.");
		} else {
			System.out.println("로그인 실패");
		}
		
		// 4. Spring 컨테이너 종료
		container.close();
	}
}

 

 

UserServiceClient.java 를 실행!

 

 

실행 결과

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Sun Oct 18 18:23:21 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[사전 처리] getUser() 메소드 ARGS 정보 : UserVO [id=test, password=test1234, name=null, role=null]
==> JDBC로 getUser() 기능 처리
test님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Sun Oct 18 18:23:21 KST 2020]; root of context hierarchy

 

 

 

 


 

2.  After Returning 어드바이스

 

 

 

 

( AfterReturningAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.JoinPoint;

import com.heidi.biz.user.UserVO;

public class AfterReturningAdvice {

	public AfterReturningAdvice() {
	}

	public void afterLog(JoinPoint jp, Object returnObj) {
		String method = jp.getSignature().getName();
		if (returnObj instanceof UserVO) {
			UserVO user = (UserVO) returnObj;
			if (user.getRole().equals("stu")) {
				System.out.println(user.getName() + " 로그인 (stu)");
			}
		}
		System.out.println("[사후 처리] " + method + "() 메소드 리턴값 : " + returnObj.toString());
	}
	
}

afterLog() 메소드는 클라이언트가 호출한 비즈니스 메소드의 정보를 알아내기 위해서

첫 번째 매개변수로 JoinPoint 객체를 선언한다.

 

그리고 두 번째 매개변수로 Object 타입의 returnObj가 선언되어 있는데 얘가 바인드변수 이다.

 

바인드변수는 비즈니스 메소드가 리턴한 결과값을 바인딩할 목적으로 사용하며,

어떤 값이 리턴될지 모르기 때문에 타입은 Object 로 선언해주는 것이다.

 

그리고 이 바인드변수를 선언했기 때문에 스프링 설정 파일에서도 바인드변수에 대한 매핑 설정을 해줘야한다 !

이 매핑 설정은 <aop:after-returnng> 엘리먼트의 returning 속성을 사용한다.

 

 

스프링 설정 파일에서 지정해주는 returning 속성 값은 반드시 어드바이스 메소드의 매개변수에 선언된

바인드변수의 이름과 동일해야 매핑이 되는점 주의!

 

 

 

 

( applicationContext.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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
   
	<context:component-scan base-package="com.heidi.biz"></context:component-scan>
	
	<bean id="afterReturning" class="com.heidi.biz.board.common.AfterReturningAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* com.heidi.biz..*Impl.get*(..))" id="getPointcut"/>
		<aop:aspect ref="afterReturning">
			<aop:after-returning method="afterLog" pointcut-ref="getPointcut" returning="returnObj" />
		</aop:aspect>
	</aop:config>

</beans>

 

 

( UserServiceClient.java )

package com.heidi.biz.user;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class UserServiceClient {

	public static void main(String[] args) {
		// 1. Spring 컨테이너를 구동한다.
		AbstractApplicationContext container = new GenericXmlApplicationContext("applicationContext.xml");
				
		// 2. Spring 컨테이너로부터 UserServiceImpl 객체를 Lookup 한다.
		UserService userService = (UserService) container.getBean("userService");
		
		// 3. 로그인 기능 테스트
		UserVO vo = new UserVO();
		vo.setId("Admin");
		vo.setPassword("admin");
		
		UserVO user = userService.getUser(vo);
		if (user != null) {
			System.out.println(user.getName() + "님 환영합니다.");
		} else {
			System.out.println("로그인 실패");
		}
		
		// 4. Spring 컨테이너 종료
		container.close();
	}
}

 

 

UserServiceClient.java 를 실행!

 

 

실행 결과

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Sun Oct 18 18:36:47 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
==> JDBC로 getUser() 기능 처리
administrator 로그인 (Admin)
[사후 처리] getUser() 메소드 리턴값 : UserVO [id=Admin, password=admin, name=administrator, role=Admin]
administrator님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Sun Oct 18 18:36:47 KST 2020]; root of context hierarchy

 

 

 

 


 

3.  After Throwing 어드바이스

 

 

 

 

( AfterThrowingAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.JoinPoint;

public class AfterThrowingAdvice {

	public AfterThrowingAdvice() {
	}
	
	public void exceptionLog(JoinPoint jp, Exception exceptObj) {
		String method = jp.getSignature().getName();
		
		System.out.println("[예외 처리] " + method + "() 메소드 수행 중 발생된 예외 메시지 : " + exceptObj.toString());
	}
}

 

 

( applicationContext.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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.heidi.biz"></context:component-scan>
	
	<bean id="afterThrowing" class="com.heidi.biz.board.common.AfterThrowingAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* com.heidi.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref="afterThrowing">
			<aop:after-throwing method="exceptionLog" pointcut-ref="allPointcut" throwing="exceptObj"/>
		</aop:aspect>
	</aop:config>

</beans>

 

 

( BoardServiceImpl.java  내부의 insertBoar() 메서드 수정 )

	@Override
	public void insertBoard(BoardVO vo) {
		if (vo.getSeq() == 0) {
			throw new IllegalArgumentException("0번 글은 등록하실 수 없습니다.");
		}
		this.boardDAO.insertBoard(vo);
	}

 

 

BoardServiceClient.java 를 실행!

 

 

실행 결과

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Sun Oct 18 18:49:38 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[예외 처리] insertBoard() 메소드 수행 중 발생된 예외 메시지 : java.lang.IllegalArgumentException: 0번 글은 등록하실 수 없습니다.
Exception in thread "main" java.lang.IllegalArgumentException: 0번 글은 등록하실 수 없습니다.
	at com.heidi.biz.board.impl.BoardServiceImpl.insertBoard(BoardServiceImpl.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:58)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
	at com.sun.proxy.$Proxy10.insertBoard(Unknown Source)
	at com.hedidi.biz.board.BoardServiceClient.main(BoardServiceClient.java:25)

 

 


또는 exceptionLog() 메소드를 구현할 때, 발생하는 예외 객체의 종류에 따라서

아래와 같이 다양하게 예외처리도 가능하다.

 

*  AfterThrowingAdvice.java

package com.heidi.biz.board.common;

import org.aspectj.lang.JoinPoint;

public class AfterThrowingAdvice {

	public AfterThrowingAdvice() {
	}
	
	public void exceptionLog(JoinPoint jp, Exception exceptObj) {
		String method = jp.getSignature().getName();
		System.out.println(method + "() 메소드 수행 중 예외 발생!");
		
		if (exceptObj instanceof IllegalArgumentException) {
			System.out.println("부적합한 값이 입력되었습니다.");
		} else if (exceptObj instanceof NumberFormatException) {
			System.out.println("숫자 형식의 값이 아닙니다.");
		} else if (exceptObj instanceof Exception) {
			System.out.println("문제가 발생했습니다.");
		}
		
	}

}

 

실행 결과

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 12 12:50:18 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
insertBoard() 메소드 수행 중 예외 발생!
부적합한 값이 입력되었습니다.
Exception in thread "main" java.lang.IllegalArgumentException: 0번 글은 등록하실 수 없습니다.

 

 


 

4.  Around 어드바이스

 

 

 

( AroundAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class AroundAdvice {

	public AroundAdvice() {
	}

	public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
		String method = pjp.getSignature().getName();
		
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		
		Object obj = pjp.proceed();
		
		stopWatch.stop();
		System.out.println(method + "() 메소드 수행에 걸린 시간 : " + stopWatch.getTotalTimeMillis() + "(ms)초");
		
		return obj;
	}
	
}

 

 

*  applicationContext.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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.heidi.biz"></context:component-scan>
	
	<bean id="around" class="com.heidi.biz.board.common.AroundAdvice"></bean>
	
	<aop:config>
		<aop:pointcut expression="execution(* com.heidi.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:aspect ref="around">
			<aop:around method="aroundLog" pointcut-ref="allPointcut" />
		</aop:aspect>
	</aop:config>

</beans>

 

 

 BoardServiceImpl.java  내부의 insertBoar() 메서드 수정

	@Override
	public void insertBoard(BoardVO vo) {
//		if (vo.getSeq() == 0) {
//			throw new IllegalArgumentException("0번 글은 등록하실 수 없습니다.");
//		}
		this.boardDAO.insertBoard(vo);
	}

 

( BoardServiceImpl.java 의 insertBoard() 메서드 내부의 if문은 주석처리 후, )

 

UserServiceClient.java 를 실행!

 

 

실행 결과

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 12 13:59:07 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
==> JDBC로 getUser() 기능 처리
getUser() 메소드 수행에 걸린 시간 : 683(ms)초
heidi님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 12 13:59:07 KST 2020]; root of context hierarchy

 

 

 

 

 

반응형

댓글