Backend 예외처리: Exception, ExceptionHandler, Try-Catch (Day 57)

코딩기록·2024년 12월 30일

[🍦Vanilla Code로 Exception 처리하기 ]

try {
	// 예외가 발생할 수 있는 상황 제시
    String name = "김갑돌"
    // 예외 발생 조건
    if (name="김갑돌") {
    	throw new Exception("김갑돌은 안 받아줘요");
    }
} // try 끝
catch(Exception e) {
  예외 발생 시 구현할 내용
} finally {
  예외 발생 여부, return 여부 등과 상관 없이 무조건 실행할 코드
}🌱

[ 🌱 Spring ExceptionHandler로 처리하기 - @Valid 미사용 ]

📌 원리 :

   🔹 i. @Service, @Repository, @Controller에서 예외 처리 할 상황을 명시

   🔹 예외 발생 시 구현할 내용을
      @ControllerAdvice 클래스의 @ExceptionHandler(Exception.class) 메소드에 적어준다.

   🔹 iii. 이 작업을 위해서는, 아래의 선행 작업이 필요하다
      - ErrorResponse 클래스 생성
      - Custom Exception 만들기

📌 코드

   🔹1. Custom Exception을 아래 방법 중 한 가지로 만든다

       - (쉬운 버전) RunTimeException을 상속하는 CustomException 클래스를 여러 개 만든다.

// RunTimEeXCEPTION 상속하여 클래스 만들기
public class CustomException extends RuntimeException{

	// 이 때 HttpStatus을 따로 필드로 만들어 주는 까닭은,
    // 나중에 예외처리할 때 이 값을 써주기 위해서임
    private HttpStatus status;

	// 생성자를 만들어 주면서 이 객체의 HttpStatus와 detailedMessage 생성
    public CustomException(HttpStatus httpStatus, String message) {
        super(message);
        this.status = httpStatus;
    }
}

       - (어려운 버전) ENUM 사용

➡️ CustomException 클래스 파일을 여러 개 만들면 유지보수가 힘듬
➡️ CustomException들을 하나의 ENUM 상수들로 관리해준다.
➡️ 근데 이대로 그냥 쓰기에는 Enum은 RunTimeException을 상속 불가하므로 RuntimeException.getDefaultMessage()등의 함수를 사용 못하므로,
➡️ 대분류별로 CustomException extends RuntimeException 클래스를 만들줘서, 그 클래스에서 또 ErrorCode ENUM을 객체로 받아준다.

// CustomException 클래스 파일을 여러 개 만들면 유지보수가 힘듬
// , 따라서 CustomException들을 하나의 ENUM 상수들로 관리해준다.
@RequiredArgsConstructor
@Getter
public enum ErrorCode {


    FILE_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");

    private final HttpStatus status;
    private final String message;

}
// ➡️ ENUM을 그냥 바로 쓰기에는, ENUM은 RunTimeException을 상속 불가하여 RuntimeException.getDefaultMessage()등의 함수를 사용 못하므로,
// ➡️ 대분류별로 CustomException extends RuntimeException 클래스를 만들줘서, 그 클래스에서 또 ErrorCode ENUM을 객체로 받아준다. 
@Getter
public class PostException extends RuntimeException {

    private final ErrorCode errorCode;

    public PostException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public PostException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
}

   🔹 2. 예외 발생 조건을 설정한다.(@Controller, @Service, @Repository에서)

@PostMapping("")
public void createMember(
	@PathVariable Member member,
    BindingResult bindingResult    
) {
	// 여기에서는 member 객체의 name 필드에 null이면 에러 생성
	if (member.name == null) {
    	throw new CustomException(
}   

   🔹 3. 에러 발생 시 응답할 상세내용을 ErrorResponse 객체로 만들어 준다.

public class ErrorResponse {

    private LocalDateTime timestamp; // 에러가 발생한 시간
    private int status;  // 에러 상태코드
    private String error; // 에러의 이름
    private String message; // 에러의 구체적인 원인 메시지
    private String path; // 에러가 발생한 경로
}

   🔹 4. ExceptionHandler 클래스를 만들고, 각 에러 발생 시 호출한 메서드 위에 @ExceptionHandler(Exception Class명.class)를 붙여준다.

@Slf4j
public class GlobalExceptionHandler {


    // 2. Exception이 발생하면 호출될 메서드에 @ExceptionHandler(Exception.class)를 붙여준다.
    @ExceptionHandler(MemberException.class)
    // - 이 때, 생성자 parameter로 아래 2개를 받아준다.
    //   i. Exception 발생 시 자동으로 생성되는 Exception e(e.getDefaultMessage, e.status.value()등 사용 목적)
    //   ii. 클라이언트 요청 시 자동 생성되는 HttpServletRequest를 인자로 받아준다.(request.getReqeusturl() 사용 목적)
    public ResponseEntity<?> handleClientException(
            MemberException e
            , HttpServletRequest request
    ) {

        // 로깅 처리
        log.warn("exception occurred!! caused by: {}", e.getMessage());

        // 4. 별도 클래스에서 아래 내용을 담은 클래스를 만들어 주고
        // 5. 구체적인 에러 객체 생성해서 클라이언트에 전달해준다.
        ErrorResponse error = ErrorResponse.builder()
                .path(request.getRequestURI()) // path는 클라이언트가 호출 시 자돵 생성되는 HttlRequestServlet에서 가져옴
                .message(e.getMessage())
                .timestamp(LocalDateTime.now())
                .status(e.getStatus().value()) // customException 만들 때, 생성자에 HttpStatus 타입인 status 필드 생성해 두었음
                .error(e.getStatus().getReasonPhrase()) // HttpStatus객체.getReasonPhrase() : NOT FOUND, BAD REQUEST...
                .build();

        return ResponseEntity
                .status(error.getStatus())
                .body(error);
    }



[ 🌱 Spring ExceptionHandler로 처리하기 - @Valid 사용 ]

📌원리

i. @Service, @Repository, @Controller에서 예외 처리 할 상황을 명시 해 준다.

ii. 예외 발생 시 구현할 내용을 @ControllerAdvice 클래스의 @ExceptionHandler(Exception.class) 메소드에 적어준다.

iii. 이 작업을 위해서는, 아래의 선행 작업이 필요하다

  • ErrorResponse 클래스 생성(예외 발생 시, 클라이언트에 에러에 대한 구체적인 상황을 담아서 줄 것이므로)
  • Custom Exception 만들기

📌코드

   🔹 1. @Valid는 항상 MethodArgumentNotValidException이므로, Custom Exception은 따로 만들어 줄 필요 없다.


   🔹 2. 예외 발생 조건은 @Repository에서 메소드에 생성하게 @Valid 을 걸어 자동으로 발생된다.

@PostMapping("")
public void createMember(
	// 여기에서 자동으로 MethodArgumentNotValidException 발생 됨
    // 이 때, BindingResult도 parameter로 받아주면 exception 발생 안 됨 주의!
	@PathVariable @Valid Member member,    
) {
	// 여기에는 정상 코드
}   

   🔹 3. 에러 발생 시 응답할 상세내용을 ErrorResponse 객체로 만들어 준다.

public class ErrorResponse {

    private LocalDateTime timestamp; // 에러가 발생한 시간
    private int status;  // 에러 상태코드
    private String error; // 에러의 이름
    private String message; // 에러의 구체적인 원인 메시지
    private String path; // 에러가 발생한 경로
}

   🔹 4. ExceptionHandler 클래스를 만들고, @Valid에서 유효성 검증에 실패 시 MethodArgumentNotValidException이 터지므로, @ExceptionHandler(MethodArgumentNotValidException.class) + 메소드 구현

// 1. 클래스 이름에 '@ControllerAdvice'를 붙여 에러 발생하면 이 클래스로 향하게 한다.
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 2. Exception이 발생하면 호출될 메서드에 @ExceptionHandler(Exception.class)를 붙여준다.
    @ExceptionHandler(MethodArgumentNotValidException.class)
    // - 이 때, 생성자 parameter로 아래 2개를 받아준다.
    //   i. Exception 발생 시 자동으로 생성되는 Exception e(e.getDefaultMessage, e.status.value()등 사용 목적)
    //   ii. 클라이언트 요청 시 자동 생성되는 HttpServletRequest를 인자로 받아준다.(request.getReqeusturl() 사용 목적)
    public ResponseEntity<?> handleBindingError(
            MethodArgumentNotValidException e
            , HttpServletRequest request
    ) {

        // @valid를 쓸 경우 에러 발생 시 BindingResult가 자동생성 되고,
        // BindingResult.getFielderrors.getDefaultMessage()에 @NotBlank(message="")의 메시지 전달됨
        String message = e.getBindingResult().getFieldError().getDefaultMessage();

        // 로깅 처리
        log.warn("input value exception occurred!! caused by: {}", message);

        // 구체적인 에러 객체 생성
        ErrorResponse error = ErrorResponse.builder()
                .path(request.getRequestURI()) // path는 클라이언트가 호출 시 자돵 생성되는 HttlRequestServlet에서 가져옴
                .message(message) // @valid를 쓸 경우 에러 발생 시 BindingResult가 자동생성 되고,
                                  // BindingResult.getFielderrors.getDefaultMessage()에 @NotBlank(message="")의 메시지 전달됨
                .timestamp(LocalDateTime.now())
                // MethodArgumentNotValidException의 내장 함수인 getStatusCode()는 HttpStatus.BAD_REQUEST를 retutrn함
                .status(e.getStatusCode().value()) 
                // HttpStatus 클래스의 내장함수인 toString()은 에러 메시지를 리턴함
                .error(e.getStatusCode().toString())
                .build();

        return ResponseEntity
                .status(error.getStatus())
                .body(error);
    }


}

0개의 댓글