cso6005 / TIL-Troubleshooting

배움 기록 및 트러블 슈팅 정리
0 stars 0 forks source link

[SpringBoot, JSP] SSR에서 Validation 시, BindingResult 사용, 주의점 #65

Open cso6005 opened 1 year ago

cso6005 commented 1 year ago

[SpringBoot, JSP] SSR에서 Validation 시, BindingResult 사용, 주의점

스프링에서는 import org.springframework.validation.Validator 인터페이스를 통해 객체 검증, 에러 메시지 등을 지원하여, 이를 통해 validation 을 쉽게 구현할 수 있다.

이전 포스트에선 CSR에서 진행했다면, SSR JSP 개발에서 form 으로 던진 데이터에 대해 어떻게 데이터 유효성 결과를 보내줄지에 초점을 맞춰 정리하고자 한다.

그리고 사용 예제 뿐만 아니라, 사용 시, 주의점을 알아보자.

예제

0. build.gradle

spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성 내부에 있던 validation이 사라졌다. spring boot version이 2.3 이상이라면 validation 의존성을 따로 추가해줘야 함.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

1. dto

public class SignUpRequest {

    @NotEmpty(message = "이메일을 입력하지 않았습니다.")
    @Email(message = "이메일 형식이 맞지 않습니다.")
    private String userEmail;

    @NotEmpty(message = "비밀번호을 입력하지 않았습니다.")
    @Size(min = 5, message = "비밀번호을 5글자 이상 입력해주세요.")
    private String password;

    @NotEmpty(message = "이름을 입력하지 않았습니다.")
    @Size(min = 2, max = 8, message = "이름을 2 ~ 8 자 사이로 입력해주세요.")
    private String userName;

    @NotEmpty(message = "휴대폰 번호를 입력하지 않았습니다.")
    @Size(min = 3, max = 11, message = "휴대폰 번호 형식이 맞지 않습니다.")
    private String phoneNumber;

    @NotEmpty(message = "주소를 입력하지 않았습니다.")
    private String address;

}

2. controller

    @PostMapping("/sign-up")
    public String signUp(HttpSession httpSession, @Valid SignUpRequest signUpRequest, BindingResult bindingResult, Model model) throws InvalidAlgorithmParameterException, UnsupportedEncodingException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {

            if (bindingResult.hasErrors()) {
                model.addAttribute("errorMsg", bindingResult.getFieldErrors().get(0).getDefaultMessage());
                return "userLoginRegister";
            }
            userService.signUp(signUpRequest);
            return "redirect:/";
}
  1. validation 을 할 객체 SignUpRequest 에 @Valid 붙이기
  2. 컨트롤러에서 BindingResult 을 통해 데이터 유효성 검사 시, 발생한 error 관련 내용을 받아온다.
  3. 원하는 처리 코드를 짜준다.

3. errorMsgAlert.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Object errorMsg = request.getAttribute("errorMsg");
    if (request.getAttribute("errorMsg") != null) {
%>

<input type="hidden" id="errorMsg" value="<%= errorMsg %>">
<%
    }
%>

<!-- 에러 메시지 alert JS -->
<script>
    document.addEventListener("DOMContentLoaded", function() {
        var errorMsgInput = document.getElementById("errorMsg");
        if (errorMsgInput) {
            showAlert(errorMsgInput.value);
        }
    });
    function showAlert(message) {
        alert(message);
    }
</script>

동작 원리 및 순서

  1. @Valid 에서 데이터 유효성 검사 실패 시, MethodArgumentNotValidException 를 발생시킨다.
  2. MethodArgumentNotValidException 안에는 여러 필드가 있고, 그 중 bindingResult 객체 안에는 예외와 관련된 여러 값들이 담겨 있다.
  3. 그 객체 안에서 error와 관련된 값을 가져오기 위해 BindingReulst.getFieldErrors() 메소드를 이용해 BindingResult 가 갖고 있는 errors를 List 로 반환 받는다. 데이터 유효성 검사에 대해 발생한 모든 예외에 대한 내용을 각각의 FieldError객체를 리스트에 담아 받는다. (나의 예외 처리)
  4. 그 중 get(0) 첫 번째 예외만 가지고 온다.
  5. 예외 메시지를 화면에 뿌릴 것이기에 getDefaultMessage를 들고 와, model을 통해 응답한다.

FieldError

public class FieldError extends ObjectError {

private final String field;
@Nullable
private final Object rejectedValue;
private final boolean bindingFailure;

public FieldError(String objectName, String field, String defaultMessage) {
    this(objectName, field, null, false, null, null, defaultMessage);
}

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/FieldError.html

위 중 필요한 것들을 getter을 통해 받아, 원하는 예외 처리를 해주면 된다.

Binding Reulst 시, 주의점

유효성 검사의 오류에 접근하려면 유효성 검사 메서드 직후에 즉, 검사할 객체 다음에 Errors 또는 BindingResult를 선언해야 한다. 아니면, BindExceptionn이 발생한다고 한다.

스프링 레퍼런스 For access to errors from validation and data binding for a command object (that is, a @ModelAttribute argument) or errors from the validation of a @RequestBody or @RequestPart arguments. You must declare an Errors, or BindingResult argument immediately after the validated method argument.

기타

❓bindingResult.getFieldErrors() 에서 여러 에러가 발생했을 때, 에러 순서는?

bindingResult.getFieldErrors()를 호출하여 여러 에러가 발생했을 때, 에러의 순서는 일반적으로 다음과 같이 처리된다.

  1. 검증(annotation-based validation)이나 바인딩(binding) 과정 중에 발생한 에러는 해당하는 필드 순서대로 에러가 저장됩니다.
  2. DTO 클래스 내에서의 선언 순서와는 관계없이, 실제 요청 데이터의 필드 순서에 따라 에러가 배열됩니다.
  3. 만약 검증(annotation-based validation)에 실패한 경우, 각 필드에 대한 에러가 순차적으로 발생하며 순서는 해당 필드의 데이터가 들어온 순서와 동일합니다.

즉, 에러의 순서는 요청 데이터의 필드 순서에 따라 결정되며, DTO 클래스 내의 선언 순서와는 관련이 없다.

❓ 로그인의 경우, 데이터 유효성 검사를 하지 않는 이유는?

  1. 보안 문제 로그인 시에 사용자의 인증 정보를 전송하는데, @Valid를 사용하여 검증하게 되면 검증 실패 시에도 요청이 서버로 전송되게 되어, 인증 정보가 노출될 가능성 있고 이는 보안의 취약성으로 이어질 것이다.

  2. 성능 문제 로그인은 매우 빈번하게 발생하는 동작 중 하나이며, 요청마다 검증 로직을 실행하면 성능에 부담이 될 수 있다.

  3. 사용자 경험 문제 로그인 화면에서 필수 항목에 대한 검증 오류 메시지가 표시되면 사용자 경험이 저하될 수 있습니다. 로그인은 사용자가 빈번하게 하는 요청이므로 사용자 경험을 최우선으로 고려해야 한다.

따라서 로그인 요청을 검증하는 것보다는 클라이언트 측에서 먼저 필수 입력 항목의 유효성을 확인하고, 서버로 전송하기 전에 검증하는 방법이 더 안전하고 적절하다. 실제 서버에서는 인증 처리를 수행하는 방식을 사용하여 보안을 강화하고, 클라이언트 측에서는 사용자 경험을 개선하는데 집중하는 것이 좋다.

cso6005 commented 1 year ago

https://giantdwarf.tistory.com/32