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());
}
}
테스트 성공