스프링

[Spring] Validation 종류 및 적용

Ash_jisu 2024. 3. 15. 11:52

Validation

https://spring.io/guides/gs/validating-form-input

 

Getting Started | Validating Form Input

The application involves validating a user’s name and age, so you first need to create a class that backs the form used to create a person. The following listing (from src/main/java/com/example/validatingforminput/PersonForm.java) shows how to do so: pac

spring.io

  • 특정 메서드나 파타미터에 사용되어 해당 객체가 유효성 검샅를 통과해야함을 나타낸다
  • 주로 Spring mvc에서 사용되며 HTTP 요청의 데이터를 바인딩한 후 유효성 검사를 수행해야 한다
  • 이를 통해 데이터의 일관성과 유효성을 보장하고 안정적인 애플리케이션을 구축 할 수 있다

 


검증 흐름

  1. 클라이언트는 데이터를 담아서 @RequestBody, @RequestParam, @PathVariable Annotation을 이용하여 API로 호출한다
  2. API에서는 @Valid 또는 @Vadlidated 어노테이션으로 데이터 유효성을 검증한다
  3. 이후 유효성 검증을 통과한 경우는 클라이언트로 ‘성공 응답’데이터를 전송하고 실패한 경우에는 MethodArgumentNotValidException에러가 발생하는데 이것을 @ControllerAdvice, @ExceptionHandler로 구성한 ‘GlobalException’에서 에러를 캐치한다
  4. 이후 에러는 클라이언트에게 ‘에러 응답’ 데이터로 전송된다

유효성 검사 어노테이션 종류

- 참고: https://dev-coco.tistory.com/123

 


적용

코드작성

  • 의존성추가
    • spring boot 2.3 version 이전에는 spring-boot-starter-web 의존성 내부에 validation이 있었지만, spring boot 2.3 version 이상부터는 모듈로 빠져 별도의 의존성을 추가해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • QuestionsOption
    • tags: List<String>여서 별도의 Valid를 생성해주었다 - customValidation문서 참고
    • count: @Positive 어노테이션을 통해 1이상의 양수만 받아준다
    • includes: 2글자 이상 10글자 이하만 가능, 특수문자는 불가능하고 한영, 숫자만 가능
public class QuestionsOption {
    @ValidTags
    private List<String> tags;

    @Builder.Default
    @Positive
    private Integer count = 10;

    @Pattern(regexp = "^[a-zA-z가-힣0-9]+$", message = "검색은 허용되는 문자 내에서 입력해주세요.")
    @Size(min=2, max = 10, message = "검색은 최소 2자 이상 10자 이하로 입력해주세요.")
    private String includes;
}
  • QuestionController
    • @Valid를 통해 적용시킨다
@RequestMapping("api/v1/exams/{examId}/questions")
public class QuestionsController {
    private final QuestionsService questionsService;
    @GetMapping
    public QuestionsList getExamQuestions(@PathVariable @ValidExamId Long examId, @ModelAttribute @Valid QuestionsOption questionsOption) {
			
		...이하 생략
}

 

 

  • QuestionControllerTest
    • Valid가 제대로 적용되었는지 테스트 해준다

Tags 테스트

      mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("tags", "상황")
                        .queryParam("tags", "없는태그"))
                .andExpect(status().isBadRequest());

Count 테스트

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("count", String.valueOf(-3)))
                .andExpect(status().isBadRequest());

Includes 테스트

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("count", String.valueOf(-3)))
                .andExpect(status().isBadRequest());

 

전체 테스트 코드 

@WebMvcTest(QuestionsController.class)
@Tag("basic_test")
class QuestionsControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private QuestionsService questionsService;

    @MockBean
    private ExamRepository examRepository;

    @MockBean
    private DriverLicenseQuestionsRepository driverLicenseQuestionsRepository;

    private final Long existExamId = 1L;

    private final List<String> existTags = List.of("상황", "법");

    @BeforeEach
    public void beforeTest() {
        when(examRepository.existsById(existExamId)).thenReturn(true);
        when(questionsService.findByDriverLicenseQuestions(any(), any())).thenReturn(new QuestionsList());
        when(driverLicenseQuestionsRepository.existsByTags(existTags.get(0))).thenReturn(true);
        when(driverLicenseQuestionsRepository.existsByTags(existTags.get(1))).thenReturn(true);
    }

    @Test
    void validQuestionControllerTest() throws Exception {
        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("tags", "상황")
                        .queryParam("tags", "법")
                        .queryParam("count", String.valueOf(3))
                        .queryParam("includes", "고속도로"))
                .andExpect(status().isOk());
    }

    // 없는 태그로 요청을 보내면 400 Bad Request를 반환하는 테스트
    @Test
    void invalidTagsTest() throws Exception {
        // Then
        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("tags", "상황")
                        .queryParam("tags", "없는태그"))
                .andExpect(status().isBadRequest());

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("tags", "상황")
                        .queryParam("tags", "법"))
                .andExpect(status().isOk());
    }

    // 양수가 아닌 count로 요청을 보내면 400 Bad Request를 반환하는 테스트
    @Test
    void invalidCountTest() throws Exception {
        // Then
        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("count", String.valueOf(-3)))
                .andExpect(status().isBadRequest());

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("count", String.valueOf(3)))
                .andExpect(status().isOk());
    }

    // 글자수 및 빈칸 적용된 유효하지않은 inlucds 요청시 400 Bad Request를 반환하는 테스트
    @Test
    void invalidIncludesTest() throws Exception {
        // Then
        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("includes", "두 단어"))
                .andExpect(status().isBadRequest());

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("includes", "한"))
                .andExpect(status().isBadRequest());

        mockMvc.perform(get("/api/v1/exams/{examId}/questions", existExamId)
                        .queryParam("includes", "한단어한글자이상"))
                .andExpect(status().isOk());
    }
}

 

 


출처

- https://dev-coco.tistory.com/123

- https://adjh54.tistory.com/77#5.%20Validation%20적용-1