구리

TIL_210618_AOP 본문

SPRING FRAMEWORK

TIL_210618_AOP

guriguriguri 2021. 6. 18. 11:14

목차

 

 

Before 어드바이스 

비즈니스 메소드 실행 전 동작

예시

어드바이스 클래스

package com.springbook.biz.common;

public class BeforeAdvice {
	public void beforeLog() {
		System.out.println("[사전 처리] 비즈니스 로직 수행 전 동작");
	}
}
applincationContext.xml
<bean id="before" class="com.springbook.biz.common.BeforeAdvice" />
	
	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		
		<aop:aspect ref="before">
			<aop:before pointcut-ref="allPointcut" method="beforeLog" />
		</aop:aspect>
	</aop:config>

allPointcut으로 지정한 모든 Impl 클래스의 메소드가 실행되기 전 before로 지정한 어드바이스의 beforeLog() 메소드가 실행된 것을 볼 수 있습니다.

 

 

After Returning

비즈니스 메소드가 성공적으로 리턴되면 동작 (void 메소드에선 적용되지 않음)

 

예시

어드바이스 클래스

package com.springbook.biz.common;

public class AfterReturningAdvice {
	public void exceptionLog() {
		System.out.println("[사후 처리] 비즈니스 로직 수행 후 동작 ");
	}
}
applicationContext.xml
<bean id="afterReturning" class="com.springbook.biz.common.AfterReturningAdvice" />
	
	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		
		<aop:aspect ref="afterReturning">
			<aop:after-returning pointcut-ref="getPointcut" method="afterLog" />
		</aop:aspect>
	</aop:config>

위 코드를 보면 XX Impl 클래스의 getXX() 메소드 실행 후 리턴시 afterReturningAdvice로 지정한 afterLog() 메소드가 실행되는 것을 볼 수 있습니다.

 

 

After Throwing

비즈니스 메소드 실행 중 예외가 발생하면 동작 (java의 try catch 블록에서 catch 블록에 해당)

예시

어드바이스 클래스

package com.springbook.biz.common;

public class AfterThrowingAdvice {
	public void exceptionLog() {
		System.out.println("[예외 처리] 비즈니스 로직 수행 중 예외 발생 ");
	}
}
applicationContext.xml
	<bean id="afterThrowing" class="com.springbook.biz.common.AfterThrowingAdvice" />
	
	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		
		<aop:aspect ref="afterThrowing">
			<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog" />
		</aop:aspect>
        
	</aop:config>

어드바이스 클래스의 메소드가 실행하는지 확인하기 위해 BoardServiceImpl 클래스에 예외가 발생하는 코드를 추가합니다.

package com.springbook.biz.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.springbook.biz.board.BoardService;
import com.springbook.biz.board.BoardVO;

@Service("boardSerivce")
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardDAOSpring boardDAO;
	
	public BoardServiceImpl() {
	}

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

실행 결과
insert할 시 BoardVo에는 따로 글번호를 삽입하지 않기에 0번으로 지정되어 IllegalArgumentException이 발생하였고 afterThrowing로 지정한 어드바이스 클래스의 ecceptionLog() 메소드가 실행된 것을 볼 수 있습니다.

 

 

After

비즈니스 메소드 실행 후, 무조건 실행 (try-catch 블록에서 finally 블록에 해당)

예시

어드바이스 클래스

package com.springbook.biz.common;

public class AfterAdvice {
	public void finallyLog() {
		System.out.println("[사후 처리] 비즈니스 로직 수행 후 무조건 동작 ");
	}
}
applicationContext.xml
	<bean id="after" class="com.springbook.biz.common.AfterAdvice" />
	<bean id="afterThrowing" class="com.springbook.biz.common.AfterThrowingAdvice" />
	
	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		
		<aop:aspect ref="afterThrowing">
			<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog" />
		</aop:aspect>
		
		<aop:aspect ref="after">
			<aop:after pointcut-ref="allPointcut" method="finallyLog" />
		</aop:aspect>
	</aop:config>

결과를 보면 예외가 발생한 상황에도 <aop:after>로 설정한 finallyLog() 메소드가 먼저 실행되고, exceptionLog()가 실행된 것을 볼 수 있습니다.

 

 

Around

비즈니스 메소드 호출 자체를 가로채 비즈니스 메소드 실행 전후에 처리할 로직을 삽입할 수 있습니다.

 

예시

어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.ProceedingJoinPoint;

public class AroundAdvice {
	public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
		System.out.println("[BEFORE] 비즈니스 메소드 수행 전에 처리할 내용 ");
		Object returnObj = pjp.proceed();
		System.out.println("[AFTER] 비즈니스 메소드 수행 후에 처리할 내용 ");
		return returnObj;
	}
}

클라이언트의 요청을 가로챈 어드바이스는 클라이언트가 호출한 비즈니스 메소드를 호출하기 위해서 ProceedJoinPoint 객체를 매개변수로 받아야 하며 ProceedJoinPoint 객체의 proceed() 메소드는 비즈니스 메소드를 호출할 수 있는 기능을 합니다.

 

applicationContext.xml
<bean id="around" class="com.springbook.biz.common.AroundAdvice" />

	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		

		<aop:aspect ref="around">
			<aop:around pointcut-ref="allPointcut" method="aroundLog" />
		</aop:aspect>
	</aop:config>
package com.springbook.biz.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.springbook.biz.board.BoardService;
import com.springbook.biz.board.BoardVO;

@Service("boardSerivce")
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardDAOSpring boardDAO;
	
	public BoardServiceImpl() {
	}

	public void insertBoard(BoardVO vo) {
		boardDAO.insertBoard(vo);
	}
 }

<aop:around>를 테스트하기 위해 BoardServiceImpl에서 작성했던 오류 발생 코드는 제거하였습니다.

 

글 등록과 글 목록 조회를 요청했을 때, [BEFORE] / [AFTER] 메시지가 각각 출력되는 것을 볼 수 있습니다.

참고로 어드바이스 메소드가 비즈니스 메소드 위에서 실행하는 것이 아닌 어드바이스 클래스에서 비즈니스 메소드를 가져와서 실행하는 것입니다.

 

 

JointPoint Interface

클라이언트가 호출한 비즈니스 메소드의 정보를 읽어오기 위한 인터페이스

 

JointPoint 메소드

메소드 설명
Signature getSignature() 클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴
Object getTarget() 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴
Object[] getArgs() 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴 

 

여기서 주의할 점은 Before, After, After Returning, After Throwing 어드바이스는 JoinPoint를 사용해야하고, Around 어드바이스에서만 ProceedJoinPoint를 매개변수로 사용해야 합니다. 이유는 Aorund 어드바이스에서만 proceed() 메소드가 필요하기 때문입니다. 그리고 JoinPoint 객체를 사용하려면 어드바이스 메소드 매개변수로 선언만 하면 됩니다.

그러면 클라이언트가 비즈니스 메소드를 호출할 때, 스프링 컨테이너가 JoinPoint 객체를 생성하여 메소드 호출 관련 모든 정보를 JoinPoint 객체에 저장하여 어드바이스 메소드 호출시 인자로 넘겨줍니다.

 

Signature 메소드

메소드명 설명
String getName() 클라이언트가 호출한 메소드 이름 리턴
String toLongString() 클라이언트가 호출한 메소드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함하여 리턴
String toShortString() 클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴

 

예시

어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;

public class BeforeAdvice {
	public void beforeLog(JoinPoint jp) {
		String method = jp.getSignature().getName();
		Object[] args = jp.getArgs();
		
		System.out.println("[사전 처리] " + method + "() 메소드 ARGS 정보 : " + args[0].toString());
	}
}
<?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:p="http://www.springframework.org/schema/p"
	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.springbook.biz"></context:component-scan>
	
	<bean id="before" class="com.springbook.biz.common.BeforeAdvice" />

	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		

		<aop:aspect ref="before">
			<aop:before pointcut-ref="allPointcut" method="beforeLog" />
		</aop:aspect>
	</aop:config>
	
</beans>
package com.springbook.biz.board;

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("1234");
		
		UserVO user = userService.getUser(vo);
		if(user != null){
			System.out.println(user.getName() + "님 환영합니다 ^^");
		}else{
			System.out.println("로그인 실패");
		}
		
		// 4. Spring 컨테이너 종료
		container.close();	
	}
}

실행 결과를 보면 getUser() 메소드가 호출되었고, 이때 UserVO 객체가 인자로 전달되어 객체에 설정된 아이디, 비밀번호 정보를 알 수 있습니다.

 

After Returning 어드바이스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;

import com.springbook.biz.board.UserVO;

public class 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("admin")){
				System.out.println(user.getName() + "로그인(admin)");
			}
		}
		System.out.println("[사후 처리] " + method + "()메소드 리턴값 : " + returnObj.toString());
	}
}

afterLog() 메소드의 두번째 매개변수가 Object 타입인데 이를 바인드 변수라고 합니다.

바인드 변수는 비즈니스 메소드가 리턴한 결괏값을 바인딩할 목적으로 사용하여, 어떤 값인지 모르기에 Object 타입으로 선언합니다

바인드 변수가 추가되었다면 그에 대한 매핑 설정을 스프링 설정 파일에 추가해야 합니다.

 

	<bean id="afterReturning" class="com.springbook.biz.common.AfterReturningAdvice" />

	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		

		<aop:aspect ref="afterReturning">
			<aop:after-returning pointcut-ref="getPointcut" method="afterLog" returning="returnObj"/>
		</aop:aspect>
	</aop:config>

 

After Throwing 어드바이스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;

public class AfterThrowingAdvice {
	public void exceptionLog(JoinPoint jp, Exception exceptObj) {
		String method = jp.getSignature().getName();
		
		System.out.println("[예외 처리] " + method + "() 메소드 수행 중 발생된 예외 메시지 : " + exceptObj.getMessage() );
	}
}
<bean id="afterThrowing" class="com.springbook.biz.common.AfterThrowingAdvice" />

	<aop:config>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
		<aop:pointcut expression="execution(* com.springbook.biz..*Impl.get*(..))" id="getPointcut"/>	
		

		<aop:aspect ref="afterThrowing">
			<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog" throwing="exceptObj"/>
		</aop:aspect>

위 설정은 비즈니스 메소드에서 발생한 예외 객체를 exceptObj라는 바인드 변수에 바인드하라는 설정입니다.

throwing 속성은 <aop:after-throwing> 엘리먼트에서만 사용할 수 있는 속성으로, 속성값은 어드바이스 메소드 매개변수로 선언된 바인드 변수 이름과 같아야 합니다.

@Service("boardSerivce")
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardDAOSpring boardDAO;
	
	public BoardServiceImpl() {
	}

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

위 코드처럼 예외 발생시킬 코드를 추가 후 실행하면 예외가 발생한 메소드와 예외 객체 메시지를 출력하는 것을 확인할 수 있습니다.

 

 

Around 어드바이스 

package com.springbook.biz.common;

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

public class 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;
	}
}