[Spring] Spring MVC Handle Exception

GoghΒ·2023λ…„ 1μ›” 2일
0

Spring

λͺ©λ‘ 보기
11/23

🎯 λͺ©ν‘œ : Spring MVCμ—μ„œμ˜ μ˜ˆμ™Έ 처리 ν•™μŠ΅

πŸ“’ Spring MVC μ˜ˆμ™Έ 처리

  • Controller λ ˆλ²¨μ—μ„œμ˜ 처리 = @ExceptionHandler
  • Global μ˜ˆμ™Έ 처리 = @ControllerAdvice & @RestControllerAdvice
  • REST API에 λŒ€ν•œ μ˜ˆμ™Έμ²˜λ¦¬λ§Œ λ‹€λ£° μ˜ˆμ •μ΄λ©°, view에 κ΄€λ ¨λ˜κ±°λ‚˜ Json 맡핑에 κ΄€λ ¨λœ λ‚΄μš©μ€ λ‚˜μ€‘μ— λ”°λ‘œ μ •λ¦¬ν•˜κ² λ‹€.

πŸ“Œ Controller 단 μ—μ„œμ˜ μ˜ˆμ™Έμ²˜λ¦¬

  • Controller의 λ©”μ†Œλ“œμ—μ„œ λ˜μ Έμ§„ μ˜ˆμ™Έμ— λŒ€ν•œ 곡톡적인 처리λ₯Ό ν• μˆ˜μžˆλ„λ‘ μ§€μ›ν•΄μ£ΌλŠ” @ExceptionHandler μ–΄λ…Έν…Œμ΄μ…˜μ΄ μžˆλ‹€.

예제 μ½”λ“œ

@RestController
@RequestMapping("/members")
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
      Member member = mapper.memberPostDtoToMember(memberDto);

      Member response = memberService.createMember(member);

      return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
        HttpStatus.CREATED);
    }

    @ExceptionHandler
    public ResponseEntity handlerException(MethodArgumentNotValidException e) {
      final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
      List<ErrorResponse.FieldError> errors =
                        fieldErrors.stream()
                              .map(error -> ErrorResponse.FieldError.builder()
                                            .field(error.getField())
                                            .rejectedValue(error.getRejectedValue())
                                            .reason(error.getDefaultMessage())
                                            .build())
                              .collect(Collectors.toList());

      return new ResponseEntity<>(ErrorResponse.of(errors), HttpStatus.BAD_REQUEST);
    }

}
  • μœ„ μ˜ˆμ œμ—μ„œ Member 데이터 μˆ˜μ§‘ κ³Όμ •μ—μ„œ μœ νš¨μ„± 검증에 μ‹€νŒ¨ ν–ˆμ„λ•Œμ˜ μ˜ˆμ™Έμ²˜λ¦¬ 상황을 μ˜ˆμ‹œλ‘œ λ“€μ—ˆλ‹€.
    • RequestBody에 μœ νš¨ν•˜μ§€ μ•Šμ€ μš”μ²­ 데이터가 포함 λ˜μ–΄ μžˆμ„λ•Œ μœ νš¨μ„± 검증에 μ‹€νŒ¨ν•˜κ²Œ 되면 MethodArgumentNotValidException이 λ°œμƒν•œλ‹€.
    • ν•΄λ‹Ή μ˜ˆμ™Έκ°€ λ°œμƒ ν–ˆμ„λ•Œ FieldError 객체듀을 λ°›μ•„ 별도 클래슀둜 μ •μ˜ν•œ 응닡 객체둜 ν•„μš”ν•œ λ°μ΄ν„°λ§Œ 뽑아 Response Body둜 μ „λ‹¬ν•˜κ³  μžˆλ‹€.
    • @ExceptionHandlerλŠ” μ†ν•΄μžˆλŠ” 클래슀 λ‚΄λΆ€μ˜ μ˜ˆμ™Έμ— λŒ€ν•΄μ„œλ§Œ μ²˜λ¦¬ν• μˆ˜ μžˆλŠ” Local λ²”μœ„λ₯Ό 가지고 μžˆλ‹€.
    • λ§Œμ•½, λ‹€λ₯Έ μ»¨νŠΈλ‘€λŸ¬μ—μ„œ λ™μΌν•œ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•΄μ•Ό ν•œλ‹€λ©΄, 컨트둀러 λ§ˆλ‹€ 쀑볡 μ½”λ“œκ°€ λ°œμƒν•˜κ³  μœ μ§€ λ³΄μˆ˜λ„ μ–΄λ €μ›Œ 질 것이닀.

πŸ“Œ μ „μ—­ μ˜ˆμ™Έ 처리

  • @ExceptionHandler Local μ˜ˆμ™Έ 처리의 단점을 κ·Ήλ³΅ν•˜κΈ° μœ„ν•΄ Spring μ—μ„œλŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ „μ—­μœΌλ‘œ @ExceptionHandlerλ₯Ό μ‚¬μš©ν• μˆ˜ 있게 지원해쀀닀.
    • @ControllerAdvice 와 @RestControllerAdviceκ°€ μ „μ—­ μ˜ˆμ™Έ 처리λ₯Ό μ§€μ›ν•΄μ£ΌλŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.
    • @ControllerAdviceλŠ” μ˜ˆμ™Έ μ²˜λ¦¬ν›„ μ—λŸ¬ View 등을 ν†΅ν•˜μ—¬ 처리 ν• μˆ˜ 있게 ν•΄μ€€λ‹€.
    • @RestControllerAdviceλŠ” REST API에 λŒ€ν•œ μ˜ˆμ™Έ 처리λ₯Ό νŽΈλ¦¬ν•˜κ²Œ 지원해주며, @ResponseBodyλ₯Ό λ‚΄μž₯ν•˜κ³  μžˆμ–΄ 응닡 데이터 포맷에 μš©μ΄ν•˜λ‹€.
    • @RestControllerAdvice = @ControllerAdvice + @ResponseBody
    • @ControllerAdvice의 μ˜ˆμ™Έ 처리 λ²”μœ„λŠ” Dispatcher Servlet λ‚΄λΆ€μ—μ„œ 이루어 진닀.

예제 μ½”λ“œ

// MemberController.java
@RestController
@RequestMapping("/members")
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
      Member member = mapper.memberPostDtoToMember(memberDto);

      Member response = memberService.createMember(member);

      return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
        HttpStatus.CREATED);
    }
}

// GlobalExceptionAdvice.java
@RestControllerAdvice
public class GlobalExceptionAdvice {

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e
    ) {
        ErrorResponse response = ErrorResponse.of(e.getBindingResult());
        return response;
    }
}

// ErrorResponse.java
@Getter
public class ErrorResponse {

    private List<FieldError> fieldErrors;

    private List<ConstraintViolationError> violationErrors;

    private ErrorResponse(final List<FieldError> fieldErrors,
                          final List<ConstraintViolationError> violationErrors) {
      this.fieldErrors = fieldErrors;
      this.violationErrors = violationErrors;
    }
    public static ErrorResponse of(BindingResult bindingResult) {
      return new ErrorResponse(FieldError.of(bindingResult), null);
    }

    @Getter
    @Builder
    public static class FieldError {
      private String field;
      private Object rejectedValue;
      private String reason;

      private FieldError(String field, Object rejectedValue, String reason) {
        this.field = field;
        this.rejectedValue = rejectedValue;
        this.reason = reason;
      }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors =
                                          bindingResult.getFieldErrors();
            return fieldErrors.stream()
                        .map(error -> ErrorResponse.FieldError.builder()
                                .field(error.getField())
                                .rejectedValue(error.getRejectedValue())
                                .reason(error.getDefaultMessage())
                                .build())
                        .collect(Collectors.toList());
        }
      }
}
  • GlobalExceptionAdvice.javaλ₯Ό μ •μ˜ν•˜μ—¬ λͺ¨λ“  Controller μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό κ³΅ν†΅μ μœΌλ‘œ μ²˜λ¦¬ν• μˆ˜ μžˆλ„λ‘ ν•˜μ˜€λ‹€.
  • GlobalExceptionAdvice.javaμ—μ„œ handleMethodArgumentNotValidExceptionλ©”μ†Œλ“œμ— ErrorResponseλ₯Ό λ°˜ν™˜ν•˜κ²Œ ν•˜μ—¬ μ½”λ“œλ₯Ό μ‘°κΈˆλ” κ°„κ²°ν•΄μ‘Œλ‹€.
    • @RestControllerAdviceμ—μ„œ μ§€μ›ν•΄μ£ΌλŠ” @ResponseBodyκΈ°λŠ₯으둜 νŠΉλ³„ν•œ 맡핑 없이 JSON νƒ€μž…μ˜ λ°μ΄ν„°λ‘œ 응닡을 ν• μˆ˜ 있게 ν•˜μ˜€λ‹€.
    • @ResponseStatus(HttpStatus.BAD_REQUEST)λ₯Ό 지정 ν•΄ μ€ŒμœΌλ‘œμ„œ HTTP μƒνƒœμ½”λ“œκΉŒμ§€ 응닡 메세지에 보내 쀄 수 μžˆλ‹€.
  • GlobalExceptionAdvice.javaμ—μ„œ μ—λŸ¬ 데이터λ₯Ό 가지고 ν•„μš”ν•œ 데이터λ₯Ό μΆ”μΆœν•˜κ³  κ°€κ³΅ν•˜λŠ” 역할을 ErrorResponse.java에 μœ„μž„ ν•˜μ˜€μœΌλ©° μƒμ„±μžλ₯Ό private처리 ν•¨μœΌλ‘œμ„œ of λ©”μ†Œλ“œλ₯Ό ν†΅ν•΄μ„œλ§Œ 응닡 객체λ₯Ό μƒμ„±ν• μˆ˜ 있게 ν•˜μ˜€λ‹€.
  • ErrorResponse.java λ‚΄λΆ€μ—μ„œλŠ” νŒŒλΌλ―Έν„°λ‘œ 전달 받은 BindingResult λ°μ΄ν„°μ—μ„œ ν•„μš”ν•œ λ°μ΄ν„°λ§Œ μΆ”μΆœν•˜κ³  κ°€κ³΅ν•˜λŠ” 역할을 λ‚΄λΆ€ static 클래슀 FieldError에 μœ„μž„ν•˜μ—¬ λͺ¨λ“  클래슀 λ“€μ˜ μ—­ν• κ³Ό μ±…μž„μ„ λͺ…ν™•νžˆ ν•΄ μ£Όμ—ˆλ‹€.



Reference

https://www.digitalocean.com/community/tutorials/spring-mvc-exception-handling-controlleradvice-exceptionhandler-handlerexceptionresolver

https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

https://jaehun2841.github.io/2018/08/30/2018-08-25-spring-mvc-handle-exception/#HandlerExceptionResolver%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B2%98%EB%A6%AC

profile
컴퓨터가 할일은 컴퓨터가

0개의 λŒ“κΈ€