프로그래밍/spring

API, BindingResult, MessageSource를 이용한 에러 메시지 출력 방법

승민아 2024. 7. 28. 22:15

다음과 같은 상황에 문제 해결을 하고자 생각해낸 구현 방법이다.

 

사용자가 게시글과 같은 도메인 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 응답을 받게 된다.

 

우리는 이 메시지를 이용해 에러 메시지를 출력하면 된다.

 

에러 메시지 출력

 

 

이렇게 생각해서 에러 메시지 처리를 생각해냈지만

위지윅 에디터 방식으로 변경할거라 코드들을 또 뜯어고치게 될것같다... ㅠ