Spring AOP (23.09.01)

·2023년 9월 1일
0

Spring

목록 보기
31/36
post-thumbnail

🌷 Spring AOP

관점 지향 프로그래밍(Aspect Oriented Programming)의 약자

일반적으로 사용하는 클래스(Service, Dao 등) 에서 중복되는 공통 코드 부분(commit, rollback, log 처리)을 별도의 영역으로 분리해 내고,
코드가 실행 되기 전이나 이후의 시점에 해당 코드를 붙여 넣음으로써
소스 코드의 중복을 줄이고, 필요할 때마다 가져다 쓸 수 있게 객체화하는 기술

위 사진과 같이 공통되는 부분을 따로 빼내어 필요한 시점에 해당 코드르 추가해 주는 기술을 의미한다.

🌼 구조


🌼 동작 구조


🌼 관련 용어

🌱 Aspect

(Advice + Pointcut = Aspect)

  • 실제로 동작 코드를 의미하는 Advice와 작성한 Advice가 실제로 적용된 메소드인 Pointcut을 합친 개념
  • 부가 기능(로깅, 보안, 트랜잭션 등)을 나타내는 공통 관심사에 대한 추상적인 명칭
    (여러 객체에 공통으로 적용되는 부가 기능을 작성한 클래스를 나타냄)

AOP 개념을 적용하면 핵심기능 코드 사이에 끼어있는 부가 기능을 독립적인 요소로 구분해 낼 수 있고, 이렇게 구분된 부가 기능 Aspect는 런타임 시에 필요한 위치에 동적으로 참여하게 할 수 있다.

🌱 Advice

  • 여러 객체에 공통으로 적용되는 기능을 분리하여 작성한 클래스
  • JoinPoint에 삽입되어 동작될 코드, 메소드

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

🌱 JoinPoint

  • Advice가 적용될 수 있는 모든 관점(시점, 메소드)
  • 객체(인스턴스) 생성 지점, 메소드 호출 시점, 예외 발생 시점 등 특정 작업이 시작되는 시점

🌱 Pointcut

  • JoinPoint 부분 집합
  • 실제 Advice가 적용되는 부분

🌱 Weaving

  • 작성한 Advice (공통 코드)를 핵심 로직 코드에 삽입
  • 그 시점에 공통 코드를 끼워 넣는 작업

Weaving을 하는 경우내용
컴파일 시 위빙컴파일 시 AOP가 적용된 클래스 파일이 새로 생성 (AspectJ)
클래스 로딩 시 위빙JVM에서 로딩한 클래스의 바이트 코드를 AOP가 변경하여 사용
런타임 시 위빙클래스 정보 자체를 변경하지 않고, 중간에 프록시를 생성하여 경유 (Spring)

🌱 Introduction

  • 정적인 방식의 AOP 기술

🌱 Proxy

  • 대상 객체에 Advice가 적용된 후 생성되는 객체

🌱 Target Object

  • Advice를 삽입할 대상 객체

Advice와 JoinPoint는 해당 포스팅 아래에서 좀 더 자세히 살펴보도록 하자!

🌼 특징

1. Spring은 프록시(Proxy) 기반 AOP를 지원한다.

Spring은 대상 객체(Target Object)에 대한 프록시를 만들어 제공하며,
타겟을 감싸는 프록시는 Server Runtime 시에 생성된다.

이렇게 생성된 프록시는 대상 객체를 호출할 때 먼저 호출되어
Advice의 로직을 처리 후 대상 객체를 호출한다.

2. Proxy는 대상 객체의 호출을 가로챈다. (Intercept)

Proxy는 그 역할에 따라 대상 객체에 대한 호출을 가로챈 다음,
Advice의 부가 기능 로직을 수행하고 난 후에 타겟의 핵심 기능 로직을 호출하거나, (전처리 Advice)
타겟의 핵심 기능 로직 메소드를 호출한 후에 Advice의 부가 기능을 수행한다. (후처리 Advice)

3. Spring AOP는 메소드 조인포인트만 지원한다.

Spring은 동적 프록시를 기반으로 AOP를 구현하기 때문에 메소드 조인포인트만 지원한다.
즉, 핵심 기능(대상 객체)의 메소드가 호출되는 런타임 시점에만 부가 기능(어드바이스)을 적용할 수 있다.

하지만, AspectJ 같은 고급 AOP 프레임워크를 사용하면 객체의 생성, 필드 값의 조회와 조작, static 메소드 호출 및 초기화 등의 다양한 작업에 부가 기능을 적용할 수 있다.


🌼 구현 방식

🌱 XML 기반의 aop 네임스페이스를 통한 AOP 구현

  1. 부가 기능을 제공하는 Advice 클래스를 작성한다.
  2. XML 설정 파일<aop:config>를 이용해서 Aspect를 설정한다.
    (즉, 어드바이스와 포인트컷을 설정)

💡 Advice 정의 태그

태그 이름의미
<aop:before>메소드 실행 전에 적용되는 어드바이스를 정의
<aop:around>메소드 호출 이전, 이후, 예외 발생 등 모든 시점에 적용 가능한 어드바이스를 정의
<aop:after>메소드가 정상적으로 실행되는지 또는 예외를 발생시키는지 여부에 상관 없는 어드바이스를 정의
<aop:after-returning>메소드가 정상적으로 실행된 후에 적용되는 어드바이스를 정의
<aop:after-throwing>메소드가 예외를 발생시킬 때 적용되는 어드바이스를 정의
(try-catch 블록에서 catch 블록과 유사함)

🌱 @Aspect 어노테이션 기반의 AOP 구현

  1. @Aspect 어노테이션을 이용해서 부가기능을 제공하는 Aspect 클래스를 작성한다.
    (이 때, Aspect 클래스는 어드바이스를 구현하는 메소드와 포인트컷을 포함한다.)
  2. dispatcher-servlet으로 지정된 XML 설정 파일에 <aop:aspect-autoproxy />를 설정한다.

💡 Advice 정의 어노테이션

태그 이름의미
@Before("pointcut")타겟 객체의 메소드가 실행되기 전에 호출되는 어드바이스
-> JoinPoint를 통해 파라미터 정보를 참조할 수 있음
@Around("pointcut")타겟 객체의 메소드 호출 전과 후에 실행될 코드를 구현할 어드바이스
@After("pointcut")타겟 객체의 메소드가 정상 종료되었을 때와 예외가 발생했을 때 모두 호출되는 어드바이스
-> 반환 값을 받을 수 없음
@AfterReturning(Pointcut="", Returning="")타겟 객체의 메소드가 정상적으로 실행을 마친 후에 호출되는 어드바이스
-> 리턴 값을 참조할 때는 returning 속성에 리턴 값을 저장할 변수 이름을 지정해야 함
@AfterThrowing(Pointcut="", throwing="")타겟 객체의 메소드에서 예외가 발생하면 호출되는 어드바이스
-> 발생된 예외를 참조할 때는 throwing 속성에 발생한 예외를 저장할 변수 이름을 지정해야 함


🌼 Advice

🌱 종류

💡 Before Advice

타겟의 메소드가 실행되기 이전 (before) 시점에 처리해야 할 필요가 있는 부가 기능 정의
(JoinPoint 앞에서 실행되는 Advice)

💡 Around Advice

타겟의 메소드가 호출되기 이전 (before) 시점과 이후 (after) 시점에 모두 처리해야 할 필요가 있는 부가 기능 정의
(JoinPoint 앞과 뒤에서 실행되는 Advice)

💡 After Returning Advice

타겟의 메소드가 정상적으로 실행된 이후 (after) 시점에 처리해야 할 필요가 있는 부가 기능 정의
(JoinPoint 메소드 호출이 정상적으로 종료된 뒤에 실행되는 Advice)

💡 After Throwing Advice

타겟의 메소드에 예외가 발생된 이후 (after) 시점에 처리해야 할 필요가 있는 부가 기능 정의
(예외가 던져질 때 실행되는 Advice)

🌼 JoinPoint

Spring AOP 혹은 AspectJ에서 AOP의 부가 기능을 지닌 코드가 적용되는 지점

🌱 JoinPoint Interface

모든 어드바이스는 org.aspectj.lang.JoinPoint 타입의 파라미터를 어드바이스 메소드의 첫 번째 매개변수로 선언해야 한다.

단, Around 어드바이스는 JoinPoint의 하위 타입인 ProceedingJoinPoint 타입의 파라미터를 필수적으로 선언해야 한다.

🌱 JoinPoint Interface 메소드

메소드 이름내용
getArgs()메소드의 매개 변수 반환
getThis()현재 사용 중인 프록시 객체 반환
getTarget()대상 객체 반환
getSignature()대상 객체 메소드의 설명(메소드명, 리턴 타입 등) 반환
toString()대상 객체 메소드의 정보 출력

대혼란의 Spring AOP 개념을 알아보았다.. 😮
이제 Spring에서 코드로 직접 작성해 보자.

👀 코드로 살펴보기

🌱 pom.xml

AOP 기능을 사용하기 위해서는 아래와 같이 pom.xml에 AspectJ 관련 라이브러리를 추가해야 한다.

...
		<!-- AspectJ : AOP 기능을 사용하기 위한 언어 문법 -->
		<!-- aspectjrt : AspectJ 런타임 프로그램 -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

		<!-- AspectJ Weaver : aspect의 정보를 바탕으로 aspect를 구성한 코드를 생성하는데 필요한 유틸리티 프로그램 -->
		<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>
...

🌱 servlet-context.xml

또한 servlet-context.xml에 AOP Proxy를 이용한 관점 제어 자동화를 위해 아래 코드를 추가 작성해 준다.

...
	<!-- AOP Proxy를 이용한 관점 제어 자동화 -->	
	<aop:aspectj-autoproxy/>

이번 포스팅에서는 서버에 접속한 유저의 IP 주소를 Spring 콘솔창에 출력해 볼 것이다.

Java는 OS에서 IPv4와 IPv6가 모두 사용 가능할 경우 IPv6를 우선적으로 사용한다. 그러나 IPv6 주소는 익숙하지 않고 보기에 어렵기 때문에 IPv4를 우선적으로 사용하게 설정해 보자.

해당 프로젝트의 서버 ▶️ Overview 탭 ▶️ General Information의 Open launch configuration 클릭 ▶️

Edit Configuration의 Arguments 탭 ▶️ VM arguments에 -Djava.net.preferIPv4Stack=true 작성
(하이픈 앞에 띄어쓰기 한 칸 꼭 써야 함!)


🌱 BeforeAspect.java

package edu.kh.project.common.aop;

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import edu.kh.project.member.model.dto.Member;

@Component
@Aspect // 공통 관심사가 작성된 클래스임을 지정
		// Pointcut(타겟 지정) + Advice(수행할 코드)
public class BeforeAspect {

	private Logger logger = LoggerFactory.getLogger(BeforeAspect.class);
	
	// execution( [접근제한자(생략가능)] 리턴타입 클래스명 메소드명 ([파라미터]) )
	//@Before("execution(* edu.kh.project..*Impl*.*(..))") // 타겟 수행 전
	
	// 지정된 클래스.메소드에 작성된 @Pointcut() 어노테이션의 내용을 타겟으로 삼음
	
	@Order(1) // 순서, 하나의 타겟에 대한 여러 advice 수행 시 순서 지정
	@Before("CommonPointcut.serviceImplPointcut()")
	public void beforeLog(JoinPoint jp) { // Advice(수행할 코드)
		
		// 매개변수 JoinPoint : AOP의 부가 기능이 적용된 대상의
		//					 객체, 메소드, 파라미터 정보를 얻을 수 있게 해 주는 객체
		
		// 대상 객체의 간단한 클래스명(패키지명 제외)
		String className = jp.getTarget().getClass().getSimpleName();
		
		// 메소드 선언부(== 메소드 시그니처)에서 메소드명만 얻어옴
		String methodName = jp.getSignature().getName();
		
		String str = "-------------------------------------\n";
		
		str += "[Start] : " + className + " - " + methodName + "()\n";
		// [Start] : MemberServiceImpl - login()
		
		// jp.getArgs() : 파라미터 묶음(배열)
		str += "[Parameter] : " + Arrays.toString(jp.getArgs()) + "\n";
		
		try {
	         // 접속자 IP 얻어오기
	         HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
	         Member loginMember = (Member)req.getSession().getAttribute("loginMember");
	         
	         str += "[ip]" + getRemoteAddr(req);
	         
	         if(loginMember != null) {
	            str += "(email:" + loginMember.getMemberEmail() + ")";
	         }
	         
		} catch (Exception e) {
	         str += "[스프링 스케쥴러]";
	    }
		
		logger.info(str);
	}
	
	public static String getRemoteAddr(HttpServletRequest request) {

        String ip = null;

        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.getHeader("X-Real-IP"); 
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("X-RealIP"); 
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getHeader("REMOTE_ADDR");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 
            ip = request.getRemoteAddr(); 
        }

      return ip;
   }
}

🌱 AfterAspect.java

package edu.kh.project.common.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

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

	@Order(3)
	@After("CommonPointcut.serviceImplPointcut()")
	public void afterLog() {
		logger.info("----------------------------------------\n\n");
	}
}

🌱 AroundAspect.java

package edu.kh.project.common.aop;


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

@Component
@Aspect
public class AroundAspect {
   
   // Logger : 로그를 작성할 수 있게하는 객체
   private Logger logger = LoggerFactory.getLogger(AroundAspect.class);
   
   // 전처리, 후처리를 모두 해결하고자 할 때 사용 하는 어드바이스
   // proceed() 메소드 호출 전  : @Before advice 작성
   // proceed() 메소드 호출 후  : @After advice 작성
   // 메소드 마지막에 proceed()의 반환값을 리턴해야함. 
   @Order(2) 
   @Around("CommonPointcut.serviceImplPointcut()")
   public Object aroundServiceLogs(ProceedingJoinPoint pp) throws Throwable {
      // @Around advice는 JoinPoint Interface가 아닌
      //  하위 타입인 ProceedingJoinPoint를 사용해야 함.
      
      
      long startMs = System.currentTimeMillis(); // 서비스 시작 시의 ms 값
      
      Object obj = pp.proceed(); // 여기가 기준
      
      long endMs = System.currentTimeMillis(); // 서비스 종료 시의 ms 값
      
      String str = "Running Time : " + (endMs- startMs) + "ms";   

      logger.info(str);
      
      return obj;
      
   }
   
}
profile
풀스택 개발자 기록집 📁

0개의 댓글