Controller 테스트코드를 왜 짜는거지 생각하던 도중
요청으로 알맞는 요청만 들어오는 경우에는 크게 문제가 되지않지만, 잘못된 값이 들어오는 경우도 테스트코드로 구현하려다보니, 검증로직이 필요해짐을 느끼게 되었다.
검증로직은 딱! Controller에 값이 들어올때! 받는 requestDto에서 처리해주면 된다 생각하였고,
이것에 맞춰서, 원시적으로 try-catch로 예외처리를 해도 되지만, 보다 깔끔하고 유지보수를 수월하게 하기 위해 Validation
을 사용하기러 했다.
전에도 사용해봤던 @Valid
으로 편하게 검증처리를 하려고 하는데,MethodArgumentNotValidException
뿐만 아니라 MissingRequestHeaderException
,MissingPathVariableException
, MethodArgumentConversionNotSupportedException
, HttpMessageNotReadableException
등 몇몇 상황에는 내가 알던 방식과 다른 결과가 나오다 보니, 제대로 파헤쳐보기위해, 정리해보고자 한다.
물론, 해당 내용이 진짜 맞는지 검증하는 과정도 들어가있기때문에 다소 지저분해질 수 있기에, 결론에 깔끔하게 정리해놓을 예정이다.
@PostMapping("/test/valid/post")
public String postValidCheck(@Valid @RequestBody ValidCheck validCheck) // 메서드 파라미터에 적용
{
log.info("region : {}", validCheck.getRegion());
return "OK";
}
@Getter
@Setter
@NoArgsConstructor
public static class ValidCheck {
@Valid // 필드에서 적용
private SomethingObject somethingObject;
@NotNull
private Region region;
}
@RestController
@Validated // 클래스 레벨 적용
public class HealthController {
// .. 중략
@Validated // 메서드 레벨 적용
@GetMapping("/test/valid/{password}")
public String getPathValidCheck(@PathVariable("password") String validCheck)
{
return "OK";
}
}
이 외에도 여러가지 차이점이 있지만, 일단 적용가능한 레벨부터 다른것부터 알고 출발하면 된다.
@RestControllerAdvice
에서 @ExceptionHandler()
로 처리해줘야하는 것을 알고 있어야한다.(다음에 한번 다룰예정)MethodArgumentNotValidException
이 발생한다.ConstraintViolationException
이 발생한다.BindException
이 발생할 수 도 있다.어차피 둘다 예외처리 핸들링해야됨으로, 둘 다 알고 있으면 된다.
보통 게시판 기능 같은걸 만들다보면 PathVariable로 값을 요청받을 수 있다.
@GetMapping("/test/valid/{id}")
public String getPathValidCheck(@PathVariable("id") String validCheck)
{
log.info("path : {}", validCheck);
return "OK";
}
MethodArgumentTypeMismatchException
로 예외가 발생한다. @GetMapping("/test/valid/{id}")
public String getPathValidCheck(@PathVariable("id") Integer validCheck)
/** int가 아니라 Intger 추천! **/
{
log.info("path : {}", validCheck);
return "OK";
}
MethodArgumentTypeMismatchException
가 발생한다.MissingPathVariableException
에러가 발생한다. @GetMapping("/test/valid/{id}")
public String getPathValidCheck(@PathVariable("id") Boolean validCheck)
{
log.info("path : {}", validCheck);
return "OK";
}
MethodArgumentConversionNotSupportedException
이 뜬다. public class RequestDto {
@Getter
@Setter
@NoArgsConstructor
public static class SomethingObject {
private Integer id;
}
}
@GetMapping("/test/valid/{id}")
public String getPathValidCheck(@PathVariable("id") Boolean validCheck)
{
log.info("path : {}", validCheck);
return "OK";
}
public class RequestDto {
@Getter
@Setter
@NoArgsConstructor
public static class SomethingObject {
private Integer id;
}
public SomethingObject(String id) { // 생성자
this.id = Integer.valueOf(id);
}
}
@Validated
를 사용할 곳이 나왔다.위에 같은 Type에 관련된 검증은 이미 Spring의 ArgumentResolver가 대부분 핸들링을 하고 있기때문에, 우리는 커스텀한 검증만 손봐주면 된다. @Validated // 적용
@GetMapping("/test/valid/{id}")
public String getPathValidCheck(@PathVariable("id") @Size(min = 6, max = 6) // 검증
String validCheck)
{
log.info("path : {}", validCheck);
return "OK";
}
@Size
어노테이션으로 인자의 크기를 검증할 수 있다.크기가 6에서 6 사이여야 합니다
@Size(min = 6, max = 6, message = "6자리여야합니다.")
으로 변경 가능! ConstraintViolationException
가 발생한다. 입력값 | 결과값 |
---|---|
http://localhost:8080/test/valid/123456 | 123456 |
http://localhost:8080/test/valid/123 | ConstraintViolationException.class |
MissingRequestHeaderException
이 발생한다.
@RestController
@Validated
public class HealthController {
@GetMapping("/test/valid/header")
public String getHeaderValidCheck(@RequestHeader(name = "id") @Size(min = 3, max = 4) String validCheck)
{
log.info("header : {}", validCheck);
return "OK";
}
}
Header나 PathVariable은 들어오는 인자 값이 여러 타입을 가지는 객체형태가 불가능하기 때문이지않을 까 싶다. 그렇기 때문에 둘다 객체로 바인딩을 할수 없고, 단일 타입으로만 받아야하는데 @Valid는 객체에 최적화 되어 있어 @Validated로 처리해야 된다.
다만, 약간의 차이점이 있다면 Header의 경우 인자의 값이 null인지 아닌지가 확실히 알 수 있다.
@Getter
@Setter
@NoArgsConstructor
public static class ValidCheck {
// private SomethingObject somethingObject;
private Region region; //enum
private String password;
private Integer id;
}
@GetMapping("/test/valid/param")
public String getParamValidCheck(@Valid RequestDto.ValidCheck validCheck)
{
log.info("region : {}", validCheck.getRegion());
return "OK";
}
@NotNull
,@NotBlank
등으로 검증로직을 추가해주면 잘 적용된다. @Getter
@Setter
@NoArgsConstructor
public static class ValidCheck {
// private SomethingObject somethingObject;
@NotNull
private Region region;
@NotBlank
private String password;
@NotNull
private Integer id;
}
여기서 Region은 Enum 타입의 경우, 잘못된 값을 받게되면 typeMismatch이 발생하고 자연스럽게
MethodArgumentNotValidException
가 발생하기때문에, Param 객체로 적용하게 되면 @Valid 효과를 똑똑히 보게 된다.
@RequestBody
어노테이션이 필수로 들어가 줘야한다.(안하면 Param으로 인식된다..) @PostMapping("/test/valid/post")
public String postValidCheck(@Valid @RequestBody RequestDto.ValidCheck validCheck)
{
log.info("region : {}", validCheck.getRegion());
// log.info("age : {}", validCheck.getAge());
return "OK";
}
{
"region" : "kr",
"password" : "ddd",
"parentId": 1 ,
"somethingObject" : {
"id" : 2
}
}
@Getter
@Setter
@NoArgsConstructor
public static class ValidCheck {
private SomethingObject somethingObject;
@NotNull
private Region region;
@NotBlank
private String password;
@NotNull
private Integer parentId;
}
@Getter
@Setter
@NoArgsConstructor
public static class SomethingObject {
@NotNull
private Integer id;
}
@Getter
@Setter
@NoArgsConstructor
public static class ValidCheck {
@Valid
private SomethingObject somethingObject;
@NotNull
private Region region;
@NotBlank
private String password;
@NotNull
private Integer parentId;
}
@Getter
@Setter
@NoArgsConstructor
public static class SomethingObject {
@NotNull
private Integer id;
}
{
"region" : "kl" ,// 잘못된 enum
"password" : "ddd",
"parentId": 1
// ,"somethingObject" : { // 객체값 없음
// "id" : 2
// }
}
HttpMessageNotReadableException
예외로 json형식의 데이터를 java객체로 파싱하려다가 실패했을때 나타나는 에러이다. 그렇기 때문에, 여기서는 Param 객체처럼 타입에러보다 파싱에러인 HttpMessageNotReadableException
이 먼저 발생한다. 객체의 경우
@NotNull
과@Valid
를 같이 적용하면되지만, Enum의 경우는 쉽게 해결할 수 없다.
@Getter
public enum Region {
//.. 중간 생략
@JsonCreator // 선언
public static Region findByDescription(String description) {
return Arrays.stream(Region.values())
.filter(region -> region.name().equals(description))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("해당하는 리전이 없습니다. " +description));
}
}
이 방법외에도 JsonDeserializer 구현하여 사용하거나 하는 다른 방법도 있겠지만, 나는 이방법이 가장 직관적이다 생각하여, 이방법으로 해결하였다.
그동안 아무생각없이 편하게 사용해왔던 예외처리방식 중 극히 일부분인 검증 예외처리이지만, 이번에 정리하면서 전 보다는 확실히 뚜렷해진 것 같다. 언제 어떻게 사용하는지랑, Enum처리등 고민해보면서 내가 한단계 성장했음을 보여주는 포스팅같아 좀 뿌듯하다.
검증처리관련 내용 정리(추천)
Request데이터 종류 | 검증Annotation | 추천Type | Exception |
---|---|---|---|
PathVariable | @Validated | String,Integer | MethodArgumentTypeMismatchException MissingPathVariableException ConstraintViolationException |
Header | @Validated | String,Integer,Enum,Boolean | MethodArgumentTypeMismatchException MissingRequestHeaderException ConstraintViolationException |
Param | @Valid | Object | MethodArgumentNotValidException |
Body | @Valid | Object(필수) | MethodArgumentNotValidException HttpMessageNotReadableException |
가만보면 예외처리는 끝도 없는 것 같다.