본문 바로가기

WEB/Spring

예외 처리(@ExceptionHandler) - 예시 코드를 중심으로

스프링에서 예외 처리하는 3가지 방법

1. try - catch

“각 메소드” 안에서 처리

 

2. 어노테이션 @ExceptionHandler

-> 이 방법을 적용하여 설명 예정

“컨트롤러 자체”에서 처리

@controller, @RestController가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능

Controller 내부에서 호출한 Service에서 예외가 발생 시 에러 처리 대상이 된다.  

예외 처리 메시지를 예외 발생마다 새롭게 작성하면 번거롭고 복잡하다ㅜ 

이젠 한 번에 처리해보자!

3. Global level에서 처리 → 추천

클라이언트에서 전달되기 직전에 처리

[ 실행흐름 ]

[컨트롤러에서 예외 발생]

  1. 에러가 터지면
  2. Dispatcher Servlet을 통해 ExceptionResolver 에게 간다.
  3. ExceptionHandlerExceptionResolver에게 간다.
  4. 이것이 컨트롤러에게 가서 “@ExceptionHandler”부분을 찾는다.
  5. 해당 메소드를 실행시킨다.

[자세한 version]

컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.

예외가 발생했으로 ExceptionResolver 가 작동한다.

가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.

ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.

illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다.

따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다. @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.

 

Custom Exception (사용자 정의 예외)

프로젝트 상황에 맞춤형 예외를 만들 수 있다. 

커스텀 예외의 이름만 봐도 어떤 예외인지 알아볼 수 있도록 하자. 

표준 예외의 예시 : NullPointerException , IllegalArgumentException, IndexOutBoundsException 등

 

 

하지만 표준 예외를 사용하고 errorMessage 로 오류 상황을 나타내는 정도로도 충분히 표현이 가능하다면, 굳이 커스텀 예외를 사용하지 않는것이 좋다. 반대로 CustomException을 받는 곳에서 처리하기 쉽기 위해 추가로 에러에 관한 정보를 넣어주거나, 여러 타입의 Exception이 발생할 수 있는 코드에서 한 가지로 Exception 타입으로 묶어 처리할때는 custome exception을 사용할만 하다.
참고 : https://w97ww.tistory.com/74

 

 


직접 구현한 코드를 통해 구체적으로 알아보자 

 

1) 파일의 구조를 살펴보자

 

먼저 global 폴더에 다음과 같이 3개의 파일들을 생성한다. 

필요한 파일들

그리고 각 에러 타입마다 클래스 파일을 만든다. 여기에서는 UserNotFoundException에 대해서만 다룰 것이기 때문에 다음과 같이 Exception 파일(커스텀 Exception)을 만든다. 

UserNotFoundException

2) 각 파일의 내용을 살펴보자

[ ErrorCode ]

enum 타입으로 만들어서 에러 코드 메시지를 통일성있게, 재사용 가능하도록 구성했다. 

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

	// Basic - C0***
	RUNTIME_EXCEPTION(BAD_REQUEST, "C0001", "RUNTIME_EXCEPTION"),

	// User - C2***
	DUPLICATE_NICKNAME(CONFLICT, "C2001", "DUPLICATE_NICKNAME_EXISTS"),
	DUPLICATE_EMAIL(CONFLICT, "C2002", "DUPLICATE_EMAIL_EXISTS"),
	USER_NOT_FOUND(NOT_FOUND, "C2003", "USER_NOT_FOUND"),
	PASSWORD_NOT_MATCH(BAD_REQUEST, "C2004", "PASSWORD_NOT_MATCH")

	private final HttpStatus status;
	private final String code;
	private final String message;
}

 

[ErrorResponse]

에러 발생 시 어떤 내용들을 보여줄 것인지에 따라 다르게 구성할 수 있다. 

필자는 다음의 에러 메시지 내용들(JSON 응답)을 보여줄 것을 고려하여 코드를 작성했다. 

{
                "status": "NOT_FOUND",
                "code": "USER_NOT_FOUND",
                "message": "해당하는 사용자를 찾을 수 없습니다.",
                "date": "2022-08-01T14:48:14.150235"
}

이 에러 응답 클래스를 통해 다양한 에러 타입들을 통일성있게 표현할 수 있다.  

@Getter
@ToString
@NoArgsConstructor
public class ErrorResponse {
	private HttpStatus status;
	private ErrorCode code;
	private String message;
	private LocalDateTime date = LocalDateTime.now();

	@Builder
	public ErrorResponse(HttpStatus status, ErrorCode code, String message) {
		this.status = status;
		this.code = code;
		this.message = message;
	}
}

 

[UserNotFoundException]

에러 타입마다 클래스를 만든다. 

뒤에 나올  GlobalExceptionHandler에서 각 예외 타입마다 어떻게 핸들링 할 것인가(예외 보내는 양식)에 대한 메소드들이 따로 존재한다.  (참고 : ResourceNotFoundException에 이미 message를 포함하는 생성자가 있다)

import org.springframework.data.rest.webmvc.ResourceNotFoundException;

import static com.example.jiyun.global.constant.ResponseConstant.NOTFOUND_USER;

public class UserNotFoundException extends ResourceNotFoundException {

	public UserNotFoundException() {
		super(NOTFOUND_USER);
	}
    
	@Override
	public String getMessage() {
		return super.getMessage();
	}
}

 

[GlobalExceptionHandler]

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
     
    @ExceptionHandler(UserNotFoundException.class)
    protected final ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException e) {
		final ErrorResponse response = ErrorResponse.builder()
				.status(HttpStatus.NOT_FOUND)
				.code(ErrorCode.USER_NOT_FOUND)
				.message(e.getMessage())
				.build();
		return ResponseEntity.status(response.getStatus()).body(response);
	}
    
   
                       ....
   
}

 

[참고]

위의 UserNotFoundException에서 ResourceNotFoundException을 상속했다. 

import org.springframework.data.rest.webmvc.ResourceNotFoundException;

이를 사용하기 위해서는 build.gradle에서 다음을 추가하고 리프레쉬해줘야 에러 표시가 사라진다. 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
}