2022.01.27 - [Spring|Spring-boot/Spring AOP] - Spring AOP
2022.01.29 - [Spring|Spring-boot/Spring AOP] - Spring AOP (2)
AOP는 비즈니스 로직의 코드와 그 외 부가적인 관심을 코드상에서 분리하기 위해 나온 하나의 방법이다.
예를 들어 HTTP통신을 통해 들어온 responseBody를 들어올 땐 복호화를 나갈 땐 암호화를 해야 한다면
모든 비즈니스 앞 뒤에 동일한 코드가 반복되어 사용될 것이다.
또 각 메서드의 실행시간을 구한다던지, 혹은 들어온 파라미터와 실행된 메서드 이름, 그리고 리턴한 값 등을 로깅하기 위해서 aop를 사용하기도 한다.
첫 번째로 실행된 메서드의 이름과 들어온 파라미터의 값, 리턴 값을 로깅하고 추가적으로 메서드의 실행시간까지 로깅해 보자.
우선 단순 CRUD를 지원하는 API서버를 구동시켜 놓겠다.
package com.example.aop.controller;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class Controller {
@GetMapping("/get/{id}")
public String httpGetMethod(@PathVariable Long id, @RequestParam String name){
return id + " " + name;
}
@PostMapping("/post")
public User httpPostMethod(@RequestBody User user){
return user;
}
@DeleteMapping("/delete")
public void httpDeleteMethod() throws InterruptedException {
Thread.sleep(2000);
}
@PutMapping("/put")
public User httpPutMethod(@PathVariable Long id, @RequestBody User user){
return user;
}
}
이제 각 메서드의 정보에 접근할 수 있는 AOP 빈객체를 만들어야 한다. 당연히 Spring을 활용하기 때문에 AOP자체는 @Component로 활용해야 한다.
@Component
@Aspect
public class AopExample {
}
@Component 어노테이션은 클래스단위를 빈으로 등록하겠다는 어노테이션이다.
@Aspect는 Spring AOP를 위한 어노테이션이다.
Aspect를 사용하기 전 Pointcut으로 AOP가 실행되어야 하는 실행 시점을 정한다. Spring에서는 한정적인 범위에서의
Pointcut을 기본적으로 제공하지만 추가적으로 라이브러리를 사용하면 더 넓은 범위의 AOP를 활용할 수 있을 것이다.
2022.01.29 - [Spring|Spring-boot/Spring AOP] - Pointcut - execution
@Pointcut("execution(* com.example.aop.controller.*.*(..))")
public void pointCut(){}
pointcut의 표현식은 자료가 많이 때문에 추가적인 설명은 하지 않고 controller패키지 하위 모든 접근자, 모든 타입, 파라미터의 유무와 상관없이 모든 메서드에 pointcut을 걸었다.
이 pointCut()은 메서드의 정보를 가져오기 위한 포인트 컷이고 다음은 시간을 재기 위한 포인트 컷을 만들 것이다.
시간을 측정하기 위한 포인트 컷은 Custom Annotation을 활용해 볼 생각이다.
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
별도의 커스텀 어노테이션을 만들어 사용해 본다.
@Target은 해당 어노테이션을 활용할 지점을 정하는데 TYPE 경우 종류가 많이 때문에 검색을 추천한다.
@Retention 어노테이션은 해당 어노테이션의 생명 주기를 정한다.
이제 해당 어노테이션으로 포인트 컷(어디에서 aop를 적용할지?)을 추가할 것이다.
이제 적용시킬 메서드에 커스텀 어노테이션을 붙여보자.
package com.example.aop.controller;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class Controller {
@Timer
@GetMapping("/get/{id}")
public String httpGetMethod(@PathVariable Long id, @RequestParam String name){
return id + " " + name;
}
@Timer
@PostMapping("/post")
public User httpPostMethod(@RequestBody User user){
return user;
}
@Timer
@DeleteMapping("/delete")
public void httpDeleteMethod() throws InterruptedException {
Thread.sleep(2000);
}
@Timer
@PutMapping("/put/{id}")
public User httpPutMethod(@PathVariable Long id, @RequestBody User user){
return user;
}
}
이제 메서드는 정했기 때문에 메서드 전, 후, 또는 앞 뒤, 에러 포함 , 미포함을 고려해야 한다.
연습을 위해 메소드 실행 전과 예외 없이 성공한 후, 예외 상관없이 앞 뒤로 발생하는 around를 적용할 것이다.
2022.01.29 - [Spring|Spring-boot/Spring AOP] - Spring AOP @target, @within
이제 실행되는 메서드의 이름과 입력받은 파라미터, 리턴한 값을 로그로 찍어보자.
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
@Component
@Aspect
public class AopExample {
private final static Logger LOG = Logger.getGlobal();
@Pointcut("execution(* com.example.aop.controller.*.*(..))")
public void pointCut(){}
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
public void timerPointCut(){}
@Before("pointCut()")
public void before(JoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] objects = joinPoint.getArgs();
for(Object param: objects ){
LOG.log(Level.INFO, "들어온 파라미터 값 : "+param );
}
LOG.log(Level.INFO, "실행된 Method Name : "+ method.getName() );
}
@AfterReturning(value = "pointCut()",returning = "returnValue")
public void afterReturning(JoinPoint joinPoint,Object returnValue){
LOG.log(Level.INFO, "실행된 메소드가 리턴한 값 : "+ returnValue );
}
}
@Before는 예외와 상관없이 무조건 메서드 이전에 실행한다.
Postman을 사용해서 로컬 서버에 @RequsetParam 값으로 name = steve와 @PathVariable 값으로 100을 넣어주고 요청을 보냈다.
들어온 파라미터와 실행된 메서드, 리턴한 값까지 확인할 수 있다. 원래 비즈니스 로직에서 이러한 공통된 작업을 처리하기 위해선 반복적으로 코드가 중복될 수밖에 없기 때문에 AOP는 관심사를 분리하여 코드를 분리했다.
이러한 AOP를 통해 로그를 찍어 TEST를 유용하게 진행할 수 있으며, 로그파일을 생성하거나, 혹은 실제 들어온 값을 복호화하여 내부 비즈니스 로직을 이용하고 값을 리턴할 때는 다시 암호화를 할 수도 있다.
다음 호출 순서를 기억하자 aop를 적용하고 호출순서를 헷갈려해서 오류가 발생한 경우도 있다.
호출 순서(오류 없을 때)
- Before
- Around
- After
- AfterReturning
호출 순서(오류 났을 때)
- Before
- Around 에러 시 실행되지 않음
- After
- AfterReturning 에러 시 실행되지 않음
- AfterThrowing
그럼 이제 추가적으로 어노테이션을 활용해서 포인트 컷을 걸고 두 개의 포인트 컷을 합쳐보자
이번에는 HTTP POST METHOD를 보낼 것인데, 미리 받을 @RequestBody DTO를 정의해 놨다.
package com.example.aop.dto;
public class User {
private String id;
private String pw;
private String email;
public void setId(String id) {
this.id = id;
}
public void setPw(String pw) {
this.pw = pw;
}
public void setEmail(String email) {
this.email = email;
}
public String getId() {
return id;
}
public String getPw() {
return pw;
}
public String getEmail() {
return email;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", pw='" + pw + '\'' +
", email='" + email + '\'' +
'}';
}
}
스프링 부트는 기본적으로 Jackson라이브러리를 사용하기 때문에 디폴트 생성자와 getter는 필수로 생성해야 한다.
또한 return 값으로 객체가 반환되는 경우 JSON으로 변환된다. 그럼 JSON형식으로 요청을 보내고 RETURN값을 확인해 본다.
@Component
@Aspect
public class AopExample {
private final static Logger LOG = Logger.getGlobal();
@Pointcut("execution(* com.example.aop.controller.*.*(..))")
public void pointCut(){}
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
public void timerPointCut(){}
/* @Before("pointCut()")
public void before(JoinPoint joinPoint){}
@AfterReturning(value = "pointCut()",returning = "returnValue")
public void afterReturning(JoinPoint joinPoint,Object returnValue){}*/
@Around("timerPointCut() && pointCut()")
public void logAOP(ProceedingJoinPoint joinPoint){
StopWatch stopWatch = new StopWatch();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] objects = joinPoint.getArgs();
for(Object param: objects ){
LOG.log(Level.INFO, "들어온 파라미터 값 : "+param );
}
LOG.log(Level.INFO, "실행된 Method Name : "+ method.getName() );
try {
stopWatch.start();
Object returnValue = joinPoint.proceed();
LOG.log(Level.INFO, "Http Method Return Value = " + returnValue);
stopWatch.stop();
LOG.log(Level.INFO, "Around AOP 실행시간 = " + stopWatch.getTotalTimeSeconds() );
} catch (Throwable throwable) {
LOG.log(Level.INFO, "Around AOP Error : Method nmae = "+ method.getName() );
}
}
}
proceed() 메서드를 기준으로 메서드 전 후를 구별할 수 있기 때문에 다양한 용도로 많이 사용되는 어노테이션이다.
또한 포인트 컷을 연산자를 통해 중첩할 수 있다.
요청을 보내기 전에 httpPostMethod에 3초 정도 대기시간을 강제로 부여한다
2022.01.31 - [Spring|Spring-boot/Spring AOP] - Spring AOP 한계
'Spring|Spring-boot > Spring AOP' 카테고리의 다른 글
Pointcut - within, args (0) | 2022.01.29 |
---|---|
Pointcut - execution (0) | 2022.01.29 |
Spring AOP (2) (0) | 2022.01.29 |
Spring AOP (0) | 2022.01.27 |
[Spring] AOP(Aspect Oriented Programming) 방법론 (0) | 2020.06.22 |
댓글