Spring Framework AOP (21) - Day 68

jaehoon ahn·2025년 12월 24일

Spring boot

목록 보기
19/20
post-thumbnail

Spring AOP 개요

Spring AOP(Aspect Oriented Programmed) - 관점지향 프로그램이란?

💡

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

예시

A Service + 로그 (log.info())
B Service + 로그 (log.info())
C Service + 로그 (log.info())

즉, log.info()라는 메서드를 각 A, B, C라는 메서드에 중복적인 코드를 작성안해도
어디 한군데에 작성을 해두면 일일이 호출하지 않아도 각 메서드가 동작할 때마다
log.info()를 원하는 시점에 호출할 수 있다.

=> 예시로 service단에 **Transactional(rollbackFor = Exception.class)**가 있다
=> AOP 기술이 적용되어 만들어졌음

  • Logging
    • 로그 찍기
  • Security
    • 보안적인 처리
  • 트랜잭션
    • 트랜잭션 처리

⇒ 즉, AOP란 공통되는 부분을 따로 빼내어 필요한 시점에 해당 코드를 추가해주는 기술

Spring AOP의 동작 구조

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

  • Target Object
    • 컨트롤러
  • Advice
    • 로그를 찍겠다, 보안처리를 해라 등 메서드를 지정하는 것
    • 즉, 지시를 내리는 역할
    • 공통된 관심사 코드를 작성하는 부분
  • JoinPoint
    • Advice가 적용될 수 있는 모든 관점, 시점을 의미
    • 즉, 클라이언트가 요청을 보내고 요청 응답을 보낼 때, 시점, 메서드를 의미하는 것
    • 로그를 찍겠다, 보안을 적용하겠다와 같이 Advise를 넣을 수 있는 대상들
  • PointCut
    • Advice를 실제로 적용시킬 지점, 시점
  • Weaving
    • PointCut에 Advice를 실제로 끼워넣는 작업

흐름

  • Client가 요청을 보내면서 모든게 시작됌. 클라이언트가 요청을 하면 컨트롤러가 먼저 받는게 아닌 AOP Proxy가 먼저 받음. 처리하고나서 Target Object로 도달.

⇒ 프록시란? 전달자 역할, 즉 로그를 찍고 싶을 때, Target Object로 가기전에 프록시가 먼저 받아서 로그를 먼저 수행, 그리고 Target Object 로그인 컨트롤러, 서비스, DAO등을 수행

처리가 된 이후에도 프록시에 설정된 값이 있다면 수행됌

⇒ 보안적인 응답을 추가하고싶다면 프록시가 클라이언트에게 전달

Aspect 란?

“ Advice + Pointcut = Aspect “

  • 실제로 동작 코드를 의미하는 Advice와 작성한 Advice가 실제로 적용된 메소드인 Pointcut을 합친 개념으로 부가기능(로깅, 보안, 트랜잭션 등)을 나타내는 공통 관심사에 대한 추상적인 명칭. (여러 객체에 공통으로 적용되는 부가기능을 작성한 클래스 나타냄)
  • AOP 개념을 적용하면 핵심기능 코드 사이에 끼어있는 부가기능을 독립적인 요소로 구분해 낼 수 있고,
    이렇게 구분된 부가기능 Aspect는 런타임 시에 필요한 위치에 동적으로 참여하게 할 수 있다.

Spring AOP 핵심 용어

  • Around
    • Before, After를 합친 것

Spring AOP 구조 정리

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 메소드 호출 및 초기화 등의 다양한 작업에 부가기능을 적용할 수 있다.

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 />를 설정한다.

Advice를 정의하는 태그


Advice의 종류

JointPoint Interface

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

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

JoinPoint Interface 메서드


실습

AOP는 모듈을 추가해줘야 사용할 수 있음

https://mvnrepository.com/

⇒ 본인의 spring 프로젝트 버전과 맞는걸로 선택

  • Compile, Groovy Short 선택

⇒ build.gradle에 dependencies에 추가

⇒ 끝에 버전을 지워도 Spring이 알아서 설치를 해준다. (지워도, 안지워도 상관 X)

TestAspect.java

@Before(포인트컷)

package edu.kh.project.common.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/*
 * AOP(Aspect-Oriented Programming) : 분산되어 있는 관심사/관점을 모듈화 시키는 기법
 * - 주요 비즈니스 로직과 관련이 없는 부가적인 기능을 추가할 때 유용
 * ex) 코드 중간중간 로그 찍을 때, 트랜잭션 처리하고 싶을 때, 보안 처리 추가 등...
 * 
 * 주요 어노테이션
 * - @Aspect : Aspect를 정의하는데 사용되는 어노테이션으로, 클래스 상단에 작성함
 * => 즉, 공통된 관심사가 모여있는 클래스에 붙이라는 것
 * - @Before(포인트컷) : 대상 메서드(포인트컷), 즉 이곳에 advice를 추가하겠다라고 지정한 joinPoint중 하나
 * => 대상 메서드(포인트컷) 실행 전에 Advice를 실행함.
 * - @After(포인트컷) : 대상 메서드 실행 후에 Advice를 실행함.
 * - @Around(포인트컷) : 대상 메서드 실행 전/후로 Advice를 실행함 (@Before + @After)
 * */

@Component // bean 등록
@Aspect // 공통 관심사가 작성된 클래스임을 명시(AOP 동작용 클래스)
@Slf4j // log 찍을 수 있는 객체(Logger) 생성 코드를 추가
public class TestAspect {
	// Advice : 끼워 넣을 코드(메서드)
	// PointCut : 실제로 Advice가 적용될 JoinPoint(여러 지점) 특점 시점을 지정
	
	// * 클래스명은 패키지명부터 모두 작
	
	// execution(* edu.kh.project..*Controller*.*(..))
	// execution : 메서드 실행 지점을 가리키는 키워드
	// * : 모든 리턴타입을 나타냄
	// => 맨 앞 *은 리턴타입을 말함
	// edu.kh.project : 3레벨 패키지명을 나타냄
	// .. : 0개 이상의 하위 패키지를 나타냄
	// -> 즉, edu.kh.project 하위에 Controller를 찾겠다는 의미
	// *Controller* : 이름에 "Controller" 라는 문자열을 포함하는 모든 클래스를 대상으로 함
	// .* : 모든 메서드를 나타냄
	// (..) : 0개 이상의 파라미터를 나타냄
	// -> 즉, edu.kh.project 하위에 모든 Controller에 모든 메서드를 의미
	// @Before는 edu.kh.project 하위에 모든 Controller에 모든 메서드가 수행되기 전에
	// advice를 수행시키겠다는 의미
	
	// @Before(포인트컷)
	@Before("execution(* edu.kh.project..*Controller*.*(..))")
	public void testAdvice() {
		log.info("-----------------testAdvice() 수행됨-----------------");
	}
}

  • 현재 boardProject에서는 localhost로 가게되면, MainController가 main으로 forward하는 컨트롤러가 정의되어 있기 때문에, AOP가 수행된 것

@After(포인트컷)

// 컨트롤러의 모든 메서드가 수행되고 난 이후에 수행되게끔 PointCut을 지정
	@After("execution(* edu.kh.project..*Controller*.*(..))")
	public void controllerEnd(JoinPoint jp) {
		// JoinPoint : AOP 기능이 적용된 대상
		// AOP가 적용된 클래스 이름을 얻어오기
		String className = jp.getTarget().getClass().getSimpleName(); 
		// ex) MainController, MemberController.. 이런 형식으로 전달됌
		
		// 실행된 컨트롤러의 메서드명 얻어오기
		String methodName = jp.getSignature().getName(); // ex) mainPage(), login...
		
		log.info("---------{}.{} 수행 완료---------", className, methodName);
	}

AOP 응용

LoggingAspect.java

package edu.kh.project.common.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
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;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

@Component // 빈 등록
@Aspect // 관심사 코드가 작성된 클래스를 의미
// @Aspect가 있어야 AOP로써 역할을 할 수 있음
@Slf4j
public class LoggingAspect {
	/**
	 * 컨트롤러 수행 전 로그 출력(어떤 클래스인지/어떤 메서드인지/어떤 IP로부터 들어오는 요청인지)
	 */
	@Before("PointcutBundle.controllerPointCut()")
	public void beforeController(JoinPoint jp) {

		String className = jp.getTarget().getClass().getSimpleName();
		String methodName = jp.getTarget().getClass().getSimpleName();

		// ---------------------------------------------------------
		// 요청한 클라이언트 ip 로그 출력

		HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
				.getRequest();
		// 클라이언트 ip 얻어오기
		String ip = getRemoteAddr(req);

		StringBuilder sb = new StringBuilder();

		sb.append(String.format("[%s.%s] 요청 / ip : %s", className, methodName, ip));

		// 로그인 상태인 경우
		if (req.getSession().getAttribute("loginMember") != null) {
			String memberEmail = ((Member) req.getSession().getAttribute("loginMember")).getMemberEmail();
			
			sb.append(String.format(", 요청 회원 : %s", memberEmail));
		}
		log.info(sb.toString());
	}

	/**
	 * 접속자 IP 얻어오는 메서드
	 *
	 * @param request
	 * @return ip
	 */
	private String getRemoteAddr(HttpServletRequest request) {
		// 클라이언트(사용자)의 실제 IP 주소를 찾아내기 위한 로직
		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;
	}
}

PointcutBundle.java

package edu.kh.project.common.aop;

import org.aspectj.lang.annotation.Pointcut;

// Pointcut을 모아두는 클래스
public class PointcutBundle {
	// 작성하기 어려운 Pointcut을 미리 작성해두고
	// 필요한 곳에서 클래스명.메서드명() 으로 호출해서 사용
	
	// @Before("execution(* edu.kh.project..*Controller*.*(..))")
	// == @Before("PointcutBundle.controllerPointCut()")
	@Pointcut("execution(* edu.kh.project..*Controller*.*(..))")
	public void controllerPointCut() {}

	@Pointcut("execution(* edu.kh.project..*ServiceImpl*.*(..))")
	public void serviceImplPointCut() {}
}

ServiceImpl

/**
	 * 서비스 수행 전/후로 동작하는 코드(클래스/메서드/파라미터/실행시간)
	 * 
	 * @return
	 */
	@Around("PointcutBundle.serviceImplPointCut()")
	public Object aroundServiceImpl(ProceedingJoinPoint pjp) throws Throwable {
		// Throwable - 예외 처리의 최상위 클래스
		// Exception은 Throwable의 자식
		// Throwable의 주요 자식으로는
		// Exception (예외) - 개발자가 처리할 수 있는 문제
		// Error (오류) - 시스템 레벨의 심각한 문제, ex) 하드가 고장이 난 상황

		String className = pjp.getTarget().getClass().getSimpleName();
		String methodName = pjp.getTarget().getClass().getSimpleName();

		log.info("=============={}.{} 서비스 호출==============", className, methodName);

		// 파라미터를 로그로 출력
		log.info("Parameter : {}", Arrays.toString(pjp.getArgs()));

		// 서비스 주요 비즈니스 로직 코드 실행 소요 시간 기록
		long startMs = System.currentTimeMillis();

		// ----------------------------------------
		// 실제 Service 메서드 실행
		Object obj = pjp.proceed(); // 전/후를 나누는 기준점

		long endMs = System.currentTimeMillis();

		log.info("RunningTime : {}ms", endMs - startMs);
		log.info("====================================");

		return obj;
	}

0개의 댓글