[Spring] Exception

hyewon jeong·2023년 6월 23일
1

Spring

목록 보기
46/59

0. 예외 처리를 따로 다루는 이유?

  • 첫 번째로 우리는 웹 어플리게이션에서의 “예외”에 대하여 다시 한 번 인지할 필요가 있다.

웹 어플리케이션에서의 에러를 프론트엔드와 백엔드 모두가 잘 알지 못하면,

서비스하는 환경에서 발생하는 에러에 대해서 제대로 대응 할 수 없다.

  • 두 번째는 우리는 aop를 배웠던 만큼, 에러를 처리하는 것 역시 관심사를 분리해서 더 효율적으로 처리 할 수 있다.

1. 스프링 예외처리 방법

1-1. ResponseEntity 클래스 사용

  • ResponseEntity는 HTTP response object 를 위한 Wrapper입니다. 아래와 같은 것들을 담아서 response로 내려주면 아주 간편하게 처리가 가능합니다.
    - HTTP status code
    - HTTP headers
    - HTTP body
  • [코드스니펫] RestApiException.java
    package com.sparta.myselectshop.exception;
    
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.http.HttpStatus;
    
    @Getter
    @Setter
    public class RestApiException {
        private String errorMessage;
        private HttpStatus httpStatus;
    }

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class FolderController {

    private final FolderService folderService;

    @PostMapping("/folders")
    public ResponseEntity addFolders(
            @RequestBody FolderRequestDto folderRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try { 
            List<String> folderNames = folderRequestDto.getFolderNames();
            User user = userDetails.getUser();

            List<Folder> folders = folderService.addFolders(folderNames, user.getUsername());
            return new ResponseEntity(folders, HttpStatus.OK);
        } catch(IllegalArgumentException ex) {
            RestApiException restApiException = new RestApiException();
            restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
            restApiException.setErrorMessage(ex.getMessage());
            return new ResponseEntity(
                    // HTTP body
                    restApiException,
                    // HTTP status code
                    HttpStatus.BAD_REQUEST);
        }
    }

1-2. @ExceptionHandler 사용

  • FolderController 의 모든 함수에 예외처리 적용 (AOP)
  • [코드스니펫] @ExceptionHandler 적용
        @ExceptionHandler({ IllegalArgumentException.class })
        public ResponseEntity handleException(IllegalArgumentException ex) {
            RestApiException restApiException = new RestApiException();
            restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
            restApiException.setErrorMessage(ex.getMessage());
            return new ResponseEntity(
                    // HTTP body
                    restApiException,
                    // HTTP status code
                    HttpStatus.BAD_REQUEST
            );
        }
  • [코드스니펫] FolderController.java 예외 처리 추가

org.springframework.security.core.annotation.AuthenticationPrincipal;
        import org.springframework.web.bind.annotation.*;
        
        import java.util.List;
        
        @RestController
        @RequestMapping("/api")
        @RequiredArgsConstructor
        public class FolderController {
        
            private final FolderService folderService;
        
            @PostMapping("/folders")
            public List<Folder> addFolders(
                    @RequestBody FolderRequestDto folderRequestDto,
                    @AuthenticationPrincipal UserDetailsImpl userDetails
            ) {
        
                List<String> folderNames = folderRequestDto.getFolderNames();
        
                return folderService.addFolders(folderNames, userDetails.getUsername());
            }
        
            // 회원이 등록한 모든 폴더 조회
            @GetMapping("/folders")
            public List<Folder> getFolders(
                    @AuthenticationPrincipal UserDetailsImpl userDetails
            ) {
                return folderService.getFolders(userDetails.getUser());
            }
        
            // 회원이 등록한 폴더 내 모든 상품 조회
            @GetMapping("/folders/{folderId}/products")
            public Page<Product> getProductsInFolder(
                    @PathVariable Long folderId,
                    @RequestParam int page,
                    @RequestParam int size,
                    @RequestParam String sortBy,
                    @RequestParam boolean isAsc,
                    @AuthenticationPrincipal UserDetailsImpl userDetails
            ) {
                return folderService.getProductsInFolder(
                        folderId,
                        page-1,
                        size,
                        sortBy,
                        isAsc,
                        userDetails.getUser()
                );
            }
        
            @ExceptionHandler({ IllegalArgumentException.class })
            public ResponseEntity handleException(IllegalArgumentException ex) {
                RestApiException restApiException = new RestApiException();
                restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
                restApiException.setErrorMessage(ex.getMessage());
                return new ResponseEntity(
                        // HTTP body
                        restApiException,
                        // HTTP status code
                        HttpStatus.BAD_REQUEST
                );
            }
        
        }

1-3. 스프링 Global 예외 처리 방법(@RestControllerAdvice)

사실 예외처리 로직 자체는 매우 공통적이다.
그렇다면 지금처럼 매 컨트롤러마다 예외처리를 해주는 것이 아니라, 글로벌하게 처리 하기가능~

  • @ControllerAdvice 사용
  • @RestControllerAdvice
    • @ControllerAdvice + @ResponseBody

ErrorDTO

@Getter
@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED)
public class ErrorDto {

  private final int statusCode;
  private final String statusMessage;

  public ErrorDto(int statusCode, String statusMessage) {
    this.statusCode = statusCode;
    this.statusMessage = statusMessage;
  }
}

CustomException
전역으로 예외처리 할 커스텀 클래스 
```java
@AllArgsConstructor
@Getter
public class CustomException extends  RuntimeException{
    private final ExceptionStatus exceptionStatus;
}

ExceptionStatus
에러코드

@AllArgsConstructor
@Getter
public enum ExceptionStatus {
    DUPLICATED_USERNAME(409, "이미 사용중인 아이디입니다."),
    DUPLICATED_NICKNAME(408, "이미 사용중인 닉네임입니다."),
    DUPLICATED_EMAIL(407, "이미 사용중인 이메일입니다."),
    DUPLICATED_PHONENUMBER(406, "이미 사용중인 휴대폰번호입니다."),
    SIGNUP_WRONG_USERNAME(409, "최소 4자 이상, 10자 이하이며, 영문과 숫자만 입력하세요."),
    WRONG_USERNAME(404, "아이디를 잘못 입력 하였거나 등록되지 않은 아이디 입니다."),

    WRONG_PASSWORD(400, "잘못된 비밀번호 입니다."),
    WRONG_PROFILE(404, "프로필이 존재하지 않습니다."),
    WRONG_AUTHORITY_DEMAND(404, "판매자 권한 신청서가 존재하지 않습니다"),
    ALREADY_EXIST_SELLER(409,"셀러로 등록된 아이디 입니다,"),
    ALREADY_EXIST_ADMIN(409,"어드민으로 등록된 아이디 입니다."),
    ALREADY_EXIST_REQUEST(409,"이미 전송된 요청입니다."),
    ALREADY_PROCESSED_REQUEST(409,"이미 처리된 요청입니다."),
    WRONG_ADMINTOKEN(400, "잘못된 관리자 비밀번호 입니다."),
    ACCESS_DENINED(500, "접근 권한이 없습니다."),
    AUTHENTICATION(500, "인증 실패"),
    AUTH_EXPIRED(501, "인증 만료"),
    NOT_FOUNT_USER(404,"해당 사용자가 존재하지 않습니다."),
    NOT_FOUNT_TOKEN(404,"토큰이 일치하지 않습니다."),
    ROOM_NOT_EXIST(404, "방이 삭제되어 존재하지 않습니다."),

    BOARD_NOT_EXIST(404, "게시물이 삭제되어 존재하지 않습니다."),
    COMMENT_NOT_EXIST(404, "해당 댓글이 삭제되어 존재 하지 않습니다."),
    COMMENT_REPLY_NOT_EXIST(404, "해당하는 댓글이 삭제되어 존재 하지 않습니다."),
    REQUEST_NOT_EXIST(404,"해당하는 요청이 존재하지 않습니다."),
    WRONG_POST_ID(404,"게시글 번호가 일치하지 않습니다."),
    POST_IS_EMPTY(404,"해당 페이지는 게시글이 존재하지 않습니다."),
    SECRET_POST(403,"비밀글입니다. 해당 사용자 외엔 조회 불가능 합니다. "),
    WRONG_SELLER_ID_T0_BOARD(403,"다른 판매자의 게시물에는 접근 할 수 없습니다."),
    WRONG_USER_T0_COMMENT(403,"다른 유저의 댓글에는 접근 할 수 없습니다."),
    WRONG_USER_T0_CONTACT(403,"다른 유저의 게시글에는 접근 할 수 없습니다."),
    WRONG_USER_T0_COMMENT_REPLY(403,"다른 유저의 대댓글에는 접근 할 수 없습니다."),
    WRONG_SELLER_ID_TO_USER_REQUEST(403,"다른 판매자의 요청목록에는 접근 할 수 없습니다."),

    IS_NOT_CORRECT_FORMAT(400,"지원하지 않는 형식입니다.");

    private final int statusCode;
    private final String message;
}

에러코드 참고용

package com.sparta.myselectshop.exception;

import org.springframework.http.HttpStatus;

import static com.sparta.myselectshop.service.ProductService.MIN_MY_PRICE;


public enum ErrorCode {
    // 400 Bad Request
    DUPLICATED_FOLDER_NAME(HttpStatus.BAD_REQUEST, "400_1", "중복폴더명이 이미 존재합니다."),
    BELOW_MIN_MY_PRICE(HttpStatus.BAD_REQUEST, "400_2", "최저 희망가는 최소 " + MIN_MY_PRICE + " 원 이상으로 설정해 주세요."),

    // 404 Not Found
    NOT_FOUND_PRODUCT(HttpStatus.NOT_FOUND, "404_1", "해당 관심상품 아이디가 존재하지 않습니다."),
    NOT_FOUND_FOLDER(HttpStatus.NOT_FOUND, "404_2", "해당 폴더 아이디가 존재하지 않습니다."),
    ;

    private final HttpStatus httpStatus;
    private final String errorCode;
    private final String errorMessage;

    ErrorCode(HttpStatus httpStatus, String errorCode, String errorMessage) {
        this.httpStatus = httpStatus;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
}

ExceptionAdviceHandler
예외 처리를 담당하는 클래스

@Slf4j
@RestControllerAdvice
public class ExceptionAdviceHandler {


  @ExceptionHandler({CustomException.class})
  //   @ResponseStatus(HttpStatus.BAD_REQUEST)  정적으로 예외에 대한 값 설정
  protected ResponseEntity handleApiException(CustomException customException) {
    return new ResponseEntity<>(new ErrorDto(
        customException.getExceptionStatus().getStatusCode(),
        customException.getExceptionStatus().getMessage()),
        HttpStatus.valueOf(customException.getExceptionStatus()
            .getStatusCode())); //예외처리의 상태코드를 가진 HttpStatus 객체를 반환, 동적으로 예외값 처리
    //HttpStatus.valueOf(customException.getExceptionStatus().getStatusCode()) 을 간략히   HttpStatus.BAD_REQUEST 대신 사용 가능 
  }

  //상태코드를 404 로 넣어줌
  @ResponseStatus(HttpStatus.NOT_FOUND) // 별도로 ResponseEntity를 생성할 필요 없이 예외가 발생할 때 자동으로 NOT_FOUND 응답이 반환됩니다.
  @ExceptionHandler(IllegalArgumentException.class)
  protected ErrorDto handleIllegalArgumentException(IllegalArgumentException e) {
    log.warn(e.getMessage());
    return new ErrorDto(404, e.getMessage());
  }

  @ResponseStatus(HttpStatus.FORBIDDEN)
  @ExceptionHandler({SecurityException.class})
  protected ErrorDto SecurityExceptionHandler(SecurityException e){
    log.warn(e.getMessage());
    return new ErrorDto(403,e.getMessage());
  }

  @ExceptionHandler({NullPointerException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  protected ResponseEntity handleNullPointerException(NullPointerException e){
    return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
  }

  @ExceptionHandler({NoSuchElementException.class})
  protected ErrorDto handleMethodNotFindException(NoSuchElementException e){
    log.warn(e.getMessage());
    return new ErrorDto(403,e.getMessage());
  }

  @ExceptionHandler({MethodArgumentNotValidException.class})
  protected ResponseEntity handleMethodNotValidException(MethodArgumentNotValidException e){
    log.warn(e.getMessage());
    return new ResponseEntity<>(e.getBindingResult().getFieldError().getDefaultMessage(),HttpStatus.BAD_REQUEST);
    //e.getBindingResult().getFieldError().getDefaultMessage()를 통해 유효성 검사에서 실패한 필드의 기본 메시지를 가져옵니다.
  }
  
  @ExceptionHandler({RuntimeException.class})
  protected ResponseEntity handleEtcException(RuntimeException e){
    return new ResponseEntity<>(e.getMessage(),HttpStatus.BAD_REQUEST );
  }
}

    @ExceptionHandler({CustomException.class}) 
   // @ResponseStatus(HttpStatus.BAD_REQUEST)
    protected ResponseEntity handleApiException(CustomException customException) {
        return new ResponseEntity(new ErrorDTO(
                customException.getExceptionStatus().getStatusCode(),
                customException.getExceptionStatus().getMessage()),
                HttpStatus.valueOf(customException.getExceptionStatus().getStatusCode()));

    }
  • handleApiException 메서드는
    CustomException을 처리하고, HTTP 응답으로 적절한 에러 메시지와 상태 코드를 반환한다.

  • @ExceptionHandler({CustomException.class}) 어노테이션은
    CustomException을 처리하는 예외 처리 핸들러임을 나타냅니다.

  • 응답상태 코드 설정 방법으로 두가지가 있다.

  1. @ResponseStatus(HttpStatus.BAD_REQUEST) 어노테이션 설정 방법
    해당 예외 처리가 발생한 경우 별도로 ResponseEntity를 생성할 필요 없이 HTTP 응답 상태 코드를 BAD_REQUEST로 설정

  2. 직접 에러 응답 처리 하는 방법 ResponseEntity 이요
    ResponseEntity를 사용하여 직접 에러 응답을 생성하고 반환

  • 생성된 ErrorDTO 객체는 예외 상태 코드와 메시지를 포함합니다. 상태 코드는 customException.getExceptionStatus().getStatusCode()를 통해 가져옵니다.
    HttpStatus.valueOf(customException.getExceptionStatus().getStatusCode())를 통해 HttpStatus 객체를 생성하여 ResponseEntity의 상태 코드로 설정합니다.
 @ExceptionHandler({MethodArgumentNotValidException.class})
    protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.warn(e.getMessage());
        return new ResponseEntity<>(e.getBindingResult().getFieldError().getDefaultMessage(), HttpStatus.BAD_REQUEST);
    }
  • e.getBindingResult().getFieldError().getDefaultMessage()를 통해 유효성 검사에서 실패한 필드의 기본 메시지를 가져옵니다.

일반적으로 스프링에서는 @Valid 어노테이션을 사용하여 메소드 인자나 객체의 유효성 검사를 수행합니다. 만약 유효성 검사에 실패하면 MethodArgumentNotValidException이 발생하고, 이를 예외 처리하여 적절한 응답을 반환할 수 있습니다.
MethodArgumentNotValidException은 주로 BindingResult 객체와 함께 사용됩니다. BindingResult는 유효성 검사 실패에 대한 정보를 포함하고 있으며, 필드 에러(FieldError)와 글로벌 에러(ObjectError) 등의 정보를 제공합니다. 이를 통해 어떤 필드에서 어떤 유효성 검사가 실패했는지 등의 정보를 확인할 수 있습니다.

profile
개발자꿈나무

4개의 댓글

comment-user-thumbnail
2023년 6월 26일

혜원님! 백틱 세개 후에 ```java 이런식으로 하시면, 코드 하이라이트 기능을 사용 하실 수 있답니다

답글 달기
comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/10/military-jeep-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/piggy-book-2-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/pack-of-household-appliances-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/attack-on-titan-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/unusual-weapon-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/dump-for-melon-playground-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/bendy-ax-bendys-ax-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/theme-of-the-game-pack-man-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/parachute-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/gamer-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/spider-man-and-venom-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/school-desks-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mixue-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/ersimov-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/ferrari-296-gtb-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/gun-sight-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/police-officers-against-criminals-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/t-rex-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/combat-robotthe_foxan_mp-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/alan-becker-mod-for-melon-playground/

답글 달기
comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/11/japanese-traditional-house-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/plants-v-s-zombies2-0-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/gta-sa-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/melon-five-story-building-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/the-man-in-the-clothesnpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/wha-2112-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/sonic-the-hedgehog-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/npc-ant-man-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/complete-minecraft-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/bone-monster-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/11/fnaf-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/mechanical-vehicle-mod-for-melon-playground-3/
https://melonplaymods.com/2023/06/10/troll-face-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/animal-human-military-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/masked-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/resident-evil-4-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/alan-becker-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/meloray-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/mini-pack-for-the-anime-berserk-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/characters-in-metal-gear-rising-revengeance-mod-for-melon-playground/

답글 달기
comment-user-thumbnail
2024년 1월 20일

안녕하세요! 작성해주신 포스팅 덕분에 많이 배웠습니다! 감사합니다!
글을 읽다가 질문이 생겼습니다!

ExceptionAdviceHandler 클래스의 handleNullPointerException() 메서드가
ErrorDto 객체를 반환하지 않고 상태코드와 메시지를 실어서 ResponseEntity 객체를 반환한 특별한 이유가 있을까요?

답글 달기