본문 바로가기
Spring

[Spring] 예외 처리 - Controller에서 처리

by Bhinney 2022. 10. 27.

❗️여기는 예외 처리를 Controller에서 해본 것이다. ❗️


유효성 검증이 통과되지 않아 에러가 발생한 화면

이렇게 유효성 검증과 같이 잘못된 요청이 들어가면 에러가 발생합니다. 이 때, 이 에러가 왜 발생했는지 위의 사진에서는 알 수 없다. 이러한 에러메세지를 조금 더 구체적으로 알 수 있도록 예외 처리를 하려고 한다.

 

예외 처리를 하는 방법은 여러가지가 있다.

  1. 메서드 내에서 예외 사항을 예측하여 처리하는 try-catch문 이용
  2. 요구사항에 의한 예외 처리
  3. Spring Security에서 인터셉터로 잡아서 UnauthorizedException 같은 예외 처리

하지만 이런식으로 예외 처리를 여러 개 만들면 만들 수록 유지 보수가 어렵다. 

이러한 부분을 개선하기 위해서 어노테이션을 이용한 예외처리를 하려고 한다.


✅ @ExceptionHandler 사용

  • 메세지를 출력해서 어느 곳에 문제가 있는지 확인 가능
  • 하지만 아래의 사진에서 확인할 수 있듯이 필요없이 광범위하게 정보를 받는다.
@RestController
@RequestMapping("/members")
@Validated
public class MemberController {
	...
    @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 handleException(MethodArgumentNotValidException exception) {
        final List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();

        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
    }
}


✔️ FieldError

  • 필드에 오류가 있는 에러
  • 타입이 맞지 않을 때 스프링이 생성
  • 개발자의 검증을 통해 오류가 있다면 직접 생성해서 BindingResult 의 addError() 메서드를 통해 넣을수 있다.
  • 생성자의 매개 변수는 2가지.
    • objectName: 오류가 발생한 객체 이름
    • field: 오류 필드
    • rejectedValue: 사용자가 입력한 값(거절된 값)
    • bindingFailure: 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
    • codes: 메시지 코드
    • arguments: 메시지에서 사용하는 인자
    • defaultMessage: 기본 오류 메시지
public FieldError(String objectName, String field, String defaultMessage);

public FieldError(String objectName, String field, 
		    @Nullable Object rejectedValue, boolean bindingFailure, 
                    @Nullable String[] codes, @Nullable Object[] arguments, 
                    @Nullable String defaultMessage);

✔️ BindingResult

  • 검증 오류를 보관하는 객체
  • 검증 오류를 보관하는 세 가지 방법
    1. @ModelAttribute 객체에 타입 오류등으로 바인딩이 실패하는 경우
      (예 : 정수형 필드에 문자를 넣는 경우)
    2. 개발자가 직접 넣어주는 경우
    3. Validator를 사용하는 경우 << 나의 케이스!

참고한 사이트 : https://velog.io/@imcool2551/Spring-%EA%B2%80%EC%A6%9D1-BindingResult-MessageCodesResolver


✅ MethodArgumentNotValidException 와 ConstraintViolationException

  • MethodArgumentNotValidException
    • RequestBody로 들어오는 파라미터(여기서는 DTO)의 유효성을 검사
    • 통과하지 못하면 발생하는 예외
    • 해당 예외의 이유를 List<FieldError>로 받아 응답하였다.
  • ConstraintViolationException
    • URI의 유효성을 검사 (❗️HTTP Method 아님❗️)
    • 예 : "localhost:8080/members/{member_id}" 
    • 마지막에 들어가는 member_id의 유효성을 검사
    • List<ConstraintViolationError>으로 받아 응답할 예정이다.
    • 헷갈리면 아래 코드와 사진 예시를 참조하면 된다.

✅ ErrorResponse 생성

  • 위에 예시처럼 필요 없는 정보까지는 받을 필요가 없다.
  • 그래서 ErrorResponse 클래스를 만들어 필요한 메세지만 받으려고 한다.
  • 이번에는 MethodArgumentNotValidException가 아닌 ConstraintViolationException를 받아보려 한다.
  • ErrorResponse를 만들어 주고, Controller에서 필요한 정보만 받아(stream 사용) List로 반환하여 ResponseEntity로 전달한다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.validation.BindingResult;

import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Getter
@AllArgsConstructor
public class ErrorResponse {
	private List<ConstraintViolationError> violationErrors;

	@Getter
	@AllArgsConstructor
	public static class ConstraintViolationError {
		private String field;
		private Object rejectedValue;
		private String reason;
	}
}
@RestController
@RequestMapping("/members")
@Validated
public class MemberController {

	...

    @ExceptionHandler
    public ResponseEntity handleConstraintException(ConstraintViolationException exception) {
        final Set<ConstraintViolation<?>> violationExceptions = exception.getConstraintViolations();

        List<ErrorResponse.ConstraintViolationError> errors = violationExceptions.stream()
            .map(constraintViolation -> new ErrorResponse.ConstraintViolationError(
                					constraintViolation.getPropertyPath().toString(),
                					constraintViolation.getInvalidValue().toString(),
                					constraintViolation.getMessage()
                				)).collect(Collectors.toList());

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


🚨 이렇게 컨트롤러에서 처리하면 발생하는 문제

  • 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야됨 -->  각 Controller 클래스마다 코드 중복 발생
  • Controller에서 처리해야 되는 예외(Exception)가 하나만 있는것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어나게 된다.
  • 따라서 다음 포스팅에서는 예외 처리를 한 번에 처리할 예정이다.

댓글