티스토리 뷰

반응형

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있습니다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수 있습니다. 이번에는 객체의 검증을 손쉽게 하는 방법에 대해 알아보고자 합니다.

 

 

 

1. @Valid와 @Validated


[ @Valid란? ]

@Valid는 JSR-303 표준 스펙으로써 제약 조건이 부여된 객체에 대해 빈 검증기(Bean Validator)를 이용해서 검증하도록 지시하는 어노테이션이다. Spring에서는 LocalValidatorFactoryBean을 이용해 JSR 표준의 검증 기능을 사용할 수 있는데, LocalValidatorFactoryBean은 JSR-303의 검증 기능을 이용할 수 있도록 해주는 일종의 어댑터에 해당한다.

JSR 표준의 빈 검증 기술의 특징은 객체의 필드에 달린 제약조건 어노테이션을 참고해 검증을 편리하게 할 수 있다는 것이다.

예를 들어 @NotNull 어노테이션은 필드의 값이 null이 아님을 확인하도록 하며 @Min은 해당 값의 최솟값을 지정할 수 있도록 한다.

이 빈 검증을 위한 기능을 이용하려면 LocalValidatorFactoryBean을 빈으로 등록하고 VaidationService를 제공해주어야 하는데, SpringBoot에서는 의존성만 추가하면 검증을 위한 빈들이 자동으로 등록된다.

그리고 만약 검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생하게 되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생할 것이다.

 

 

[ 다양한 제약조건 어노테이션 ]

JSR 표준 스펙은 다양한 제약 조건 어노테이션을 제공하고 있는데, 대표적인 어노테이션으로는 다음과 같은 것들이 있다.

  • @NotNull: 해당 값이 null이 아닌지 검증함
  • @NotEmpty: 해당 값이 null이 아니고, 빈 스트링("") 아닌지 검증함(" "은 허용됨)
  • @NotBlank: 해당 값이 null이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증함
  • @AssertTrue: 해당 값이 true인지 검증함
  • @Size: 해당 값이 주어진 값 사이에 해당하는지 검증함(String, Collection, Map, Array에도 적용 가능)
  • @Min: 해당 값이 주어진 값보다 작지 않은지 검증함
  • @Max: 해당 값이 주어진 값보다 크지 않은지 검증함

그 외에도 주어진 값이 이메일 형식에 해당하는지 검증하는 @Email 등 다양한 어노테이션을 제공하고 있는데, 필요한 어노테이션이 있는지는 자바 공식 문서(Java 8 기준 링크입니다)를 참고하면 된다.

그 외에도 hibernate의 Validator는 해당 값이 URL인지를 검증하는 @URL 등과 같은 어노테이션을 제공하고 있다. 즉, 우리가 필요로 하는 대부분의 제약 사항 어노테이션은 이미 구현되어 있으므로 잘 찾아서 이를 활용하면 된다.

 

 

[ @Validated를 이용한 검증 그룹의 지정 ]

객체를 검증하기 위한 방법이 경우에 따라 달라질 수 있다. 예를 들어 일반 사용자의 요청과 관리자의 요청을 보내는 경우에 같은 객체로 요청이 오지만 다른 방식으로 검증해야 할 수 있는 것이다. 이런 경우에는 검증에 사용할 제약 조건이 2가지로 나누어져야 한다.

Spring에서는 이런 경우를 위해 제약 조건 어노테이션에 조건이 적용될 검증 그룹을 지정하여 적용할 수 있도록 @Validated를 제공하고 있다. (이는 JSR 표준 기술이 아니다.)

검증 그룹을 지정하기 위해서는 (내용이 없는) 마커 인터페이스를 간단히 정의해야 한다. 위의 예시의 경우에는 사용자인 경우와 관리자인 경우를 분리해야 하므로 다음과 같은 2개의 마커 인터페이스를 만들 수 있다.

public interface UserValidationGroup {} 
public interface AdminValidationGroup {}

 

그리고 해당 제약 조건이 적용될 그룹을 groups로 지정해줄 수 있다. 제약 조건이 적용될 그룹이 여러 개라면 {}를 이용해 그룹의 이름을 모두 넣어주면 된다. 예를 들어 다음과 같이 DTO에 그룹 속성을 지정해줄 수 있다.

@NotEmpty(groups = {UserValidationGroup.class, AdminValidationGroup.class} ) 
private String name; 

@NotEmpty(groups = UserValidationGroup.class) 
private String userId; 

@NotEmpty(groups = AdminValidationGroup.class) 
private String adminId;

 

그리고 컨트롤러에서도 다음과 같이 제약조건 검증을 적용할 클래스를 지정해주면 된다.

@PostMapping("/user/add") 
public ResponseEntity<Void> addUser(@RequestBody @Validated(UserValidationGroup.class) UserRequestDto userRequestDto) {
    
      ...
}

 

만약 위와 같이 UserValidationGroup를 @Validated의 파라미터로 넣어주었다면 UserValidationGroup에 해당하는 제약 조건만 검증이 된다. 만약 @Validated에 특정 마커를 지정해주지 않았거나, groups가 지정되어 있는데 @Valid를 이용하면 다음과 같이 처리된다.

  • @Validated에 특정 클래스를 지정하지 않는 경우: groups가 없는 속성 들만 처리 
  • @Valid or @Validated에 특정 클래스를 지정한 경우: 지정된 클래스를 groups로 가진 제약사항만 처리

 

 

 

 

2. @Valid를 이용한 검증 예시


[ @Valid를 이용한 검증 예시 ]

 

1. 의존성 추가

제약조건 검증을 사용하기 위해서는 다음의 의존성을 build.gradle에 추가해주어야 한다.

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

 

 

2. 검증할 객체 및 제약사항 추가

예를 들어 새로운 사용자를 추가하는 API를 개발중이라고 하자. 그리고 이에 대한 입력값의 요구사항 및 제약사항은 다음과 같다.

  • 이메일: 이메일 형식으로 입력을 받아야 한다.
  • 비밀번호: 빈 값을 받으면 안된다.
  • 역할: 사용자의 역할은 null이면 안된다.
  • 나이: 사용자의 나이는 12세 이상이여야 한다.

우리는 위와 같은 요구사항과 제약사항을 보고 다음과 같은 UserRequestDto를 생성할 수 있다.

@Getter
@RequiredArgsConstructor
public class UserRequestDto {

	@Email
	private final String email;

	@NotBlank
	private final String pw;

	@NotNull
	private final UserRole userRole;

	@Min(12)
	private final int age;

}

 

그리고 이러한 DTO를 요청으로 받는 UserController를 다음과 같이 작성해줄 수 있다.

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/user/add")
    public ResponseEntity<Void> addUser(final @RequestBody @Valid UserRequestDto userRequestDto) {
        userService.registerUser(userRequestDto);

        return ResponseEntity.noContent().build();
    }

}

 

 

3. 테스트 코드 작성 및 실행

해당 제약 조건들이 정상적으로 검증되는지 확인하기 위해서 테스트 코드를 작성해야 한다.

UserController에서 email 값이 email 형식이 아닌 경우에 대한 테스트 코드는 다음과 같이 작성할 수 있는데, @Valid에 의한 반환값은 400 BadRequest이므로 이를 결과로 예측하면 된다.

@ExtendWith(MockitoExtension.class)
class UserControllerTest {

	@InjectMocks
	private UserController target;

	@Mock
	private UserService userService;

	private MockMvc mockMvc;

	private Gson gson;

	@BeforeEach
	public void init() {
		mockMvc = MockMvcBuilders.standaloneSetup(target).build();
		gson = new Gson();
	}

	@DisplayName("사용자 추가 실패 - 이메일 형식이 아님")
	@Test
	void addUserFail_NotEmailFormat() throws Exception {
		// given
		final UserRequestDto userRequestDto = UserRequestDto.builder()
				.email("mangkyu")
				.pw("password")
				.userRole(UserRole.USER)
				.age(28).build();

		// when
		final ResultActions resultActions = mockMvc.perform(
			MockMvcRequestBuilders.post("/user/add")
				.content(gson.toJson(userRequestDto))
				.contentType(MediaType.APPLICATION_JSON)
		).andExpect(status().isBadRequest());

		// then
	}

}

 

 

3. 테스트 코드 실행 및 결과 확인

테스트 코드를 실행해보면 다음과 같은 로그를 확인할 수 있다.

WARN org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.Void> com.example.simpletest.app.test.controller.UserController.addUser(com.example.simpletest.app.test.dto.UserRequestDto): [Field error in object 'userRequestDto' on field 'email': rejected value [mangkyu]; codes [Email.userRequestDto.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userRequestDto.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@6730b6c1,.*]; default message [must be a well-formed email address]] ]
DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Completed 400 BAD_REQUEST

 

내용을 살펴보면 email이 올바른 이메일 포맷이 아니고, 그 결과로 400 BAD_REQUEST를 반환하였음을 확인할 수 있다.

 

 

 

Message Interpolator를 이용하면 유효성 검증을 진행한 이후에 던져줄 메세지를 다국어로 처리할 수도 있다. 이에 대해서는 나중에 기회가 되면 알아보도록 하자.

반응형
댓글
댓글쓰기 폼
반응형
공지사항
Total
1,479,682
Today
3,003
Yesterday
4,488
TAG more
«   2021/09   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
글 보관함