백엔드/Spring Boot

[SpringBoot] @Valid 이용하여 Validation 예외 처리 - CustomException 적용

금호박 2024. 8. 25. 22:15

적용 배경

 api 개발 및 배포 후 프론트 쪽에서 api를 연결하던 과정중에 프론트에서 요청 파라미터로 null이나 공백 등 들어와서는 안 되는 값을 보내어 예상하지 못했던 여러 에러가 발생하는 문제를 겪었다. api 명세서에 null은 보내주지 말라고 적어두기도 했고, 예상하지 못했기 때문에 왜 이런 값이 DB에 들어왔지? 라고 당황했다. 이 부분은 백엔드 쪽에서 예외처리를 시켜주어야 하는 부분이라고 생각했고, 기존에 적용해두었던 CustomError를 이용하여 Validation을 체크하기로 했다. (예외 처리는 정말정말 중요한 것 같다!)

 

 

구현 코드

ApiExceptionHandler.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler(value = CustomException.class)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        log.error("[handleCustomException] {} : {}",e.getErrorCode().name(), e.getErrorCode().getMessage());
        return ErrorResponse.error(e);
    }

    /* @Valid 유효성 체크 */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> methodValidException(MethodArgumentNotValidException e){
        log.warn("MethodArgumentNotValidException 발생!!!  trace:{}", e.getStackTrace());
        return ErrorResponse.error(new CustomException(ErrorCode.INPUT_IS_BLANK));
    }
}

 

  해당 프로젝트의 경우 요청으로 빈 값(null, "", " ")이 들어오는지만 체크하면 되었기 때문에 위의 방식으로 예외 처리를 시켜주었다.

 

 

 

 

CustomException.java

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException{
    private ErrorCode errorCode;

    private String info;

    public CustomException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public CustomException(ErrorCode errorCode, String info){
        this.errorCode = errorCode;
        this.info = info;
    }
}

 

 

 

 

ErrorResponse.java

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

@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {
    private final HttpStatus status;
    private final String code;
    private final String message;

    public ErrorResponse(ErrorCode errorCode) {
        this.status = errorCode.getStatus();
        this.code = errorCode.name();
        this.message = errorCode.getMessage();
    }

    public static ResponseEntity<ErrorResponse> error(CustomException e) {
        if(e.getInfo()!=null){
            return ResponseEntity
                    .status(e.getErrorCode().getStatus())
                    .body(ErrorResponse.builder()
                            .status(e.getErrorCode().getStatus())
                            .code(e.getErrorCode().name())
                            .message(e.getErrorCode().getMessage()+e.getInfo())
                            .build());
        }
        return ResponseEntity
                .status(e.getErrorCode().getStatus())
                .body(ErrorResponse.builder()
                        .status(e.getErrorCode().getStatus())
                        .code(e.getErrorCode().name())
                        .message(e.getErrorCode().getMessage())
                        .build());
    }
}

 

 

 

PrivateException.java

public class PrivateException extends Throwable {
    public PrivateException(Object p0) {
    }
}

 

 

 

ErrorCode.java

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

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

    /* S3 */
    FILE_CONVERT_ERROR(HttpStatus.BAD_REQUEST,"파일 전환에 실패하였습니다."),
    FILE_UPLOAD_ERROR(HttpStatus.BAD_REQUEST,"이미지 업로드에 실패하였습니다."),
    FILE_DELETE_ERROR(HttpStatus.BAD_REQUEST,"이미지 삭제에 실패하였습니다."),


    /* input */
    INPUT_IS_BLANK(HttpStatus.BAD_REQUEST,"입력으로 빈 값이 들어왔습니다."),
    IMAGE_CANNOT_BE_NULL(HttpStatus.BAD_REQUEST,"이미지 url로 null이 들어왔습니다."),

    /* null 반환 */
    NO_CONTENT_EXIST(HttpStatus.BAD_REQUEST,"데이터가 존재하지 않습니다."),

    /* 관리자 로그인 */
    PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
    ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 관리자 계정이 존재합니다."),

    /* JWT */
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"만료된 토큰입니다."),

    /* member */
    INVALID_NUM(HttpStatus.BAD_REQUEST,"존재하지 않는 기수입니다.")


    ;


    private final HttpStatus status;
    private final String message;
}

 

 

여기까지가 예외 처리를 위한 코드이다.


 

 

NetworkController.java

import com.epris.homepage.activity.network.dto.NetworkReqeustDto;
import com.epris.homepage.activity.network.dto.NetworkResponseDto;
import com.epris.homepage.activity.network.service.NetworkService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/networks")
public class NetworkController {

    private final NetworkService networkService;

    @PostMapping
    public ResponseEntity<NetworkResponseDto> updateNetwork(@RequestParam("type")String type, @RequestBody @Valid NetworkReqeustDto reqeustDto) throws IOException {
        return networkService.updateNetwork(type,reqeustDto);
    }
}

  

  Post 요청 시 request body로 들어온 dto로 빈 값이 들어오는지를 확인하기 위해 @Valid 를 붙여 준다.

 

 

NetworkRequestDto.java

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class NetworkReqeustDto {

    @NotBlank
    private String networkInfo;

    @NotBlank
    private String imageUrl;
}

 

 요청으로 빈 값이 들어왔을 경우에 대해 체크하기 위한 방법으로는 @NotNull, @NotEmpty, @NotBlank 이렇게 3가지가 대표적이다.

  • @NotNull : null 만 체크
  • @NotEmpty : null, "" 체크
  • @NotBlank : null, "", " " 체크