[Spring] 예외 처리 - API 계층

·2022년 11월 16일
0

Spring

목록 보기
18/24
post-thumbnail

예외처리
위 내용에 복습하며 추가학습한 내용을 정리했다.

예외 처리는 왜 하지?

개발자의 의도가 담겨있다.

잘못된 코드나 서버의 문제로 인해 발생한 예외가 아닌 (언체크 예외), 외부의 영향이나 클라이언트의 잘못된 요청으로 인한 예외는 반드시 특정한 처리를 해주어야 할 필요가 있다. 이를 예외를 던져 처리(throw) 한다고 표현한다.

최대한 오류 발생을 줄이고 정확한 데이터를 서버에 넘길 수 있도록 개발자가 의도적으로 예외를 처리하도록 구현한다.

예외 처리의 구현

Controller 클래스에서의 구현

API계층인 Controller 클래스에서 @ExceptionHandler애너테이션이 추가된 예외 처리 메서드를 생성하면 된다.

@RestController
public class Controller{
	...
    ...
    
    @ExceptionHandler
    public ResponseEntity handleXException(XException x) {
    		...
    	return new ResponseEntity<>(new ErrorResponse(), HttpStatus.BAD_REQUEST); 

☝️ 예외 처리 메서드는 발생한 예외를 파라미터로 받아 클라이언트에게 발생한 예외와 그 이유를 보내줘야 한다는 것을 잊지말자!

예외 처리 클래스 구현

Controller 클래스에서 예외 처리 메서드 생성? 좋지~
중복인지 미친인지 커피 주문 애플리케이션을 만드는데 필요한 Controller회원, 주문, 커피 최소 3개다.

☝️ Controller 클래스에 예외 처리 메서드를 작성하는 것은 본질을 흐리는 것과 마찬가지!
각각의 클래스에서는 자신에게 정해진 일만 처리해야 한다.

Advice 클래스

예외 처리 메서드만 담긴 클래스를 우리는 Advice라고 지칭한다.
Spring은 POJO 프로그래밍을 지향하는 언어임을 꼭, 항상 기억하고 있어야 한다. 그 중 AOP(관점 지향 프로그래밍) 이 지금 예외 처리의 핵심(?)이다!

AOP핵심 기능과 부가 기능을 분리하여 중복 코드를 줄이는 것에 중점을 두는 개념이다. 이 개념에서 Advice란 부가 기능을 의미한다.

우리가 하는 예외 처리 메서드? 당연히 부가 기능이지 이걸로 서비스할 건 아니잖어 분리시켜

@RestControllerAdvice
public class GlobalExceptionAdvice {

	 @ExceptionHandler
    public ResponseEntity handleXException(XException x) {
    		...
    	return new ResponseEntity<>(new ErrorResponse(), HttpStatus.BAD_REQUEST); 
        
         @ExceptionHandler
    public ResponseEntity handleX2Exception(X2Exception x2) {
    		...
    	return new ResponseEntity<>(new ErrorResponse(), HttpStatus.BAD_REQUEST); 
        
}

@RestControllerAdvice

해당 애너테이션을 Advice 클래스에 추가하면 Controller 클래스에 공통적으로 사용할 수 있다.

예외 처리 메서드가 하는 일

예외 처리 메서드는 Controller 클래스의 메서드와 하는 일이 같다고 생각하자. 클라이언트로부터 요청 데이터를 받아 처리 후 결과 데이터, 응답 데이터를 클라이언트에게 되돌려 주는 일을 한다. 단지, 예외 처리 메서드는 발생한 예외를 전달 받아 그 예외의 원인을 클라이언트에게 돌려주는 것일 뿐.

예외의 원인을 돌려주는 법

매우 간단하다.

...

	 @ExceptionHandler
    public ResponseEntity handleXException(XException x) {
    
    		final fieldError errors = x.getBindingResult().getFieldErrors();
            
    	return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); 

...
        
}

잡은 예외의 getBindingResult().getFieldErrors() 메서드를 실행하면 해당 예외의 원인을응답 데이터로 보낼 수 있다.

근데.

[
    {
        "codes": [
            "Email.memberPostDto.email",
            "Email.email",
            "Email.java.lang.String",
            "Email"
        ],
        "arguments": [
            {
                "codes": [
                    "memberPostDto.email",
                    "email"
                ],
                "arguments": null,
                "defaultMessage": "email",
                "code": "email"
            },
            [],
            {
                "arguments": null,
                "defaultMessage": ".*",
                "codes": [
                    ".*"
                ]
            }
        ],
        "defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
        "objectName": "memberPostDto",
        "field": "email",
        "rejectedValue": "hgd",
        "bindingFailure": false,
        "code": "Email"
    }
]

이렇게 나옴. 감당 가능? 내가 클라이언트면 울었음 ㅠㅠ 이뭔씹 상태로 창 다 끄고 서비스가 사용자한테 욕해도 되나요? 타닥타닥- 한다.

☝️ 우리는 특정 정보만 클라이언트에게 제공해야 한다. 따라서 Response 객체가 별도로 필요하다.

ErrorResponse

Response 클래스를 작성하기 전에 어떤 멤버 변수를 필요로 하는지에 대한 고민이 필요하다.🤯

1. 어디서? -> 에러의 원인을 클라이언트에게 전달해야 한다.
2. 무엇이? -> 마찬가지로 에러의 원인이다.
3. 왜? -> 왜 해당 입력값이 에러의 원인이 되었는지 알려줘야 한다.

를 기억하자!

이 정보를 어떻게 구해오지🤯

걱정할 필요는 전혀 없다. 위 getBindingResult().getFieldErrors() 메서드를 사용하여 응답 데이터를 보낸 것에 힌트가 있다. 그 데이터 안에는 우리가 원하는 정보가 다 들어있다.

  1. field : 에러가 발생한 위치를 의미한다.
  2. rejectedValue : 거절당한 값, 에러의 원인을 말한다.
  3. defaultMessage : 에러의 이유를 설명한다.
...

	 @ExceptionHandler
    public ResponseEntity handleXException(XException x) {
    
    		final List<fieldError> errors = x.getBindingResult().getFieldErrors();
            
            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); 

...

☝️ List의 타입을 가지는 이유
클라이언트가 단 한번만 실수를 한다는 보장이 없다. 여러개의 에러를 담아야 할 수도 있기 때문에 List 타입으로 설정한다.

다 끝났다고 생각하면 오산이다.

@Getter
public class ErrorResponse {  

    private List<FieldError> fieldErrors;

    public ErrorResponse(List<FieldError> fieldErrors) {
        this.fieldErrors = fieldErrors;

    }

    private ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult));
    }

    @Getter
    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 -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                    "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()
                    )).collect(Collectors.toList());
        }

    }
}

🤯
ExceptionHandler 에서 직접 ErrorResponse 객체를 생성하지 않고 ErrorResponse의 내부 클래스인 FieldError를 통해 객체를 생성하고 있다.

그럼 왜 ExceptionHandler에서 ErrorResponse객체를 생성하지 않고 ErrorResponse 클래스 내부에서 생성하여끔 할까?

사실 이 부분은 개발자의 판단에 따라 달라질 수 있다고 생각한다. 하지만 어찌되었든 ExceptionHandler의 역할은 데이터를 클라이언트에게 전달이라고 생각한다. 따라서 에러 정보를 담는 역할은ErrorResponse에서 행해야 하는 것이 맞지 않을까 싶다.

of() 메서드
네이밍 컨벤션, 주로 객체 생성 시 어떤 값들의(of~) 객체를 생성한다는 의미를 가진 메서드.

리팩토링 후 ControllerAdvice Class

@RestControllerAdvice
public class GlobalExceptionAdvice {

	 @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleXException(XException e) {
    		final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
            
    	return response;
    }
   
}

@ResponseStatus(HttpStatus.BAD_REQUEST)

Response body를 클라이언트에게 보낼 때, 해당 애너테이션의 HttpStatus 정보도 함께 포함된다.

반환타입 ErrorResponse

리팩토링 후 ExceptionHandlerErrorResponse의 객체를 Response body로 반환하고 있다.
DTO에서 학습했듯이 클라이언트가 서버에 보내고 서버에서 클라이언트에게 보내는 데이터의 타입은 Json이다. 객체로는 보낼 수가 없다. 그렇기 때문에 자동으로 Json타입으로 변환해주는 ResponseEntity를 사용했다.

🤯 야 이거 큰일났다. 우리 코드 다 수정해야 해.🤯

@RestControllerAdvice

ControllerAdvice 클래스에 추가된 애너테이션 @RestControllerAdvice를 알아보자.
해당 애너테이션은 기존에 Spring MVC 4.3버전 이후로 추가된 애너테이션이다. 기존의 @ControllerAdvice 기능을 포함하면서 @ResponseBody 애너테이션의 기능도 포함하고 있다.

☝️ @ResponseBody 애너테이션은 우리가 생성한 데이터 객체를 자동으로 Json 타입으로 변경해 반환해주는 역할을 한다.

다 조져주마 하고 시작했는데 조져진 것은 나였음을....😂
한번에 이해하면 좀 좋은가.. 여러번 반복해봐야 할 것 같다 휴 인생 역시 쉽지 않지 쉽게만 살아가면 재미없어 빙고

profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게

0개의 댓글