
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 기술이 적용되어 만들어졌음

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

공통되는 부분을 따로 빼내어 작성하는 메소드를 Advice라고 이야기 하며, Advice를 적용될 수 있는 모든 관점(시점, 메소드)을 JoinPoint, JoinPoint 중 실제 Advice를 적용할 부분을 Pointcut, 그리고 그 시점에 공통 코드를 끼워 넣는 작업을 Weaving 이라고 말한다.
흐름
⇒ 프록시란? 전달자 역할, 즉 로그를 찍고 싶을 때, Target Object로 가기전에 프록시가 먼저 받아서 로그를 먼저 수행, 그리고 Target Object 로그인 컨트롤러, 서비스, DAO등을 수행
처리가 된 이후에도 프록시에 설정된 값이 있다면 수행됌
⇒ 보안적인 응답을 추가하고싶다면 프록시가 클라이언트에게 전달
“ Advice + Pointcut = 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 타입의 파라미터를
필수적으로 선언해야 한다.

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

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

⇒ build.gradle에 dependencies에 추가

⇒ 끝에 버전을 지워도 Spring이 알아서 설치를 해준다. (지워도, 안지워도 상관 X)
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() 수행됨-----------------");
}
}

// 컨트롤러의 모든 메서드가 수행되고 난 이후에 수행되게끔 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);
}

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;
}
}
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() {}
}

/**
* 서비스 수행 전/후로 동작하는 코드(클래스/메서드/파라미터/실행시간)
*
* @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;
}

