본문 바로가기
Spring Boot

Spring Boot - AOP (Aspect Oriented Programming)

by sinabeuro 2021. 9. 3.
728x90

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

 

728x90

'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

댓글