[스프링] 검증1

gyeol·2023년 11월 24일

스프링

목록 보기
30/50
post-thumbnail

김영한 님의 '스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술'을 듣고 적은 글입니다.

검증

우리가 앞서 만들었던 웹 애플리케이션에선 숫자를 문자로 작성하는 등 오류가 발생하면 오류 화면으로 바로 이동한다. 이렇게 되면 사용자는 처음부터 해당 폼으로 다시 이동해 입력해야 하므로 사용자는 금방 떠나버릴 것이다. 웹 서비스는 폼 입력 시 오류가 발생하면 고객이 입력한 데이터를 유지하는 것이 좋다.

컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다.

  • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
  • 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
  • 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수이다.
  • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 한다.

일반적으로 상품 등록, 저장, 상세 폼은

이와 같은 구조로 작동한다.
하지만 상품 등록 시, 요구한 조건에 만족하지 않았을 때 사용자에게 오류 메시지를 보여주는 것이 적절하다.

검증 로직

map 사용

검증 오류가 발생하면 Map<String, String> errors = new HashMap<>(); 을 이용해 어떤 오류가 발생했는지 정보를 담아둔다.

if(!StringUtils.hasText(item.getItemName()){
	errors.put("itemName", "상품 이름은 필수입니다.");
}

위와 같은 검증 로직을 사용한다. org.Springframework.util.StringUtils 를 임포트해줘야한다.
검증시 오류가 발생하면 errors에 담아둔다. 이때 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 key로 사용하고 이후 이 데이터를 사용해 고객에게 오류 메시지를 보여줄 수 있다.

if(!errors.isEmpty()){
	model.addAttribute("errors", errors);
    return "validation/v1/addForm"; 
}

만약 검증에서 오류 메시지가 하나라도 있으면 위의 로직을 사용해 뷰 템플릿으로 보낸다.

그리고 해당 폼에서 오류를 보여주기 위한 처리도 따로 진행해준다.

<div th:if="${errors?.containsKey('globalError')}">
	<p class="field-error" th:text"${errors['globalError']}"> 전체 오류 </p>
</div>

위 코드에서 errors?를 사용하는 이유가 있다. 등록폼에 진입한 시점에는 errors가 존재하지 않기에 errors.containsKey()를 호출하는 순간 NullPointerException이 발생한다. errors?errors가 NULL 일 때 NullPointerException을 발생하는 대신 NULL 을 반환하는 문법이다.

BindingResult 사용

타임리프는 스프링의 BlindingResult를 활용해 편리하게 검증 오류를 표현하는 기능을 제공한다. BindingResult는 검증할 대상 바로 다음에 와야한다.
BindingResult가 오류를 저장할 때 사용하는 것은 FieldError 생성자이다.

public FieldError(String objectName, String field, String defaultMessage) { }

필드에 오류가 생겼을 시, FieldError객체를 생성해 bindingResult에 담아두면 된다.

  • objectName : @ModelAttribute 이름
  • field : 오류가 발생한 필드 이름
  • defaultMessage : 오류 기본 메시지

특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해 bindingResult에 담아두면 된다.

public ObjectError(String objectName, String defaultMessage) { }
  • objectName : @ModelAttribute의 이름
  • defaultMessage : 오류 기본 메시지

타임리프 스프링 검증 오류통합 기능

  • #fields : #fieldsBindingResult가 제공하는 검증 오류에 접근할 수 있다.
  • th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다.th:if 편의버전이다.
  • th:errorclass : th:field에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

BindingResult가 있으면 @ModelAttribute 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.

  • BindingResult 없을 때 -> 400 오류 발생 + 컨트롤러 유지 X + 오류 페이지 이동
  • BindingResult 있을 때 -> 오류 정보를 BindingResult에 담아 컨트롤러 정상 호출

BindingResult와 Errors

BindingResult는 인터페이스고, Errors 인터페이스를 상속받고 있다. 실제 넘어오는 구현체는 BeanPropertyBindingResult라는 것인데, 둘 다 구현하고 있으므로 BindingResult 대신 Errors를 사용해도 된다. Errors 인터페이스는 단순한 오류저장과 조회 기능을 제공한다. BindingResult는 여기에 더해 추가적인 기능들을 제공한다.

FieldError, ObjectError

FieldError는 두가지 생성자를 제공한다.

public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage)
  • objectName : 오류가 발생한 객체 이름
  • field : 오류 필드
  • rejectedValue : 사용자가 입력한 값
  • bindingFailure : 타입 오류같은 바인딩 실패인지, 검증 실패인지 구분 값
  • codes : 메시지 코드
  • arguments : 메시지에서 사용하는 인자
  • defaultMessage : 기본 오류 메시지

ObjectError도 유사하게 두 가지 생성자를 제공한다.

FieldError는 오류 발생 시, 사용자 입력값을 저장하는 기능을 제공한다. 저장한 값을 검증 오류 발생 시 화면에 다시 출력하면 된다.

특히나 타임리프의 th:field는 정상 상황에서는 모델 객체의 값을 사용하지만, 오류 발생 시 FieldError에서 보관한 값을 사용해 출력한다. 개발자들의 수고를 덜어주는 고마운 존재다..

errors.properties 파일 생성해 오류 관리

errors 메시지 파일을 따로 생성해 오류 메시지를 관리할 수 있다. 단, 스프링부트가 해당 메시지 파일을 인식할 수 있게 application.properties

spring.messages.basename=messages,errors

#messages와 errors파일을 기본으로 인식한다는 의미

코드를 추가해준다.

rejectValue(), reject()

BindingResult가 제공하는 rejectValue(), reject()를 사용하면 FieldError, ObjectError를 직접 생성하지 않고 검증 오류를 다룰 수 있다.

void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
  • field : 오류 필드명
  • errorCode : 오류 코드 (메시지에 등록된 코드 아님)
  • errorArgs : 오류 메시지에서 {0}으로 치환하기 위한 값
  • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

오류 코드를 만들 때 단순하게 만들면 범용성이 좋아 여러곳에서 사용할 수 있지만 메시지를 세밀하게 작성하기 어렵다. 반대로 너무 자세히 만들면 범용성이 떨어진다. 가장 좋은 방법은 범용성으로 사용하다가 세밀하게 작성해야 하는 경우 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다.
reject()rejectValue()를 사용해 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고 없으면 그 다음 레벨의 메시지를 사용하도록 한다.

스프링은 이런 기능을 MessageCodesResolver 라는 것으로 기능을 지원한다.

MessageCodeResolver

MessageCodeResolver 는 인터페이스고 DefaultMessageCodesResolver는 기본 구현체다. 검증 오류 코드로 메시지 코드를 생성하며 ObjectError, FieldError와 함께 주로 쓰인다.

기본 메시지 생성에 규칙이 존재한다.

  • 객체 오류
객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required
  • 필드 오류
필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code

예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"

rejectValue(), 와 reject()는 내부에서 MessageCodesResolver를 사용한다. 여기에서 메시지 코드를 생성한다.
FieldError, ObjectError의 생성자를 보면 오류 코드를 하나가 아닌 여러 오류 코드를 가질 수 있다. MessageCodesResolver를 통해 생성된 순서대로 오류코드를 보관한다.

스프링은 타입 오류가 발생하면 typeMismatch라는 오류 코드를 사용한다. 이 오류코드가 MessageCodesResolver를 통하면서 위의 필드 오류같은 네가지 메시지 코드가 생성된 것이다.

Validator 분리

위의 방식처럼 계속 컨트롤러에 검증 로직을 추가하면 컨트롤러가 하는 역할이 많아진다. 이런 경우 별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용할 수도 있다.

스프링음 검증을 체계적으로 제공하기 위해 Validator라는 인터페이스를 제공한다.

public interface Validator {
  boolean supports(Class<?> clazz);
  void validate(Object target, Errors errors);
}
  • supports() {} : 해당 검증기를 지원하는 여부 확인
  • validate(Object target, Errors errors) : 검증 대상 객체와 BlindingResult

Validator 인터페이스를 사용해 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

//controller에 추가
@InitBinder 
//해당 컨트롤러에만 영향
public void init(WebDataBinder dataBinder) {
  log.info("init binder {}", dataBinder);
  dataBinder.addValidators(itemValidator);
}

WebDataBinder는 스프링 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다. 이렇게 WebDataBinder에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
검증기를 실행하기 위해서는 @Validated라는 애노테이션이 붙어야한다. 이 애노테이션이 붙으면 앞서 등록한 검증기를 찾아서 실행한다. 근데 여러 검증기를 등록하면 그 중 어떤 검증기가 실행되어야할지 구분이 필요하다. 이때 위 Validator 인터페이스에서 소개한 supports()가 사용된다.

참고
검증 시 @Validated, @Valid 둘다 사용 가능하다.
javax.validation.@Valid를 사용하려면 build.gradle에 의존관계 추가가 필요하다.
@Validated는 스프링 전용 검증 애노테이션이고, @Valid는 자바 표준 검증 애노테이션이다.

profile
공부 기록 공간 '◡'

0개의 댓글