
@Valid 어노테이션을 사용하여 유효성 검사를 진행할 것이다.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
해당 텍스트를 build.gradle에 추가한 후, gradle을 업데이트 해줘야 사용할 수 있다.

@Valid를 추가하였다.
RequestBody로 들어오는 객체에 대한 유효성 검사를 진행할 것이기에,
매개변수에 @Valid 어노테이션을 추가하였다.
package com.shoppingmall.shoppingmall.member;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.constraints.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class MemberDto {
/**
* Member를 구분하기 위한 id 정보
*/
@NotBlank
@Size(min = 1, max = 15, message = "아이디는 1 ~ 15자 이여야 합니다!")
private String userId;
/**
* Member의 비밀번호 정보
*/
@NotBlank
@Size(min = 1, max = 15, message = "비밀번호는 1 ~ 15자 이여야 합니다!")
private String pw;
/**
* Member의 실제 이름 정보
*/
@NotBlank
@Size(min = 1, max = 10, message = "이름은 1 ~ 10자 이여야 합니다!")
private String name;
/**
* Member의 이메일 정보
*/
@Email
@Size(min = 1, max = 10, message = "이메일은 1 ~ 10자 이여야 합니다!")
private String email;
/**
* Member의 전화번호
*/
@NotBlank
@Size(min = 12, max = 13, message = "전화번호는 12 ~ 13자 이여야 합니다!")
private String contact;
public Member convertToEntity() {
return new Member(userId, pw, name, email, contact);
}
@Override
public String toString() {
return "MemberDTO{" +
"userId='" + userId + '\'' +
", pw='" + pw + '\'' +
", name='" + name + '\'' +
", email='" + email + '\'' +
", contact='" + contact + '\'' +
'}';
}
}

응답은 다음과 같이 bad request가 전달되었다.

Spring 터미널에는 다음과 같은 에러가 발생하였다.
2024-05-17T10:39:17.295+09:00 WARN 35788 --- [shoppingmall] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.shoppingmall.shoppingmall.utils.ApiUtils$ApiResult<java.lang.String> com.shoppingmall.shoppingmall.member.MemberController.joinByApiResult(com.shoppingmall.shoppingmall.member.MemberDto) with 2 errors: [Field error in object 'memberDto' on field 'userId': rejected value [null]; codes [NotBlank.memberDto.userId,NotBlank.userId,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberDto.userId,userId]; arguments []; default message [userId]]; default message [must not be blank]] [Field error in object 'memberDto' on field 'email': rejected value [tjdbwns19@naver.com]; codes [Size.memberDto.email,Size.email,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberDto.email,email]; arguments []; default message [email],10,1]; default message [이메일은 1 ~ 10자 이여야 합니다!]]
어떻게 해결해야 할까?
@Valid에 의해 발생한 Validation Error는 기본적으로 Errors 인터페이스 타입의 객체에 담긴다.
따라서 메서드의 인자로 Errors 타입의 객체를 받는지, 안받는지에 따라 처리가 달라진다.
아래는 Errors를 처리하기전 코드이다.
@PostMapping("/join/api/result") // After
public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto) {
if (Validator.isAlpha(memberDto.getName())) {
// 유저 이름을 log로 출력
log.info(memberDto.toString());
이제 Errors를 처리해보았다.
@PostMapping("/join/api/result") // After
public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto, Errors errors) {
try {
if (errors.hasErrors()) {
throw new NotValidException(errors.getObjectName());
}
} catch(NotValidException e) {
log.info(e.getMessage() + " 가 유효성 검사를 통과하지 못하였습니다!");
return error("유효성 검사 실패", HttpStatus.CONFLICT);
}
if (Validator.isAlpha(memberDto.getName())) {
// 유저 이름을 log로 출력
log.info(memberDto.toString());
// ID 중복 체크
// 중복이면 사용자 예외 클래스 소환
// 1) 예외 클래스한테 니가 return 해!
// 2) 예외만 발생 시키고.. 메세지는 내가 보낼게
if (isDuplicateId(memberDto)) {
return error("아이디 중복", HttpStatus.CONFLICT);
}
Member requestMember = memberDto.convertToEntity();
// Repository에 저장 시도
String userId = memberService.join(requestMember);
// {
// “success” : True,
// “response” : 응답 데이터(객체),
// “error” : null
// }
try {
log.info(userId);
} catch (NullPointerException e) {
return success(userId);
}
return success(userId);
} else
return error("아이디 숫자 포함", HttpStatus.BAD_REQUEST);
}
위 방식으로 해결하였을때, DTO의 Error Message에 대한 궁금증이 생겼다.
MemberController.java
@PostMapping("/join/api/result") // After
public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return error(bindingResult.getAllErrors().toString(), HttpStatus.BAD_REQUEST);
}
if (Validator.isAlpha(memberDto.getName())) {
// 유저 이름을 log로 출력
log.info(memberDto.toString());
// ID 중복 체크
// 중복이면 사용자 예외 클래스 소환
// 1) 예외 클래스한테 니가 return 해!
// 2) 예외만 발생 시키고.. 메세지는 내가 보낼게
if (isDuplicateId(memberDto)) {
return error("아이디 중복", HttpStatus.CONFLICT);
}
Member requestMember = memberDto.convertToEntity();
// Repository에 저장 시도
String userId = memberService.join(requestMember);
// {
// “success” : True,
// “response” : 응답 데이터(객체),
// “error” : null
// }
try {
log.info(userId);
} catch (NullPointerException e) {
return success(userId);
}
return success(userId);
} else
return error("아이디 숫자 포함", HttpStatus.BAD_REQUEST);
}
GlobalExceptionController.java
@ControllerAdvice
public class GlobalExceptionController {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(BindingResult bindingResult) {
Map<String, String> errors = new HashMap<>();
bindingResult.getAllErrors().forEach(c -> errors.put(((FieldError)c).getField() , c.getDefaultMessage()));
return errors;
}
}
위 방법은
유효성 검사에 통과하지 못할 경우
GlobalExceptionController 클래스의 handleValidationExceptions 메서드가 호출됩니다.
해당 메서드는 발생한 모든 에러들을 Map<String, String> 으로 변환하여 Cotroller의 Response Format에 전달합니다.
현재는 Response Format의 Error 객체가 message가 String 타입을 받게 되어있어, Map<String, String> -> String 으로 변환하여 전달하였습니다.
그러나, Map을 toString()으로 반환하여 전달하니
다음과 같이 응답이 찍히는 것을 확인하였다.

앞으로 다음과 같이 수정할 것이다.
1. Response Format에서 getError 리스트를 저장
2. 클라이언트 메시지를 가공하여 전달
참고자료
https://sanghye.tistory.com/36