스프링 #8 요청 검증 및 에러메시지

함형주·2022년 10월 16일
0

spring

목록 보기
8/12

질문, 피드백 등 모든 댓글 환영합니다.

스프링에서 HTTP 요청을 처리할 때 컨트롤러에서는 HTTP 요청이 정상 요청인지를 검증해야 합니다.

예를 들어 HTTP Form 으로 전송된 값이 영문만 가능한데 한글이 넘어오는 경우에는 컨트롤러에서 비정상 요청으로 처리해야 합니다.

스프링은 이러한 검증로직을 크게 두 가지 방식으로 제공합니다.

BindingResult

스프링은 검증 오류를 처리하기 위해 BindingResult를 제공합니다.

@Getter @Setter
public class User{
	String name; // 필수
    String nickName // 3자 이상
}

컨트롤러에서 위 객체에 대한 요청을 BindingResult를 이용하여 검증해보겠습니다.

  • @Getter @Setter는 롬복이 제공하는 어노테이션으로 해당 클래스의 모든 필드에 getter와 setter를 자동으로 생성해줍니다.

addForm.html에서 /user/add 경로로 잘못된 형식의 post 요청이 발생한다면

@Controller
public class ExController{
	@PostMapping("/user/add")
   public String addUser(@ModelAttribute User user, BindingResult bindingResult) {
     if (!StringUtils.hasText(user.getName)) 
        bindingResult.rejectValue("name", "required", null, null, "필수");
         
     if (user.getNickName < 3) 
		bindingResult.rejectValue("age", "range", null, null, "3글자 이상");
       
     if (bindingResult.hasErrors())
     	return "addForm";
       
     ...
   }
}

위와 같이 처리 가능하고 BindingResult의 위치는 반드시 검증해야 할 대상 바로 뒤에 위치해야 합니다.

rejectValue()는 필드 에러를 처리하는 메서드로 FieldError를 생성하고 모델에 해당 에러를 저장하는 기능을 제공합니다.

뷰 템플릿으로 타임리프를 사용한다면 #{fields.}로 에러에 접근할 수 있습니다.

rejectValue() 파라미터 목록은 아래와 같습니다.

rejectValue(@Nullable String field, String errorCode)
rejectValue(@Nullable String field, String errorCode, String defaultMessage)
rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)

field는 오류가 발생한 필드명이고 errorCodemessageCodesResolver에 쓰이는 code 입니다.(메시지 소스에 사용되는 code와는 다름, messageCodesResolver는 밑에서 설명합니다.)

특정 필드에 관한 오류가 아니라면 reject()로 처리 가능합니다.

reject(String errorCode)
reject(String errorCode, String defaultMessage)
reject(String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)

검증 오류 처리 후 에러 메시지를 출력하는 경우에도 메시지 소스를 활용하여 처리합니다.
하지만 BindingResultrejectValuereject에선 메시지 소스의 코드를 지정하지 않습니다.

스프링이 검증 오류시 메시지 코드를 사용하는 방법을 알아보겠습니다.

MessageCodesResolver

우선 메시지 소스를 간단히 작성하겠습니다.

range=글자 수 오류입니다.
range.user.nickName=별명은 3글자 이상 가능합니다.

required=필수입니다.
required.user.name=이름은 필수입니다.

스프링은 MessageCodesResolver(인터페이스, 기본 구현체는 DefaultMessageCodesResolver)로 검증 에러메시지를 처리합니다.

MessageCodesResolver는 아래의 규칙으로 메시지 소스에 접근하여 사용합니다.

필드 에러의 경우

  1. code + "." + 객체 이름 + field
  2. code + "." + field
  3. code + "." + field type
  4. code

객체 에러의 경우

  1. code + "." + object name
  2. code

순서대로 에러 메시지 코드에 접근하고 없다면 defaultMessage를 사용합니다.

예제에서 user.name의 경우엔

  1. required.user.name
  2. required.name
  3. required.java.lang.String
  4. required

순서대로 접근하고 메시지 소스에 require.user.name이 존재하므로 이름은 필수입니다.가 출력됩니다.

만약 메시지 소스에서 MessageCodesResolver로 매칭되는 코드가 없다면 defaultMessage인 필수가 출력되고 defaultMessage마저 지정하지 않았다면 스프링이 기본으로 등록한 defaultMessage가 사용됩니다.

타입 미스매치

필드 검증 문제는 BindingResultreject()rejectValue()를 통해 해결했습니다.

만약 int age; 필드에 "A"가 입력된다면 어떻게 될까요?
이 경우 타입미스매치 에러가 발생하고 이 또한 BingdingResult를 이용하여 해결합니다.

@Getter @Setter
public class User{
	String name; // 필수
    int age; 
}

@Controller
public class ExController{
	@PostMapping("/user/add")
   public String addUser(@ModelAttribute User user, BindingResult bindingResult) {
     ...
   }
}

User 클래스로 name=user&age=A 요청이 오면 별도로 코드를 작성하지 않아도 스프링(BingdingResult)이 FieldError를 생성하고 typeMismatch를 에러코드로 등록하여 사용합니다.
(만약 컨트롤러가 파라미터로 BingdingResult를 사용하지 않는다면 컨트롤러 호출 전 서블릿이 400 예외를 발생시킴)

이 경우도 FieldError가 생성되므로 네 가지 규칙에 따라 메시지 소스를 참고하기 때문에 typeMismatch... 형식의 메시지 코드를 등록하여 사용할 수 있습니다.

Bean Validation

컨트롤러에서 필드를 검증하는 로직은 값 존재 여부나 값의 범위 등 그 주제가 대부분 비슷한 경우가 많습니다.

때문에 Bean Validation이라는 검증 기술 표준이 존재합니다.(javax 표준)
Bean Validation은 매우 편리한 어노테이션 기반의 검증 기능을 제공합니다.(예제는 아래에서 확인하겠습니다.)

그 중 구현체로 hibernate validator를 사용합니다.

공식 사이트
어노테이션 목록 블로그에서 어노테이션 종류에 대해선 자세히 설명하지 않습니다.

Bean Validation을 사용하기 위해선 build.gradle에 아래와 같이 의존관계를 추가해야 합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

이제 본격적으로 Bean Validation을 사용해보겠습니다.


@Getter @Setter
public class User{
	@NotEmpty
	String name;
    
    @Range(min=20, message="20살 이상만 가입할 수 있습니다.")
    int age; 
}
@Controller
public class ExController{
	@PostMapping("/user/add")
   public String addUser(@Valided @ModelAttribute User user) {
     ...
   }
}

어노테이션만으로 일반적인 필드 검증 로직은 한 번에 해결할 수 있습니다.
검증할 필드 앞에 @Validated(@Valid, @Validated는 스프링이 제공하는 어노테이션으로 javax 표준인 @Valid에 groups 기능이 추가 된 것)를 사용하면 타입 변환에 성공한 필드에 대해서만 검증 로직이 작동하며 타입 변환에 실패하거나 검증 오류시 BindingResult에 해당 에러를 저장합니다.

Bean ValidationBindingResult를 사용하여 에러를 처리하므로 에러 메시지 또한 같은 방식으로 처리합니다.

errorCode는 어노테이션 이름이 지정되고, defaultMessage는 어노테이션의 message 속성을 활용하여 지정할 수 있습니다.

만약 특정 필드가 아닌 객체 레벨의 검증을 해야할 경우에는 @ScriptAssert를 사용하여 검증할 수 있으나..... 해당 어노테이션은 한 객체 레벨에만 적용 가능하므로 범용성이 떨어지기 때문에 필드 검증은 Bean Validation으로, 그 외의 검증은BindingResultreject() 등을 사용하여 직접 코드로 작성하는 것을 권장드립니다.

해당 기능은 어노테이션 목록을 참조해주세요.

Bean Validation 활용

만약 상황에 따라 검증 조건이 바뀌는 경우엔 위와 같은 방법으론 한계가 있습니다

예를들어 등록할 때는 id 값이 없지만 수정할 때는 id 값이 필수로 받을 수 있고 조건에 따라(회원 등급 등) 검증 세부 로직이 다를 수 있습니다.

@Validatedgroups라는 속성을 사용하여 해당 문제를 해결할 수 있으나..... 잘 사용하지 않는 기능입니다.
스프링에서 보통 User와 같이 핵심적인 객체를 검증하게 되는데 이 때 groups를 사용하면 해당 클래스의 코드가 지저분해지기도 하고 특히 핵심 클래스를 컨트롤러에서 직접적으로 사용하면 큰 문제가 발생할 수 있습니다.
때문에 groups를 사용하기 보단 경우에 따라 DTO를 사용하거나 용도에 따라 객체를 분리하여 사용합니다.(이에 관한 내용은 후에 JPA 파트에서 기술하겠습니다.)

해당 기능은 어노테이션 목록을 참조해주세요.

컨트롤러에서 HTTP 요청을 받아 객체로 매핑할 때는 요청용 객체를 따로 만들어 해당 객체에서 검증 로직을 처리하고 이를 핵심 객체로 다시 변환하여 사용합니다.

@Getter
public class User{
	String name;
    int age; 
}

핵심 클래스에서 Bean Validation 관련 어노테이션을 제거하고 Form 객체를 만들어 주겠습니다.

@Getter 
public class UserForm { // 등록용, 수정용 등 분리하여 사용
	@NotEmpty
	String name;
    @Range(min=20, message="20살 이상만 가입할 수 있습니다.")
    int age; 
}

해당 예제는 너무 간단하기에 잘 와닿지 않을 수 있지만 실제 서비스에선 User 클래스에 주민번호, 개인정보 처리 약관 같이 등록할 때만 필요하거나 서버 내에서만 관리하는 정보들이 많이 포함되어 있습니다. 이들을 항상 사용하는 것은 자원의 낭비가 발생할 뿐더러 보안상으로도 위험할 수 있기에 별도의 클래스를 사용하는 것이 좋습니다.

Http Message Converter로 API 요청 검증
HTTP 요청 데이터가 쿼리파라미터나 Form 형식이 아닌 메시지 바디를 통해 전달되는 경우에도 @Validate를 적용할 수 있습니다.
@ModelAttribute를 사용할 때는 각 필드마다 바인딩이 따로 이루어지므로 세밀하게 검증을 할 수 있지만 @RequestBody를 사용하게 되면 Http Message Converter가 사용되어 검증 시 더 많은 것을 고려해야 합니다. 또한 Http Message Converter에서 검증 실패 시 error가 아닌 exception이 발생합니다. 이 부분은 예외를 처리하는 파트에서 더 자세히 기술하겠습니다.

profile
평범한 대학생의 공부 일기?

0개의 댓글