@Named("partialUpdate") @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) voidpartialUpdate(@MappingTarget E entity, D dto); // null value 는 dto -> entity 업데이트시에 적용하지 않는다. // @MappingTarget 으로 데이터 매핑 방향 설정 }
Entity와 Dto 마다 인터페이스를 재정의해야 한다는 단점이 있지만 필드명이 똑같을경우 재정의 필요 없이 EntityMapper 상속만으로 대부분의 함수는 자동 생성되어 개발자의 실수를 줄여준다.
Car 클래스와 같이 Mapping 할 필드명이 많이 다를경우 아래처럼 모든 함수를 재정의해주어야 한다.
publicclassMessage { // groups 가 javax.validation.groups.Default 로 지정되어 있을때만 검증 진행 @Length(max = 128) @NotEmpty private String title; @Length(max = 1024) @NotEmpty private String body; // groups 가 Ad 로 지정되어 있을때만 검증 진행 @Length(max = 32, groups = Ad.class) @NotEmpty(groups = Ad.class) private String contact; // groups 가 Default, Ad 로 지정되어 있을때만 검증 진행 @Length(max = 64, groups = {Default.class, Ad.class}) @NotEmpty(groups = Ad.class) private String removeGuide; }
1 2 3 4 5 6 7 8 9
@Validated(Ad.class)// 메서드 호출 시 Ad 그룹이 지정된 제약만 검사한다. publicvoidsendAdMessage(@Valid Message message) { // Do Something }
// @Validated(javax.validation.groups.Default ) 가 정의되어있는것과 동일하다. publicvoidsendNormalMessage(@Valid Message message) { // Do Something }
Validation Message
Custom Validation
검증할 조건을 커스텀하게 작성 가능하다. 입력값이 해당 CustomEnum 에 부합하는지를 검증하고 싶을때,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
publicenumCustomEnum { INFO, WARN, ERROR, DEBUG;
@JsonValue public String getValue() { returnthis.name(); }
@JsonCreator publicstatic CustomEnum fromString(String value) { for (CustomEnum customEnum : values()) { if (customEnum.name().equals(value)) return customEnum; } returnnull; } }
@Override publicbooleanisValid(String value, ConstraintValidatorContext context) { if (value == null) { returnfalse; // null 값은 에러 } for (String enumName : enumNames) { if (enumName.equals(value)) { returntrue; // 입력된 값이 Enum에 존재하는 경우 유효 } } returnfalse; // 입력된 값이 Enum에 존재하지 않는 경우 유효하지 않음 } }
@Override publicbooleanisValid(Message value, ConstraintValidatorContext context) { if (value.isAd()) { // 위반한 검증 목록 final Set<ConstraintViolation<Object>> constraintViolations = validator.validate(value, Ad.class); if (constraintViolations != null && constraintViolations.size() != 0) { // 기본 메시지 제거하고 새로운 MethodArgumentNotValidException 에 message 에 정의된 문자열를 넣는 과정 context.disableDefaultConstraintViolation(); constraintViolations.stream() .forEach(constraintViolation -> context .buildConstraintViolationWithTemplate(constraintViolation.getMessageTemplate()) // 에러메세지 삽입 .addPropertyNode(constraintViolation.getPropertyPath().toString()) .addConstraintViolation()); returnfalse; } } returntrue; } }
Enum Validation
특정 필드가 Enum 타입과 일치해야 하는 값을 가져야할 경우에도 동일하게 ConstraintValidator 를 사용할 수 있다.
dataclassCreateCustomerRequest( @field:NotEmpty(message = "username is required") val username: String, @field:NotEmpty(message = "password is required") var password: String?, @field:NotEmpty var nickname: String, @field:NotEmpty var name: String, var birth: String, )
위와 같은 data class 를 @Valid @RequestBody 로 수신받을 때,
username 을 빼면 org.springframework.http.converter.HttpMessageNotReadableException 에러가 발생하고 password 를 빼면 org.springframework.web.bind.support.WebExchangeBindException 에러가 발생한다.
username 의 경우 not null property 로 정의되어 있어 json 을 dto 로 변환하는 과정에서 에러가 발생하고 password 의 경우 nullable property 로 정의되어 있어 Validation 처리에서 예외가 발생한다.
WebFlux 에선 HttpMessageNotReadableException 대신 org.springframework.web.server.ServerWebInputException 에러가 발생함.
아래와 같이 기본값을 추가하면 json 변환에서 not null 로 인한 오류는 뜨지 않고 validation 로직을 수행한다.
하지만 null 로 값을 설정할수 없기 때문에 반쪽자리 코드이긴 하다.
1 2 3 4
@field:NotEmpty(message = "username is required") val username: String = "", @field:NotEmpty(message = "password is required") var password: String = "",
굳이 validation 로직을 수행할 필요없이 에러 응답을 수행하려면 HttpMessageNotReadableException 내부 cause 필드를 이용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@ExceptionHandler(value = [HttpMessageNotReadableException::class]) funhandleException(e: HttpMessageNotReadableException): ResponseEntity<ErrorResponseDto> { val description = when (val cause = e.cause) { is MismatchedInputException -> "${cause.path.joinToString { it.fieldName }} 필드에 값이 없습니다." else -> messageSource.getMessage("error.BAD_REQUEST", null, Locale.getDefault()) } return ResponseEntity( ErrorResponseDto( code = ErrorCode.BAD_REQUEST.code, error = ErrorCode.BAD_REQUEST.error, description = description ), HttpStatus.BAD_REQUEST ) }
cause.path.joinToString 을 통해 depth 가 깊은 필드도 참조할 수 있다. compute.resourceLimit.cpu 와 같은 형태
WebFlux 의 ServerWebInputException 에선 에러발생 유발, 필드이름을 가져오는 방법이 없음으로 기본값 사용을 통해 validation 처리해야 할듯.