AOP (Aspect Oriented Programming)
관점지향 프로그램
스프링 어플리케이션은 대부분 특별한 경우를 제외하고는 MVC웹 어플리케이션에서는 Web Layer, Business Layer, Data Layer로 정의.
- Web Layer : REST API를 제공하며, Client 중심의 로직 적용
- Business Layer : 내부 정책에 따른 logic을 개발하며, 주로 해당 부분을 개발
- Data Layer : 데이터 베이스 및 외부와의 연동을 처리
AOP는 메소드나 특정 구역에 반복되는 로직들을 한 곳으로 모아서 코딩할 수 있게 해줍니다.
비지니스 로직 이외의 반복되고, 일괄적으로 적용되고, 조건에 따라 변동될 수 있는 코드를 횡단관심이라고 합니다.
이 횡단관심을 비지니스 로직으로부터 분리하여, AOP로 한 곳에 모아 관리할 수 있으며, AOP를 구현한 클래스에서 어디에 횡단 관심사를 적용할지 타겟을 지정할 수 있습니다.
즉, 주요 관심사(비지니스 로직)에서 횡단 관심사를 호출하는 것이 아니라, 횡단 관심에서 어디에 부차적인 로직을 호출할지 설정할 수 있습니다.
어디에 횡단관심을 적용할지를 선택하는 것을 JointPoints입니다.
AOP를 실행하는 어노테이션(JointPoints)
@Aspect를 제외한 나머지 어노테이션이 jointPoints에 해당됩니다.
AOP를 사용하기 위해서는 의존성을 추가해야 합니다.
gradle을 예시로 다음과 같은 의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
AOP 예제 - Before, AfterReturing
@RestController
@RequestMapping("/api")
public class RestApiController {
...
@Decode
@PutMapping("/put")
public User put(@RequestBody User user) {
System.out.println("put");
System.out.println(user);
return user;
}
...
}
@Decode 어노테이션을 붙여서 AOP를 실행시킬 대상을 지정합니다.
// Decode.java - 사용자 정의 annotation 구현
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {
}
// DecodeAop.java
import com.example.aop.dto.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
@Aspect
@Component
public class DecodeAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))") // 어디에 적용시킬지 타겟을 설정.
private void cut() { };
@Pointcut("@annotation(com.example.aop.annotation.Decode)")
private void enableDecode(){}
@Before("cut() && enableDecode()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
Object[] args = joinPoint.getArgs();
for(Object arg : args){
/**
instanceof의 사용형식은 ‘객체 + instanceof + 클래스’ 이다.
A를 부모, B를 자식 클래스로 세팅하고 두 클래스 간 형변환이 가능한지 확인.
**/
if(arg instanceof User){
User user = User.class.cast(arg);
String base64Email = user.getEmail();
String email = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
user.setEmail(email);
}
}
}
@AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj){
if(returnObj instanceof User){
User user = User.class.cast(returnObj);
String email = user.getEmail();
String base64Email = Base64.getEncoder().encodeToString(email.getBytes());
user.setEmail(base64Email);
}
}
}
@Pointcut 어노테이션을 사용해서 어디에 AOP를 적용할지 지정해줍니다.
@Pointcut 표현식과 execution 문법은 아래 링크를 참조해주세요.
https://icarus8050.tistory.com/8
@Pointcut 어노테이션으로 대상 클래스 위치와 Decode 어노테이션이 붙은 메소드로 AOP 적용 타켓을 설정합니다.
@Before는 대상 로직이 실행되기 전에 호출되고 @AfterReturning 대상 로직이 에러없이 실행완료되면 호출됩니다.
AOP 예제 - Around
@RestController
@RequestMapping("/api")
public class RestApiController {
...
@Timer
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
Thread.sleep(1000 * 2);
}
...
}
// Timer.java 사용자 정의 annotation 구현
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Aspect
@Component
public class TimerAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))") // 어디에 적용시킬지 타겟을 설정.
private void cut() { };
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
private void enableTimer(){}
// around - 비지니스로직의 전과 후에 작동한다.
@Around("cut() && enableTimer()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
}
}
@around는 비지니스 로직의 전과 후에 작동합니다.
@around 사용하여 스탑워치를 비지니스 로직이 시작 시에 동작시키고, 프로세스 종료 시에 결과값을 반환하는 등의 기능을 구현할 수 있습니다.
AOP arg 로 파라미터 받기
@Slf4j
@Aspect
@Component
public class OliveExceptionLogAop {
private static final String EXCEPTION_FORMAT = "Exception Message : {}";
private static final Map<Level, Consumer<Exception>> LOG_LEVEL_CONSUMER = new ConcurrentHashMap<>();
public OliveExceptionLogAop() {
LOG_LEVEL_CONSUMER.put(Level.INFO, o -> log.info(EXCEPTION_FORMAT, o.getMessage()));
LOG_LEVEL_CONSUMER.put(Level.DEBUG, o -> log.debug(EXCEPTION_FORMAT, o.getMessage()));
LOG_LEVEL_CONSUMER.put(Level.WARN, o -> log.warn(EXCEPTION_FORMAT, o.getMessage()));
LOG_LEVEL_CONSUMER.put(Level.ERROR, o -> log.error(EXCEPTION_FORMAT, o.getMessage()));
LOG_LEVEL_CONSUMER.put(Level.TRACE, o -> log.trace(EXCEPTION_FORMAT, o.getMessage()));
}
@Before(value = "@annotation(com.oliveone.common.rest.aop.ExceptionLog) && args(e)")
public void exceptionLog(JoinPoint joinPoint, Exception e){
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
ExceptionLog exceptionLog = methodSignature.getMethod().getAnnotation(ExceptionLog.class);
LOG_LEVEL_CONSUMER.get(exceptionLog.logLevel()).accept(e);
}
}
@ExcelUploadHistorySave
public <T> void insertXlsUploadDtlWithMultipleAop(List<T> historyList) {}
@Aspect
@Component
public class ExcelUploadHistoryAop {
private final ExcelUploadService excelUploadService;
@Pointcut("@annotation(com.oliveone.api.annotation.ExcelUploadHistorySave)")
private void atAnnotation() {}
@Before("atAnnotation() && args(historyList, ..)")
public <T> void insertExcelUploadHistory(List<T> historyList) {
if (historyList.size() > 0)
excelUploadService.insertXlsUploadDtlWithMultiple(historyList);
}
}
'Spring Boot' 카테고리의 다른 글
Spring Boot - Exception 개괄 (0) | 2021.09.10 |
---|---|
Spring Boot - Validation (0) | 2021.09.09 |
Spring Boot - Annotation 참고자료 (0) | 2021.09.07 |
Spring Boot - IoC (Inversion of Control) (0) | 2021.09.03 |
Spring Boot - DI (Dependency Injection) (0) | 2021.09.02 |
댓글