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

AOP Proxy
앞뒤로 넣어서 proxy가 실행해준다
공통된 부분을 따로 빼내어 작성하는 메소드 : Advice
Adivce를 적용 모든 관점(시점, 메소드) : JoinPoing
JoinPoint 중 실제 Adivce적용 부분 : 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 메소드 호출 및 초기화 등의 다양한 작업에 부가기능을 적용할 수 있다.
<aop:config>를 이용해서 Aspect를 설정한다.<aop:aspect-autoproxy />를 설정한다.


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


✔️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;
}
}