티스토리 뷰

728x90
반응형

 

 

 

지난 글까지 해서 API 계층을 완성하고, 유효성 검증 및 서비스 계층과의 연동을 완료했다.

 

member 패키지에 클래스가 많이 늘어나 재배치를 했다. 구성은 아래와 같다.

 

이번 글에선 작성한 프로젝트에 예외처리를 적용해 본다.

 

예외 처리(Exception Handling)오류 처리(Trouble Shooting)라고도 불리며

 

실행 흐름상 오류가 발생했을 때 오류를 그대로 실행시키지 않고 오류에 대응하는 방법을 제시하는 개념이다.

 

쉽게 말해 문제가 발생했을 때 그 문제의 처리방법을 정의하는 거라고 보면 된다.

 

Spring에서는 애플리케이션에 문제가 발생할 경우 문제를 알려서 처리하기도 하지만

 

유효성 검증에 실패했을 때 역시 실패를 하나의 예외로 간주하여 이 예외를 던져서(throw) 예외 처리를 유도한다.

 

여기서 throw란 일종의 의도적 예외 발생이라고 생각하면 된다.

 

예를 들어 Request Body에 유효성 검증을 통과하지 못하는 값을 입력하면 아래와 같은 오류를 만난다.

 

하지만 돌려주는 Response Body의 정보만으로는 어떤 항목이 검증을 통과하지 못했는지 알 수 없는데,

 

에러가 발생했을 때 더 구체적으로 클라이언트에게 데이터를 전달해야 할 필요성이 생기는 것이다.

 

이 글에선 먼저 @ExceptionHandler를 이용해 컨트롤러 레벨에서 예외처리를 하는 방법을 살핀다.

 

목표는 위의 에러 응답 메시지를 개발자가 직접 처리하도록 만드는 것이다.

 

 

MemberController에 @ExceptionHandler 적용

 

먼저 컨트롤러 클래스를 수정하자.

package com.gnidinger.member.controller;

import com.gnidinger.member.dto.MemberPatchDto;
import com.gnidinger.member.dto.MemberPostDto;
import com.gnidinger.member.dto.MemberResponseDto;
import com.gnidinger.member.entity.Member;
import com.gnidinger.member.mapper.MemberMapper;
import com.gnidinger.member.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;

@RestController
@RequestMapping("/v6/members")
@Validated
@Slf4j // (1)
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 e) {
				// (2)
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

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

(1) SLF4J(Simple Logging Facade for Java) - 로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음

 

로깅(Logging)이란 오류를 디버깅하거나 프로그램 상태를 모니터링하기 위해 필요한 정보(로그)를 기록하는 것이다.

 

간단하게 컨트롤러 클래스에 @ExceptionHandler 애너테이션을 가진 handleException() 메서드를 작성했다.

 

위의 예에서 Request Body의 유효성 검증 실패에 대한 예외처리 과정은 아래와 같다.

 

  • 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청 전송
  • 유효하지 않은 요청 데이터로 인한 유효성 검증 실패, MethodArgumentNotValidException 발생
  • MethodArgumentNotValidException을 @ExceptionHandler를 가진 handleException() 메서드가 받음
  • (2) - MethodArgumentNotValidException 객체에서 getBindingResult().getFieldErrors()를 통해 에러 정보 확인
  • (3) - (2)에서 얻은 에러 정보를 ResponseEntity 통해 Response Body 전달

실제로 요청을 보내보면 아래와 같은 화면을 만난다.

 

처음과는 달리 구체적인 응답메시지를 표시하는 것을 확인할 수 있다.

 

Response Body 전체를 보면 아래와 같다.

[
    {
        "codes": [
            "NotBlank.memberPostDto.email",
            "NotBlank.email",
            "NotBlank.java.lang.String",
            "NotBlank"
        ],
        "arguments": [
            {
                "codes": [
                    "memberPostDto.email",
                    "email"
                ],
                "arguments": null,
                "defaultMessage": "email",
                "code": "email"
            }
        ],
        "defaultMessage": "공백일 수 없습니다",
        "objectName": "memberPostDto",
        "field": "email",
        "rejectedValue": "",
        "bindingFailure": false,
        "code": "NotBlank"
    }
]

하지만 이번에는 쓸모없는 정보가 너무 많이 포함되어 있어 보기 불편하다.

 

문제가 된 데이터와 그 이유 정도만 전달하도록 만들어 보자.

 

 

ErrorResponse 클래스 적용

 

위 에러메세지를 기반으로 ErrorResponse 클래스를 만들어서 필요한 정보만 전달해 보자.

 

상위 패키지에 폴더를 만들어 클래스를 생성한다.

package com.gnidinger.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class ErrorResponse {
    // (1)
    private List<FieldError> fieldErrors;

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

유효성 검증 실패 시, 실패한 변수의 Error 정보만을 담아 응답으로 전송하기 위한 ErrorResponse 클래스이다.

 

JSON Response Body배열인 이유는 하나 이상의 변수가 유효성 검증에 실패할 수 있기 때문이며,

 

(1)과 같이 fieldErrors의 반환 타입이 List객체인 것도 같은 이유이다.

 

에러 하나에 대한 정보는 FieldError라는 멤버 클래스로 따로 정의해 둔 것도 확인할 수 있다.

 

 

ErrorResponse를 사용하도록 MemberController의 handleException() 메서드 수정

 

package com.gnidinger.member.controller;

import com.gnidinger.response.ErrorResponse;
import com.gnidinger.member.dto.MemberPatchDto;
import com.gnidinger.member.dto.MemberPostDto;
import com.gnidinger.member.dto.MemberResponseDto;
import com.gnidinger.member.entity.Member;
import com.gnidinger.member.mapper.MemberMapper;
import com.gnidinger.member.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/v7/members")
@Validated
@Slf4j
public class MemberController {
    ...
		...

    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
				// (1)
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

				// (2)
        List<ErrorResponse.FieldError> errors =
                fieldErrors.stream()
                            .map(error -> new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()))
                            .collect(Collectors.toList());

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

개선 이전 코드에서는 (1)의 List<FieldError>를 통째로 ResponseEntity 클래스에 담아 전달했지만

 

개선 후에는 (2)와 같이 필요한 정보들만 선택적으로 골라서 ErrorResponse.FieldError 클래스에 담아 List로 변환 후,

 

List<ErrorResponse.FieldError>를 ResponseEntity 클래스에 실어서 전달하는 것을 확인할 수 있다.

 

실제로 요청을 보내보면 아래와 같은 데이터를 받는다.

 

유효성 검증에 실패한 변수가 두 개이기 때문에 에러메세지 역시 두개임을 알 수 있다.

 

 

@ExceptionHandler의 단점

 

@ExceptionHandler를 이용하면 위와 같이 원하는 정보만 깔끔하게 표시하는 것이 가능하다.

 

하지만, 위와 같이 에러처리를 하게 되면 금방 문제점을 만나게 되는데, 이는 다음과 같다.

 

  1. Controller(Coffee, Order,...) 마다 @ExceptionHandler를 이용한 메서드 작성, 코드 중복 발생
  2. 하나의 Controller 내에서 @ExceptionHandler 이용한 복수의 핸들러 메서드 작성(patchMember() 등을 위한), 유연성 ↓

즉, 다양한 기능을 가진 앱에서 다양한 예외를 처리하기엔 @ExceptionHandler는 적합하지 않다는 말이다.

 

이를 해결하는 방법이 @RestControllerAdvice 애너테이션이며, 다음 글에서 살펴본다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함