Spring AOP

oYJo·2025년 3월 12일

Spring

목록 보기
10/29

Spring AOP

1️⃣ Spring AOP 개요

Spring AOP 란 ?

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

  • 위 이미지와 같이 공통되는 부분을 따로 빼내어 필요한 시점에 해당코드를 추가해주는 기술

2️⃣ Spring AOP 구조

Spring AOP의 동작 구조

  • 공통되는 부분을 따로 빼내어 작성하는 메소드를 Advice라고 이야기 하며, Advice를 적용될 수 있는 모든 관점(시점, 메소드)을 JoinPoint, JoinPoint 중 실제 Advice를 적용할 부분을 Pointcut 그리고 그 시점에 공통 코드를 끼워 넣는 작업을 Weaving 이라고 말한다.

AOP Proxy
앞뒤로 넣어서 proxy가 실행해준다
공통된 부분을 따로 빼내어 작성하는 메소드 : Advice


Adivce를 적용 모든 관점(시점, 메소드) : JoinPoing
JoinPoint 중 실제 Adivce적용 부분 : Pointcut

공통 코드를 끼워 넣는 작업 : Weaving


3️⃣ Spring AOP 용어

Aspect 란 ?

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

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

Spring AOP 핵심 용어


4️⃣ Spring AOP 구조- 참고

Spring AOP 구조 정리


Spring AOP 특징 & 구현 방식

1️⃣ Spring AOP 특징

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

Spring은 대상 객체(Target Object)에 대한 프록시를 만들어 제공하며, 타겟을 감싸는 프록시는 Server Runtime 시에 생성된다.
이렇게 생성된 프록시는 대상 객체를 호출 할 때 먼저 호출되어 Advice의 로직을 처리 후 대상 객체를 호출한다.

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

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

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

Spring은 동적 프록시를 기반으로 AOP를 구현하기 때문에 메소드 조인포인트만 지원한다.

즉, 핵심기능(대상 객체)의 메소드가 호출되는 런타임 시점에만 부가기능(어드바이스)을 적용할 수 있다.

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


2️⃣ Spring AOP 구현 방식

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

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

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

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

3️⃣ Spring AOP 구현 방식- XML

Advice를 정의하는 태그


4️⃣ Spring AOP 구현 방식- Annotation

Advice를 정의하는 어노테이션


Advice 작성하기

1️⃣ Advice

Advice의 종류


2️⃣ JoinPoint

JoinPoint Interface

JoinPoint는 Spring AOP 혹은 AspectJ에서 AOP의 부가기능을 지닌 코드가 적용되는 지점을 뜻하며, 모든 어드바이스는 org.aspectj.lang.JoinPoint 타입의 파라미터를 어드바이스 메소드의 첫 번째 매개변수로 선언해야 한다.
단, Around 어드바이스는 JoinPoint의 하위 타입인 ProceedingJoinPoint 타입의 파라미터를
필수적으로 선언해야 한다.

JoinPoint Interface 메소드

전체코드

✔️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>
...

트랜잭션을 위해서 이미 설정되어 있긴 하다


위에는 전에 사용할 떄
아래는 weaving 맘대로 지정한 곳에 넣게 해주는 것

✔️servlet-context.xml

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

✔️project(common(aop)) → 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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;

import edu.kh.project.member.model.dto.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.ServletRequestAttributes;

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

	// execution( [접근제한자(생략가능)] 리턴타입 클래스명 메소드명 ([파라미터]) )
	// @Before("execution(* edu.kh.project..*Impl*.*(..))") // 타겟 수행 전
	// -> 너무 복잡해서 불편하다

	@Order(1) // 순서, 하나의 타겟에 대한 여러 advice 수행 시 순서 지정
	// AOP 코딩 순서를 정해줌
	// 지정된 클래스.메소드에 작성된 @Pointcut() 어노테이션의 내용을 타겟으로 함
	@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 += "[스프링 스케쥴러]";
		}

		log.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;
	}

}

Aspect = pointer

public void beforeLog(JoinPoint jp) { // Advice(수행할 코드)
		// 매개변수 JoinPoint : AOP의 부가 기능이 적용된 대상의 객체, 메소드, 파라미터 정보를 얻을 수 있게 해주는 객체
		// 대상 객체의 간단한 클래스명(패키지명 제외) 얻어오기
		String className = jp.getTarget().getClass().getSimpleName();
		// 메소드 선언부(== 메소드 시그니처)에서 메소드명만 얻어오기
		String methodName = jp.getSignature().getName();

시그니처라고 부름

// execution( [접근제한자(생략가능)] 리턴타입 클래스명 메소드명 ([파라미터]) )
	// @Before("execution(* edu.kh.project..*Impl*.*(..))") // 타겟 수행 전
	// -> 너무 복잡해서 불편하다

✔️project(common(aop)) → CommonPointcut.java

package edu.kh.project.common.aop;

import org.aspectj.lang.annotation.Pointcut;

// Poitncut을 모아둘 클래스
public class CommonPointcut {
	
	@Pointcut("execution(* edu.kh.project..*Impl*.*(..))")
	public void ServiceImplPointcut() {}

}

불편해서 만든 것

✔️project(common(aop)) → 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");
	}
}

✔️project(common(aop)) → 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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Slf4j
public class AroundAspect {
	
	@Order(2)
	// 굳이 처리해줘야하나? before, after 썼는데?
	// before가 여러개가 있을 때 효과가 있는 것
	// 같은 친구들 순서를 지정할 때 사용하는 것
	
	// 전처리, 후처리를 모두 해결하고자 할 때 사용하는 어드바이스
	// proceed() 메소드 호출 전 : @Before advice 작성
	// proceed() 메소드 호출 후 : @After advice 작성
	// 메소드 마지막에 proceed()의 반환값을 리턴해야함
	@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";   

	      log.info(str);
	      
	      return obj;
	      
	   }

}

profile
Hello! My Name is oYJo

0개의 댓글