Spring AOP 란, 관점 지향 프로그래밍(Aspect Oriented Programming)의 약자로 일반적으로 사용하는 클래스(Service, Dao 등) 에서 중복되는 공통 코드 부분(commit, rollback, log 처리)을 별도의 영역으로 분리해 내고, 코드가 실행 되기 전이나 이 후의 시점에 해당 코드를 붙여 넣음으로써 소스 코드의 중복을 줄이고, 필요할 때마다 가져다 쓸 수 있게 객체화하는 기술을 말한다.
Aspect 관점
Oriented 지향
Programming 프로그래밍
-> 중복되는 공통 코드 부분을 별도의 영역으로 분리
AOP 없는 상황 (흩어진 관심사, 공통 기능)
-> 여러 서비스 메서드에 로깅 추가
-> 클라스에 다 추가해줘야함
AOP 사용 시
log 찍는 하나의 관심사를 Aspect
Aspect를 Controller, Service 클래스 명을 가진 모든 메서드에 log를 붙여주겠다고 할 수 있음

횡단 관점
controller service db 갔다가 다시 돌아오고..
중간 중간에 내가 하고 싶은 AOP 기능을 쓸 수 있음
공통되는 부분 == 관심사 == aspect

공통되는 부분을 따로 빼내어 작성하는 메소드를 Advice라고 이야기 하며, Advice를 적용될 수 있는 모든 관점(시점, 메소드)을 JoinPoint, JoinPoint 중 실제 Advice를 적용할 부분을 Pointcut, 그리고 그 시점에 공통 코드를 끼워 넣는 작업을 Weaving 이라고 말한다.
중간 중간 끼어들어서 이거 어떻게 해라 저렇게 해라 충고나 조언을 던지는 거 Advice 이면서 JoinPoint
advice 행위 자체
joinpoint 끼어드는 지점
JoinPoint 중에 딱 하나 골라낸 거 PointCut
갔다가 집어 넣는 행위 Weaving (개념적 존재)
“ Advice + Pointcut = Aspect “
실제로 동작 코드를 의미하는 Advice와 작성한 Advice가 실제로 적용된 메소드인 Pointcut을 합친 개념으로 부가기능(로깅, 보안, 트랜잭션 등)을 나타내는 공통 관심사에 대한 추상적인 명칭.
(여러 객체에 공통으로 적용되는 부가기능을 작성한 클래스 나타냄)
AOP 개념을 적용하면 핵심기능 코드 사이에 끼어있는 부가기능을 독립적인 요소로 구분해 낼 수 있고, 이렇게 구분된 부가기능 Aspect는 런타임 시에 필요한 위치에 동적으로 참여하게 할 수 있다.



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

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

Spring은 동적 프록시를 기반으로 AOP를 구현하기 때문에 메소드 조인포인트만 지원한다.
즉, 핵심기능(대상 객체)의 메소드가 호출되는 런타임 시점에만 부가기능(어드바이스)을 적용할 수 있다.
하지만, AspectJ 같은 고급 AOP 프레임워크를 사용하면 객체의 생성, 필드 값의 조회와 조작,
static 메소드 호출 및 초기화 등의 다양한 작업에 부가기능을 적용할 수 있다.



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

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class AroundLog {
public Object aroundLog(ProceedingJoinPoint pp) throws Throwable{
String methodName = pp.getSignature().getName();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object obj = pp.proceed();
stopWatch.stop();
System.out.println(methodName + "() 메소드 수행에 걸린 시간 : " +
stopWatch.getTotalTimeMillis() + "(ms)초");
return obj;
}
}
spring AOP 검색
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
AspectJ 에서 사용할 수 있게 해줌
refresh gradle 해서 다운로드 받아줌
단순히 Bean으로 등록 + Spring 관리
공통 관심사가 작성된 클래스임을 명시 (AOP 동작용 클래스임을 선언)
주석처리하면 AOP로서 동작 못함
execution( [접근제한자] 리턴타입 클래스명 메서드명 ([파라미터]) )
클래스명은 패키지명부터 모두 작성해줘야함
접근제한자, 파라미터는 생략 가능
TestAspect 클래스
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Aspect
@Slf4j
public class TestAspect {
@Before("execution(* edu.kh.project..*Controller*.*(..))")
public void testAdvice() {
log.info("----------- testAdvice() 수행됨 -----------");
}
}
-> 접근제한자 생략
return 타입 * 모든 return type
패키지명 edu.kh.project 하위 패키지 이하 모든 클래스 대상으로
패키지 뒤에 .. 0개 이상의 하위 패키지
edu.kh.project.. 0개 이상의 하위 패키지 모든 클래스 대상
*Controller* Controller 문자 포함한 모든 클래스를 대상으로 한다.
.* 모든 메서드
메서드 부를 때 class명.메서드명
(..) 메서드의 파라미터를 나타냄 ..은 0개 이상을 나타냄
0개 이상의 파라미터
-> 메서드에 파라미터가 있든 없든
TestAspect 클래스
@After("execution(* edu.kh.project..*Controller*.*(..))")
public void controllerEnd(JoinPoint jp) {
// JoinPoint : AOP 기능이 적용된 대상
// AOP가 적용된 클래스 이름 얻어오기
// .getSimpleName() 클래스 이름만 가져온 거
String className = jp.getTarget().getClass().getSimpleName(); // ex) MainController
// 실행된 컨트롤러 메서드 이름을 얻어오기
String methodName = jp.getSignature().getName(); // mainPage 메서드
log.info("--------------- {}.{} 수행 완료 ---------------", className, methodName);
}
로그 확인
--------------- MainController.mainPage 수행 완료 ---------------
pointcut bundle
따로 클래스로 빼놓고 쉽게 이름만 불러서 사용할 bundle 생성
common.aop 패키지 안에 PointcutBundle 생성
import org.aspectj.lang.annotation.Pointcut;
// Pointcut 모아두는 클래스
// Bundle 묶음
// Pointcut : 실제 advice 가 적용될 지점
public class PointcutBundle {
// 작성하기 어려운 Pointcut 을 미리 작성해놓고
// 필요한 곳에서 클래스명.메서드명() 으로 호출해서 사용 가능
// ex) @After("execution(* edu.kh.project..*Controller*.*(..))")
// ==> @After("PointcutBundle.controllerPointCut()")
@Pointcut("execution(* edu.kh.project..*Controller*.*(..))")
public void controllerPointCut() {}
@Pointcut("execution(* edu.kh.project..*ServiceImpl*.*(..))")
public void serviceImplPointCut() {}
}
PointcutBundle.controllerPointCut() 만 호출해서 사용하면 됨
접속자 IP 얻어오는 보조 메서드 작성
LoggingAspect
private 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;
}
컨트롤러 수행 전 로그 출력
클래스, 메서드, ip, 로그인한 멤버가 있을 경우 멤버 email
LoggingAspect
@Before("PointcutBundle.controllerPointCut()")
public void beforeController(JoinPoint jp) {
// AOP가 적용된 클래스 이름 얻어오기
String className = jp.getTarget().getClass().getSimpleName();
// 실행된 컨트롤러 메서드 이름을 얻어오기
String methodName = jp.getSignature().getName() + "()";
// 요청한 클라이언트 ip 얻어오기
// 요청한 클라이언트의 HttpServletRequest 객체 얻어오기
HttpServletRequest req =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
// Holder 는 현재 thread 에 연관된 helper 클래스
// current 현재 요청이랑 연관된 속성들 반환
// 반환 타입이 requestAttributes -> interface
// 실제로 구현이 안됨 interface를 상속 받아서 구현한 ServletRequestAttributes 로 캐스팅 한 것
// -> Http 요청 객체 얻어옴
// 매개변수에 HttpServletRequest 로 얻어올 수 없음 (Controller 가 아님)
// 클라이언트 ip 얻어오기
String ip = getRemoteAddr(req);
// log 에 보여줄 문자열 조합
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());
}
서비스 수행 전/후로 동작하는 코드(advice)
Throwable - 예외 처리의 최상위 클래스 (Exception도 잡고 Error 도 잡음)
Exception 의 부모 (예외 중 최상위)
Throwable 아래
Exception(예외) / Error(에러)
LoggingAspect 클래스
@Around("PointcutBundle.serviceImplPointCut()")
public Object aroundServiceImpl(ProceedingJoinPoint pjp) throws Throwable {
// @Before 부분
// 클래스명
String className = pjp.getTarget().getClass().getSimpleName();
// 메서드명
String methodName = pjp.getSignature().getName() + "()";
log.info("========== {}.{} 서비스 호출 =========", className, methodName);
// 파라미터 .getArgs() 매개변수 얻어오는 거
log.info("Parameter : {}", Arrays.toString(pjp.getArgs()));
// 서비스 코드 실행 시 시간 기록 (대상 메서드가 실행되기 전)
long startMs = System.currentTimeMillis();
Object obj = pjp.proceed(); // 기준으로 전 후 나뉨
// @After 부분
long endMs = System.currentTimeMillis();
log.info("Running Time : {}ms", endMs - startMs);
log.info("=============================================================");
return obj;
}
메서드가 예외를 던진 후에 실행되는 Advice 를 정의
@Transactional 이 어노테이션을 pointcut 으로 지정 이 어노테이션에서 예외가 발생했을 때 도는 코드
LoggingAspect 클래스
@AfterThrowing(pointcut = "@annotation(org.springframework.transaction.annotation.Transactional)",
throwing = "ex")
public void transactionRollback(JoinPoint jp, Throwable ex) {
// 트랜잭셔널에서 ex 로 던진 걸 매개변수로 받아옴
// 메서드 정보 jp
// 발생한 예외 객체 ex
log.info("*** 트랜잭션이 롤백됨 {} ***", jp.getSignature().getName());
log.error("[롤백 원인] : {}", ex.getMessage());
}