1. @ExceptionHandler를 이용한 예외 처리

  • 위와 같이 postman의 GET에서 요청을 보냈을 경우 데이터를 찾지 못하여 에러가 발생하는 경우가 있다
  • 클라이언트가 전달 받는 Response Body는 애플리케이션에서 예외(Exception)가 발생했을 때, 내부적으로 Spring에서 전송해주는 에러 응답 메시지 중의 하나이다 (위에서는 '404 Not Found' 메시지를 전송하고 있다)

 1) Spring에서의 예외 처리

  • 애플리케이션의 문제가 발생하였을 경우 알려주어 처리를 유도한다
  • 유효성 검증에 실패하였을 경우 예외로 간주하고 처리를 유도한다
  • MemberController(v6)에 @ExceptionHandler 애너테이션을 이용해서 예외를 처리하도록 handleException() 메서드를 추가한다
  • handleException() 메서드에서 유효성 검사 실패에 대한 에러 메시지를 구체적으로 전송해 주는 역활을 한다
package com.codestates.member.controller;

import com.codestates.member.dto.MemberPatchDto;
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
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;


/**
 * - DI 적용
 * - Mapstruct Mapper 적용
 */

@RestController
@RequestMapping("/v6/members")
@Validated
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);
    }

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(
            @PathVariable("member-id") @Positive long memberId,
            @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        Member response =
                memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));

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

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(
            @PathVariable("member-id") @Positive long memberId) {
        Member response = memberService.findMember(memberId);
        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response), HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        List<Member> members = memberService.findMembers();
        List<MemberResponseDto> response = mapper.membersToMemberResponseDtos(members);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(
            @PathVariable("member-id") @Positive long memberId) {
        System.out.println("# delete member");
        memberService.deleteMember(memberId);

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
        //MethodArgumentNotValidException 객체에서
        //getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        //getBindingResult().getFieldErrors()를 통해 확인한 에러 정보를
        //ResponseEntity를 통해 Response Body로 전달한다
        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
    }
}
  • 위의 코드 흐름은 다음과 같이 진행된다
  1. 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청을 전송한다
  2. RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고, MethodArgumentNotValidException이 발생한다
  3. MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인 handleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidExceptionhandleException() 메서드가 전달 받는다
  4. postman으로 정확하지 않은 data를 json 형식으로 전송하면 아래와 같이 오류가 발생하고 상세한 오류 내역을 Response Body로 전송해 준다

  • 지금까지는 @ExceptionHandler 애너테이션과 handleException() 메서드를 통해 유효성 검사 결과를 그대로 전송받았다
  • 전송받은 데이터 중에는 불필요한 내용도 포함되어 있다

 

  • 필요한 내용들만 선택적으로 받을 수 있도록 코드를 수정한다
  • 별도의 ErrorResponse 클래스를 생성한다
    - DTO 클래스의 유효성 검증 실패 시, 실패한 필드(멤버 변수)에 대한 Error 정보만 담아서 응답으로 전송한다
  • MemberController(v6)에 @ExceptionHandler 애너테이션 적용 코드를 수정한다
package com.codestates.member.controller;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor

public class ErrorResponse {

    //한 개 이상의 유효성 검증에 실패한 필드의 에러 정보를 담기 위해서 List 객체를 이용한다
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    
    //한개의 필드 에러 정보는
    //FieldError 라는 별도의 static class를 ErrorResponse 클래스의 멤버 클래스로 정의한다
    public static class FieldError{
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}
  • FieldError 클래스는 ErrorResponse 클래스 내부에 정의되어 있으나 내부(Inner) 클래스라고 부르기보다는 ErrorResponse 클래스의 static 멤버 클래스라고 부르는 것이 적절하다
  • 클래스가 멤버 변수와 멤버 메서드를 포함하듯이 static 멤버 클래스를 포함할 수 있다

 

  • MemberController(v6)에 @ExceptionHandler 애너테이션 적용 코드를 수정한다
@ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
        //MethodArgumentNotValidException 객체에서
        //getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

        //필요한 정보들만 선택하여 ErrorResponse.FieldError 클래스에서 List로 변환한다
        List<ErrorResponse.FieldError> errors =
                fieldErrors.stream()
                        .map(error->new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()
                        ))
                        .collect(Collectors.toList());

        //getBindingResult().getFieldErrors()를 통해 확인한 에러 정보를 List<ErrorResponse.FieldError>에 넣은 후
        //ResponseEntity를 통해 Response Body로 전달한다
        return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
    }

 

  • post를 전송하면 아래와 같이 필요한 데이터만 응답받을 수 있다
    - email 주소가 오류인 경우

        - email 와 name 이 동시에 오류인 경우

 

2. @ExceptionHandler 의 단점

  • 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야되므로 각 Controller 클래스마다 코드 중복이 발생한다
  • Controller에서 처리해야 되는 예외(Exception)가 유효성 검증 실패에 대한 예외(MethodArgumentNotValidException)만 있는것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어날 수 있다

 

※ 참조 링크

Exceptions : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler

 

Web on Servlet Stack

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more com

docs.spring.io

BindingResult 클래스 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindingResult.html

 

BindingResult (Spring Framework 5.3.21 API)

Record the given value for the specified field. To be used when a target object cannot be constructed, making the original field values available through Errors.getFieldValue(java.lang.String). In case of a registered error, the rejected value will be expo

docs.spring.io

 ConstraintViolation 인터페이스와 구현 클래스

 : https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintViolation.html

 

ConstraintViolation (Java(TM) EE 7 Specification APIs)

Object getLeafBean() Returns: the bean instance the constraint is applied on if it is a bean constraint the bean instance hosting the property the constraint is applied on if it is a property constraint null when the ConstraintViolation is returned after c

docs.oracle.com

 : https://docs.jboss.org/hibernate/validator/5.3/api/org/hibernate/validator/internal/engine/ConstraintViolationImpl.html

 

ConstraintViolationImpl (Hibernate Validator 5.3.6.Final)

equals public boolean equals(Object o) IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF) expressionVariables and dynamicPayload are not taken into account for equality. These variables solely enric

docs.jboss.org

 

+ Recent posts