✏️ 배경
프로젝트를 진행하면서 ObjectMapper를 활용해서 Json 형식의 문자열을 직접 파싱해야 하는 경우가 있었다.
그때 javax.validator 패키지의 Validation 애노테이션을 활용해서 자동으로 검증 기능을 활용하고 싶었는데,
자주 사용하던 @Validated와 @Valid 등은 범위가 벗어나 적용이 되지 않아서 다른 대안을 찾게 되었다.
해당 포스팅은 관련 상황에 있을 때 Validator를 활용하는 방법에 대해서 공유하고자 한다. 🙄
🔎 문제
@Test
public void blackShibaTest() throws JsonProcessingException {
String example = "{\"id\":1,\"name\":\"흑시바\",\"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
BlackShibaDto blackShibaDto = objectMapper.readValue(example, BlackShibaDto.class);
System.out.println(blackShibaDto);
}
@Getter
@ToString
public static class BlackShibaDto {
@NotNull(message = "id는 null일 수 없습니다.")
private Long id;
@NotEmpty(message = "이름은 빌 수 없습니다.")
private String name;
@Min(value = 1, message = "최소 1 이상이어야 합니다.")
@Max(value = 20, message = "최대 20 이하이어야 합니다.")
@NotNull(message = "나이는 null일 수 없습니다.")
private Long age;
}
비슷한 상황의 간단한 예시를 테스트로 만들어보았다.
애플리케이션에서 Json 형식의 문자열을 받을 것이고, 그걸 ObjectMapper를 활용해서 특정 Dto로 변환한다.
하지만 알다시피 단순하게 애노테이션을 작성한다고 검증이 동작하지는 않는다.
그렇기 때문에 예시처럼 나이를 30으로 작동시켜도 테스트는 정상적으로 통과하게 된다.
이런 경우 수동으로 Validation을 검증해주어야 한다.
🔎 해결
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<BlackShibaDto>> validate = validator.validate(blackShibaDto);
if(!validate.isEmpty()) {
throw new ConstraintViolationException(validate);
}
javax.validation 패키지에는 이처럼 수동 검증을 할 수 있도록 Validation을 제공하고 있다.
이를 활용해서 ObjectMapper를 통해 파싱 된 결괏값을 검증할 수 있다.
validate 메서드는 대상 객체의 모든 제약조건을 검증해서 제약 조건에 해당하는 관련 내용을 ConstraintViolation 객체를 Set으로 담게 된다.
어떤 구현체를 구체적으로 확인해 보기 위해 디버깅을 해보면 Set 구현체로는 HashSet을 사용하고 있으며, ConstraintViolation 구현체로는 ConstraintViolationImpl을 사용하고 있다.
ConstraintViolationImpl에서 알아야 할 핵심 필드는 4가지가 있다. 🤨
1. propertyPath = 검증 실패가 발생한 위치 정보
2. rootBeanClass = 실패 대상 클래스
3. messageTemplate = 오류 메시지 Template Key
4. interpolatedMessage = messageTemplate 에서 Expression Language를 활용해 치환한 메시지
interpolatedMessage 예시는 다음과 같다.
@Min(value = 1, message = "최소 {value} 이상이어야 합니다.")
@Max(value = 20, message = "최대 {value} 이하이어야 합니다.")
@NotNull(message = "나이는 null일 수 없습니다.")
private Long age;
예를 들어, 메시지에 "{value}"를 입력하는 경우 해당 값이 입력돼서 들어간다.
그러므로 여러 곳에서 동일한 메시지로 사용할 계획이 있다면 다음 예시와 같이 Expression Message를 활용해서 문자열 상수를 생성한 뒤 여러 곳에서 활용하는 것도 고려해 볼 만하다.
public class ValidationMessage {
public final static String MAX_VALUE = "최대 {value} 이하이어야 합니다.";
}
....
@Min(value = 1, message = "최소 {value} 이상이어야 합니다.")
@Max(value = 20, message = MAX_VALUE)
@NotNull(message = "나이는 null일 수 없습니다.")
private Long age;
📌 마무리
@Test
public void blackShibaTest() throws JsonProcessingException {
String example = "{\"id\":null,\"name\":\"흑시바\",\"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
BlackShibaDto blackShibaDto = objectMapper.readValue(example, BlackShibaDto.class);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<BlackShibaDto>> validate = validator.validate(blackShibaDto);
if(!validate.isEmpty()) {
throw new ConstraintViolationException(validate);
}
System.out.println(blackShibaDto);
}
@Getter
@ToString
public static class BlackShibaDto {
@NotNull(message = "id는 null일 수 없습니다.")
private Long id;
@NotEmpty(message = "이름은 빌 수 없습니다.")
private String name;
@Min(value = 1, message = "최소 1 이상이어야 합니다.")
@Max(value = 20, message = "최대 20 이하이어야 합니다.")
@NotNull(message = "나이는 null일 수 없습니다.")
private Long age;
}
수동 검증을 적용한 뒤 위의 예시처럼 id를 null로 하고 나이를 30으로 설정해서 일부로 검증에 걸리도록 유도한 뒤 테스트를 수행하면 데이터 중 제약 조건에 해당하는 모든 예외 메시지가 출력된다.
이처럼 ObjectMapper를 사용할 때 검증이 필요하면 Validation을 활용해 보자.
REFERENCE
https://www.baeldung.com/javax-validation
https://www.baeldung.com/spring-validation-message-interpolation
'Spring' 카테고리의 다른 글
Spring Boot 3 + JPA를 활용한 Liquibase 실습하기 (1) (0) | 2023.05.14 |
---|---|
Spring DB Migration Tool Flyway, Liquibase 특징, 유사점, 차이점 (0) | 2023.05.13 |
[Spring] Redis 세션 로그아웃, 만료시 관련 처리 작업하기 (0) | 2023.04.16 |
Spring Redis Session 설정, 특징, 데이터 조회 (0) | 2023.04.08 |
[Spring] 데이터와 연관된 파일 데이터(multipart)를 목록으로 받기 (0) | 2023.04.02 |
댓글