Spring AOP 스터디 - (2) @Aspect 기반 AOP 적용
kindof
·2022. 10. 21. 19:04
지난 포스팅에서는 'AOP가 왜 필요한가'에 대해 알아보고 AOP를 이해하는 데 필요한 몇 가지 용어들을 정리했습니다.
AOP의 기본적인 개념에 대해 궁금하신 분은 위 글을 참고해주시면 좋겠습니다.
이번 시간은 지난 포스팅에서 문제 삼았던 코드를 AOP 기반으로 수정해보면서 Spring AOP가 동작하는 방식을 이해해보겠습니다.
1. 문제의 코드
지난 포스팅에서 예제로 사용했던 문제의 컨트롤러입니다.
@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@PostMapping("/user")
public String userSave(String email, String name) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 회원 가입(실제 로직은 더 복잡)
User user = new User();
user.setEmail(email);
user.setNickname(name);
userService.save(user);
stopWatch.stop();
log.info("[userSave] takes {}ms", stopWatch.getTotalTimeMillis());
return "OK";
}
@GetMapping("/users")
@PerfLogging
public List<User> users() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
List<User> userList = userService.getUserList();
stopWatch.stop();
log.info("[users] takes {}ms", stopWatch.getTotalTimeMillis());
return userList;
}
@GetMapping("/user/{email}")
public User user(@PathVariable String email) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
User user = null;
try{
user = userService.getUserByEmail(email);
} catch (UserNotFoundException e) {
log.error("[Method: user] call failed: {}", e.getMessage());
}
stopWatch.stop();
log.info("[userById] takes {}ms", stopWatch.getTotalTimeMillis());
return user;
}
}
보시다시피, StopWatch를 통해 컨트롤러의 호출 시점부터 종료 시점까지의 시간을 로그로 남기고 있는데요.
그리고 로그 정보 안에는 [userSave], [users], [userById] 처럼 호출된 메서드의 이름까지 기록해주고 있습니다.
우선, AOP의 도움없이 각 메서드를 호출(테스트 코드, CURL, Postman)하면 어떤 결과가 나타나는지 확인해보겠습니다.
당연히 의도했던 것처럼 로그가 찍히게 됩니다. 아주 간단한 비즈니스 로직이라서 0ms 밖에 걸리지 않았네요😅
2. AOP 적용
이제 그러면 이전 시간에 정리한 내용을 바탕으로 AOP를 적용해보겠습니다.
먼저 build.gradle에 spring aop를 적용하기 위한 의존성을 추가해주셔야 합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
...
}
| 2-1. Controller 전체에 적용하기
처음에는 프로젝트에 있는 모든 Contorller를 Pointcut 대상으로 해보겠습니다.
@Aspect : Spring AOP를 적용하는 보편적인 방법은 @Aspect를 사용하는 것입니다. @Aspect 어노테이션을 달면, 스프링 프록시 빈 생성기가 이를 스캔하여 Advisor로 생성해둡니다. 그리고 @Pointcut 대상을 조회하여 해당 Target이 프록시를 기반으로 생성될 지의 여부를 확인합니다.
@Pointcut(...) : AspectJ에서 포인트컷을 편리하게 관리하기 위해 제공하는 표현식입니다. 여기서는 execution 지시자를 사용했습니다.
execution 지시자 : 가장 많이 쓰이는 지시자로, 메서드 실행 JoinPoint를 찾아서 매칭합니다. execution 지시자의 문법은 아래를 따릅니다.
modifiers-pattern? ret-type-pattern declaring-type-pattern?name- pattern(param-pattern) throws-pattern?
'?'가 붙은 부분은 생략해서 사용할 수 있으며 '*'를 쓰게 되면 모든 패턴을 매칭시킵니다.
그래서 위에서 적었던 (* *..*Controller.*(..))을 해석해보면 "접근제어자는 생략했으며 모든 반환타입, 최상위 패키지의 하위 패키지 중에서 Controller 이름으로 끝나는 패키지의 하위 모든 메서드에 파라미터를 따지지 않고 적용하겠다"는 뜻이 됩니다.
@Around([Pointcut 메서드명]) : 먼저 Advice 실행 종류의 하나인 @Around는 JointPoint의 실행 전 후에서 Advice를 실행합니다. 그리고 여기서는 perfCheck() 라는 메서드 자체가 Advice가 되며, joinPoint.proceed() 호출 전 후로 필요한 로직을 수행하고 있습니다.
@joinPoint.proceed() : Advice의 실행 시점이 @Around 일 때만 필요한 부분입니다. @Around는 위에서 말한 것처럼 JoinPoint의 실행 전, 후로 필요한 로직을 호출하기 때문에 그 기준이 되는 지점을 명시해주어야 합니다.
@joinPoint.getSignature() : ProceedingJoinPoint는 JoinPoint 인터페이스를 상속하는 인터페이스이며, JoinPoint의 내용은 아래와 같습니다.
public interface JoinPoint {
String toString();
/**
* @return an abbreviated string representation of the join point.
*/
String toShortString();
/**
* @return an extended string representation of the join point.
*/
String toLongString();
/**
* Returns the currently executing object.
* 중략
* @return the currently executing object (or null if not available - e.g. static context)
*/
Object getThis();
/**
* Returns the target object. This will always be
* 중략
* @return the target object (or null if there isn't one)
*/
Object getTarget();
/**
* @return the arguments at this join point
*/
Object[] getArgs();
/**
* getStaticPart().getSignature() returns the same object
* @return the signature at the join point.
*/
Signature getSignature();
...
interface StaticPart {
/** @return the signature at the join point. */
Signature getSignature();
/** Returns the source location corresponding to the join point.
* If there is no source location available, returns null.
* @return the SourceLocation of the defining class for default constructors
*/
SourceLocation getSourceLocation();
...
}
여기서 우리가 사용한 getSignature() 메서드는 해당 join point의 signature를 리턴한다고 되어 있습니다. 이게 무슨 뜻인지는 아래 결과에서 보도록 하겠습니다.
UserController로 돌아와서 @Import(PerfAspect.class)를 추가하여 우리가 만든 Aspect를 Import 합니다. 그리고 이전에 반복되어 작성된 시간 측정 로직을 모두 지우도록 하겠습니다.
@RestController
@RequiredArgsConstructor
@Slf4j
@Import(PerfAspect.class)
public class UserController {
private final UserService userService;
@PostMapping("/user")
public String userSave(String email, String name) {
// 회원 가입(실제 로직은 더 복잡)
User user = new User();
user.setEmail(email);
user.setNickname(name);
userService.save(user);
return "OK";
}
@GetMapping("/users")
public List<User> users() {
List<User> userList = userService.getUserList();
return userList;
}
@GetMapping("/user/{email}")
public User user(@PathVariable String email) {
User user = null;
try{
user = userService.getUserByEmail(email);
} catch (UserNotFoundException e) {
log.error("[Method: user] call failed: {}", e.getMessage());
}
return user;
}
}
이전에 비해 훨씬 코드의 양이 줄어든 것을 확인할 수 있고, 메서드를 실행해보면 아래와 같은 결과를 얻을 수 있습니다.
AOP를 적용하기 이전과 거의 같은 형태의 로그를 확인할 수 있습니다.
그리고 앞서 말한 joinPoint.getSignature() 메서드의 결과로 메서드의 반환 타입과 해당 메서드가 호출된 위치를 알 수 있습니다. 이를 통해 개별 메서드마다 수동으로 메서드 정보를 적지 않아도 되는 것이죠.
| 2-2. UserController 전체에 적용하기
위에서는 모든 컨트롤러을 대상으로 포인트컷을 설정했는데요.
하지만 어떤 상황에서는 특정 컨트롤러 혹은 서비스, 레포지토리에만 적용하고 싶은 Aspect가 있을 수 있습니다. 그래서 모든 컨트롤러 대신 UserController에만 적용되는 포인트컷을 지정하겠습니다.
포인트 컷 안에 내용만 달라지고 나머지 적용은 동일합니다.
* com.sh.springaop.UserController..*(..)) 라고 지정하여 com > sh > springaop > UserController 하위의 메서드들에 파라미터는 신경쓰지 않고 포인트 컷을 지정합니다.
| 2-3. Annotation으로 필요한 곳에 직접 적용하기
우리가 지정한 Annotation을 가진 메서드만을 Join Point로 지정할 수도 있습니다. 이를 위해서 포인트 컷을 아래와 같이 작성해보겠습니다.
그리고 위에서 설명했던 것과 마찬가지로 com.sh.springaop.aop 패키지 아래에 PerfLogging이라는 Custom Annotation을 작성해주겠습니다.
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PerfLogging {
}
@Retention(value = RetentionPolicy.RUNTIME) : 어노테이션을 런타임시에까지 사용할 수 있습니다. JVM이 자바 바이트코드가 담긴 class 파일에서 런타임환경을 구성하고 런타임을 종료할 때까지 메모리는 살아있습니다.
@Target : 어노테이션이 적용될 대상입니다. 여기서는 메서드에만 적용되도록 지정했습니다.
그리고 위와 같이 원하는 메서드에 @PerLogging 어노테이션을 붙여주고, 메서드를 호출해보겠습니다.
컨트롤러 전체에 적용했을 때와 동일한 결과를 확인할 수 있습니다.
3. 정리
이번 글에서는 @Aspect를 기반으로 한 Spring AOP를 예제 코드에 적용해봤습니다.
글에서 따로 정리하지 않은 Aspect 적용의 순서, 다양한 포인트컷 지시자, 여러 Advice 실행 시점의 종류 등은 이 내용이 필요한 적절한 예제 코드를 만나게 되면 따로 정리해보도록 하겠습니다.
'Spring & Springboot' 카테고리의 다른 글
Spring Bean VS StaticClass (0) | 2022.12.10 |
---|---|
Spring AOP 스터디 - (3) 프록시 객체의 내부 메서드 호출 문제 (0) | 2022.11.20 |
Spring AOP 스터디 - (1) AOP의 필요성과 기본적인 동작 원리 (0) | 2022.10.18 |
Spring Security - SecurityContextHolder에서 로그인 사용자 정보 가져오기 (1) | 2022.08.03 |
Entity 필드가 가지는 Enum값의 목록은 어떻게 가져와야 할까? (0) | 2022.05.20 |