본문 바로가기

STUDY/국비과정

[JAVA 웹 개발 공부] 국비지원 84일차 - 인터셉터, 스프링 AOP, Advice, Pointcut

인터셉터

 

1. 인터셉터(Interceptor)
컨트롤러(Controller)의 '핸들러(Handler)'를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할수 있는 일종의 필터이다.

2. 인터셉터 사용이유
개발자는 특정 Controller의 핸들러가 실행되기 전이나 후에 추가적인 작업을 원할때 Interceptor를 사용한다.
(추가적인 작업으로는 로그인체크, 권한 체크 등이 있다.)

인터셉터를 사용하게 되면 개발자는 핸들러 수 만큼 작성했던 세션 체크 코드를 인터셉터 클래스에 한번만 작성하면된다. 이로 인해 코드의 량이 현저히 줄기 대문에 메모리 낭비를 줄일수 있다.
그리고 인터셉터 적용의 유무 기준이 되는 url을 servlet-context.xml에 설정해주게 되면 스프링에서 일괄적으로 해당 url 경로의 핸들러에 인터셉터를 적용해주기 때문에 누락에 대한 위험이 상당히 줄게된다.

3. 구현수단
스프링에서 제공하는 org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현하거나, org.springframework.web.servlet.handler.HandlerInterceptorAdapter 추상클래스를 오버라이딩 함으로써 자신만의 인터셉터를 만들수 있다.

HandlerInterceptorAdapter 추상클래스 경우  HandlerInterceptor 인터페이스를 상속받아 구현되었다.

4. 메소드

preHandle() -컨트롤러가 호출되기 전에 실행된다.
-'핸들러'에 대한 정보를 인자값으로 받기때문에 '서블릿 필터'에 비해 세밀하게 로직을 구성할수 있다.
-리턴값이 boolean이며, 리턴이 true 일경우 preHandle() 실행후 핸들러에 접근하고,
false일경우 작업을 중단하기 때문에 컨트롤러와 남은 인터셉터가 실행되지않는다.
postHandle() -핸들러가 실행은 완료 되었지만 아직 View가 생성되기 이전에 호출된다.
-ModelAndView 타입의 정보가 인자값으로 받는다. 따라서 Controller에서 View 정보를 전달하기 위해 작업한 Model 객체의 정보를 참조하거나 조작할수 있다.
-preHandle() 에서 리턴값이 false인경우 실행되지 않는다.
-적용중인 인터셉터가 여러개인경우 preHandle()는 역순으로 호출된다.
-비동기적 요청 처리시에 호출되지않는다.
afterCompletion() -모든 View에서 최종 결과를 생성하는 일을 포함한 모든 작업이 완료된 후에 실행된다.
-요청 처리중에 사용한 리소스를 반환해주기 적당한 메서드 이다.
-preHandle() 에서 리턴값이 false인경우 실행되지 않는다.
-적용중인 인터셉터가 여러개인경우 preHandle()는 역순으로 호출된다.
-비동기적 요청 처리시에 호출되지않는다.

 

5. 필터 vs 인터셉터

대상 필터(Filter) 인터셉터(Interceptor)
관리되는 컨테이너 웹 컨테이너 스프링 컨테이너
Request/Response
객체 조작 가능 여부
O X
특징 -스프링과 무관하게 전역적으로 처리해야 하는 작업들을 처리
-인터셉터보다 앞단에서 동작
-클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리
-클라이언트의 IP나 요청 정보들을 포함해 기록하기에 용이
용도 -공통된 보안 및 인증/인가 관련 작업
-모든 요청에 대한 로깅 또는 감사
-이미지/데이터 압축 및 문자열 인코딩
-Spring과 분리되어야 하는 기능
-세부적인 보안 및 인증/인가 공통 작업
-API 호출에 대한 로깅 또는 감사
-Controller로 넘겨주는 정보(데이터)의 가공

 

 

AOP(Aspect Oriented Programming)

 

1. 스프링 AOP ( Aspect Oriented Programming )
AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다. 
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다. 
위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

 

2. AOP 주요 개념

Aspect 흩어진 관심사를 모듈화 한 것
Aspect는 부가기능을 정의한 코드인 어드바이스와 어드바이스를 어디에 적용하지를 결정하는 포인트컷을 합친 개념
(Advice + PointCut = Aspect)
Target Aspect를 적용하는 곳(클래스, 메서드 등), 핵심 로직을 담고 있는 객체
Advice 실질적으로 어떤 일을 해야 할 지에 대한 것, 실질적인 부가기능을 담은 구현체
JoinPoint Advice가 적용될 위치 혹은 끼어들 수 있는 시점. 메서드 진입 시점, 생성자 호줄 시점, 필드에서 꺼내올 시점 등 끼어들 시점을 의미. 참고로 스프링에서 Join Point는 언제나 메서드 실행 시점을 의미
PointCut Join Point의 상세한 스펙을 정의한 것. "A란 메서드의 진입 시점에 호출할 것"처럼 구체적으로 Advice가 실행될 시점을 정함
Weaving AOP에서 joinpoint들을 Advice로 감싸는 과정

 

3. 스프링 AOP 특징
프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서이다.
스프링 빈에만 AOP를 적용 가능하며, 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 ...)에 대한 해결책을 지원하는 것이 목적이다.

4. 스프링 AOP 구현방법

(1️) XML 기반의 POJO 클래스를 이용한 AOP 구현

부가 기능을 제공하는 Advice 클래스를 작성하고, XML 설정파일에 <aop:config> 을 이용해서 Aspect를 설정한다. (Advice와 Pointcut을 설정함)
(2️) @Aspect 어노테이션을 이용한 AOP 구현 
@Aspect 어노테이션을 이용해서 부가 기능을 제공하는 Aspect 클래스를 작성한다. 
Aspect 클래스에는 Advice를 구현하는 메서드와 Pointcut을 포함한다. 
그리고 XML 설정파일에 <aop:aspectj-autoproxy/>을 설정한다. 

 

 

Advice

 

1. Advice / Adviser

포인트 컷(Pointcut) 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것
어디에 부가기능(Advice)을 적용할지 판단하는 필터링 로직
어드바이스(Advice) 실제로 기능을 구현한 객체
프록시가 호출하는 부가 기능
어드바이저(Advisor) Pointcut 1 + Advice 1

 

2. Advice 어노테이션

실행 순서 : @Around → @Before → @After → @AfterReturning → @AfterThrowing

Type 설명
@Around 메서드 호출 전후에 수행 (조인포인트 실행 여부 선택, 반환 값 변환, 예외 변환, try~catch~finally 구문 처리 가능 등), 가장 강력한 어드바이스이다.
@Before 조인포인트 실행 이전에 실행, 일반적으로 리턴타입 void
@After (finally) 조인포인트의 동작과 관계없이 실행
@AfterReturning 조인포인트 완료후 실행 (ex. 메서드가 예외없이 실행될 때)
@AfterThrowing 메서드가 예외를 던지는 경우 실행

 

 

Pointcut, Advice 사용

 

package kr.co.greenart.advice;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MathServiceAspect {
	private static final Logger logger = LoggerFactory.getLogger(MathServiceAspect.class);

	// 포인트컷
	@Pointcut(value = "execution(public Integer kr.co.greenart.math.MathService.add(..)) && args(a, b)")
	public void pointcutAdd(Integer a, Integer b) {
	}

	// 어드바이스
	@Around(value = "pointcutAdd(a, b)")
	public Object adviceAround(ProceedingJoinPoint jp, Integer a, Integer b) throws Throwable {
		if (a == null || b == null) {
			return 0;
		}
		return jp.proceed();
	}

	@Before(value = "pointcutAdd(a, b)")
	public void adviceBefore(JoinPoint jp, Integer a, Integer b) {
		logger.info("Before 실행");
	}

	@AfterReturning(value = "pointcutAdd(a, b)")
	public void adviceAfterReturning(JoinPoint jp, Integer a, Integer b) {
		logger.info("AfterReturning 실행");
	}

	@AfterThrowing(value = "pointcutAdd(a, b)")
	public void adviceAfterThrowing(JoinPoint jp, Integer a, Integer b) {
		logger.info("AfterThrowing 실행");
	}
}
package kr.co.greenart.math;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/math")
public class MathController {
	private static final Logger logger = LoggerFactory.getLogger(MathController.class);
	
	@Autowired
	private MathService service;
	
	@GetMapping("/add")
	public @ResponseBody Integer add(@RequestParam(required = false) Integer a
			, @RequestParam(required = false) Integer b) {
		logger.info("핸들러 메소드가 실행되는 시점");
		return service.add(a, b);
	}
}
package kr.co.greenart.math;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class MathService {
	private static final Logger logger = LoggerFactory.getLogger(MathService.class);
	
	public Integer add(Integer a, Integer b) {
		logger.info("서비스 객체의 더하기 메소드가 호출되는 시점");
		return a + b;
	}
}