핵심적인 부분(시스템의 핵심인 가치와 목적)에 부가적으로 필요한 것들 중 반복적으로 등장하는 로깅, 인코딩, 보안, 에러체크 등의 관심영역들을 뜻한다.
아래는 이에 대한 예시를 보여준다.

위와 같이 흩어진 관심사(Method Parameter Log, Runtime, Parameter Encode 등)를 모듈화하고 핵심 Logic에서 분리하여 재사용하는 것이 AOP의 목적이다.
만약 Framework가 AOP를 지원하지 않는다면, Business Logic이 작성되어야 하는 부분에 상관없는 코드(부가적 기능들)가 작성되어야 할 것이다.
그렇지만 Spring Boot Framework에서는 AOP를 지원하고 있다.
AOP(Aspect Oriented Programming)는 관점 지향 프로그래밍이라 불리며, 횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍을 뜻한다.
즉, 어떠한 Logic을 기준으로 핵심적인 부분과 부가적인 부분으로 나누어 그 관점을 기준으로 모듈화 하는것을 뜻한다.

이미지 출처 : itwiki.kr
Target : 핵심 기능을 담고 있는 모듈
Advice : Target에 제공할 부가기능을 포함한 모듈
JoinPoint : Advice가 적용될 수 있는 위치
PointCut : Advice를 적용할 Target의 메서드를 선별하는 정규표현식
Aspect : 부가기능을 정의한 코드인 Advice와 Advice를 어디에 적용할지 결정하는 PointCut을 합친 개념을 뜻한다.
AOP의 기본 모듈이며, 싱글톤 형태의 객체로 존재한다.
Advisor : Advice와 PointCut을 합친 개념
Weaving : PointCut에 의해 결정된 Target의 JoinPoint에 Advice(부가 기능)를 삽입하는 과정을 뜻한다.
즉, Target(핵심 기능)에 영향을 주지 않으면서 필요한 Advice(부가 기능)을 추가할 수 있도록 해주는 핵심 처리과정

와일드 카드를 이용하여 작성한다. ( ..은 0개 이상을 뜻하고, *는 모든 것을 뜻한다.)
execution : 특정 메서드를 지정하는 Pattern을 작성할 수 있는 방법이다.
execution( [접근제어자] 패키지.패키지.패키지.패키지.클래스명.메소드이름(인자) )
within : 특정 클래스 안에 있는 모든 Method까지 지정할 수 있는 패턴
within(패키지.패키지.패키지.패키지.클래스)
Custom Annotation을 만들 때 사용되는 어노테이션이다.
@Targer : Java 컴파일러에게 해당 Annotation이 어디에 적용될지 결정하기 위해 사용한다.
ElementType.PACKAGE : 패키지 선언
ElementType.TYPE : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR : 생성자 선언
ElementType.FIELD : 멤버 변수 선언
ElementType.LOCAL_VARIABLE : 지역 변수 선언
ElementType.METHOD : 메서드 선언
ElementType.PARAMETER : 전달인자 선언
ElementType.TYPE_PARAMETER : 전달인자 타입 선언
ElementType.TYPE_USE : 타입 선언
@Retention : 해당 Annotation이 언제까지 살아 남아 있을지를 정할 때 사용한다.
RetentionPolicy.SOURCE : 컴파일러가 컴파일할때 해당 Annotation의 메모리를 버린다.
RetentionPolicy.CLASS : 컴파일에서는 Annotation의 메모리를 가져가지만 실질적으로 런타임시 사라진다.
RetentionPolicy.RUNTIME : 런타임을 종료할 때까지 해당 Annotation의 메모리는 살아있다.
AOP를 활용한 3개의 예시를 작성해보고자 한다. 먼저 Controller와 User 클래스는 아래에 작성된 코드가 기본 base라고 가정하고, Spring에서 AOP를 활용한 3개의 예시를 살펴보자
package com.example.aop.controller;
...
/**
* RestController.java
*/
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String requestGet(@PathVariable Long id, @RequestParam String name) {
/*
핵심 Logic
*/
return id + " " + name;
}
@PostMapping("/post")
public User requestPost(@RequestBody User user) {
/*
핵심 Logic
*/
return user;
}
@DeleteMapping("/delete")
public void requestDelete() {
/*
핵심 Logic
*/
}
@PutMapping("/put")
public void requestPut() {
/*
핵심 Logic
*/
}
}
package com.example.aop.dto;
...
/**
* User.java
*/
public class User {
private String id;
private String pwd;
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
...
}
PointCut에 지정된 Method가 실행되기 Before(지정된 Method가 실행 전)과 After(지정된 Method가 실행 후)로 나누어,
Before에는 Method의 name 및 Parameter type과 value를 출력하고,
After에는 return되는 value를 출력하고자 한다.
/**
* ParameterAop.java
*/
@Aspect
@Component
public class ParameterAop {
// 범위 지정
@Pointcut("execution(* com.example.aop.controller.*.*(..))")
private void pointCut() { }
// pointCut 메소드(PointCut 어노테이션 범위에 해당되는 메소드)가 실행되는 시점의 바로 전
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println("method : " + method.getName()); // Method name
Object[] args = joinPoint.getArgs();
for(Object obj : args) {
System.out.println("type : " + obj.getClass().getSimpleName(); // Parameter 타입
System.out.println("value : " + obj); // Parameter 값
}
}
// pointCut 메소드가 실행이 끝난 후 시점
@AfterReturning(value = "pointCut()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println(returnObj);
}
}
// POST로 Request
{
"id" : "steve",
"pwd" : "1234",
"email" : "steve@gmail.com"
}
// Terminal 출력 결과
method : requestPost
type : User
value : User{id='platinouss', pwd='1234', email='platinouss@gmail.com'}
returnObj : User{id='platinouss', pwd='1234', email='platinouss@gmail.com'}
이번에는 Custom Annotation인 Timer를 작성하여 특정 Method에 @Timer를 선언하면 해당 Method의 runtime이 측정되도록 구현하고자 한다.
package com.example.aop.annotation;
...
/**
* Custom Annotation 구현
*
* Timer.java
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {}
package com.example.aop.controller;
...
/**
* RestController.java
*/
@RestController
@RequestMapping("/api")
public class RestApiController {
...
// custom Annotation인 Timer 추가
@Timer
@DeleteMapping("/delete")
public void requestDelete() throws InterruptedException {
/*
핵심 Logic
*/
// 실행 시간 출력 테스트를 위해 2초 sleep을 걸어준다.
Thread.sleep(1000 * 2);
}
}
/**
* TimerAop.java
*/
@Aspect
@Component
public class TimerAop {
// 범위 지정
@Pointcut("execution(* com.example.aop.controller.*.*(..))")
private void pointCut() {}
// Custom Annotation인 Timer 경로 지정
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
private void enableTimer() {}
@Around("pointCut() && enableTimer()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
// ----- 실행 전 -----
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 핵심 Method가 실행 되고, return 값이 존재 할 경우 result에 반환
Object result = jointPoint.proceed();
// ----- 실행 후 -----
stopWatch.stop();
// 핵심 Method의 실행 시간 출력
System.out.println("total : " + stopWatch.getTotalTimeSecond());
}
}
total : 2.0080194
암호화 또는 인코딩 된 값을 핵심 Logic 부분에서 복호화나 디코딩을 거치는 것이 아닌, AOP단에서 이미 완료된 상태로 들어오게끔 구현하여 핵심 Logic에서는 본질적인 data를 가지고 활용하도록 구현하고자 한다.
또한, 후에 외부로 전달 시 AOP단에서 암호화나 인코딩을 적용하도록 구현하고자 한다.
이번에도 Custom Annotation인 Decode를 작성하여 AOP를 구현하고자 한다.
package com.example.aop.annotation;
...
/**
* Custom Annotation 구현
*
* Decode.java
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {}
package com.example.aop.controller;
...
/**
* RestController.java
*/
@RestController
@RequestMapping("/api")
public class RestApiController {
...
// custom Annotation인 Decode 추가
@Decode
@putMapping("/put")
public User requestPut() {
/*
핵심 Logic
*/
return user;
}
}
/**
* DecodeAop.java
*/
@Aspect
@Component
public class DecodeAop {
@Pointcut("execution(* com.example.aop2.controller.*.*(..))")
private void pointCut() {}
@Pointcut("@annotation(com.example.aop2.annotation.Decode)")
private void enableDecode() {}
@Before("pointCut() && enableDecode()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
Object[] args = joinPoint.getArgs();
for(Object arg : args) {
if(arg instanceof User) {
User user = User.class.cast(arg);
String base64Email = user.getEmail();
// base64 디코딩
String decodeEmail = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
user.setEmail(decodeEmail);
// Decode된 email 출력
System.out.println("decodeEmail : " + decodeEmail);
}
}
}
@AfterReturning(value = "pointCut() && enableDecode()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
if(returnObj instanceof User) {
User user = User.class.cast(returnObj);
String email = user.getEmail();
// base64 인코딩
String base64Email = Base64.getEncoder().encodeToString(email.getBytes());
user.setEmail(base64Email);
// base64로 인코딩 된 email 출력
System.out.println("base64Email : " + base64Email);
}
}
}
// PUT으로 Request
{
"id" : "platinouss",
"pwd" : "1234",
"email" : "cGxhdGlub3Vzc0BnbWFpbC5jb20="
}
// Terminal 출력 결과
decodeEmail : platinouss@gmail.com
base64Email : cGxhdGlub3Vzc0BnbWFpbC5jb20=
// Response
{
"id": "platinouss",
"pwd": "1234",
"email": "cGxhdGlub3Vzc0BnbWFpbC5jb20="
}