백엔드/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, "", " " 체크