본문 바로가기
스프링 공부/공부내용

[스프링 부트] custom Exception api 예외처리

반응형

스프링 부트에서 api를 만들고 간단하게 예외처리를 반환하는 글입니다.

 

먼저 커스텀 예외처리는 다음과 같이 동작하게 됩니다.

1. 에러코드가 발생한다. ( RunTimeException 에서 발생한 예외를 httpStatus 체크)

2. 해당 httpStatus 에러 코드를 잡아(헨들러) enum으로 설정한 클래스와 연결시키고 커스텀한 내용으로 예외를 반환한다.

 

사용한 클래스

ErrorResponse : 에러코드에 대한내용을 커스텀해서 반환하는 클래스
NotFoundClassException : 예외처리로 반환할 코드가 없을 때 반환하는 클래스
GlobalExceptionHandler : 예외처리를 핸들링하기 위한 클래스 
ErrorCode : 커스텀한 내용의 이넘클래스와 반환할 이넘클래스가 없을 때는 NotFoundClassException 반환
CustomException : 런타임 중 발생하는 발생하는 예외를 연동하는 클래스

 

예외처리 코드

ErrorResponse

ErrorResponse코드를 통해 동일한 포멧의 예외 문자를 반환하도록 설정합니다.

package com.dongjae.nearChatProejct.Common.Error;
import com.dongjae.nearChatProejct.Common.Error.Exception.ErrorCode;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;

@Getter
@Setter
@Builder
public class ErrorResponse {
    private final LocalDateTime time = LocalDateTime.now();
    private final int status;
    private final String message;
    private final String code;
    private final String error;

    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
        return ResponseEntity
                .status(errorCode.getHttpStatus())
                .body(ErrorResponse.builder()
                        .status(errorCode.getHttpStatus().value())
                        .error(errorCode.getHttpStatus().name())
                        .code(errorCode.name())
                        .message(errorCode.getMessage())
                        .build());
    }

}

저는 시간, 응답상태 코드(200,400...) , 오류 코드(이넘에서 생성한 오류 이름), 메시지(커스텀한 메세지 내용), 응답상태에 해당하는 에러 메시지(BAD_REQUEST, NOT_FOUND 이런 내용) 형태로 반환해주도록 설정했습니다.

여기서 ResponseEntity를 이용해 해당 내용을 바디에 넣어서 반환해주도록 합니다.

그리고 파라미터로는 아래에서 구현한 ErrorCode에 대한 이넘 내용을 받도록 하였습니다.

아래에 ErrorCode를 이넘을 받아 클래스로 구현해야합니다!

 

ErrorCode

이넘클래스를 생성해 발생할 예외에 대한 내용을 기록합니다.

package com.dongjae.nearChatProejct.Common.Error.Exception;

import com.dongjae.nearChatProejct.User.exception.UserAlreadyDeviceId;
import com.dongjae.nearChatProejct.User.exception.UserBlankNickName;
import com.dongjae.nearChatProejct.User.exception.UserNotFoundException;
import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.util.Arrays;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;


@Getter
public enum ErrorCode {
    USER_NOT_FOUND(BAD_REQUEST, "사용자를 찾지 못했습니다.", UserNotFoundException.class),
    CLASS_NOT_FOUND(NOT_FOUND, "에러 클래스를 찾을 수 없습니다", NotFoundClassException.class),
    USER_DEVICEID_ALREADY(BAD_REQUEST,"이미 존재하는 디바이스 아이디입니다.", UserAlreadyDeviceId.class),
    USER_BLANK_NICKNAME(BAD_REQUEST,"아이디를 입력하지 않았습니다.", UserBlankNickName.class);
    private final HttpStatus httpStatus;
    private final String message;
    private final Class<? extends Exception> klass;


    ErrorCode(HttpStatus httpStatus, String message, Class<? extends Exception> klass) {
        this.httpStatus = httpStatus;
        this.message = message;
        this.klass = klass;
    }

    public static ErrorCode findByClass(Class<? extends Exception> klass) {
        return Arrays.stream(ErrorCode.values())
                .filter(code -> code.klass.equals(
                        klass
                ))
                .findAny()
                .orElseThrow(NotFoundClassException::new);
    }

    }

 

이넘 클래스로 발생하는 응답상태코드, 메시지, 클래스를 선언해주고 생성자를 만듭니다.

findByClass 를 통해 해당하는 예외 클래스가 있으면 반환해주고 없으면 NotFoundClassException을 반환하도록 설정합니다.

 

CustomException

RunTimeException을 상속받고 에러코드에 대한 클래스를 찾은 뒤 커스텀한 예외를 처리합니다.

RunTimeException을 상속받는 이유는 런타임 중 발생하는 예외에 해당하는 것을 처리하기 위함입니다.

package com.dongjae.nearChatProejct.Common.Error.Exception;

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

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
    private final ErrorCode errorCode = ErrorCode.findByClass(this.getClass());

    private HttpStatus httpStatus;
    private String Message;

    public CustomException(){
        this.httpStatus = errorCode.getHttpStatus();
        this.Message = errorCode.getMessage();
    }
}

 

NotFoundClassException

package com.dongjae.nearChatProejct.Common.Error.Exception;

public class NotFoundClassException extends CustomException{
}

NotFoundClassException을 CustomException을 상속받고 클래스로 구현해 줍니다. 이 클래스는 해당하는 예외처리가 선언되지 않았을 때 발생합니다.

 

GlobalExceptionHandler

예외처리가 발생했을때 핸들링하는 함수입니다.

여기서 사용한 RestControllerAdvice 는 ResponseBody 어노테이션과 ControllerAdvice를 합친 어노테이션입니다. 

런타임 중 예외가 발생한 경우 커스텀예외에 해당하는 클래스를 가져와서 바디에 내용을 넣어서 보여주는 역할을 합니다

package com.dongjae.nearChatProejct.Common.Error.Exception;

import com.dongjae.nearChatProejct.Common.Error.ErrorResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value =  {CustomException.class})
    protected ResponseEntity<ErrorResponse> handlerCustomException(CustomException e) {
        return ErrorResponse.toResponseEntity(e.getErrorCode());
    }
}

 

간단한 예제 - 예외처리 적용(디바이스 아이디로 회원체크)

디바이스 아이디가 존재하거나 빈칸인 경우 에러코드를 발생하는 예제입니다. 회원가입에 적용할 수 있습니다.

service

@Transactional
public UserEntity CreateUser(UserEntity user){
    this.userRepository.save(user);
    return user;
}

public UserEntity CheckDeviceIdAndNickNameOrThr(String deviceId, String name) {
    this.userRepository.findByDeviceId(deviceId)
            .ifPresent(e ->{
                throw new UserAlreadyDeviceId();
            });
    if (!StringUtils.hasText(name)){
        throw new UserBlankNickName();
    }
    return UserEntity.builder().deviceId(deviceId).name(name).build();
}

해당 저장소에 디바이스 아이디가 이미 존재한 경우 UserAlreadyDeviceId 예외를 발생, 이름이 빈칸이거나 비어있을 경우 UserBlankNickName 예외가 발생하도록 하였습니다. 

 

Controller

    @PostMapping("/sign-up")
    public UserEntity signUp(@RequestBody UserSignUpRequestDto dto) {
        UserEntity userEntity = userService.CheckDeviceIdAndNickNameOrThr(dto.getDeviceId(), dto.getName());
        UserEntity userEntity1 = userService.CreateUser(userEntity);
        return userEntity1;
    }

닉네임과 디바이스아이드를 체크한 후 문제가 없으면 유저를 생성하도록 controller에 설정하였습니다.

 

회원가입 정상

 

UserAlreadyDeviceId 예외발생

똑같은 내용을 두번 입력하면 예외가 발생하게 되는데 이처럼 UserAlreadyDeviceId 예외에 해당하는 JSON이 잘 반환되는 것을 확인할 수 있습니다.

UserBlankNickName 예외 닉네임을 입력하지 않았을때

만약 닉네임을 입력하지 않으면 UserBlankNickName 예외가 발생해 반환되는 것도 확인할 수 있습니다.