본문으로 바로가기

Spring Boot - (5) AOP 설정

category Backend/Spring 2020. 11. 26. 15:28
728x90
반응형

이전 포스팅에서 logback 설정까지 했었다.

이번에는 AOP 설정을 해볼 것이다.


AOP

AOP 관련하여 역시나 잘 정리된 갓대희 님의 포스팅 글을 참고하여 재정리 해보도록 하겠다.

결론을 먼저 말하자면 자바 웹 개발을 할 때 공통적으로 처리해야하는 부분에 대해 별도로 관리하며, Life Cycle에 따라 부분적으로 원하는 기능을 실행할 수 있다.

공통적으로 처리하는 방식에는 3가지가 있다.

  1. Filter
  2. Interceptor
  3. AOP

흐름은 Filter → Interceptor → AOP → Interceptor → Filter 으로 흘러가며 이미지로 확인하면 다음과 같다.

  • Interceptor와 Filter는 Servlet 단위에서 실행된다. <> 반면 AOP는 메소드 앞에 Proxy패턴의 형태로 실행된다.
  • 실행순서를 보면 Filter가 가장 밖에 있고 그안에 Interceptor, 그안에 AOP가 있는 형태이다.

AOP는 OOP를 보완하기 위해 나온 개념으로 객체 지향의 프로그래밍을 했을 때 중복을 줄일 수 없는 부분을 줄이기 위해 종단면(관점)에서 바라보고 처리한다.

주로 '로깅', '트랜잭션', '에러 처리'등 비즈니스단의 메서드에서 조금 더 세밀하게 조정하고 싶을 때 사용한다.

Interceptor나 Filter와는 달리 메소드 전후의 지점에 자유롭게 설정이 가능하다.

Interceptor와 Filter는 주소로 대상을 구분해서 걸러내야하는 반면, AOP는 주소, 파라미터, 애노테이션 등 다양한 방법으로 대상을 지정할 수 있다.

AOP의 Advice와 HandlerInterceptor의 가장 큰 차이는 파라미터의 차이다.

Advice의 경우 JoinPoint나 ProceedingJoinPoint 등을 활용해서 호출한다.

반면 HandlerInterceptor는 Filter와 유사하게 HttpServletRequest, HttpServletResponse를 파라미터로 사용한다.

AOP의 포인트컷

  • @Before: 대상 메서드의 수행 전
  • @After: 대상 메서드의 수행 후
  • @After-returning: 대상 메서드의 정상적인 수행 후
  • @After-throwing: 예외발생 후
  • @Around: 대상 메서드의 수행 전/후

예제

aspectj 추가하기

기존 pom.xml에 아래를 추가한다.

<!-- aspectj -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

ApageSessionAspect.java

예제 내용은 로그인 후 접속자의 IP를 Console에 찍어보는 내용이다.

package com.sample.common.aop;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ApageSessionAspect {

    protected final Logger log = LoggerFactory.getLogger(getClass());

    // 어디서 관여할 것인지
    // execution을 이용하여 추가
    // 예제는 apage 폴더 내 Controller에 대해서 접근한다는 의미
    @Pointcut("execution(public * com.sample.apage..*Controller.*(..))")
    private void apageSessionAspect() {
    }

    @Around(value = "apageSessionAspect() ")
    public Object apageSessionInterceptor(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest request = null;
        HttpServletResponse response = null;

        String[] exceptionUrl = { "/WEB-INF/", "/apage.do", "/apage/adminLoginJson.do" };

        for (Object o : joinPoint.getArgs()) {
            if (o instanceof HttpServletRequest) {
                request = (HttpServletRequest) o;
            } else if (o instanceof HttpServletResponse) {
                response = (HttpServletResponse) o;
            }
        }
        // controller에 request, response 가 없을 경우
        if (request == null || response == null) {
            return joinPoint.proceed();
        }

        String getUrl = request.getRequestURI();

        // 세션 예외 URL
        for (int i = 0; i < exceptionUrl.length; i++) {

            if (getUrl.indexOf(exceptionUrl[i]) > -1) {
                return joinPoint.proceed();
            }
        }

        System.out.println("ip >>> " + getClientIpAddr(request));

        return joinPoint.proceed();
    }

    private String getClientIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

}

실행결과

ip >>> 127.0.0.1 이라고 잘 찍혀나왔다.


2022-05-15 Update

Spring은 기본적으로 JDK Dynamic Proxy 를 제공하고있었다. 최신 스프링 버전에서는 CGLib를 Proxy로 제공한다.
이전에는 왜 CGLib를 채택하지 못했었는지는 아래의 3가지 이유가 있다.

  • net.sf.cglib.proxy.Enhancer 의존성 추가
  • default 생성자
  • 타깃의 생성자 두 번 호출

첫 번째는 Spring에서 기본적으로 지원하지 않는 방식이었기 때문에 별도로 의존성을 추가하여 개발했다. 그다음으론 CGLib을 구현하기 위해선 반드시 파라미터가 없는 default 생성자가 필요했고, 생성된 CGLib Proxy의 메소드를 호출하게 되면 타깃의 생성자가 2번 호출된다는 단점이 존재하였는데 현재는 아래 이미지 처럼 개선되어 안정화되었기 때문에 Spring Boot 에서도 기본적으로 CGLib로 사용한다.


특징이 각각 어떤 것인지는 아래와 같다.

JDK Dynamic Proxy

  • 타겟이 하나 이상의 interface 를 구현하고 있는 Class 인 경우
  • Java의 Reflection 패키지에 존재하는 Proxy 클래스를 통해 생성된 객체
  • 인터페이스 기준으로 DI(Dependency Injection)를 받아줘야함

CGLib

  • 타겟이 interface 를 구현하지 않은 Class 인 경우 (final이면 호출불가)
  • 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리
  • 성능 차이의 근본적인 이유는 CGLib는 타겟에 대한 정보를 제공받아 바이트코드 조작하여 Proxy를 생성해주기 때문에 JDK Dynamic Proxy 보다 좋음

Previous Chapter


참고
JDK Dynamic Proxy와 CGLib의 차이점

728x90
반응형