카테고리 없음

Custom Validation 만들기

코딩루이지 2024. 3. 8. 17:12

Spring Web MVC 2강의를 들으면, HandlerMethodArgumentResolver 를 통해서 실제 로직을 실행해서 매개변수 값을 할당하는 방법을 Member 매개변수를 통해서 배운다.

 

또, 단순 어노테이션으로 검증하지 못한 복잡한 검증 로직을 수행해서 검증하고 BindingResult 를 통해서 오류를 발생하는 방법을 배운다.

 

하지만, 복잡한 검증 로직이 반복되면 매번 BindingResult 를 사용할 수 없기 때문에 Custom Validation Annotation을 만드는법을 알아보자.

 

상황

PathVariable로 들어오는 examId 값은 데이터베이스 조회를 통해서 유효한 id인지 검증해야한다.

 

단순 어노테이션이 아닌 데이터베이스 까지 조회해야하므로 검증 로직이 필요하다.

@RequiredArgsConstructor
@RestController
@RequestMapping("api/v1/exams")
public class ExamsController {

    private final ExamsService examsService;

    @GetMapping
    public ExamList getExams() {
        return examsService.getExamList();
    }

    @GetMapping("/{examId}/type")
    public ExamType getExamType(@PathVariable Long examId) {
        return examsService.getExamType(examId);
    }
}

@RequiredArgsConstructor
@RestController
@RequestMapping("api/v1/exams/{examId}/questions")
public class QuestionsController {

    private final QuestionsService questionsService;

    @GetMapping
    public QuestionsList getExamQuestions(@PathVariable Long examId, @ModelAttribute QuestionsOption questionsOption) {
        return questionsService.findByDriverLicenseQuestions(examId, questionsOption);
    }
}

 

해결

1. 사용자 정의 어노테이션 생성

 

먼저, examId가 데이터베이스에 존재하는지 검증하는 어노테이션을 생성

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ExamIdValidator.class)
public @interface ValidExamId {
    String message() default "Invalid Exam ID";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

 

2. Validator 구현

다음으로, ValidExamId 어노테이션을 처리할 ExamIdValidator 클래스를 구현한다.

이 클래스는 ConstraintValidator 인터페이스를 구현해야 하며, 데이터베이스에서 examId의 존재 여부를 검증하는 로직을 포함해야 한다.

@Component
@RequiredArgsConstructor
public class ExamIdValidator implements ConstraintValidator<ValidExamId, Long> {

    private final ExamRepository examRepository;

    @Override
    public void initialize(ValidExamId constraintAnnotation) {
    }

    @Override
    public boolean isValid(Long examId, ConstraintValidatorContext context) {
        if (examId == null) {
            return false;
        }
        boolean exists = examRepository.existsById(examId);
        return examRepository.existsById(examId);
    }
}

 

3. 컨트롤러에서 어노테이션 사용

마지막으로, 컨트롤러에서 @ValidExamId 어노테이션을 @PathVariable과 함께 사용하여 examId의 유효성을 검증합니다.

@RequiredArgsConstructor
@RestController
@RequestMapping("api/v1/exams")
public class ExamsController {

    private final ExamsService examsService;

    @GetMapping
    public ExamList getExams() {
        return examsService.getExamList();
    }

    @GetMapping("/{examId}/type")
    public ExamType getExamType(@PathVariable @ValidExamId Long examId) {
        return examsService.getExamType(examId);
    }
}

@RequiredArgsConstructor
@RestController
@RequestMapping("api/v1/exams/{examId}/questions")
public class QuestionsController {

    private final QuestionsService questionsService;

    @GetMapping
    public QuestionsList getExamQuestions(@PathVariable @ValidExamId Long examId, @ModelAttribute QuestionsOption questionsOption) {
        return questionsService.findByDriverLicenseQuestions(examId, questionsOption);
    }
}

이 방법을 통해, 컨트롤러에서 별도의 검증 로직을 작성하지 않고도 examId가 유효한지 자동으로 검증할 수 있게 된다.

자동화된 검증 로직을 통해 코드의 가독성을 높이고, 유지보수를 용이하게 만들 수 있다.

 

테스트하기

없는 문제 ID로 요청을 보내면 400 Bad Request를 반환하는지 테스트를 해보자.

@WebMvcTest(ExamsController.class)
class ExamsControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ExamsService examsService;

    @MockBean
    private ExamRepository examRepository;

    // 없는 문제 ID로 요청을 보내면 400 Bad Request를 반환하는지 테스트
    @Test
    void validExamIdTest() throws Exception {
        //when
        Long examId = 0L;
        when(examRepository.existsById(examId)).thenReturn(false);

        mockMvc.perform(get("/api/v1/exams/{examId}/type", examId))
                .andExpect(status().isBadRequest());
    }
}

테스트 성공