일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- UE4
- 유클리드호제법
- 문자열
- 워크플로
- 이분탐색
- c#
- C++
- 포톤
- UnrealEngine
- 알고리즘
- 프로그래머스
- BFS
- Photon
- QueryDSL
- 내일배움캠프
- 언리얼엔진
- Firebase
- unityui
- Unity3d
- FSM
- Unity
- 구현
- Inventory
- 스파르타내일배움캠프TIL
- Unity2D
- 스택
- 순열
- 유니티
- 스파르타내일배움캠프
- 해시
- Today
- Total
개발 낙서장
[TIL] 내일배움캠프 38일차 - AOP 본문
오늘의 학습 키워드📚
AOP(Aspect Oriented Programming)
어떤 쇼핑몰 사이트를 서비스하고 있다고 했을 때, 이용자들의 평균 이용 시간을 집계하고 싶다면 어떻게 해야 될까?
API 요청부터 응답까지의 시간을 체크해 유저 별로 DB에 저장하는 방법이 있을 것이다.
@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();
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
apiUseTime.addUseTime(runTime);
}
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
이렇게 API에 시간을 측정하는 로직을 추가하면 간단히(?) 해결될 것이다. 일단 어떻게든 저장은 가능할 것이다...
근데 측정해야 할 API가 10개라면? 100개라면? 모든 API에 해당 로직을 추가했는데, 만약에 이용자가 몰릴 때 시간이 제대로 저장이 되지 않는 문제가 발생해 로직을 수정해야 한다면?
100군데에 퍼져 있는 코드를 전부 수정해줘야 할 것이다. 말도 안 되는 짓이다.
따라서 Spring에서 제공하는 AOP 애너테이션을 통해 이런 부가 기능들을 모듈화 할 필요가 있다.
그래서 어떻게 쓰는데
당연히 @AOP 이런식으로 달아서 사용하는 것은 아니다.
AOP로 사용한다는 것을 알려주고 어디에, 어떤 메소드를, 언제, 어떻게 사용할지 전부 지정해줘야 한다.
- @Aspect : Bean 클래스에 등록 가능하며 클래스 상단부에 등록한다. 해당 클래스가 AOP라는 것을 지정해 주는 어노테이션이다.
- 어드바이스 : 부가 기능이 포함된 메소드 상단부에 등록한다. 해당 메소드가 어느 시점에 실행되는지를 지정해 주는 어노테이션이다.
- @Around : '핵심 기능' 전과 후 모두 실행
- @Before : '핵심 기능' 전에 실행
- @After : '핵심 기능' 후에 실행
- @AfterReturning : '핵심 기능' 호출 성공 시(함수의 Return 값 사용 가능)
- @AfterThrowing : '핵심 기능' 호출 실패(예외 발생) 시
- 포인트컷 : AOP가 실행될 위치를 지정해주는 어노테이션이다.
- 포인트컷 Expression(?가 붙은 부분은 생략 가능)
- execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
- 예제
@Around("execution(public \* com.sparta.myselectshop.controller..\*(..))") public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
- modifiers-pattern : public, private, *
- return-type-pattern : void, String, List<String>, ...
- declaring-type-pattern : 클래스 명(패키지 명 포함)
- method-name-pattern : 함수 명(ex. addProducts : addProducts 함수만, add* : add로 시작하는 함수 전부)
- (param-pattern) : 파라미터 패턴. () : 0개, (*) : 1개, (..) : 0~N개
- 예제
- @Pointcut : 설정한 포인트컷을 재사용 가능하게 해주는 어노테이션.
- execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
- 포인트컷 Expression(?가 붙은 부분은 생략 가능)
@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() {
...
}
}
뭔가 되게 많아서 복잡해 보이지만 생각보다 어렵진 않다.(물론 기능별로 세세하게 다루게 되면 어려워지겠지만?)
product 관련 API를 처리할 때 발생한 시간을 유저 별로 저장하는 부가 기능을 구현하려면?
1. AOP 클래스 생성
@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
}
2. Pointcut 지정
@Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
private void product() {
}
3. 어드바이스 지정 및 부가 기능 구현
@Around("product()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object output = joinPoint.proceed();
return output;
} 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();
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
if (apiUseTime == null) {
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
apiUseTime.addUseTime(runTime);
}
apiUseTimeRepository.save(apiUseTime);
}
}
}
User API를 다뤘을 때도 추가하고 싶다면 해당 과정을 반복하면 된다.
상품을 검색했더니 자동으로 부가 기능이 실행돼 시간이 저장되는 모습이다.
당연하지만 검색할 때마다(해당 API가 호출될 때마다) 시간이 늘어난다.
특정 API에만 추가하고 싶다면?
공통적으로 AOP를 추가하고 관리하는 방법은 알았는데, 만약 상품을 구매하는데 평균적으로 얼마나 걸리는지 알고 싶다면? 전체 API 호출이 아닌 그때그때 원하는 API를 지정해 주고 로그를 보고 싶은데 메소드랑 패키지 명이 전부 다르다면?
뭐 AOP 클래스에서 일일이 포인트컷을 지정하는 방법도 있겠지만 너무 비효율적이고 가독성도 떨어진다.
이럴 때 가능한 것이 애너테이션을 직접 구현해 AOP를 적용하는 방법이 있다.
구현된 어노테이션을 쓰는 것뿐만이 아니라 직접 구현할 수 있다는 게 굉장히 신기했는데, 방법은 되게 간단하다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
1. @inteface로 해당 인터페이스를 어노테이션으로 지정한다.
2. @Target으로 어떤 대상에 사용되는 어노테이션인지 설정한다.(필드, 어노테이션, 메소드 등)
ANNOTATION_TYPE | 어노테이션 |
CONSTRUCTOR | 생성자 |
FIELD | 필드 선언 (emum 정수 포함) |
LOCAL_VARIABLE | 로컬 변수 |
METHOD | 메서드 |
PARAMETER | 파라미터 |
PACKAGE | 패키지 |
TYPE | 클래스, 인터페이스 (어노테이션을 포함), enum |
3. @Retention으로 어노테이션의 지속 시간을 설정한다.
- RetentionPolicy.SOURCE : 컴파일 후에 정보들이 사라진다. 이 어노테이션은 컴파일이 완료된 후에는 의미가 없으므로, 바이트 코드에 기록되지 않는다. 예시) @Override와 @SuppressWarnings
- RetentionPolicy.CLASS : default 값입니다. 컴파일 타임 때만 .class 파일에 존재하며, 런타임 때는 없어진다. 바이트 코드 레벨에서 어떤 작업을 해야 할 때 유용하다. Reflection 사용이 불가하다.
- RetentionPlicy.RUNTIME : 이 어노테이션은 런타임시에도 .class 파일에 존재한다. 커스텀 어노테이션을 만들 때 주로 사용한다. Reflection 사용이 가능하다.
이렇게 만든 어노테이션은 AOP 클래스에서 포인트컷으로 지정해 줄 수 있다.
아까는 패키지, 클래스로 포인트컷을 지정했지만 이제는 해당 어노테이션으로 지정할 수도 있다.
@Pointcut("@annotation(com.sparta.myselectshop.aop.annotation.Timer)")
private void annotationPointcut() {
}
오늘의 회고💬
Mock을 활용한 테스트 코드라던가 AOP라는 새로운 걸 배우면서도 이해하기 크게 어렵지 않아서 꽤 재밌던 파트였다.
내일의 계획📜
이제 테스트 코드를 작성하는 개인 과제를 해야 한다!
'Java > Sparta' 카테고리의 다른 글
[TIL] 내일배움캠프 40일차 - Service 테스트 (0) | 2024.02.22 |
---|---|
[TIL] 내일배움캠프 39일차 - 테스트 Validate (0) | 2024.02.20 |
[TIL] 내일배움캠프 37일차 - 단위 테스트 (0) | 2024.02.16 |
[TIL] 내일배움캠프 36일차 - DTO와 Entity (0) | 2024.02.15 |
[TIL] 내일배움캠프 35일차 - Filter 단에서 발생하는 403 에러 해결 (0) | 2024.02.14 |