아래 디펜던시를 추가 후 진행해야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Valid 어노테이션을 사용하여 검증할 수 있다.
// Controller 코드 일부
@PostMapping
public ApiResult<HelloInsertRspDto> insert(@Valid @RequestBody HelloInsertReqDto reqDto) {
return ok(helloService.insert(reqDto));
}
// RequestDto 내 Bean Validator 설정
public class HelloInsertReqDto {
@Email
@Schema(description = "이메일", required = true, example = "test@email.com")
private String email;
@NotBlank
@Schema(description = "이름", required = true, example = "kdy")
private String name;
@Schema(description = "생일", example = "19930724")
private String birthday;
public Hello toEntity() {
return Hello.builder()
.email(email)
.name(name)
.birthday(birthday)
.build();
}
}
설정 시, 컨트롤러 진입 전 Validator 를 통해 Dto 의 Bean Validator
값들을 검증한다.
검증 실패 시, MethodArgumentNotValidException
이 발생한다.
@Test
public void 헬로추가_실패_이름없음() throws Exception {
// given
var helloInsertReqDto = HelloInsertReqDto.builder()
.email("email@base.com")
.build();
var reqJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(helloInsertReqDto);
// then
mockMvc.perform(post("/hello/v1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(reqJson))
.andExpect(status().isBadRequest())
.andExpect(result -> assertThat(getApiResultExceptionClass(result))
.isEqualTo(ConstraintViolationException.class) // 다른 예외 클래스와 비교
)
.andDo(this::printExceptionMessage);
}
예외발생 메시지템플릿은 인터페이스 별로 상이하며, 스프링부트 기본 값을 기준으로 resources 하위 ValidationMessages.properties 에 설정하면 수정할 수 있다.
ValidationAutoConfiguration 클래스를 통해 LocalValidatorFactoryBean 으로 Validator 를 생성한다.
이 때, LocalValidatorFactoryBean 는 ValidationMessages.properties 를 클래스패스 내 기본 설정파일로 지정한다.
// 따라서 Bean Validator 의 메시지 템플릿을 확인한 뒤
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
/* 이하 생략 */
}
// resources(Spring Boot 기본 classpath) 하위 ValidationMessages.properties 에 설정하여 기본 메시지를 수정할 수 있다.
javax.validation.constraints.Email.message=이메일 형식에 맞게 입력해주세요.
javax.validation.constraints.NotBlank.message=유효한 값을 입력해주세요.
@Validated // 어노테이션 적용 시, 해당 클래스 내 @Valid 를 인식
@RequiredArgsConstructor
@Service
public class HelloService {
private final HelloRepository helloRepository;
// @Valid가 선언된 경우, 해당 메소드는 Spring AOP Proxy 객체가 대신 수행
// 실제 작업 전 MethodValidationInterceptor.invoke() 로 검증
// AOP 검증 예외로 ConstraintViolationException 발생
public HelloInsertRspDto insert(@Valid HelloInsertReqDto reqDto) {
return new HelloInsertRspDto(helloRepository.save(reqDto.toEntity()));
}
}
@Test
public void 헬로추가_실패_잘못된이메일형식() {
// given
var reqDto = HelloInsertReqDto.builder()
.email("email.com")
.name("name")
.birthday("0724")
.build();
// when
assertThrows(ConstraintViolationException.class,
() -> helloService.insert(reqDto)
);
}
JPA save 시, AOPProxy 객체가 작업을 대신 수행한다.
JpaRepository 기본 Save 시
EntityIdentityInsertAction.preInsert() 메소드를 통해
실제 DB Insert 전 검증과정을 거친다. 이 때, 검증 작업이 진행된다.
package org.hibernate.action.internal;
public class EntityIdentityInsertAction extends AbstractEntityInsertAction {
@Override
public void execute() throws HibernateException {
nullifyTransientReferencesIfNotAlready();
final EntityPersister persister = getPersister();
final SharedSessionContractImplementor session = getSession();
final Object instance = getInstance();
setVeto( preInsert() ); // DB 작업 전 검증
// Don't need to lock the cache here, since if someone
// else inserted the same pk first, the insert would fail
// 이후 DB 작업 진행
if ( !isVeto() ) {
generatedId = persister.insert( getState(), instance, session );
if ( persister.hasInsertGeneratedProperties() ) {
persister.processInsertGeneratedProperties( generatedId, instance, getState(), session );
}
//need to do that here rather than in the save event listener to let
//the post insert events to have a id-filled entity when IDENTITY is used (EJB3)
persister.setIdentifier( instance, generatedId, session );
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
persistenceContext.registerInsertedKey( getPersister(), generatedId );
entityKey = session.generateEntityKey( generatedId, persister );
persistenceContext.checkUniqueness( entityKey, getInstance() );
}
postInsert();
final StatisticsImplementor statistics = session.getFactory().getStatistics();
if ( statistics.isStatisticsEnabled() && !isVeto() ) {
statistics.insertEntity( getPersister().getEntityName() );
}
markExecuted();
}
}
JPA 엔티티 컬럼에 Bean Validator
설정 시, Spring Boot 기본 설정으로 hibernate-core 패키지의 BeanValidationEventListener 으로 검증한다.
이는 JdkDynamicAopProxy
객체를 통해 JPA 기능 수행 중 발생한 오류로 ConstraintViolationException
을 발생시킨다.
@Test
public void 사용자추가_실패_필수값누락_이름() {
// given
var hello = Hello.builder()
.email("insertNameTest@email.com")
.birthday("19990101")
.build();
// when
assertThrows(ConstraintViolationException.class,
() -> hellosRepository.save(hello)
);
}
예외메시지는 컨트롤러어드바이스에서 받아 처리할 수 있습니다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// @Valid 어노테이션으로 Dto 를 검증하는 경우 발생한다.
BindingResult bindingResult = e.getBindingResult();
StringBuilder builder = new StringBuilder();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
builder.append("[");
builder.append(fieldError.getField());
builder.append("](은)는 ");
builder.append(fieldError.getDefaultMessage());
builder.append(" 입력된 값: [");
builder.append(fieldError.getRejectedValue());
builder.append("]");
}
log.error(builder.toString());
return new ResponseEntity<>(ApiResult.error(e), HttpStatus.BAD_REQUEST);
}
좋은 글이네요!! 👍 혹시
이 부분에 대한 내용은
"초보 웹 개발자를 위한 스프링5 프로그래밍 입문" 에 수록 된 내용인가요?!