검증 (Validation)

Lilac-_-P·2023년 4월 17일
0

스프링 MVC

목록 보기
8/15

검증은 웹 애플리케이션에서 필수적인 기능 중 하나이다. 검증이라는 단어 자체가 포함하고 있는 의미가 포괄적이기 때문에 구체적으로 어떤 검증을 말하는 것인지 감을 잡기가 쉽지않다. 스프링에서 말하는 검증이 무엇인지 알아보자.

가장 쉬운 예시는 웹 애플리케이션에서 폼을 작성하는 경우이다. 웹 애플리케이션은 폼 입력시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야한다.

스프링 MVC Controller의 중요한 역할 중 하나는 들어온 HTTP 요청이 정상인지 검증하는 것이다. 이 말은 즉, 스프링 MVC Controller의 메서드의 파라미터로 들어온 HTTP 요청과 관련된 데이터를 검증해야한다는 것이다. 한 발 더 가서, 개발자 입장에서는 스프링 MVC Controller의 메서드로 들어온 파라미터를 검증해야한다는 것이다.

스프링 MVC Controller의 메서드로 들어온 파라미터를 검증하는데 있어서, 두 가지 경우를 생각해야한다.

  1. 파라미터는 정상적으로 들어왔는데, 파라미터로 들어온 값이 비즈니스적 로직에 어긋나는 경우
  2. HTTP 요청과 관련된 데이터를 파라미터로 바인딩 자체가 실패하는 경우 (주로 타입 불일치 오류)

1번의 경우는 메서드의 파라미터로 값이 들어왔기 때문에, 메서드 내에서 검증을 수행할 수 있지만, 2번의 경우에는 메서드의 파라미터로 값이 들어오기 전, 그 전 단계에서 오류가 발생하기 때문에 아예 컨트롤러를 호출하지 않게 되고, 따라서 메서드에서 검증을 수행할 수가 없다.

또, 검증이 실패했을 경우, 검증 실패한 데이터를 그대로 사용자에게 제공해야하는데, 2번의 경우는 Controller로 데이터가 넘어오기 전에 오류가 발생하기에, 검증 실패한 데이터를 저장해놓을 수 있는 공간이 별도로 필요하다.

참고. 웹 애플리케이션에서는 클라이언트에서 검증할수도(프론트엔드에서 검증), 서버에서 검증할 수도 있다.
클라이언트 검증은 UX를 개선해주지만 조작이 가능하여 보안에 취약하다.
반대로 서버 검증은 조작이 불가하지만, UX를 개선해주지는 못한다.
따라서, 둘이 적절히 섞어서 사용하되, 최종적으로 서버 검증은 보안을 위해 필수적으로 해야한다.

BindingResult

스프링이 제공하는 검증 오류 처리 방법중 하나는 BindingResult를 사용하는 것이다.

이 BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이다. 만약 검증 도중 오류가 발생하면, 해당 오류를 여기에 보관하면 된다. 개발자는 위에서 얘기한 1번의 경우에는 스프링 MVC Controller 메서드 내에서 비즈니스적 오류 발생시 BindingResult에 FieldError 또는 ObjectError 객체를 보관하면 되고, 2번의 경우처럼 개발자가 손쓸 수 없는 데이터 바인딩 과정에서 발생하는 오류는 스프링이 내부적으로 BindingResult에 넣어준다.

그렇기 때문에, BindingResult는 검증하고자하는 대상 바로 뒤에 와야한다. 스프링 MVC Controller 메서드에서는 @ModelAttribute로 지정된 파라미터 뒤에 와야하는 것이다. 이렇게 하면 위에서 얘기했던 2번의 경우를 커버할 수 있다. 즉, BindingResult가 있으면 @ModelAttribute에 HTTP 요청 데이터를 바인딩 시에 오류가 발생해도 컨트롤러가 호출되는 것이다.

참고.
0. BindingResult는 본인이 검증해야할 객체(target)를 이미 알고 있다.
1. BindingResult는 Model에 자동으로 포함된다.
2. FieldError와 ObjectError에서도 스프링이 제공하는 메시지 소스를 이용한 메시지 기능을 사용할 수 있다.
3. 바인딩 실패 혹은 특정 필드가 비즈니스 로직에 어긋나는 경우, 검증 실패한 값을 FieldError의 rejectedValue에 저장하여 사용자에게 어떤 값이 검증에 실패했는지에 대한 정보를 제공할 수 있다.

또한, rejectValue(), reject() 와 같은 편의성 메서드를 사용하면 직접 FieldError 또는 ObjectError의 생성자를 호출하여 객체를 생성하지 않아도 된다.

MessageCodeResolver

위에서 "FieldError와 ObjectError에서도 스프링이 제공하는 메시지 소스를 이용한 메시지 기능을 사용할 수 있다"라고 언급하였는데, 이게 어떻게 가능한건지 간단히 알아보자.

스프링은 MessageCodeResolver를 통해서 오류 발생시에도 메시지 기능을 사용할 수 있게 해준다.
메시지 기능을 사용하려면 어떤 메시지를 갖고올지 결정하는 키 값인 code가 필요한데, MessageCodeResolver는 오류에 들어있는 정보를 이용하여 code값을 만들어낸다.

MessageCodeResolver에서는 code 값을 만들 때, 다음과 같은 4가지 요소를 이용한다.

  1. 개발자가 코드에 입력한 에러코드(문자열) -> 에러코드
  2. 오류가 발생한 객체의 클래스 타입 -> 클래스 타입
  3. 오류가 발생한 객체의 클래스의 필드 이름 -> 필드 이름
  4. 오류가 발생한 객체의 클래스의 필드의 클래스 타입 -> 필드 이름의 클래스 타입

그리고 위의 4가지를 이용하여 총 4종류의 다음과 같은 code를 만들어낸다.

  1. 에러코드.클래스 타입.필드 이름
  2. 에러코드.필드 이름
  3. 에러코드.필드 이름의 클래스 타입
  4. 에러코드

특정 필드와 관련이 없는 오류라면, 2종류의 code만 만들어낸다.

  1. 에러코드
  2. 에러코드.클래스타입

이렇게 만들어진 code 값을 기반으로 메시지 소스를 이용하여 순서대로 메시지를 찾아보고, code 값에 해당하는 메시지가 없다면, 디폴트 메시지를 출력한다. 위의 만들어진 코드 순서를 보면 구체적인 code에서 덜 구체적인 code로 가는 것을 볼 수 있는데, 이 순서덕에 개발자는 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다.

중요하지 않은 메시지는 덜 구체적인 code로 관리하고, 정말 중요한 메시지는 구체적인 code를 사용하여 관리하는 것이다.

Validator로의 분리

컨트롤러에서 검증을 위한 로직들이 차지하는 부분은 매우 크다. 이런 경우에는 별도의 클래스로 역할을 분리하는 것이 유지보수성과 재사용성 측면에서 좋다.

스프링은 검증을 체계적으로 제공하기 위해 Validator 인터페이스를 제공한다.
스프링도 Validator 인터페이스를 제공하고, 자바 자체적으로도 Validator 인터페이스를 제공한다. 헷갈리지 말것.

public interface Validator {

	boolean supports(Class<?> clazz);
    
	void validate(Object target, Errors errors);

}

supports() 함수는 Validator가 어떤 클래스의 객체를 대상으로 동작(검증)하는지 판단해주는 역할을 하고,
validate() 함수는 특정 객체에 대한 support() 함수의 결과값이 true라면, 해당 객체에 대해 검증을 수행한다.

Validator를 사용하면 검증과 관련된 부분이 깔끔하게 분리되어, Controller에서는 validate()를 호출하면 된다.

그런데, 스프링은 여기서 멈추지 않는다. Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다. 직접 정의한 Validator 구현체를 WebDataBinder + @InitBinder 나 WebMvcConfigurer를 통해 Validator를 추가하면, validate() 함수를 호출하지 않고 @Validated 또는 @Valid annotation을 이용하여 검증기를 자동으로 적용할 수 있다.

아래와 같이 사용하면 된다.

@PostMapping("/ex")
public String ex(@Validated @ModelAttribute ExClass exClass, BindingResult bindingResult){
	// validate() 호출 안해도 됌!
}

@Validated는 검증기를 실행하라는 annotation이다. 이 annotation이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다. 검증기가 여러개일 경우, supports()함수로 어떤 검증기가 실행되어야할지 결정한다.

참고.
검증기를 추가할 때, @InitBinder는 해당 컨트롤러에만 영향을 줄 수 있게 하는 옵션이다.
만약 글로벌 설정(WebMvcConfigurer)을 한다면, WebDataBinder + @InitBinder가 없더라도 정상적으로 동작한다.

profile
열심히 하자

0개의 댓글