스프링 프레임워크는 3가지 특징인 IoC/DI, PSA, AOP 가 있는데, 그 중 AOP 에 대해 알아보자
AOP
AOP는 Aspect Orient Programming (관점 지향 프로그래밍) 으로, 어떤 로직을 핵심 기능과 부가 기능으로 나누고, 이를 각각 모듈화를 하는 프로그래밍이다.
핵심 기능은 핵심 비즈니스 로직이고, 부가 기능으로는 핵심 로직을 수행하기 위해 공통적으로 필요한 기능들, 예를 들면 로깅, DB연결, 등이 있다.
만약 특정 유저의 요청과 응답 사이의 시간을 기록하고 싶다면 AOP를 적용하지 않는다면 다음과 같이 해야 할 것이다.
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 응답 보내기
return productService.createProduct(requestDto, userDetails.getUser());
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원 정보
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElseGet(() -> new ApiUseTime(loginUser, 0));
apiUseTime.addUseTime(runTime);
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
이건 상품을 등록하는 API에만 적용한 모습인데 핵심 기능과 부가 기능이 섞여있고, 또 이걸 모든 API에 대해 한다고 하면 한세월 걸리고, 만약 수정이라도 해야 한다면 일일이 찾아가서 수정하느라 시간이 너무 걸리면서도 빠진 기능도 있을 것이다.
그래서 이를 따로 모듈로 분리를 해버리도록 했다.
우선 부가 기능을 따로 모듈화 하려면 클래스를 만들어서 @Aspect 어노테이션을 달아주면 되는데, 스프링 빈 만 가능하기 때문에 @Component도 달아주면 된다.
그리고 어드바이스와 포인트컷 이라는 개념이 있는데, 어드바이스는 언제 실행될 것인지에 대한 것이고, 포인트컷은 무엇이 실행될 것인지에 대한 것이라고 보면 된다.
어드바이스의 종류에는 다음과 같은 것들이 있다.
- @Around: '핵심기능' 수행 전과 후 (@Before + @After)
- @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
- @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
- @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
- @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
포인트컷은 Expression 으로 어느 클래스나 메서드에 적용할지를 정해줄 수 있다.
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
?는 생략이 가능하다고, 밑은 각각마다 들어갈 수 있는 문자열이다.
- modifiers-pattern (접근 제어자, *는 아무거나)
- public, private, *
- return-type-pattern (리턴 타입, *는 아무거나)
- void, String, List<String>, *
- declaring-type-pattern
- 클래스명 (패키지명 필요)
- com.sparta.myselectshop.controller.* - controller 패키지의 모든 클래스에 적용
- com.sparta.myselectshop.controller.. - controller 패키지 및 하위 패키지의 모든 클래스에 적용
- method-name-pattern(param-pattern)
- 함수명
- addFolders : addFolders() 함수에만 적용
- add* : add 로 시작하는 모든 함수에 적용
- 파라미터 패턴 (param-pattern)
- (com.sparta.myselectshop.dto.FolderRequestDto) - FolderRequestDto 인수 (arguments) 만 적용
- () - 인수 없음
- (*) - 인수 1개 (타입 상관없음)
- (..) - 인수 0~N개 (타입 상관없음)
- 함수명
예를 들어보면 다음과 같다.
@Around("execution(public * com.sparta.myselectshop.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
modifiers-pattern 는 public, (접근제어자)
return-type-pattern 는 *, (리턴 타입은 아무거나 와도 된다)
com.sparta.myselectshop.controller.. 는 ~~~.controller 패키지를 포함한 하위 패키지의 모든 클래스에 적용되며,
*(..) 에서 *는 모든 메서드를, (..)는 인수가 타입 상관없이 0개 이상의 모든 메서드가 적용된다는 의미이다.
그리고 @Point 어노테이션을 통해 포인트컷을 재사용 할 수 있는데, 아래처럼 쓰면 된다.
@Component
@Aspect
public class Aspect {
@Pointcut("execution(* com.sparta.myselectshop.controller.*.*(..))")
private void forAllController() {}
@Pointcut("execution(String com.sparta.myselectshop.controller.*.*())")
private void forAllViewController() {}
@Around("forAllContorller() && !forAllViewController()")
public void saveRestApiLog() {
...
}
@Around("forAllContorller()")
public void saveAllApiLog() {
...
}
}
클래스에 빈과 에스펙트로 걸어두고, 포인트컷을 2개로 두어 모든 컨트롤러를 등록하거나, 아니면 String만 반환하는 뷰컨트롤러를 등록해두고, && 및 ! 의 논리 연산자를 이용해서 포인트컷을 취사적용하여 Rest API만 하거나, 아니면 모든 API만 적용하거나 할 수 있다.
맨 처음, 요청-응답 시간을 기록하는 코드를 AOP로 고치면 다음과 같다.
@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
@Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
private void product() {
}
@Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
private void folder() {
}
@Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
private void naver() {
}
@Around("product() || folder() || naver()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
return joinPoint.proceed();
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원이 없는 경우, 수행시간 기록하지 않음
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
// 로그인 회원 정보
UserDetailsImpl userDetails = (UserDetailsImpl)auth.getPrincipal();
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElseGet(() -> new ApiUseTime(loginUser, 0L));
apiUseTime.addUseTime(runTime);
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
Spring AOP 동작 과정
AOP를 적용하기 전에는 다음과 같다.
적용하면 다음과 같다.
Spring이 프록시 객체를 중간에 삽입한다.
DispatcherServlet 과 ProductController 입장에서는 프록시가 jointPoint.proceed()를 호출하여서, 호출되는 함수의 인풋, 아웃풋이 동일하므로 변화가 없다. (맨 처음 예로 들었던 상품 추가 메서드의 경우 createProduct(requestDto); 로, 인수까지 전달함)
<일기>
SK C&C 최종면접 떨어졌다 ㅠ.ㅠ
최종이 떨어져서 슬프기보다, 코테를 다시 봐야한다는게 더 화난다 ,,
10시 반에 뒷산에 다녀왔다. 낮은 산이라 왕복 1시간도 안 걸리는 곳에, 위에는 걷기 좋은 공원이 있어서 노래 들으면서 몇 바퀴 좀 돌았다.
모.. 떨어진 건 떨어진 거고, 금융쪽 노리면 서합율 50%이긴 하니까.. 교육 들으면서 틈틈이 써봐야 겠다.
산책다녀와서 술을 주욱 마셨다 ㅋ ㅋ
오늘 아웃소싱 프로젝트를 했다.
우리 팀이야 워낙 다 잘해서 믿고 버스를 탈 생각이다. ㅎㅎ,,
9시가 지나고 기존에 뉴스피드 프로젝트 하던 팀원분들을 돌아보았는데
내가 열심히 설파?한 방법들을 열심히 적용하고 있는 모습을 보니 뿌-듯 했다 ㅎㅎ,,
피그마도 쓰고, 내 노션 템플릿도 쓰고, 이슈도 쓰고, API 명세서도 쓰고, 커밋 컨벤션도 쓰고, 풀리퀘 템플릿도 쓰고 ㅎㅎㅎㅎㅎ
기본기를 잘 알려주고 간 것 같아 다행이다. 꼭 더 좋은 팀들 만나서 무럭무럭 성장했으면 좋겠다.
다들 좋은 분들이어서 괜히 이제 다른 팀이어도 신경 쓰게 된다. 잘 됐으면 좋겠다.
</일기>
'TIL ✍️' 카테고리의 다른 글
23년 12월 7일(목요일) - 47번째 TIL (0) | 2023.12.07 |
---|---|
23년 12월 6일(수요일) - 46번째 TIL (0) | 2023.12.06 |
23년 12월 4일(월요일) - 44번째 TIL (2) | 2023.12.04 |
23년 12월 1일(금요일) - 43번째 TIL : validation 검증하기 (0) | 2023.12.01 |
23년 11월 30일(목요일) - 42번째 TIL (0) | 2023.11.30 |