@Transactional 을 어제 공부하다가 AOP로 작동한다는 것을 좀 더 파보기 위해 AOP를 좀 더 공부해보았다.
가장 많이 이용한 사용자 5명에게 선물을 주는 Event
이용의 기준을 그냥 켜놓고 놔둔 시간이 아니라 서버 사용 시간을 기준으로 했을 때,
'Controller에 요청이 들어온시간 - Controller에서 응답이 나간 시간' 으로 계산.
구현
시간 측정하는 Entity 만들어서 user_id를 FK로 불러오고 시간 측정 컬럼 만들어둠.
repo도 하나 만들고 측정하고자 하는 Controller의 API에 다음과 같이 코딩
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)
.orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면
apiUseTime.addUseTime(runTime);
}
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
핵심기능 : 각 API별 수행해야 할 비즈니스 로직
ex) 상품 검색, 회원가입, 로그인, 관심 폴더에 저장 등
부가기능 : 핵심기능을 보조하는 기능
ex) 회원 패턴 분석을 위한 로그 기록, API 수행시간 저장
위의 예시와 같이 모든 핵심기능에 부가기능을 추가해야 할 경우 가독성이 떨어지고 코드가 길어지며 번거로워질 가능성이 생긴다.
이를 해결하기 위해 AOP를 통해 부가기능을 모듈화해서 사용할 수 있다.
Aspect Oriented Programming. 관점 지향 프로그래밍.
어떤 로직을 기준으로 핵심적인 관점 / 부가적인 관점으로 나누어서 보고
그 관점을 기준으로 각각을 모듈화(공통된 로직이나 기능을 하나의 단위로 묶음) 한다는 것.
코드 상에서 다른부분에 계속 반복해서 쓰는 코드를 흩어진 관심사(Crosscutting Concerns), (주로 부가기능)라 한다. 이런 부가기능들을 Aspect로 모듈화 하고 핵심기능(비즈니스 로직)에서 분리해 재사용 하는것이 AOP의 취지이다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
@Around("execution(public * com.sparta.myselectshop.controller..*(..))")
public synchronized 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();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면
apiUseTime.addUseTime(runTime);
}
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
@Aspect : 해당 클래스가 Aspect(부가기능이 모듈화된 것)임을 명시.
@Component : 부가기능도 Bean으로 등록해야 한다. 스프링에선 Bean에만 AOP를 적용할 수 있다.
@Around("execution(public * 적용할패키지경로
..*(..))") : 해당 경로에 아래의 메서드를 동작 시키겠다. (이 포스트의 하단에 자세하게 설명)
Object output = joinPoint.proceed();
return output;
: 핵심 기능(비즈니스 로직)을 실행한다는 의미. 이 코드가 있는 위치에서 핵심기능이 수행된다.
스프링에서는 위의 경로를 통해 AOP가 적용된다.
AOP가 없을때는 DispatcherServelet에서 Controller로 바로 넘어가지만,
AOP를 사용하면 앞에 Proxy객체가 먼저오고 Advice가 실행된 다음 그 안의
joinPoint.proceed() 코드 자리에서 Controller의 핵심기능이 실행된다.
joinPoint.proceed()에 의해 원래 호출하려 했던 함수와 인수가 전달되니
부가기능이 추가된 것 말고는 DispatcherServlet과 Controller입장에서 달라질 것은 전혀 없다.
// Spring AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
이부분은 외우려하지말고 필요할때 검색으로 찾아서 사용하면 된다.
@Aspect : AOP 부가기능임을 선언. Bean에만 사용 가능하니 @Component도 꼭 같이 써야
시점
을 지정. AOP안의 메서드 위에다 단다)구체적으로 advice가 실행되는 장소
를 지정. ?는 생략가능
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
// 예시
@Around("execution(public * com.sparta.springcore.controller..*(..))")
- modifiers-pattern(제어자) : public, private, *
- return-type-pattern(반환타입) : void, String, int, List<User>, * 등
- declaring-type-pattern : 클래스명(패키지명도 필요). 아래는 예시
com.sparta.controller.* : *로 끝나면 controller 패키지의 모든 클래스에 적용
com.sparta.controller.. : ..으로 끝나면 controller 패키지 및 하위 패키지의 모든 클래스에 적용- method-name-pattern(param-pattern) : 함수명(파라미터 패턴). 아래는 예시
addFolders : addFolders() 함수에만 적용
add* : add 로 시작하는 모든 함수에 적용
(com.sparta.dto.FolderRequestDto) : FolderRequestDto의 파라미터에만 적용
() : 인수없음
(*) : 인수 1개(타입상관x)
(..) : 인수 0~N개(타입상관x)
*은 모든 경우가 다 가능하다는 의미
@Component
@Aspect
public class Aspect {
@Pointcut("excution pointcut경로1")
private void forAllController() {}
@Around("forAllController()")
public void saveAllApiLog(){...}
Spring AOP는 Spring IoC를 통한 간단한 AOP의 구현이 목적이다.
완전한 AOP 구현을 의도하지 않고 Spring Container에 의해 관리되는 Bean에만 적용이 가능.
런타임 weaving. Proxy기반 AOP. 순수 java만으로도 구현이 된다.
반면 AspectJ는 완전한 AOP를 제공하는 것이 목적인 근원적인 AOP 기술이다.
Spring AOP보다 강력하고 복잡하다. Bean이 아닌 모든 객체에 적용이 가능하다.
컴파일이나 로드타임 weaving. 클래스들이 Aspect와 같이 컴파일되기때문에 런타임시에는 아무것도 하지 않는다. 구현시 추가 도구가 필요.
Weaving : AOP가 적용되는 시점. 컴파일타임/로드타임/런타임.