API, BindingResult, MessageSource를 이용한 에러 메시지 출력 방법
다음과 같은 상황에 문제 해결을 하고자 생각해낸 구현 방법이다.
사용자가 게시글과 같은 도메인 DTO와
<input type="file"> 태그를 통해 파일도 함께 컨트롤러로 요청을 보냈을때
함께 보낸 도메인 클래스의 유효성 검사가 실패한다면 다음과 같은 컨트롤러 로직을 탈것이다.
Controller 코드
@PostMapping("/signup")
public String signup(@Validated MemberSignupRequest memberSignupRequest, BindingResult bindingResult) {
if (bindingResult.hasErrors())
return "member/signup";
memberService.signup(memberSignupRequest);
return "redirect:/";
}
BindingResult에 에러가 있을경우
다시 signup.html로 포워딩된다.
이때 <input type="file"> 태그로 업로드한 파일은 유지되지 않는다.
그래서 서버에 API 요청을 하여 업로드한 파일은 유지하도록 구현하고자 했다.
Controller
@PostMapping("/write")
@ResponseBody
public ResponseEntity<Map> createPoster(@AuthenticationPrincipal CustomUserDetails member,
@RequestPart(name = "poster") PosterWriteRequest posterWriteRequest,
BindingResult bindingResult,
@RequestPart(name = "posterImages", required = false) List<MultipartFile> uploadPosterImages) {
if (posterWriteRequest.getTitle().isBlank())
bindingResult.rejectValue("title", "blankPosterTitle", "제목 미입력");
if (uploadPosterImages != null) {
if (uploadPosterImages.size() > 5)
bindingResult.addError(new ObjectError("posterImages", new String[]{"fileCountOver"}, new Object[]{5}, "파일 최대 개수 초과."));
}
if (bindingResult.hasErrors())
throw new InValidInputException(bindingResult);
Long tempPosterId = 13L;
HashMap<String, Long> response = new HashMap<>();
response.put("posterId", tempPosterId);
return ResponseEntity
.ok(response);
}
컨트롤러단에서 유효성 검사를 실패해서 bindingResult에 에러가담겼다고하자.
hasErorrs()로 에러가 존재할시 커스텀 에러인 InvalidInputException 런타임 에러를 발생한다.
이때 에러 정보들을 가지고 가기위해 bindingResult를 함께 넣어준다.
@Valid의 경우 @ModelAttribute에만 적용이 가능하기에 업로드한 파일의 에러 메시지를 담기위해
new ObjectErorr()로 생성했다.
참고로 인자로 넘겨준 에러 code는 다음과 같다.
message.properties
fileCountOver = 파일 최대 업로드 개수는 {0}개 입니다.
#poster
blankPosterTitle = 제목을 입력해주세요.
blankPosterContent = 내용을 입력해주세요.
다음은 커스텀한 에러이다.
InvalidInputException
package com.example.bucketlist.exception;
import lombok.Getter;
import lombok.Setter;
import org.springframework.validation.BindingResult;
@Setter
@Getter
public class InValidInputException extends RuntimeException{
private BindingResult bindingResult;
public InValidInputException(BindingResult bindingResult) {
this.bindingResult = bindingResult;
}
}
RuntimeException이 아니라면 컨트롤러에서 Throws로 의존성이 생기는게 별로라 RuntimeException으로 구현했다.
GlobalExceptionHandler
package com.example.bucketlist.exception;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
private MessageSource messageSource;
@Autowired
public GlobalExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
@ExceptionHandler({InValidInputException.class})
public ResponseEntity<Map> validException(InValidInputException ex) {
Map<String, Object> map = new HashMap<>();
BindingResult bindingResult = ex.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
map.put(fieldError.getField(), messageSource.getMessage(fieldError.getCode(), fieldError.getArguments(), fieldError.getDefaultMessage(), Locale.getDefault()));
}
List<ObjectError> globalErrors = bindingResult.getGlobalErrors();
for (ObjectError globalError : globalErrors) {
map.put(globalError.getObjectName(), messageSource.getMessage(globalError.getCode(), globalError.getArguments(), globalError.getDefaultMessage(), Locale.getDefault()));
}
return ResponseEntity
.badRequest()
.body(map);
}
}
이제 InValidInputException을 전역으로 처리하기위해 위와 같은 코드를 작성해주자.
에러 메시지를 위한 API 응답은 JSON 포맷이며
{ 필드: "메시지" } 형식으로 응답한다.
우리는 위의 형식에서 응답으로 받은 필드를 통해 input id를 찾아서 에러 메시지를 넣어주면 된다!
파일의 경우 ObjectName을 필드로 보고 에러 메시지를 넣어주면 된다.
create-poster.html
<div>
<label for="title">제목</label>
<input id="title" name="title" type="text">
<label for="content">내용</label>
<input id="content" name="content" type="text">
<input id="posterImages" type="file" accept=".png, .jpg, .jpeg" multiple>
<button id="createPosterBtn">완료</button>
</div>
{ title: "제목을 입력해주세요", posterImages: "파일 최대 개수 초과" } 라는 응답이 오면
태그의 id를 찾아 아래에 에러 메시지를 추가해주면 된다.
만약
게시글의 제목을 작성하지 않았고
파일 업로드 개수를 초과했다면 다음과 같은 JSON 응답을 받게 된다.
우리는 이 메시지를 이용해 에러 메시지를 출력하면 된다.
이렇게 생각해서 에러 메시지 처리를 생각해냈지만
위지윅 에디터 방식으로 변경할거라 코드들을 또 뜯어고치게 될것같다... ㅠ