본문 바로가기
프로그래밍/스프링프레임워크

실행 흐름에 끼어들기(Filter,Interceptor,AOP) 3 - AOP

by pentode 2018. 4. 2.
반응형

실행 흐르에 끼어들기 마지막으로 AOP(Aspect Oriented Programming) 관점 지향 프로그래밍 입니다.

 

웹프로그램에서 하나의 요청의 실행 단계를 보면 공통적인 부분들이 존재합니다. 각 요청을 처리하는 비지니스 로직은 요청 마다 다르겠지만, 요청의 끝에서 작업 로그를 기록한다던지, 데이터베이스 트랜잭션을 요청의 앞에서 시작하고, 끝에서 커밋 또는 롤백을 한다던지 하는 코드 들이 있습니다.

 

이런 코드들은 모든 프로그램에 반복적으로 나타나게 됩니다. AOP 에서 이런 반복적으로 나오는 부분이 아래 이미지 에서 처럼 프로그램을 횡단하는 곳에 공통적인 코드가 나타난다고 해서 횡단적 관심사(Cross-cutting concern)라고 합니다.

 

    PGM1    PGM2    PGM3
    +-----+ +-----+ +-----+
    |     | |     | |     |
  --+-----+-+-----+-+-----+---> 트랜잭션 시작
    |     | |     | |     |
    |     | |     | |     |
    |     | |     | |     |
  --+-----+-+-----+-+-----+---> 트랙잭션 종료
    |     | |     | |     |
  --+-----+-+-----+-+-----+---> 로깅
    |     | |     | |     |
    +-----+ +-----+ +-----+


프로그래머는 이런 부분을 추출해서 클래스로 만들고, 필요한 곳에서 호출해서 쓸 수 있도록 공통 모듈로 처리합니다. 하지만 공통 모듈을 호출하는 코드는 프로그램 안에 남아 있어야 합니다.  AOP는 이런 호출 부분까지 뽑아내서 비지니스 로직을 처리하는 코드내에 호출하는 코드도 필요 없게 해줍니다. 즉, 요청을 처리하는 프로그램에서는 비지니스 로직만을 작성하면 되고, 공통적인 코드는 따로 작성을 해 두고, 어디에 끼워 넣어서 실행해라 라고 설정을 해 주면 됩니다. 두 가지 코드가 완전히 분리가 되는 것입니다.

 

기술적으로 이 끼워넣기를 어떻게 처리하는가 하는것은 생략 하겠습니다. 기회가 되면 그 부분만 따로 해볼 수 있도록 하겠습니다.

 

위 설명에서 뽑아낸 공통 코드를 어드바이스(Advice)라고 합니다. 어디에 끼워 넣어라 할때 끼워넣을 곳을 조인포인터(Joinpoint) 라고 합니다. 조인포인터 들을 모아둔것을 포인트컷(Pointcut)이라고 합니다. 그리고 어드바이스를 조인포이트에 실제로 끼워넣는 작업을 위빙(Weaving) 이라고 합니다. 그리고 스프링에만 있는 것으로 어드바이스와 포인트컷을 한데 묶어 다루는 어드바이저(Advisor) 가 있습니다.

 

A, B, C  세 개의 컨트롤러가 있다고, 가정을 했을 때 A 컨트롤러가 가지고 있는 first()라는 메소드의 끝 부분에 코드는 넣는다면 이 코드를 넣을 곳,  A  컨트롤러의 first() 메소드가 조인포인트가 됩니다. 모든 컨트롤러 내의 모든 메소드의 끝부분에 코드를 넣는다. 이렇게 하면 조인포인트가 모인 포인트컷이 되겠습니다. 물론 포인터컷은 하나의 조인포트만 가질 수도 있습니다.

 

어드바이스는 조인포인트에서 실행되는 위치에 따라 여러 타입이 있습니다.


- Before Advice : Joinpoint 앞에서 실행되는 Advice

- Around Advice : Joinpoint 앞과 뒤에서 실행되는 Advice
- After Advice : Joinpoint 호출이 리턴되기 직전에 실행되는 Advice
- After Returning Advice : Joinpoint 메서드 호출이 정상적으로 종료된 후에 실행되는 Advice
- After Throwing Advice : 예외가 발생했을때 실행되는 Advice

  

이제 예제를 보도록 하겠습니다.

 

 

1. pom.xml에 의존성을 추가합니다.

 

<!-- AspectJ -->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>${org.aspectj-version}</version>
</dependency>

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>${org.aspectj-version}</version>
</dependency>

 

${org.aspectj-version} 은  pom  상단의 properties 항목에 정의되어 있습니다. 없다면 maven repository 에서 찾아서 확인하시기 바랍니다. 예제 에서는  1.6.10 버전이 사용되었습니다.

 

반응형

 

2. Advice 클래스를 작성합니다 java/main/java/com/tistory/pentode/aop/TestAdvice.java 파일 입니다.

 

package com.tistory.pentode.aop;

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;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;

// AOP 임을 알리는 annotation 입니다.
@Aspect
public class TestAdvice {

	private static final Logger logger = LoggerFactory.getLogger(TestAdvice.class);

	// 공통으로 사용될 포인트컷을 지정합니다.
	// com.tisotry.pentode 패키지 안의 Controller 로 끝나는 클래스의 모든 메소드에 적용됩니다.
	@Pointcut("execution(* com.tistory.pentode.*Controller.*(..))")
	public void commonPointcut() { }

	// Before Advice 입니다. 위에서 정의한 공통 포인터 컷을 사용합니다.
	@Before("commonPointcut()")
	public void beforeMethod(JoinPoint jp) throws Exception {
		logger.info("beforeMethod() called.....");

		// 호출될 메소드의 인자들으 얻을 수 있습니다.
		Object arg[] = jp.getArgs();

		// 인자의 갯수 출력
		logger.info("args length : " + arg.length);

		// 첫 번재 인자의 클래스 명 출력
		logger.info("arg0 name : " + arg[0].getClass().getName());

		// 호출될 메소드 명 출력
		logger.info(jp.getSignature().getName());
	}

	// After Advice 입니다.
	@After("commonPointcut()")
	public void afterMethod(JoinPoint jp) throws Exception {
		logger.info("afterMethod() called.....");
	}

	// After Returning Advice 입니다.
	// 이 어드바이스는 반환값을 받을 수 있습니다.
	@AfterReturning(pointcut="commonPointcut()", returning="returnString")
	public void afterReturningMethod(JoinPoint jp, String returnString) throws Exception {

		logger.info("afterReturningMethod() called.....");

		// 호출된 메소드 반환값 출력
		logger.info("afterReturningMethod() returnString : " + returnString);
	}

	// Around Advice 입니다.
	// 포인트컷을 직접 지정했습니다.
	@Around("execution(* com.tistory.pentode.*Controller.*(..))")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {

		logger.info("aroundMethod() before called.....");

		Object result = pjp.proceed();
		logger.info("aroundMethod() after called.....");

		return result;
	}

	// 예외가 발생했을때 Advice 입니다.
	@AfterThrowing(pointcut="commonPointcut()", throwing="exception")
	public void afterThrowingMethod(JoinPoint jp, Exception exception) throws Exception {

		logger.info("afterThrowingMethod() called.....");

		// 발생한 예외의  메세지를 출력합니다.
		logger.info(exception.getMessage());
	}
}

 

3. 어드바이스를 등록합니다. /WEB-INF/sprin/appServlet/servlet-context.xml 파일 입니다.

 

<aop:aspectj-autoproxy />
<beans:bean id="testAdvice" class="com.tistory.pentode.aop.TestAdvice" />

 

4. 테스트에 사용된 컨트롤러와 jsp 파일 입니다. src/main/java/com/tistory/pentode/TestController.java 파일 입니다.

 

package com.tistory.pentode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller public class TestController {

	private static final Logger logger = LoggerFactory.getLogger(TestController.class);

	@RequestMapping(value = "/aop.do", method = RequestMethod.GET)
	public String aop(Model model) {
		logger.info("call aop.do");

		// 에러 발생 테스트 용 코드
		/*  boolean flag = true;
		if(flag) {
			throw new RuntimeException("Test Exception!");
		}*/

		return "aop";
	}
}

 

/WEB-INF/views/aop.jsp 파일 입니다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>AOP</title>
</head>
<body>
	<h1>AOP !</h1>
</body>
</html>

 

 

5. 실행결과입니다.

 

Advice 실행결과

 

콘솔 출력 결과 입니다. Around 어드바이스 -> Before 어드바이스 -> 컨트롤러 메소드 -> After 어드바이스 -> After Return 어드바이스  순으로 실행됩니다.

INFO : com.tistory.pentode.filter.FirstFilter - before doFilter
INFO : com.tistory.pentode.interceptor.FirstInterceptor - preHandle call......
INFO : com.tistory.pentode.interceptor.FirstInterceptor - handler method name : aop
INFO : com.tistory.pentode.aop.TestAdvice - aroundMethod() before called.....
INFO : com.tistory.pentode.aop.TestAdvice - beforeMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - args length : 1
INFO : com.tistory.pentode.aop.TestAdvice - arg0 name : org.springframework.validation.support.BindingAwareModelMap
INFO : com.tistory.pentode.aop.TestAdvice - aop - 호출한 메소드명
INFO : com.tistory.pentode.TestController - call aop.do
INFO : com.tistory.pentode.aop.TestAdvice - aroundMethod() after called.....
INFO : com.tistory.pentode.aop.TestAdvice - afterMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - afterReturningMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - afterReturningMethod() returnString : aop - 반환값
INFO : com.tistory.pentode.interceptor.FirstInterceptor - postHandle call......
INFO : com.tistory.pentode.interceptor.FirstInterceptor - afterCompletion call......
INFO : com.tistory.pentode.filter.FirstFilter - after doFilter

 

예외 발생시 콘솔 출력 입니다. Around 어드바이스 -> Before 어드바이스 -> 컨트롤러 메소드(예외발생) -> After 어드바이스 -> After Throwing 어드바이스  순으로 실행됩니다.

 

INFO : com.tistory.pentode.filter.FirstFilter - before doFilter
INFO : com.tistory.pentode.interceptor.FirstInterceptor - preHandle call......
INFO : com.tistory.pentode.interceptor.FirstInterceptor - handler method name : aop
INFO : com.tistory.pentode.aop.TestAdvice - aroundMethod() before called.....
INFO : com.tistory.pentode.aop.TestAdvice - beforeMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - args length : 1
INFO : com.tistory.pentode.aop.TestAdvice - arg0 name : org.springframework.validation.support.BindingAwareModelMap
INFO : com.tistory.pentode.aop.TestAdvice - aop
INFO : com.tistory.pentode.TestController - call aop.do
INFO : com.tistory.pentode.aop.TestAdvice - afterMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - afterThrowingMethod() called.....
INFO : com.tistory.pentode.aop.TestAdvice - Test Exception!  - 예외 메세지 출력
INFO : com.tistory.pentode.interceptor.FirstInterceptor - afterCompletion call......
INFO : com.tistory.pentode.filter.FirstFilter - after doFilter

 

이것으로 웹프로그래밍시 흐름 중간에 공통 코드를 끼워 넣는 세가지 방법을 알아 보았습니다. 내가 넣고하자는 코드는 필터, 인터셉터, AOP 중 어떤 것을 사용해서 구현해야 할지는  많은 고민이 필요할 때가 있습니다. 앞의 특성들은 잘 생각해서 적절한 선택을 할 수 있기를 바랍니다.

 

※ 예제소스

SpringTest.zip
다운로드

 

반응형

댓글2

  • 조찬조찬 2020.12.08 23:08

    pointcut에 void메소드가 들어가있는건 이해를 하겠는데(포인트컷 조건에 걸리는게 하나라도 있으면 자동실행), 정작 서블릿 파일은 TestController.java 소스에서 어디서 실행시키는지 모르겠습니다.

    답글

    • pentode 2021.01.23 21:42 신고

      이글은 스프링프레임웍을 사용하여 예제를 만들었습니다.

      스프림프레임웍에서 서블릿은 컨텍스트내에서 요청을 받아들여서 컨트롤러로 분기해주는 DispatcherServlet 하나만 있고 비지니스 로직은 컨트롤러가 서비스 객체를 호출해 수행하는 구조로 되어 있습니다.

      방문해 주셔서 감사합니다.^^