사용자가 잘못 입력한 데이터를 서버에서 처리하기 전에 입력 데이터를 검사하는 행위를 의미한다. 보통 데이터 유효성 검사 후 데이터가 유효하지 않다면, 사용자에게 에러 메시지를 보여주게 된다. 프론트엔드 부분에서도 데이터 유효성 검사를 할 수 있지만, 개발자 도구를 이용해 데이터 유효성 검사를 우회할 수 있으므로 프론엔드와 벡엔드 두 부분 모두에서 데이터 유효성 검사 로직을 구현해 놓는 것이 좋다.
이렇게 구현하게 되면, 평소에는 프론트엔드 부분에서 대부분의 데이터 유효성 검사를 진행하여 서버의 부담을 감소시키며, 위와 같이 악의적인 우회를 하려는 사용자들에게는 벡엔드 부분에 구현되어 있는 데이터 유효성 검사 로직을 통해 우회를 방지할 수 있게 된다.
스프링에서 제공하는 API이다. 해당 API 사용 시, 애너테이션을 사용하여 데이터 유효성 검사를 구현할 수 있다.
아래는 자주 사용하는 Java Bean Validation의 몇가지 예시이다.
// 문자열을 다룰 때 사용
@NotNull // null값 허용 x
@NotEmpty // null, 빈 문자열(공백) 또는 공백만으로 채워진 문자열 허용하지 않음
@Not Blank // null, 빈 문자열(공백) 허용 x
@Size(min=?, max?) // 최소 길이, 최대 길이 제한
@Null // null만 가능
// 숫자를 다룰 때 사용
@Positive // 양수만 허용
@PositiveOrZero // 양수와 0만 허용
@Negative // 음수만 허용
@NegativeOrZero // 음수와 0만 허용
@Min(?) // 최솟값 제한
@Max(?) // 최댓값 제한
// 정규식 관련
@Email // 이메일 형식만 허용
@Pattern(regexp="?") // 직접 작성한 정규식에 맞는 문자열만 허용
데이터 유효성 검사는 어떠한 계층에서 구현해도 상관은 없다. 프레젠테이션 계층에서 구현할 경우 컨트롤러에 요청이 오는 순간 검증할 수 있고, 퍼시스턴스 계층에서 엔티티에 적용할 수도 있다.
보통은 프레젠테이션 계층에서 구현하여 불필요한 서비스 로직의 실행을 방지하고, 사용자 요청마다 세부 조건을 적용한다.
// build.gradle
dependencies {
~ 기존 코드 생략 ~
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation 'com.github.javafaker:javafaker:1.0.2'
}
Faker는 테스트를 할 때, 임의의 가짜 데이터를 생성할 수 있는 오픈소스 라이브러리이다.
validation은 @NotNull, @Size와 같은 유효성 검사를 위한 라이브러리이다.
ex
Faker faker =new Faker(new Local("ko")) // Locale을 "ko"로 설정하여 한국어 데이터를 생성한다.
String name = faker.address().fullAddress(); // "서울특별시 강남구 테헤란로 123"와 같은 전체 주소를 문자열 형식으로 생성한다.
String firstName = faker.name.name(); // 전체 이름(이름과 성 포함)을 문자열로 반환한다. "홍길동"과 같은 이름이 생성된다.
String lastName = faker.food.fruit(); // 과일 이름을 문자열로 반환한다. "사과", "바나나"와 같은 과일 이름이 생성된다.
BlogApiControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
class BlogApiControllerTest {
~ 기존 코드 생략 ~
@DisplayName("addArticle: 아티클 추가할 때 title이 null이면 실패한다.") // 유효성 검사 1 - title과 content 값이 null일때
@Test
public void addArticleNullValidation() throws Exception {
// given
final String url = "/api/articles";
final String title = null;
final String content = null;
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
final String requestBody = objectMapper.writeValueAsString(userRequest);
Principal principal = Mockito.mock(Principal.class);
Mockito.when(principal.getName()).thenReturn("username");
// when
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.principal(principal)
.content(requestBody));
// then
result.andExpect(status().isBadRequest());
}
@DisplayName("addArticle: 아티클 추가할 때 title이 10자를 넘으면 실패한다.") // 우효성 검사 2 - title이 10자를 넘을 때
@Test
public void addArticleSizeValidation() throws Exception {
// given
Faker faker = new Faker(); // 무작위로 11자의 문자열을 쉽게 생성하기 위해 Faker 라이브러리 사용.
final String url = "/api/articles";
final String title = faker.lorem().characters(11);
final String content = "content";
final AddArticleRequest userRequest = new AddArticleRequest(title, content);
final String requestBody = objectMapper.writeValueAsString(userRequest);
Principal principal = Mockito.mock(Principal.class);
Mockito.when(principal.getName()).thenReturn("username");
// when
ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.principal(principal)
.content(requestBody));
// then
result.andExpect(status().isBadRequest());
}
~ 기존 코드 생략 ~
}
BlogApiControllerTest.java
public class AddArticleRequest {
@NotNull
@Size(min = 1, max = 10)
private String title;
@NotNull
private String content;
~ 기존 코드 생략 ~
}
BlogApiController.java
public class BlogApiController {
private final BlogService blogService;
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody @Validated AddArticleRequest request, Principal principal) {
Article savedArticle = blogService.save(request, principal.getName());
return ResponseEntity.status(HttpStatus.CREATED)
.body(savedArticle);
}
~ 기존 코드 생략 ~
}
@Validated로 인해 AddArticleRequest 객체의 유효성을 검사하게 된다. AddArticleRequest 클래스에 구현해놓은 @NutNull과 @Size(min = 1, max = 10)으로 인해 유효성이 검사된다.
따라서 request로 넘어온 AddArticleRequest가 1차적으로 @Validated로 인해 데이터 유효성이 검사된다.
로컬에서의 테스트는 정상 동작하는 것을 확인하였다.
깃허브 액션의 CI/CD 파이프라인도 변경사항들이 잘 반영되어 정상 동작함을 확인하였다.
아무 값도 넣지 않은 채 등록을 누르니 등록에 실패하였다는 경고창이 발생하여, 데이터 유효성 검사가 정상적으로 동작하고 있음을 확인하였다.
해당 글은 다음 도서의 내용을 정리하고 참고한 글임을 밝힙니다.
신선영, ⌜스프링 부트 3 벡엔드 개발자 되기 - 자바 편⌟, 골든래빗(주), 2023, 384쪽