Exception 가이드
Updated:
Exception Flow
Exception Sample 예제
Client가 “localhost:8083/test/xxx” 요청 ( RequestMapping(“/test”), GetMapping({id})인 경우)
→ ExceptionSampleController에서 UsersService로 요청
→ UsersLogic에서 UserRepository접근
→ 해당 User Id == null (xxx가 Repository에 존재하지 않는 경우에 해당. 존재하는 경우 정상적으로 User return)
→ UserNotFoundException발생
→ CustomExceptionHandler에서 ApiErrorDetail Object를 생성해 Error Response 전달
Exception sample Functions
1. Custom Exception Handle
:특정 Entity 별로 Exception 처리 ex) User Entity : findById(id)에서 해당 User가 없는 경우 Throw Exception
2. Global Exception
-
- MethodArgumentNotValidException
- javax.validation.Valid or @Validated 으로 binding error 발생
-
- BindException
- @ModelAttribut 으로 binding error 발생시 BindException 발생
-
- MethodArgumentTypeMismatchException
- enum type 일치하지 않아 binding 못할 경우 발생
-
- HttpRequestMethodNotSupportedException
- 지원하지 않은 HTTP method 호출 할 경우 발생
-
- AccessDeniedException
- Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
-
- Exception
- 이외에 발생하는 Exception
3. Exception Detail Handle
:Error가 발생했을 때 출력되는 메세지 Custom 처리 → status, timestamp, message, debug message 등 Custom 가능
4. Exception View
:Error가 발생했을 때 보여지는 View Custom 처리 → View에 보여질 Message, status-code, stack Trace 등 제어
5. Logging
@Slf4j 사용 → Error 발생시 Console에 Error Log 보여지도록 처리
Exception Sample Code
샘플에 작성된 코드 상세 설명입니다.
1. Error Detail
package com.skcc.demo.exceptionsample.context.exceptionhandle.apierror;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.http.HttpStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes;
import lombok.Data;
@Data
public class ApiErrorDetail {
private HttpStatus status;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss", timezone="Asia/Seoul")
private LocalDateTime timestamp;
private String message;
private String debugMessage;
private List<ApiSubError> subErrors;
private ApiErrorDetail() {
timestamp = LocalDateTime.now();
}
public ApiErrorDetail(HttpStatus status) {
this();
this.status = status;
}
public ApiErrorDetail(HttpStatus status, Throwable ex) {
this();
this.status = status;
this.message = "Unexpected error";
this.debugMessage = ex.getLocalizedMessage();
}
public ApiErrorDetail(HttpStatus status, String message, Throwable ex) {
this();
this.status = status;
this.message = message;
this.debugMessage = ex.getLocalizedMessage();
}
}
Tips
-
Error Message 객체 Format을 통일화
-
message : 에러에 대한 message를 작성
-
status : http status code를 작성 (header 정보에 포함된 정보)
-
-
Error Code는 포함 시키지 않았는데, Error Code 표준을 정한 경우 포함하는 것이 좋음
2. Custom Exception Handler
package com.skcc.demo.exceptionsample.context.exceptionhandle;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
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.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import com.skcc.demo.exceptionsample.context.exceptionhandle.apierror.ApiErrorDetail;
import lombok.extern.slf4j.Slf4j;
@ControllerAdvice
@Slf4j
public class CustomExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
protected ResponseEntity<ApiErrorDetail> handleUserNotFoundException(UserNotFoundException unfe){
log.error("handleUserNotFoundException", unfe);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.NOT_FOUND);
errorDetail.setMessage(unfe.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.NOT_FOUND);
}}
Tips
- @ControllerAdvice : 모든 예외를 한 곳에서 처리할 수 있게 함.
- handleUserNotFoundException: UserNotFoundException 처리로, 검색한 User가 존재하지 않으면 해당 Error Message Throw
- 이외에 각 Entity 마다 Exception을 정의해 사용할 수 있음.
3. UserNotFoundException.class
```java
package com.skcc.demo.exceptionsample.context.exceptionhandle;
public class UserNotFoundException extends RuntimeException{
private static final long serialVersionUID = 1L; //set UID
public UserNotFoundException() {
}
public UserNotFoundException(String message) {
super(message);
}
}
```
4. GlobalExceptionHandler.class
package com.skcc.demo.exceptionsample.context.exceptionhandle;
import java.nio.file.AccessDeniedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import com.skcc.demo.exceptionsample.context.exceptionhandle.apierror.ApiErrorDetail;
import lombok.extern.slf4j.Slf4j;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* javax.validation.Valid or @Validated 으로 binding error 발생시 발생
* HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생
* 주로 @RequestBody, @RequestPart 어노테이션에서 발생
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ApiErrorDetail> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.BAD_REQUEST);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.BAD_REQUEST);
}
/**
* @ModelAttribut 으로 binding error 발생시 BindException 발생
* ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args
*/
@ExceptionHandler(BindException.class)
protected ResponseEntity<ApiErrorDetail> handleBindException(BindException e) {
log.error("handleBindException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.BAD_REQUEST);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.BAD_REQUEST);
}
/**
* enum type 일치하지 않아 binding 못할 경우 발생
* 주로 @RequestParam enum으로 binding 못했을 경우 발생
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<ApiErrorDetail> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("handleMethodArgumentTypeMismatchException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.BAD_REQUEST);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.BAD_REQUEST);
}
/**
* 지원하지 않은 HTTP method 호출 할 경우 발생
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ApiErrorDetail> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.error("handleHttpRequestMethodNotSupportedException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.BAD_REQUEST);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.BAD_REQUEST);
}
/**
* Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
*/
@ExceptionHandler(AccessDeniedException.class)
protected ResponseEntity<ApiErrorDetail> handleAccessDeniedException(AccessDeniedException e) {
log.error("handleAccessDeniedException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.BAD_REQUEST);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
protected ResponseEntity<ApiErrorDetail> handleException(Exception e) {
log.error("handleEntityNotFoundException", e);
ApiErrorDetail errorDetail = new ApiErrorDetail(HttpStatus.INTERNAL_SERVER_ERROR);
errorDetail.setMessage(e.getMessage());
return new ResponseEntity<>(errorDetail, HttpStatus.INTERNAL_SERVER_ERROR);
} /**
* View와 연결// @ExceptionHandler(Exception.class)
// public ModelAndView handleError(HttpServletRequest req, Exception ex) {
// log.error("handleEntityNotFoundException", e);
// ModelAndView mav = new ModelAndView();
// mav.addObject("exception", ex);
// mav.addObject("url", req.getRequestURL());
// mav.setViewName("error");
// return mav;
// }
*
* */}
Tips
- CustomExceptionHandler처럼 Entity나 Business Logic의 특정 Exception 처리가 아닌, Validation/Binding 등 전역에서 발생할 수 있는 에러를 한 곳에 모아 같은 Format으로 처리.
- View와 연결 시키는 경우 return type을 ModelAndView로 설정
- Error View의 경우, Spring에서는 다음과 같은 Page가 Default로 설정되어 있음.
→ src/main/resources/templates/error 밑에 html 파일을 작성해 Error Page를 수정할 수 있음
→ 예제: 4xx.html로 파일명을 선언해, 400,404,405 등 Status Code가 4xx Error의 경우 다음과 같은 View Page로 연결
5. ExceptionSamplController.java
package com.skcc.demo.exceptionsample.context.application.sp.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.skcc.demo.exceptionsample.context.domain.UsersService;
import com.skcc.demo.exceptionsample.context.domain.users.model.User;
@RestController
@RequestMapping("/test")
public class ExceptionSampleController {
@Autowired
UsersService usersService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable("id")Long id){
User user = usersService.findUser(id);
return new ResponseEntity<>(user, HttpStatus.OK);
}
}
6. UsersService
package com.skcc.demo.exceptionsample.context.domain;
import com.skcc.demo.exceptionsample.context.domain.users.model.User;
public interface UsersService {
public User findUser(Long id);
}
7. UsersLogic
package com.skcc.demo.exceptionsample.context.domain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.skcc.demo.exceptionsample.context.domain.users.model.User;
import com.skcc.demo.exceptionsample.context.domain.users.repository.UserRepository;
import com.skcc.demo.exceptionsample.context.exceptionhandle.UserNotFoundException;
@Service
public class UsersLogic implements UsersService{
@Autowired
private UserRepository userRepository;
@Override
public User findUser(Long id) {
User user = userRepository.findById(id).orElseThrow(()->new UserNotFoundException("User not found"));
return user;
}
}
Tips
- UsersLogic 에서, UserRepository에 해당 UserId가 없는 경우 UserNotFoundException을 발생시킴
: User user = userRepository.findById(id).orElseThrow(()->new UserNotFoundException(“User not found”));
Exception 처리 Sample Github 주소:
https://github.com/Juyounglee95/exception-sample.git
Exception 처리 가이드 참고 자료:
https://www.mkyong.com/spring-boot/spring-rest-error-handling-example/ https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc https://github.com/paulc4/mvc-exceptions.git
Logging 처리:
https://www.sangkon.com/hands-on-springboot-logging/ https://meetup.toast.com/posts/149