본문 바로가기

Backend/Spring

[Spring/SpringBoot] 예외처리 Controller 정의하기

안녕하세요. 이번 포스팅에서는 restful service에서 발생하는 예외처리를 하는 방법에 대해 알아보려 합니다.

 

우선 사용할 샘플 코드는 다음과 같습니다.

 

    @GetMapping("/Account/{id}")
    public Account retrieveUser(@PathVariable int id) {
        Optional<Account> findAccount = accountRepository.findById(id);
        if(findAccount.isEmpty()) return null;

        return findAccount.get();
    }

 

 

저의 Account Controller에서 /Account/{id}라는 주소로 GetMapping을 받겠습니다.

 

내부적으로는 JpaRepository를 구현한 accountRepository의 findById 메소드를 호출하여 Optional객체를 받은 뒤, 존재하면 .get() 메소드를 호출하여 Account 객체를 반환하여 주겠습니다.

 

그리고 현재 Account 테이블에는 다음과 같은 데이터가 들어있습니다.

 

ID 90001, 90002, 90003을 사전에 미리 INSERT해 놓았습니다.

 

이 상태로 Get 요청을 보내보겠습니다.

 

 

 

id값을 1로 전달하면 당연히 데이터베이스에 id값이 없기 때문에 아무것도 반환되지 않습니다.

 

문제는 무엇이냐 하면, 

 

네 위 그림과 같이 STATUS 200 OK가 반환됩니다. 이는 저희가 특별히 예외처리를 하지 않았기 때문에 내부적인 로직이 모두 잘 작동하여 200 OK가 반환된 것이라 볼 수 있습니다.

 

하지만 일반적으로 특정 유저 정보를 얻기 위해 GetMapping을 보낸 것이라면, 200 OK가 아닌 UserNotFoundException을 기대할 것입니다.

 

이를 처리하기 위해 저희는 UserNotFoundException이라는 class를 하나 선언해주겠습니다.

 

package com.MyRestfulService.restfulwebservice.Exception;

public class UserNotFoundException extends RuntimeException{
    public UserNotFoundException(String m) {
        super(m);
    }
}

 

기본적으로 RuntimeException을 상속하고, message를 String형으로 전달받아서 구현하겠습니다.

 

    @GetMapping("/Account/{id}")
    public Account retrieveUser(@PathVariable int id) {
        Optional<Account> findAccount = accountRepository.findById(id);
        if(findAccount.isEmpty()) throw new UserNotFoundException(String.format("ID[%s] is Not Found", id));

        return findAccount.get();
    }

 

그리고 유저가 존재하지 않는다면, null을 return하지 않고 UserNotFound Exception을 throw 해주겠습니다.

 

이제 다시 요청을 보내 보겠습니다.

 

 

보기도 싫은 긴 문장이 출력됩니다. 그리고 500 Internal Server Error가 발생합니다.

 

우선 200OK가 아닌 Error가 발생했기 때문에 1차적으로는 성공했다고 볼 수 있습니다.

그리고 깨알같이 저희가 보낸 메시지도 출력이 됩니다.

 

 

하지만 여기서 문제가 무엇일까요?

 

1. 우선 500 서버 에러는 저희가 의도한 에러는 아닙니다. 잘못된 User Id를 찾아달라고 요청을 보냈으므로 정상적으로 예외처리가 되었으면 400번대 에러가 출력되어야 합니다.

 

2. 또한 에러 문장이 너무 깁니다. 

너무 많은 정보를 Client에게 되돌려주는 것은 보안상 취약점을 제공할 여지가 있습니다. 저희는 딱 필요한 정보만 Client에게 적절한 에러 매핑과 함께 보내고 싶습니다.

 

 

1번을 해결하기 위해 @ResponseStatus라는 어노테이션을 추가해줍니다.

이 어노테이션은 해당 예외가 발생할 때 Status값을 커스터마이징 하게 전달할 수 있습니다.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException{
    public UserNotFoundException(String message) {
        super(message);
    }
}

 

저희가 의도한 것처럼 404 Not Found 에러가 발생하였습니다. 이제 2번 문제를 해결해볼게요!

 

2번 문제를 해결하기 위해 스프링 AOP전략을 사용한 커스텀 에러 핸들링 class를 정의하겠습니다.

 

우선 문제는 기본적으로 제공되는 에러 로그가 너무 길고, 유출되는 정보가 보안상 취약점을 제공할 수 있기 때문에, 저희가 Response 할 객체를 생성하고, 그 객체에 맞는 데이터만 반환해주겠습니다.

 

package com.MyRestfulService.restfulwebservice.Exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor @NoArgsConstructor
public class ExceptionResponse {
    private LocalDateTime timestamp;
    private String message;
    private String details;
}

 

우선 ExceptionResponse라는 객체를 만들어주겠습니다. 저희는 기본적으로 에러가 발생한 시간, message, 그리고 간단한 정보 이렇게 세 개만 json형태로 Client에게 반환해주겠습니다.

 

그리고 모든 요청에 대해 에러를 검사하기 위해 

public abstract class ResponseEntityExceptionHandler

스프링에서 기본적으로 제공하는 에러 핸들러 추상화 클래스를 구현한 하나의 예외처리 클래스를 생성해보도록 하겠습니다.

 

package com.MyRestfulService.restfulwebservice.Exception;

import com.MyRestfulService.restfulwebservice.Account.UserNotFoundException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.LocalDateTime;

@RestController
@ControllerAdvice // 모든 Controller가 실행될 때 사전에 실행됨
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        ExceptionResponse exceptionResponse =
                new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity<Object>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public final ResponseEntity<Object> handleUserNotFoundException(Exception ex, WebRequest request) {
        ExceptionResponse exceptionResponse =
                new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity<Object>(exceptionResponse, HttpStatus.NOT_FOUND);
    }

	...
}

 

위에서 말한 ResponseEntityExceptionHandler를 상속받은 CustomizedResponseEntityExceptionHandler를 정의해 보겠습니다.

 

@RestController : 기본적으로 rest api에 대해 작동하는 Controller임을 표시합니다

@ControllerAdvice : 모든 Controller가 실행될 때 ControllerAdvice를 가진 빈이 사전에 미리 실행됩니다. -> AOP 적용

 

ResponseEntity : 스프링에서 제공하는 HttpEntity라는 클래스를 상속받은 객체입니다. 따라서 Header와 Body를 가질 수 있습니다. 

여러 가지 생성자가 존재합니다. 하지만 이번 예제에서는 기본적인 Body와 HttpStatus만을 이용하여 생성해보겠습니다.

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        ExceptionResponse exceptionResponse =
                new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity<Object>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }

 

모든 예외처리의 조상 class인 Exception class를 구현함으로써 모든 에러에 대해 처리하는 기본적인 함수를 하나 만들겠습니다. 

 

    @ExceptionHandler(UserNotFoundException.class)
    public final ResponseEntity<Object> handleUserNotFoundException(Exception ex, WebRequest request) {
        ExceptionResponse exceptionResponse =
                new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), request.getDescription(false));

        return new ResponseEntity<Object>(exceptionResponse, HttpStatus.NOT_FOUND);
    }

 

그리고 저희가 기본적으로 처리하고 싶던 UserNotFoundException class를 처리하는 메소드를 하나 선언해 주겠습니다.

 

@ExceptionHandler(UserNotFoundException.class)

이 어노테이션은 저희가 처리할 예외를 지정합니다.

 

자 이제 다시 요청을 보내보도록 하겠습니다.

 

 

보시는 바와 같이 에러가 발생한 시간, 그리고 간단하고 명확한 message, uri정보만을 표시하는 것을 확인할 수 있습니다.

 

이처럼 Spring을 활용하여 RestfulService를 개발하실 때에 적절한 예외 처리 클래스를 사용하셔서 구현하는 방법을 이 기능을 확장하여 사용하시면 좋을 것 같습니다. 

 

*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.