[내일배움캠프 Spring 4기] 50일차 TIL - Spring AOP | 예외처리

서예진·2024년 2월 21일
0

💻 오늘의 학습 키워드 💻

▸ 오늘의 코드카타
▸ Spring AOP
▸ 예외처리


▼ 오늘의 코드카타

2024년 2월 20일 - [프로그래머스] 26 : 괄호 회전하기 | 연속 부분 수열의 합의 개수


▼ Spring AOP

Spring의 AOP 애너테이션

1. @Aspect

  • Spring 빈(Bean) 클래스에만 적용 가능
  • AOP 설정하려는 클래스에 적용하면 됨

2. 어드바이스 종류: 부가기능 수행 시점

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

3. 포인트컷: 부가기능 적용 위치

  • 포인트컷 Expression Language
    • 포인트컷 Expression 형태
      execution(modifiers-pattern? return-type-pattern declaring-type-pattern? **method-name-pattern(param-pattern)** throws-pattern?)
      • ? 는 생략 가능
      • 포인트컷 Expression 예제
        @Around("execution(public * com.sparta.myselectshop.controller..*(..))")
        public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
    • modifiers-pattern
      • public, private, *
      • * -> public 이든 private 이든 상관없음
    • return-type-pattern
      • void, String, List, *
    • 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개 (타입 상관없음)
    • @Pointcut
      • 포인트컷 재사용 가능
      • 포인트컷 결합 (combine) 가능
        @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() {
        		...
        	}	
        }

Spring AOP 적용

  • ProductController 에 추가했던 부가기능 제거 (Rollback)
  • AOP 사용해 FolderController, ProductController, NaverApiController 에 부가기능 추가
    • [aop > UseTimeAop
      - @Aspect 부가기능이 있는 클래스에 적용
      @Slf4j(topic = "UseTimeAop")
      @Aspect
      @Component
      public class UseTimeAop {
      
          private final ApiUseTimeRepository apiUseTimeRepository;
      
          public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
              this.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 {
                  // 핵심기능 수행
                  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);
                  }
              }
          }
      }

Spring AOP 동작 이해

  • 개념적 이해
  • 스프링 실제 동작
  • 시퀀스 다이어그램
    - AOP 적용 전

    - AOP 적용 후

    - Spring이 프록시(가짜 혹은 대리) 객체를 중간에 삽입해줌
    - DispatcherServlet과 ProductController 입장에서는 변화가 전혀 없음
    - 호출되는 함수의 input, ouput이 완전 동일
    - "joinPoint.proceed()" 에 의해서 원래 호출하려고 했던 함수, 인수(argument)가 전달됨
    -> createProduct(requestDto);

@Transactional도 AOP에 의해서 실행됨


▼ 예외처리

API 예외처리란 무엇일까?

예외 처리를 따로 배우는 이유

  • 웹 애플리케이션에서의 에러를 Client와 Server 모두가 잘 알지 못하면, 서비스하는 환경에서 발생하는 에러에 대해서 제대로 대응 할 수 없음
  • 에러를 처리하는 것 역시 관심사를 분리해서 더 효율적으로 처리할 수 있지 않을까

웹 애플리케이션의 에러

HTTP 에러 메시지 전달 방법 이해

  • 응답 헤더에는 API 요청에 대한 상태코드를 함께 보냄
  • Response 메시지
    - start-line (상태줄) : API 요청 결과 (상태 코드, 상태 텍스트)
    HTTP/1.1 **404** **Not Found**
    • HTTP 상태 코드 종류
      - 2xx Success -> 200번대의 상태코드는 성공을 의미
      - 4xx Client Error -> 400번대의 상태코드는 클라이언트 에러, 즉 잘못된 요청을 의미
      - 5xx Server Error -> 500번대이 상태코드는 서버 에러, 즉 정확한 요청에 서버쪽 사유로 에러가 난 상황을 의미

Spring 예외처리 방법

1. Controller 코드 수정
  • ResponseEntity 클래스 사용
  • ResponseEntity는 HTTP response object 를 위한 Wrapper
  • 아래와 같은 것들을 담아서 response로 내려주면 아주 간편하게 처리가 가능
    - HTTP status code
    - HTTP headers
    - HTTP body
  • RestApiException -> exception 패키지 따로 만들어서 관리
    @Getter
    @AllArgsConstructor
    public class RestApiException {
        private String errorMessage;
        private int statusCode;
    }
  • Controller 수정
    	```java
    	@PostMapping("/folders")
    	public ResponseEntity<RestApiException> addFolders(@RequestBody FolderRequestDto folderRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
    	    try {
    	        List<String> folderNames = folderRequestDto.getFolderNames();
            
    	        folderService.addFolders(folderNames, userDetails.getUser());
    	        return new ResponseEntity<>(HttpStatus.OK);
    	    } catch(IllegalArgumentException ex) {
    	        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
    	        return new ResponseEntity<>(
    	                // HTTP body
    	                restApiException,
    	                // HTTP status code
    	                HttpStatus.BAD_REQUEST);
    	    }
    	}
    	```
    	- addFolders 메서드를 보면 try-catch문을 통해서 
    		- return new ResponseEntity<>(HttpStatus.OK);
    		- return new ResponseEntity<>( // HTTP body restApiException, // HTTP status code HttpStatus.BAD_REQUEST);
    		이렇게 나뉘어서 응답을 내주는 것을 볼 수 있음
    	- 에러문구도 캐치하고, 그에 따라서 어느 부문에서 오류가 났는지 적어서 보내 줄 수 있음
2. @ExceptionHandler 사용
  • FolderController 의 모든 메서드에 예외처리 적용 (AOP) :
    @ExceptionHandler
    • @ExceptionHandler 는 Spring에서 예외처리를 위한 애너테이션
    • 이 애너테이션은 특정 Controller에서 발생한 예외를 처리하기 위해 사용
    • @ExceptionHandler가 붙어있는 메서드는 Controller에서 예외가 발생했을 때 호출 되며, 해당 예외를 처리하는 로직을 담고 있음
    • AOP를 이용한 예외처리 방식이기때문에, 위에서 본 예시처럼 메서드 마다 try catch할 필요없이 깔끔한 예외처리 가능
  • @ExceptionHandler 예외처리 추가 -> Controller 밑에다가 붙이기

    @ExceptionHandler({IllegalArgumentException.class})
    public ResponseEntity<RestApiException> handleException(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.BAD_REQUEST
        );
    }

Spring의 Global 예외처리

Global 예외처리 방법

  • 예외처리 로직 자체는 매우 공통적 -> global하게 처리할 수 있음
Global 예외 처리

  • @ControllerAdvice 사용
    • @ControllerAdvice
      • @ControllerAdvice는 Spring에서 예외처리를 위한 클래스 레벨 애너테이션
      • 이 애너테이션은 모든 Controller에서 발생한 예외를 처리하기 위해 사용
      • @ControllerAdvice 가 붙은 클래스에서는 @ExceptionHandler메서드를 정의하여 예외를 처리하는 로직을 담을 수 있음
      • ==@ControllerAdvice 를 사용하는 이유?==
        • 예외처리를 중앙 집중화하기 좋음
        • 각각의 Controller에서 예외처리 로직을 반복하지 않아도 됨으로 코드의 중복을 방지하고 유지보수성을 높일 수 있음
        • 또한, @ControllerAdvice를 사용하면 예외 처리 로직을 모듈화하여 관리하기 쉽기 때문에, 팀 내에서 공통된 예외 처리 로직을 공유하거나 다른 팀에서 예외 처리를 참고할 수 있음
        • 이를 통해 개발 생산성을 향상시키는 것도 가능
  • @RestControllerAdvice

    • @ControllerAdvice + @ResponseBody
    • JSON 형태로 반환할 떄
@RestControllerAdvice 적용
  • exception > GlobalExceptionHandler
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler({IllegalArgumentException.class})
        public ResponseEntity<RestApiException> handleException(IllegalArgumentException ex) {
            RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
            return new ResponseEntity<>(
                    // HTTP body
                    restApiException,
                    // HTTP status code
                    HttpStatus.BAD_REQUEST
            );
        }
    }
FolderController의 예외처리 메서드 제거

Error 메시지 관리하기

Spring의 properties 파일을 이용한 에러 메시지 관리

  • Spring에서는 properties 파일을 이용하여 에러 메시지를 관리할 수 있음
  • 에러 메시지는 properties 파일에서 key-value 형태로 작성되며, 작성된 값은 messageSource 를 Bean으로 등록하여 사용할 수 있음
    - resources > messages.properties
    below.min.my.price=최저 희망가는 최소 {0}원 이상으로 설정해 주세요.
    not.found.product=해당 상품이 존재하지 않습니다.
  • Spring Boot에서는 messageSource 가 자동으로 Bean으로 등록됨
    private final MessageSource messageSource;
    
    ...
    
    @Transactional
    public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
        int myprice = requestDto.getMyprice();
        if (myprice < MIN_MY_PRICE) {
            throw new IllegalArgumentException(messageSource.getMessage(
                    "below.min.my.price",
                    new Integer[]{MIN_MY_PRICE},
                    "Wrong Price",
                    Locale.getDefault()
            ));
        }
    
        Product product = productRepository.findById(id).orElseThrow(() ->
                new ProductNotFoundException(messageSource.getMessage(
                        "not.found.product",
                        null,
                        "Not Found Product",
                        Locale.getDefault()
                ))
        );
    
        product.update(requestDto);
    
        return new ProductResponseDto(product);
    }
    • Exception 클래스를 직접 구현하여 사용할 수도 있음
      - ProductNotFoundException

      package com.sparta.myselectshop.exception;
      
      public class ProductNotFoundException extends RuntimeException{
          public ProductNotFoundException(String message) {
              super(message);
          }
      }
  • messageSource.getMessage()메서드
    • 첫번째 파라미터는 messages.properties 파일에서 가져올 메시지의 키 값을 전달
    • 두번째 파라미터는 메시지 내에서 매개변수를 사용할 경우 전달하는 값
    • 세번째 파라미터는 언어 설정을 전달
      • Locale.getDefault()메서드는 기본 언어 설정을 가져오는 메서드
  • GlobalExceptionHandler
    	```java
    	package com.sparta.myselectshop.exception;
    	import org.springframework.http.HttpStatus;
    	import org.springframework.http.ResponseEntity;
    	import org.springframework.web.bind.annotation.ExceptionHandler;
    	import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    	@RestControllerAdvice
    	public class GlobalExceptionHandler {
    
    	    @ExceptionHandler({IllegalArgumentException.class})
    	    public ResponseEntity<RestApiException> illegalArgumentExceptionHandler(IllegalArgumentException ex) {
    	        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
    	        return new ResponseEntity<>(
    	                // HTTP body
    	                restApiException,
    	                // HTTP status code
    	                HttpStatus.BAD_REQUEST
    	        );
    	    }
    
    	    @ExceptionHandler({NullPointerException.class})
    	    public ResponseEntity<RestApiException> nullPointerExceptionHandler(NullPointerException ex) {
    	        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
    	        return new ResponseEntity<>(
    	                // HTTP body
    	                restApiException,
    	                // HTTP status code
    	                HttpStatus.NOT_FOUND
    	        );
    	    }
    
    	    @ExceptionHandler({ProductNotFoundException.class})
    	    public ResponseEntity<RestApiException> notFoundProductExceptionHandler(ProductNotFoundException ex) {
    	        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
    	        return new ResponseEntity<>(
    	                // HTTP body
    	                restApiException,
    	                // HTTP status code
    	                HttpStatus.NOT_FOUND
    	        );
    	    }
    	}
    	```
  • Intellij File Encodings 설정

  • 위에 매개변수로 넣은 NOT_FOUND 는
  • 아래의 NOT_FOUND는
    => 이 부분은 개발자도구에서도 볼 수 있음
profile
안녕하세요

0개의 댓글