[클론 코딩] 네이버 카페 - 예외 처리

2024. 5. 2. 19:53Project/Naver Cafe

  이전 게시글에서 사용자가 회원 가입시 잘못된 정보를 입력하면, 어떠한 이유로 에러가 발생했는지 사용자에게 알려줄 수 있도록 예외 처리가 필요하다고 했다. 그래서 이번 글에서는 전역적(글로벌) 예외 처리 기능을 추가한 내용을 작성하였다.


RequestJoinMember

package CloneCoding.NaverCafe.domain.member.dto;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestJoinMember {

    @NotNull
    @Size(min = 5, max = 20,
            message = "계정 아이디는 5 ~ 20자로 제한됩니다.")
    @Pattern(regexp = "^[a-z0-9_-]+",
            message = "계정 아이디는 영소문자, 영대문자, 숫자만 사용 가능합니다.")
    private String accountId;

    @NotNull
    @Size(min = 8, max = 16,
            message = "계정 비밀번호는 8 ~ 16자로 제한됩니다.")
    @Pattern(regexp = "^[a-zA-Z0-9~!@#$%]+",
            message = "계정 비밀번호는 영소문자, 영대문자, 숫자, 특수문자(~, !, @, #, $, %)만 사용 가능합니다.")
    private String accountPassword;

    @NotNull
    @Pattern(regexp = "^[a-zA-Z0-9_]+@[a-zA-Z]+\\.[a-zA-Z]+(\\.[a-zA-Z]+)?",
            message = "올바른 이메일 형식이 아닙니다.")
    private String email;

    @NotNull
    @Size(min = 2, max = 18, message = "이름은 2 ~ 18자로 제한됩니다.")
    @Pattern(regexp = "^[a-zA-Z]+", message = "이름은 영소문자, 영대문자만 사용 가능합니다.")
    private String username;

    @NotNull
    @Min(value = 1924, message = "태어난 연도의 입력 가능 범위는 1924 ~ 2024 입니다.")
    @Max(value = 2024, message = "태어난 연도의 입력 가능 범위는 1924 ~ 2024 입니다.")
    private int year;

    @NotNull
    @Min(value = 1, message = "태어난 달의 입력 가능 범위는 1 ~ 12 입니다.")
    @Max(value = 12, message = "태어난 달의 입력 가능 범위는 1 ~ 12 입니다.")
    private int month;

    @NotNull
    @Min(value = 1, message = "태어난 날의 입력 가능 범위는 1 ~ 31 입니다.")
    @Max(value = 31, message = "태어난 날의 입력 가능 범위는 1 ~ 31 입니다.")
    private int day;

    @NotNull
    @Size(min = 11, max = 11, message = "휴대전화번호는 11자 입니다.")
    @Pattern(regexp = "^[0-9]+", message = "휴대전화번호는 숫자만 입력 가능합니다.")
    private String phoneNumber;

    @NotNull
    @Size(min = 2, max = 20, message = "별명은 2 ~ 20자로 제한됩니다.")
    @Pattern(regexp = "^[a-zA-Z0-9]+", message = "별명은 영소문자, 영대문자, 숫자만 입력 가능합니다.")
    private String nickname;

}

 

  이전에 전달 받는 정보를 전달하는 RequestJoinMember에 제약조건들을 설정하였는데, 여기에 각 제약조건 마다 사용자에게 알릴 메시지를 지정하였다.


ErrorCode

package CloneCoding.NaverCafe.exception;

import org.springframework.http.HttpStatus;

public interface ErrorCode {

    String name();

    HttpStatus getHttpStatus();

    String getMessage();

}

 

  사용자에게 보낼 에러코드를 정의하는 인터페이스이다. 에러명, HTTP 상태, 에러 메시지를 제공한다.


CommonErrorCode

package CloneCoding.NaverCafe.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {

    INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"),
    RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error")
    ;

    private final HttpStatus httpStatus;
    private final String message;

}

 

  예외 처리를 할 때 자주 사용되는 에러 코드를 상수로 만들어 저장해 둔 Enum 클래스이다. ErrorCode 인터페이스를 상속 받으며, 각 상수는 HttpStatus와 메시지를 가지며 상수 사용시 두 정보를 꺼내 쓸 수 있도록 하였다.


ResponseError, ValidationError

package CloneCoding.NaverCafe.exception.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.FieldError;

import java.util.List;

@Getter
@Builder
@RequiredArgsConstructor
public class ResponseError {

    private final String code;
    private final String message;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final List<ValidationError> errors;

    @Getter
    @Builder
    @RequiredArgsConstructor
    public static class ValidationError {

        private final String field;
        private final String message;

        public static ValidationError of(final FieldError fieldError) {
            return ValidationError.builder()
                    .field(fieldError.getField())
                    .message(fieldError.getDefaultMessage())
                    .build();
        }

    }

}

 

  중첩 클래스로 @Valid 검증시 발생한 문제를 사용자에게 알리기 위해 만든 응답용 DTO이다. 사용자에게는 에러 코드명과 코드 메시지 그리고 잘못 입력한 필드명, 제약조건에 지정한 메시지 정보를 알려준다.


GlobalExceptionHandler

package CloneCoding.NaverCafe.exception.handler;

import CloneCoding.NaverCafe.exception.CommonErrorCode;
import CloneCoding.NaverCafe.exception.ErrorCode;
import CloneCoding.NaverCafe.exception.dto.ResponseError;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
        return handleExceptionInternal(e, errorCode);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllException(Exception e) {
        log.warn("handleAllException : ", e);
        ErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
        return handleExceptionInternal(errorCode);
    }

    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeResponseError(errorCode));
    }

    private ResponseError makeResponseError(ErrorCode errorCode) {
        return ResponseError.builder()
                .code(errorCode.name())
                .build();
    }

    private ResponseEntity<Object> handleExceptionInternal(BindException e, ErrorCode errorCode) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeResponseError(e, errorCode));
    }

    private ResponseError makeResponseError(BindException e, ErrorCode errorCode) {
        List<ResponseError.ValidationError> validationErrors = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(ResponseError.ValidationError::of)
                .toList();

        return ResponseError.builder()
                .code(errorCode.name())
                .message(errorCode.getMessage())
                .errors(validationErrors)
                .build();
    }

}

 

  발생하는 예외를 처리하기 위한 클래스이다. 이론상으로 발생가능한 예외를 모두 처리할 수 있어야 하기에 기본적으로 Exception.class(최상위 예외 클래스)를 처리하는 예외 핸들러를 구현해 두었다. 이후 구현에서 발생하는 예외들을 처리하는 예외 핸들러들을 순차적으로 추가할 생각이다. 현재 추가한 예외 핸들러들은 아래와 같다.

  • MethodArgumentNotValidException : @Valid 검증시 발생하는 예외로 지정한 제약조건과 맞지 않는 데이터를 발견하면 발생한다.

API TEST

API TEST - 회원 가입(정보) 예외 처리

 

  일부러 잘못된 정보로 회원 가입을 시도하니 원하는 대로 어떤 값을 잘못 입력했고 원인에 대한 정보를 확인할 수 있으며, 또 단일이 아닌 다수의 정보를 잘못 입력할 수도 있는데 해당 부분도 문제 없이 출력된다.