AOP + API 예외처리

Ango·2023년 7월 10일

SPRING

목록 보기
12/13

❓ 공부할 개념

> AOP란

부가 기능을 모듈화하여 반복된 작업을 줄일 수 있는 기능

🔑 개념 정리

핵심 기능 : 각 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)

  • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
  • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
  • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
  • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
  • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)

포인트 컷 : 조인포인트를 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 )

  • 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개 (타입 상관없음)

@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);
            }
        }
    }

API 예외처리

@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;
}

profile
웹 벡엔드 개발자가 되어보자!

0개의 댓글