부가 기능을 모듈화하여 반복된 작업을 줄일 수 있는 기능
핵심 기능 : 각 API별 수행해야 할 비즈니스 로직
부가 기능 : 핵심 기능을 보조하는 기능
위의 그림과 같이 부가기능을 핵심 기능과 분리하여 API의 실행 시간 (우수 고객 기록)같은 반복되는 작업을 일 수 있다.
예를 들어보자
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
// 로그인 되어 있는 회원 테이블의 ID
Long userId = userDetails.getUser().getId();
Product product = productService.createProduct(requestDto, userId);
// 응답 보내기
return product;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 수행시간을 DB 에 기록
...
}
온라인 샵에서 회원의 사이트 사용 시간을 측정하고 싶다고 가정했을 때
해당 코드를 컨트롤러 단에 모두 추가하기에는 너무나도 많은 기능들이 있다.
이때 AOP를 사용해서 반복되는 작업을 줄일 수 있는데
Aspect : 특정 관심사를 가지는 모듈의 단위
조인 포인트 : 애플리케이션 시점중 Aspect가 적용될 수 있는 지점 @Aspect 어노테이션으로 지정해준다.
어드바이스 : 어드바이스는 조인 포인트에서 수행하는 특정 동작
ex)
포인트 컷 : 조인포인트를 Aspect에 매칭시키는 방법을 정의함.
=> 사용 방법
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
modifiers-pattern: public ,private
return-type-pattern: 리턴 타입
declaring-type-pattern : 클래스명 (패키지명 필요)
ex )
method-name-pattern(param-pattern) :
@PointCut 어노테이션
@Component
@Aspect
public class Aspect {
@Pointcut("execution(* com.sparta.myselectshop.controller.*.*(..))")
private void forAllController() {}
//com.sparta.myselectshop.controller 패키지에 속한 모든 클래스의 모든 메서드에 매칭됩니다
@Pointcut("execution(String com.sparta.myselectshop.controller.*.*())")
private void forAllViewController() {}
//이 포인트컷은 com.sparta.myselectshop.controller 패키지에 속한 모든 클래스의 반환 타입이 String인 메서드에 매칭됩니다.
}
적용 방법
@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() {}
// ...
}
ProductController,FolderController,NaverApiController
클래스의 매서드 모두에 적용합니다.
@Around("product() || folder() || naver()")
해당 메서드를 시작과 끝에 실행합니다.
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();
// 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);
}
}
}
@ExceptionHandler :
스프링의 예외처리를 위한 애너테이션이고 AOP 기능을 수행할 수 있습니다.
`@ExceptionHandler` **가 붙어있는 **메서드는 Controller에서 예외가 발생했을 때 호출 되며, 해당 예외를 처리하는 로직을 담고 있습니다.
AOP를 이용한 예외처리 방식이기때문에, 메서드 마다 try catch할 필요없이 깔금한 예외처리가 가능합니다.
과제에 직접 적용해보았다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({CustomException.class})
public ResponseEntity<?> customExceptionHandler(CustomException ex){
System.out.println("글로벌 예외처리");
return new ResponseEntity<>(ex.getErrorCode().getMessage(), ex.getErrorCode().getStatus());
}
}
CustomException이 터지면 @ExceptionHandler가 작동한다.
이후 상태코드와 메세지를 반환한다.
CustomException 클래스
@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
}
Enum 타입 ErrorCode
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
//Password
NOT_AUTHORITY(HttpStatus.BAD_REQUEST, "작성자만 삭제/수정할 수 있습니다."),
NO_POST(HttpStatus.BAD_REQUEST, "잘못된 게시글 정보입니다."),
//Login / Register
LOGIN_NO(HttpStatus.BAD_REQUEST, "회원을 찾을 수 없습니다."),
NAME_SAME(HttpStatus.BAD_REQUEST, "중복된 username 입니다."),
//Token
WRONG_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 유효하지 않습니다."),
//댓글
WRONG_BOARD_PID(HttpStatus.BAD_REQUEST, "잘못된 게시글 접근 입니다."),
WRONG_COMMENT_PID(HttpStatus.BAD_REQUEST, "잘못된 댓글 접근 입니다."),
WRONG_NAME(HttpStatus.BAD_REQUEST, "작성자만 삭제/수정할 수 있습니다.")
;
private final HttpStatus status;
private final String message;
}