본문 바로가기
Spring Boot

Spring Boot - Exception 구현 예제

by sinabeuro 2021. 9. 13.
728x90

Valid 라이브러리를 이용하여 예외처리(Exception)를 하는 예제를 살펴보겠습니다.

저번 포스트에 다루었던 Exception을 좀 더 확장하여 예제 중심으로 보겠습니다.

 

이번 예제에서는 lombok을 사용합니다. 해당 의존성을 추가해서 사용하시면됩니다.

compileOnly 'org.projectlombok:lombok'

annotationProcessor 'org.projectlombok:lombok'

 

package com.example.exception.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
@NoArgsConstructor
public class User {

    @NotEmpty
    @Size(min = 1, max = 10)
    private String name;

    @Min(1)
    @NotNull
    private Integer age;
    
}

먼저 dto를 살펴보면, 롬북(lombok)의 어노테이션 @Data를 클래스에 붙입니다. 

@Data 어노테이션으로 인해 속성의 getter, setter가 자동으로 생성됩니다.

@Data 어노테이션 외에도 @getter, @setter 어노테이션을 통해서 각각 메소드를 생성 가능합니다.

 

그 후 Validation 라이브러리가 제공하는 유효성 검사 어노테이션 @NotEmpty, @NotNull, @Size, @Min를 붙여, 속성의 유효성 기준을 부여합니다.

 

다음은 예외처리 시 ResponseEntity에 담아줄 에러 dto를 보겠습니다.

// ErrorResponse.java
package com.example.exception.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
public class ErrorResponse {

    String statusCode;
    String requestUrl;
    String code;
    String message;
    String resultCode;

    List<Error> errorList;

}

 

// Error.java
package com.example.exception.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Error {

    private String field;
    private String message;
    private String invalidValue;

}

 


위의 dto를 이용해서 몇 가지 에러를 예외처리하는 것을 보겠습니다.

예외처리 대표 예시

  • MethodArgumentNotValidException
  • ConstraintViolationException
  • MissingServletRequestParameterException

 

MethodArgumentNotValidException

package com.example.exception.controller;

import com.example.exception.dto.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@RestController
@RequestMapping("/api")
@Validated
public class ApiController {

	...
    @PostMapping("")
    public User post(@Valid @RequestBody User user) {
        return user;
    }
    
}

 

package com.example.exception.advice;

import com.example.exception.controller.ApiController;
import com.example.exception.dto.Error;
import com.example.exception.dto.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

@RestControllerAdvice(basePackageClasses = ApiController.class)
public class ApiControllerAdvice {

	...
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest httpServletRequest) {
        System.out.println("MethodArgumentNotValidException 예외처리");
        System.out.println(e.getMessage());

        List<Error> errorList = new ArrayList<>();

        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getAllErrors().forEach(objectError -> {
            FieldError field = (FieldError) objectError;

            String fieldName = field.getField();
            String message = field.getDefaultMessage();
            String value = field.getRejectedValue().toString();

            System.out.println("-----------------------");
            System.out.println("fieldName = " + fieldName);
            System.out.println("message = " + message);
            System.out.println("value = " + value);

            Error errorMessage = new Error();
            errorMessage.setField(fieldName);
            errorMessage.setMessage(message);
            errorMessage.setInvalidValue(value);

            errorList.add(errorMessage);

        });

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
    ...
 }

@RestControllerAdvice 어노테이션 속성에 basePackageClasses의 값을 주어 특정 컨트롤만 예외 처리 로직을 적용할 수 있습니다.

MethodArgumentNotValidException은 dto에 적용한 유효성 검사에 어긋날 경우 발생하는 에러입니다.

User 클래스 dto에 validation을 적용하였고 유효성에 어긋날 경우 어떤 필드가 잘못되었는지에 대한 정보를 출력할 수 있습니다.

그렇기에 MethodArgumentNotValidException의 getBindingResult()에 에러 정보가 담겨있으며,

getBindingResult().getAllErrors()에 속성들의 세부 에러 정보를 배열형식으로 출력할 수 있습니다.

 

다음과 같은 파라미터를 post로 보내면

{
  "name": "",
  "age":0
}

아래와 같은 예외처리로 만들어준 response 객체를 return 합니다.

// response
BODY
{
	"statusCode": "400 BAD_REQUEST",
	"requestUrl": "/api",
	"code": null,
	"message": "",
	"resultCode": "FAIL",
	"errorList": [{
			"field": "age",
			"message": "1 이상이어야 합니다",
			"invalidValue": "0"
		},
		{
			"field": "name",
			"message": "비어 있을 수 없습니다",
			"invalidValue": ""
		},
		{
			"field": "name",
			"message": "크기가 1에서 10 사이여야 합니다",
			"invalidValue": ""
		}
	]
}

 

ConstraintViolationException

package com.example.exception.controller;

import com.example.exception.dto.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@RestController
@RequestMapping("/api")
@Validated
public class ApiController {

	...
    @GetMapping("")
    public User get(
            @Size(min = 2)
            @RequestParam String name,

            @NotNull
            @Min(1)
            @RequestParam Integer age) {
        User user = new User();
        user.setName(name);
        user.setAge(age);

        return user;
    }
    
}

@Validated를 controller클래스에 기술하면, RequestParam에서 바로 유효성 조건을 부여할 수 있습니다. 

package com.example.exception.advice;
...

@RestControllerAdvice(basePackageClasses = ApiController.class)
public class ApiControllerAdvice {

	...
    @ExceptionHandler(value = ConstraintViolationException.class)
    public ResponseEntity ConstraintViolationException(ConstraintViolationException e, HttpServletRequest httpServletRequest){
        System.out.println("ConstraintViolationException 예외처리");
        System.out.println(e.getMessage());

        List<Error> errorList = new ArrayList<>();

        e.getConstraintViolations().forEach(error -> {
            System.out.println("-------------------------------------");
            System.out.println(error);

            Stream<Path.Node> stream = StreamSupport.stream(error.getPropertyPath().spliterator(), false);
            List<Path.Node> list = stream.collect(Collectors.toList());

            String field = list.get(list.size() - 1).getName();
            String message = error.getMessage();
            String invalidValue = error.getInvalidValue().toString();

            System.out.println("field = " + field);
            System.out.println("message = " + message);
            System.out.println("invalidValue = " + invalidValue);

            Error errorMessage = new Error();
            errorMessage.setField(field);
            errorMessage.setMessage(message);
            errorMessage.setInvalidValue(invalidValue);

            errorList.add(errorMessage);

        });

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
    ...
 }

ConstraintViolationException은 컨트롤러의 RequestParam에서 지정한 유효성 검사에 어긋날 경우 발생하는 에러입니

다.

ConstraintViolationException 클래스에는 어떤 필드가 잘못되었는지에 대한 정보를 담고 있습니다.

getPropertyPath() 메소드를 Stream으로 파싱해서 배열의 가장 마지막에 있는 값을 가져옵니다.

해당 값이 유효성에 어긋난 필드명입니다.

 

 

다음과 같은 파라미터를 get으로 보내면

http://localhost:8080/api?name&age=0

아래와 같은 예외 처리 response를 return 합니다.

{
	"statusCode": "400 BAD_REQUEST",
	"requestUrl": "/api",
	"code": null,
	"message": "",
	"resultCode": "FAIL",
	"errorList": [{
			"field": "name",
			"message": "크기가 2에서 2147483647 사이여야 합니다",
			"invalidValue": ""
		},
		{
			"field": "age",
			"message": "1 이상이어야 합니다",
			"invalidValue": "0"
		}
	]
}

 

MissingServletRequestParameterException

package com.example.exception.advice;
...

@RestControllerAdvice(basePackageClasses = ApiController.class)
public class ApiControllerAdvice {

	...
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public ResponseEntity MissingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest httpServletRequest) {
        System.out.println("MissingServletRequestParameterException 예외처리");
        System.out.println(e.getMessage());

        String fieldName = e.getParameterName();
        String fieldType = e.getParameterType();
        String invalidValue = e.getMessage();

        System.out.println("------------------------------------------");
        System.out.println("fieldName = " + fieldName);
        System.out.println("fieldType = " + fieldType);
        System.out.println("invalidValue = " + invalidValue);

        List<Error> errorList = new ArrayList<>();

        Error errorMessage = new Error();
        errorMessage.setField(fieldName);
        errorMessage.setMessage(e.getMessage());
        errorMessage.setInvalidValue(invalidValue);     // 값이 없음.
        errorList.add(errorMessage);

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
    ...
 }

MissingServletRequestParameterException은 api가 전달받은 requestParam 중 필수 파라미터임에도 값이 없는 경우 발생하는 에러입니다. 

즉, 프론트에서 실수로 파라미터를 누락하거나 파라미터의 명칭이 대소문자 구분이 안됐거나 오타가 나서 이상한 파라미터를 보내는 등의 경우에 예외 처리를 할 수 있습니다.

 

MissingServletRequestParameterException은 앞서 설명한 에러들 보다 먼저 실행되며, 한 가지 파라미터만 예외 처리가 가능합니다. 

여러 파라미터가 에러가 있을 시에도 가장 우선적으로 보내진 파라미터의 예외 처리만 가능하니, 여러 시도를 통해서 에러를 잡아야 합니다.

에러가 난 한 가지의 파라미터의 정보만 가지고 있음으로 MissingServletRequestParameterException의 메소드는 에러 정보가 배열로 되어있지 않고 단일 속성으로 정의되어있습니다.

 

다음과 같은 파라미터를 get으로 보내면

http://localhost:8080/api?Name=""&age=0    (대소문자 구분이 잘못된 케이스)

아래와 같은 예외 처리 response를 return 합니다.

{
	"statusCode": "400 BAD_REQUEST",
	"requestUrl": "/api",
	"code": null,
	"message": "",
	"resultCode": "FAIL",
	"errorList": [{
		"field": "name",
		"message": "Required request parameter 'name' for method parameter type String is not present",
		"invalidValue": "Required request parameter 'name' for method parameter type String is not present"
	}]
}

 

이러한 예외 처리 방식으로 통해서 프론트에서 명시적으로 에러의 원인을 찾을 수 있을 것입니다.

에러 처리 방식을 좀 더 알아보시고 프로젝트에 잘 구현한다면, 작업하는데 굉장히 유용할 것 같습니다.

 

 

728x90

'Spring Boot' 카테고리의 다른 글

Spring Boot - 서버 간 통신 RestTemplate 정의  (0) 2021.09.25
Spring Boot - Interceptor  (0) 2021.09.23
Spring Boot - Filter  (0) 2021.09.12
Spring Boot - Exception 개괄  (0) 2021.09.10
Spring Boot - Validation  (0) 2021.09.09

댓글