Spring Framework

비즈니스 로직에 대한 예외 처리, 사용자 정의 예외(Custom Exception)

ChaeHing 2023. 4. 18. 01:57
  • 체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외이다.
    • ClassNotFoundException 등
  • 언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미한다.
    • NullPointerException, ArrayIndexOutOfBoundsException 등
  • RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.

 

개발자가 의도적으로 예외를 던질 수(throw) 있는 상황

  • 백엔드 서버와 외부 시스템과의 연동에서 발생하는 에러 처리
  • 시스템 내부에서 조회하려는 리소스(자원, Resource)가 없는 경우

 

의도적인 예외 던지기/받기(throw/catch)

  • 서비스 계층에서 던져진 예외는 API 계층인 Controller의 핸들러 메서드 쪽에서 잡아서 처리

서비스 계층에서 예외(Exception) 던지기(throw)

@Service
public class MemberService {
    ...
		...

    public Member findMember(long memberId) {
        // TODO should business logic
				
				// (1)
        throw new RuntimeException("Not found member");
    }

		...
		...
}

GlobalExceptionAdvice 예외 잡기(catch)

  • 공통 예외 처리에서 - Controller에 예외를 처리하기 때문
@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...
		...
		
    @ExceptionHandler
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
        System.out.println(e.getMessage());

		// ErrorResponse 수정 코드 필요

        return null;
    }
}
  • DB에서 조회되는 회원이 없는 경우 RuntimeException을 던지고(throw) (memberService)
  • 던져진 RuntimeException을 예외처리 클래스에서 받는다(catch)  (GlobalExceptionAdvice)
  • 현재 코드는 회원 정보가 존재하지 않는 경우만 처리됨
    • 회원 등록시 이미 존재하는 회원인 경우 (post)
    • 패스워드 검증이 안되는 경우등
    • 예외로 던질 수 있는 여러 상황이 존재한다.
  • RuntimeException이라는 추상적인 예외를 던지기 때문에 구체적으로 어떤 예외가 발생한지 알기 어렵다.

 

 


 

 

사용자 정의 예외 (Custom Exception) 

  • RuntimeException과 같은 추상적인 예외가 아닌 구체적으로 표현할 수 있는 Custom Exception을 만들어서 예외를 던질 수있다.
  • RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.

예외 코드

package com.codestates.exception;

import lombok.Getter;

public enum ExceptionCode {
    MEMBER_NOT_FOUND(404, "Member Not Found");
    
    @Getter
    private int status;

    @Getter
    private String message;

    ExceptionCode(int code, String message) {
        this.status = code;
        this.message = message;
    }
}
  • 서비스 계층에서 던질 Custom Exception에 사용할 ExceptionCode를 enum으로 정의
  • 다양한 유형의 예외를 enum에 추가해서 사용 가능

BusinessLogicException

package com.codestates.exception;

import lombok.Getter;

public class BusinessLogicException extends RuntimeException {
    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}
  • 서비스 계층에서 사용할 BusinessLogicException이라는 Custom Exception을 정의
  • BusinessLogicExceptionRuntimeException을 상속하고 있으며 ExceptionCode를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공
  • BusinessLogicException은 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있다.

Service 클래스

@Service
public class MemberService {

public Member findMember(long memberId) {
    // should business logic
    throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
}
  • 서비스 계층에서 BusinessLogicException을 사용하여 예외를 던질 수 있다.

GlobalExceptionAdvice

  • 서비스 계층에서 던진 예외를 처리하는 공통 예외 처리 클래스
package com.codestates.advice;

import com.codestates.exception.BusinessLogicException;
import com.codestates.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionAdvice {
     
    @ExceptionHandler
    public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
        System.out.println(e.getExceptionCode().getStatus());
        System.out.println(e.getMessage());

        // TODO GlobalExceptionAdvice 기능 추가 1
        final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());

        return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode()
                .getStatus()));
    }

    // TODO GlobalExceptionAdvice 기능 추가 2
    @ExceptionHandler
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){

        // HttpRequestMethodNotSupportedException를 Argument로
        //final ErrorResponse response = ErrorResponse.of(e);

        // http stauts를 Argument로
        final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);

        return response;
    }

    // TODO GlobalExceptionAdvice 기능 추가 3
    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleException(NullPointerException e){

        // ExceptionCode(enum)를 Argument로
        //final ErrorResponse response = ErrorResponse.of(ExceptionCode.INTERNAL_SERVER_ERROR);

        // http stauts를 Argument로
        final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);

        return response;
    }

}
  • 서비스 계층에서 던진 BusinessLogicException을 처리
    • GlobalExceptionAdvice 기능 추가 1
    • 없는 멤버를 조회할때 - DB에서 조회가 안될때
  • HttpRequestMethodNotSupportedException을 처리하기 위한 handleHttpRequestMethodNotSupportedException()
    • GlobalExceptionAdvice 기능 추가 2
    •  클라이언트가 요청한 HTTP 메서드를 서버가 처리할 수 없는 경우에 발생
    •  예를 들어, 클라이언트가 POST 메서드를 사용하여 /users 엔드포인트에 요청했지만 서버에서는 POST 메서드를 허용하지 않고 GET 메서드만 허용하는 경우
  • NullpointerException 등과 같이 개발자가 구현 상의 실수로 발생하는 Exception을 처리하기 위한 handleException() 메서드
    • GlobalExceptionAdvice 기능 추가 3
    • 여기선 NullpointerException만
  • @RestControllerAdvice에서 @ResponseStatus와 ResponseEntity
    • 한 가지 유형으로 고정된 예외를 처리할 경우에는 @ResponseStatus
    • 다양한 유형의 Custom Exception을 처리하고자 할 경우에는 ResponseEntity를 사용.
      • BusinessLogicException

ErrorResponse 

  • 필요한 Error 정보만 담기 위해 Error 전용 Response 객체를 통해 데이터 가공
package com.codestates.response;

import com.codestates.exception.ExceptionCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;

@Getter
public class ErrorResponse {

    // status와 message 선언
    private int status;
    private String message;

    // 생성자
    public ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }

    // GlobalExceptionAdvice 기능 추가 1 exceptionCode를 파라미터로 받을경우
    public static ErrorResponse of(ExceptionCode exceptionCode) {
        return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
    }

    // GlobalExceptionAdvice 기능 추가 2 HttpRequestMethodNotSupportedException을 파라미터로 받을경우
    public static ErrorResponse of(HttpRequestMethodNotSupportedException methodNotSupportedException) {
        return new ErrorResponse(405, "Method Not Allowed");
    }

    // GlobalExceptionAdvice 기능 추가 2 , 3 httpstaus를 파라미터로 받을 경우
    public static ErrorResponse of(HttpStatus httpStatus){
        return new ErrorResponse(httpStatus.value(),httpStatus.getReasonPhrase());
    }
    
}
  • of() 메서드를 통한 객체 생성
  • 생성자 오버로딩을 통해 각 기능별 ErrorResponse 객체 리턴

 

Response Body 확인

handleBusinessLogicException()

 

handleHttpRequestMethodNotSupportedException()
handleException() -&nbsp;NullpointerException

 

요약

 

  • 체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외이다.
  • 언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미한다.
  • RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.
  • RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.
  • 사용자 정의 예외(Custom Exception)를 정의해서 서비스 계층의 비즈니스 로직에서 발생하는 다양한 예외를 던질 수 있고, 던져진 예외는 Exception Advice에서 처리할 수 있다.
  • @ResponseStatus 애너테이션은 고정된 예외를 처리할 경우에 사용할 수 있다.
  • HttpStatus가 동적으로 변경되는 경우에는 ResponseEntity를 사용한다.