최근 회사일이 많이 바빠서 따로 개인공부를 할 시간이 없었다....
(사실은 퇴근하면 피곤해서 아무것도 하기 싫었었다가 맞다 ㅎㅎㅎ)
아무튼 오늘은 @Valid 어노테이션을 통한 데이터 유효체크에 대해 글을 작성해보고자 한다.
최근 Map에서 DTO를 이용한 방식으로 작업방식을 바꾸면서 한가지 애를먹은 부분이 있었다.
보통 API 개발을 할 때 request parameter를 넘겨받을 때 HttpMethod가 Post 또는 Put일 경우 특정 데이터를 필수적으로 요구해야하는 경우가 있을 수 있다.
이럴 때에는 필수적으로 요구하는 데이터가 없는 경우 별도의 예외상황을 발생시켜 요청자에게
이러한 부분으로 문제가 발생했다는 것을 알려야하는데 통상적으로 400(Bad Request) 응답으로 처리했었다.
Map을 이용할 당시에는 Request 객체에서 Map으로 변환한 데이터를 Empty로 체크하는 방식을 사용해왔는데 DTO에 해당 방식을 사용하기에는 뭔가 좀 아닌 것 같다라는 생각을 했었고 당연히 찾아보니 아니였다. 보통은 DTO 클래스에서 자체적으로 데이터 유효성을 검증하는 경우가 많았다. 그러면서 나도 이 방식을 통해 필수로 넘겨받을 파라미터를 설정하고 해당 데이터가 존재하지 않으면 400(Bad Request)으로
응답해보자 라는 생각을 했었다. 그러기 위해서 사용한 어노테이션들이 있다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegMemberInfo {
@NotBlank(message = "'memberId' is a required input value")
private String memberId;
@NotBlank(message = "'memberPassword' is a required input value")
private String memberPassword;
@NotBlank(message = "'authorityLevel' is a required input value")
private String authorityLevel;
@NotBlank(message = "'memberName' is a required input value")
private String memberName;
@NotBlank(message = "'memberEmail' is a required input value")
@Email
private String memberEmail;
@NotBlank(message = "'memberMobile' is a required input value")
private String memberMobile;
}
이것은 사용자 등록을 할때 사용하는 DTO 클래스다. 보면 알 수 있겠지만 @NotBlank와 @Email
어노테이션이 붙은 걸 알 수 있다. @NotBlank부터 알아보자.
우선 이 Annotiation 은 Bean Validation (Hibernate Validation) 에서 제공하는 표준 Validation 이다.
이거 말고 @NotNull, @NotEmpty 등등 총 3가지의 Validation을 제공한다.
@NotNull : 이름처럼 Null만 허용하지 않는다. 그러기에 "" 또는 " "같은 공백은 허용한다.
@NotEmpty : Null과 ""를 허용하지 않는다.
@NotBlank : Null과 "", " " 셋을 모두 허용하지 않는다.
따라서 마지막 @NotBlank가 가장 기준이 엄격하다고 볼 수 있다. 나는 @NotBlank를 선택해 작업을 진행했다.
어노테이션을 선언한 후 message를 통해 해당 데이터에 대한 예외가 발생할 시 반환할 메시지를 정의할 수 있다. 그리고 중간에 @Email이라는 Annotiation이 보일 것이다. 이것은 해당 데이터가 이메일 형식인지 아닌지를 검사해주는 기능이 있다. 따라서 불필요한 로직을 만들지 않고 해당 어노테이션을 붙여줌으로써 이메일 형식까지 검사할 수 있다. 나는 이 두가지만 사용하고 있는데 이것 말고도 정말 다양하게 각자의 기능을 품고 있는
어노테이션을 제공하니까 자세한 것은 beanvalidation.org/2.0/spec/ 해당 링크에서 확인해보면 될 것 같다.
API 별로 요구하는 데이터는 다르기 때문에 당연히 DTO는 각 API 요구사항에 맞는 모습으로 분리 될 수밖에 없었다. 파일이 많이 생성되는 것은 좀 그랬지만 대신 유지보수를 할 때 각각 사용하는 DTO만 체크해서 작업하면 될 것이고 컨트롤러나 서비스단에서 별도로 데이터 유효성 검증작업을 생략할 수 있다는 부분이 맘에 들었다.
@PostMapping(value = { "/signup" })
public ResponseEntity<Boolean> apiUserSignUp(
@RequestBody @Valid RegMemberInfo info)throws Exception {
return ResponseEntity.ok()
.body(apiSignService.insertUserInfo(info));
}
이렇게 DTO를 작성하고 POST나 PUT인경우 JSON 타입을 받기위해 @RequestBody 사용한다
그옆에 @Valid 어노테이션을 붙여줌으로서 유효성 체크를 진행한다. 그러면 서비스 단에서는
@Override
public boolean insertUserInfo(RegMemberInfo regMember) {
regMember.setMemberPassword(passwordEncoder.encode(regMember.getMemberPassword()));
if(!IsEmpty.check(apiSignDao.selectDuplicatedMemberFindById(regMember.getMemberId())))
throw new DuplicatedException("There is Duplicated ID");
if(IsEmpty.check(apiSignDao.insertUserInfo(regMember)))
throw new Code700Exception("The query was executed normally, but not a single data was affected");
return true;
}
데이터 검사에 대한 부분을 생략하고 아이디 중복체크 및 비밀번호 일치여부만 확인 후 등록작업을 진행하면 된다.
(위 아이디 중복여부 체크나 비밀번호 일치 체크는 자신의 기준으로 작성하면 된다. 위 방법이 정답은 아니다)
이렇게 만들고 테스트를 진행해보면
아이디가 누락된 경우
이메일 형식이 아닌 경우
이런식으로 직접 작성한 메시지와 함께 400 상태의 응답상태를 확인 할 수 있다. 물론 응답 형태가 보기
어려우니 이건 개인취향에 맞게 응답구조를 커스터마이징하여 사용하면 훨씬 더 보기 좋은 응답구조를
볼 수 있을 것이다. 이렇게 작업하다보니 나는 이제 보통 요청용 DTO와 응답용 DTO를 분리하고 또한
Http Method에 따라 유효성 검증을 하는경우가 있게 안하는 경우가 있기때문에 그에 맞게 분리했다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberDetailResult {
private int regNo;
private String memberId;
private String memberPassword;
private String authorityLevel;
private String memberName;
private String memberEmail;
private String memberMobile;
private String regDate;
}
이런식으로 요구할 때가 아니라 아닌 결과용으로 DTO를 분리하여 쿼리 결과를 저장하기도 하고
/** 로그인 API **/
@PostMapping(value = "/signin")
public ResponseEntity<MemberResultDetail>apiUserSignin(
@RequestBody LoginInfo login)throws Exception {
MemberResultDetail result = apiSignService.loginUserProcessService(login);
return ResponseEntity.ok()
.header("x-accss-token", jwtTokenProvider.createToken(result))
.body(result);
}
/** 로그인 API 요구 DTO **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginInfo {
@NotBlank(message = "'memberId' is a required input value")
private String memberId;
@NotBlank(message = "'memberPassword' is a required input value")
private String memberPassword;
}
/** 로그인 API Service 클래스 **/
@Override
public MemberResultDetailloginUserProcessService(LoginInfo login)throws Exception {
MemberResultDetail result = apiSignDao.selectMemberFindById(login);
if (IsEmpty.check(result))
throw new Code700Exception("There is no Result Data");
if (!passwordEncoder.matches(login.getMemberPassword(), result.getMemberPassword()))
throw new ForbiddenException("Passwords do not match");
return result;
}
해당API는 원래 회원 등록과 같은 DTO를 쓰던 API였는데 그에 맞는 DTO를 따로 생성해서 작성하여 사용하고 있다. 물론 100% 좋은 코드라고 자신 할수는 없지만 그래도 점점 깔끔하고 질 좋은 코드로 개선되어가고 있지않나 라는 생각은 가지고 있다. 개발코드를 작성하는데 "100% 정답" 이라는 것은 없다. 상황에 따라 유동적으로 사용하면 좋다. 어쨌든 @Valid와 몇몇 Annotiation을 통해 데이터 유효성을 체크하는 부분에 대해 알아보았다.