spring에서 api 요청시 들어오는 값들을 체크해주는 라이브러리이다. validation을 반복적으로 작성하지 않아도 되도록 도와준다!
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.5'
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostBoardRequest {
@NotEmpty(message = "제목을 입력해주세요.")
@Size(max = 30, message = "제목은 30글자를 넘을 수 없습니다.")
private String title;
@NotEmpty(message = "내용을 입력해주세요.")
@Size(max = 3000, message = "내용이 너무 깁니다!")
private String content;
}
dto의 내용을 다음과 같이 @NotEmpty
와 @Size
를 통해 에러 메세지도 미리 지정해줄 수 있다.
@RestController
@RequestMapping("/v1/board")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@PostMapping("")
public ResponseEntity<CommonApi<PostBoardResponse>> postBoard(@Valid @RequestBody PostBoardRequest postBoardRequest, BindingResult bindingResult){
System.out.println("-------");
// 원래대로라면 service 매개변수로 넘겨서 매번 체크해주어야 함.
System.out.println(bindingResult.hasErrors());
bindingResult.getFieldErrors().forEach(fieldError -> System.out.println(fieldError.getDefaultMessage()));
System.out.println("-------");
return ResponseEntity.status(HttpStatus.CREATED)
.body(
CommonApi.<PostBoardResponse>builder()
.resultCode(ResultCode.SUCCESS)
.resultType(ResultType.NONE)
.data(boardService.postBoard(postBoardRequest))
.build()
);
}
}
테스트를 위해 다음과 같이 작성해주자.
여기서 Controller에서 유효성 체크를 하고자 하는 Dto에 @Valid
어노테이션을 꼭 추가해주어야 한다.
@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
public class BoardControllerTest extends ControllerTest {
@Autowired
private BoardRepository boardRepository;
private final String PREFIX = "/v1/board";
@Test
@DisplayName("title 존재하지 않은 게시물 등록은 실패한다.")
void postBoardFail1() throws Exception {
//given
PostBoardRequest postBoardRequest = new PostBoardRequest(null, "내용만 입력");
//when
ResultActions perform = mockMvc.perform(post(PREFIX)
.contentType(MediaType.APPLICATION_JSON)
.content(convertToString(postBoardRequest)))
.andDo(print());
//then
}
}
그리고 다음과 같이 테스트 코드를 통해 실행해보자!
우리가 위에 적어둔 print 내용이 찍혀 나오는 것을 확인할 수 있었다.
물론 이를 통해서 Controller 다음에 실행될 각 Service에서 bindingResult를 체크해줄 순 있겠지만... 그거 너무 귀찮다. 그냥 AOP로 풀어보자!
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-aop:2.7.0'
}
@Slf4j
@Aspect
@Component
public class ValidAdvice {
@Around(value = "execution(* com.molu.molu.controller..*.*(..))")
public Object validAdviceHandler(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object arg : args) {
BindingResult bindingResult = getBindingResult(arg);
if(bindingResult == null) continue;
if(bindingResult.hasErrors()){
FieldError error = bindingResult.getFieldErrors().get(0);
String errorMessage = error.getDefaultMessage();
throw new IllegalArgumentException(errorMessage);
}
}
return pjp.proceed();
}
private BindingResult getBindingResult(Object arg) {
BindingResult bindingResult = null;
if(arg instanceof BindingResult){
bindingResult = (BindingResult) arg;
}
return bindingResult;
}
}
aop가 적용되도록 작성해주었고 RestControllerAdvcie를 통해 IllegalArgumentException을 미리 정의해놔서 그냥 바로 Exception 터트렸다.
그리고 만약 테스트 코드에도 aop가 적용되게 해주려면
dependencies {
...
testImplementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-aop:2.7.0'
}
의존성을 추가로 작성해주어야 테스트 코드에서도 aop가 정상적으로 작동한다.
@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
public class BoardControllerTest extends ControllerTest {
@Autowired
private BoardRepository boardRepository;
private final String PREFIX = "/v1/board";
@Test
@DisplayName("title 존재하지 않은 게시물 등록은 실패한다.")
void postBoardFail1() throws Exception {
//given
PostBoardRequest postBoardRequest = new PostBoardRequest(null, "내용만 입력");
//when
ResultActions perform = mockMvc.perform(post(PREFIX)
.contentType(MediaType.APPLICATION_JSON)
.content(convertToString(postBoardRequest)))
.andDo(print());
//then
}
}
그리고 위에서 작성했던 내용으로 다시 시도하면
다음과 같이 잘 나오고
실제로 포스트맨을 사용해서 실행해도
잘 찍혀 나오는 것을 확인할 수 있다!