AOP

  • 관점 지향 프로그래밍 특별한 경우를 제외한 스프링 MVC 웹 어플리케이션에서는 Web Layer, Buisness Layer, Data Layer로 정의.

  • Web Layer : REST API를 제공하며, Client중심 로직 적용
  • Business Layer : 내부 정책에 따른 로직개발
  • Data Layer : 데이터베이스 및 외부와 연동을 처리

주요 어노테이션

Annotation 의미
@Aspect 자바에서 널리 사용하는 AOP프레임워크에 포함되며,
AOP를 정의하는 Class에 할당
@Pointcut AOP기능을 적용시킬 지점을 설정
@Before 메소드 실행 이전
@After 메소드 실행 후, 예외무시
@AfterRunning 메소드 호출 성공 실행 시
@AfterThrowing 메소드 호출 실패 예외 발생
@Around Before/After모두 제어

AOP 실무사례(1)

스프링 이니셜라이저로 스프링 웹 프로젝트를 하나 만들어준다. build.gradle의 dependency에다가 implementation 'org.springframework.boot:spring-boot-starter-aop' 를 추가하고 refresh해준다.

일단 RestController를 만들어준다.

@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
    System.out.println("get method : " + id + ", " + name);
    return id + " " + name;
}

@PostMapping("/post")
public Account post(@RequestBody Account account) {
    System.out.println("post method : " + account);
    return account;
}

이제 각 메소드가 실행되는 곳에서 로그를 찍도록 AOP를 작성해보자.

@Aspect //AOP할당
@Component
public class ParameterAop {

    @Pointcut("execution(* com.example.helloboot.controller..*.*(..))") //AOP기능 적용지점 설정
    private void cut(){}

    @Before("cut()") //cut메소드 실행 이전에 실행
    public void before(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        System.out.println(method.getName());

        Object[] args = joinPoint.getArgs(); //메소드에 들어가는 매개변수들

        for(Object obj : args){
            System.out.println("type : " + obj.getClass().getSimpleName());
            System.out.println("value : " + obj);
        }
    }

    @AfterReturning(value = "cut()", returning = "returnObj")
    public void after(JoinPoint joinPoint, Object returnObj) {
        System.out.println("returnObj : " + returnObj);
    }
}

@Before에서는 어떤 메소드가 실행되었고, 어떤 파라미터가 들어왔는지, @AfterRunning 에서는 어떤 응답 이 return되었는지 알 수 있다.

AOP 실무사례(2)

커스텀 어노테이션을 하나 만들고,이를 활용하여 서버의 상태를 AOP를 이용해서 로깅해보자.

일단 커스텀 어노테이션을 하나 만들자.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {}

그리고 컨트롤러에 delete메소드가 2초가 걸린다고 가정하고 메소드를 하나 만든다.

@Timer
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
    // db login
    Thread.sleep(1000*2);
}

마지막으로 우리가 붙인 @Timer어노테이션이 붙은 클래스 하위메소드에 대해 적용시키기 위해 AOP를 다음과 같 이 작성한다.

@Aspect
@Component
public class TimerAop {
    @Pointcut("execution(* com.example.helloboot.controller..*.*(..))")
    private void cut(){}

    @Pointcut("@annotation(com.example.helloboot.annotation.Timer)")
    private void enableTimer(){
    }

    @Around("cut() && enableTimer()")
    public void arround(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();

        //before
        stopWatch.start();

        // joinPorint.preceed(); 할때 cut(), enableTimer()가 실행되는 시점이라고 보면됨
        Object result = joinPoint.proceed();

        //after
        stopWatch.stop();

        System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
    }
}

AOP에서 암호화된 정보 복호화를 먼저 진행시키고 실제 서비스로직에 넘겨줄 수 있다. 또한 서비스로직이 끝났을 때 암호화를 다시 해서 return시킬 수 있다.

일단 DecodeAop를 먼저 작성해보자.

@Aspect
@Component
public class DecodeAop {
    @Pointcut("execution(* com.example.helloboot.controller..*.*(..))")
    private void cut(){}

    @Pointcut("@annotation(com.example.helloboot.annotation.Decode)")
    private void enableDecode(){
    }

    @Before("cut() && enableDecode()")
    public void before(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        for(Object arg: args){
            if(arg instanceof Account){
                Account account = Account.class.cast(arg);
                String base64Email = account.getEmail();
                String email = new String(Base64.getDecoder().decode(base64Email), StandardCharsets.UTF_8);
                account.setEmail(email);
            }
        }
    }

    @AfterReturning(value= "cut() && enableDecode()", returning = "returnObj")
    public void afterReturn(JoinPoint joinPoint, Object returnObj){
        if(returnObj instanceof Account){
            Account account = Account.class.cast(returnObj);
            String email = account.getEmail();
            String base64Email = Base64.getEncoder().encodeToString(email.getBytes(StandardCharsets.UTF_8));
            account.setEmail(base64Email);
        }
    }
}

이제 DecodeAop가 작동할 수 있도록 joinPoint걸린 Decode어노테이션이 붙은 api를 하나 만들어주자.

@Timer
@Decode
@DeleteMapping("/put")
public Account put(@RequestBody Account account) {
    System.out.println("put user : " + account);
    return account;
}

요청 바디에다가 base64로 암호화된 데이터값을 보내게 되면 알아서 복호화해서 로그로 찍고, 다시 암호화해서 에코하는 AOP를 적용시켰다.