Regex 란 요청에 데이터가 어떤 특정 형태 및 조건을 충족하는지를 확인하는 방법입니다.
보통 회원가입과 같은 중요한 정보에 대한 API 를 개발할때 Regex 를 활용하곤합니다. 만일 Regex(정규 표현식) 을 사용하지 않는다면, 회원가입을 진행할때 "a@naver.com" 이라고 지정한 사용자가 나중에 계정을 읽어버려서 이메일로 임시 비밀번호를 발급받고 싶어도, 불가능하겠죠.
스프링부트에서는 Regex (정규 표현식)을 쉽게 사용할 수 있도로 기능을 제공해줍니다. Dto 의 필드에 정규 표현식 조건을 작성해주면, @Valid 어노테이션과 함께 유효성 검사를 할 수 있습니다.
이 포스팅에서는 RestAPI 기반의 회원가입 진행시 어떻게 백엔드에서 검증 처리를 할 수 있을지 다루어보고자 합니다.
우선 UserEntity 입니다. 아래와 같이 간단히 아이디와 비밀번호만 필드로 가지고 있는 유저를 생성하는 것으로 설계해봤습니다.
@Entity
@Table(name = "User")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Data @Builder
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int userIdx;
@Column(unique = true)
private String identification; // 아이디
private String password; // 비밀번호
}
다음으로는 회원가입시 요청받는 SignupUserReq 클래스입니다. 이 부분에서 바로 Regex 표현식이 사용되는 것이죠. @Pattern 이라는 어노테이션에다 각 요청 필드에 대한 조건문을 걸어주시면 됩니다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignupUserReq {
@ApiModelProperty(example = "msung1234")
@NotEmpty(message = "아이디는 필수 입력값입니다")
@Pattern(regexp = "^[a-z0-9]{5,20}$", message = "아이디는 영어 소문자와 숫자만 사용하여 5~20자리여야 합니다.")
private String identification;
@ApiModelProperty(example = "Mypassword123@")
@NotEmpty(message = "비밀번호는 필수 입력값입니다")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,16}$", message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
private String password;
@ApiModelProperty(example = "Mypassword123@")
@NotEmpty(message = "비밀번호 확인은 필수 입력값입니다")
private String repass;
//@ApiModelProperty(example = "msung99")
// @NotEmpty(message = "닉네임은 필수 입력값입니다")
// @Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,10}$" , message = "닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.")
// private String nickname;
public UserEntity toEntity(){
return UserEntity.builder()
.password(password)
.identification(identification)
.build();
}
}
다음으로는 컨트롤러입니다. @Valid는 클라이언트의 입력 데이터가 SignupUserReq 라는 클래스로 캡슐화되어 넘어올 때, 유효성을 체크하라는 어노테이션입니다.
즉, @Valid 란 Request 로 넘어온 객체에 대한 검증을 수행하라는 것입니다. 그리고 그 검증 기준은 @Pattern 어노테이션에 명시해놓은 정규표현식 인 것입니다.
직전에 SignupUserReq 클래스에서 작성한 @Pattern 어노테이션을 기반을 유효성을 체크하는 것이죠.
import org.springframework.validation.Errors;
import org.springframework.ui.Model;
import javax.validation.Valid;
@ResponseBody
@PostMapping("/signup")
@Operation(summary = "회원가입", description = "아이디, 비밀번호, 재확인 비밀번호를 한꺼번에 입력받고 회원가입하는 API 입니다. 이거 지울까말까 고민하다가 일단 남겨둔 API임")
public BaseResponse createUser(final @Valid @RequestBody SignupUserReq signupUserReq){
try{
userService.createUser(signupUserReq);
return new BaseResponse();
} catch (BaseException exception){
return new BaseResponse(exception.getStatus());
}
}
그리고 Service 단에서는 보시듯이 간단한 예외처리를 진행하고 유저 데이터를 생성하는 모습을 볼 수 있습니다. 핵심적인 내용은 아니니 자세한 설명은 넘어가겠습니다.
@Service
public class UserService {
private final UserRepository userRepository;
private final JwtService jwtService;
@Autowired
public UserService(UserRepository userRepository, JwtService jwtService) {
this.userRepository = userRepository;
this.jwtService = jwtService;
}
public void createUser(SignupUserReq signupUserReq) throws BaseException{
// 중복된 아이디를 가지는 유저가 또 존재하는지 확인
String signupIdentification = signupUserReq.getIdentification();
if(userRepository.existsUserEntityByIdentification(signupIdentification)){
throw new BaseException(BaseResponseStatus.EXISTS_USER);
}
// 비밓번호와 재입력받은 비밓번호가 같은지 다른지 유효성 검사 (다르면 예외 발생)
if(!CheckValidForm.isEqual_Passwrord_Check(signupUserReq.getPassword(), signupUserReq.getRepass())){
throw new BaseException(BaseResponseStatus.NOT_EQUAL_PASSWORD_REPASSWORD);
}
if(!CheckValidForm.isValid_Password_Form(signupUserReq.getPassword())){
throw new BaseException(BaseResponseStatus.NOT_EQUAL_PASSWORD_REPASSWORD);
}
try{
UserEntity userEntity = signupUserReq.toEntity(); // DTO -> Entity 변환
userRepository.save(userEntity);
} catch (Exception exception){
throw new BaseException(BaseResponseStatus.SERVER_ERROR);
}
}
앞서 살펴본 Controller 에서 @Valid로 requestBody로 들어온 객체의 검증이 이루어지면서 BadRequest가 나가는 경우에 custom 한 errorhandling을 할 수 있습니다.
이는 @ControllerAdvice를 이용한 전역 에러 핸들링, 혹은 @Controller단에서의 지역 에러 핸들링을 사용하면 됩니다. MethodArgumentNotValidException에 대한 @ExceptionHandler 어노테이션을 지정하여 커스텀 에러 핸들링을 해봅시다.
package hyundai.hyundai.User;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class ApiControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handValidationExceptions(MethodArgumentNotValidException validException){
Map<String, String> errors = new HashMap<>();
validException.getBindingResult().getAllErrors()
.forEach(c -> errors.put("message", c.getDefaultMessage()));
// ((FieldError) c).getField() => error가 난 field 값을 ResponseEntity
// 의 key값으로 활용하고 싶다면 "message" 대신에 이걸 넣자!
return ResponseEntity.badRequest().body(errors);
}
}
ResponseEntity 값으로 key값에는 "message" 를 넣었고, 에러 메시지를 Map 형태로 만들어서 Response로 넣어주었습니다.
이때 Map으로 선언하여 forEach를 한 이유는 @Valid를 사용할 때, 해당 객체에서 valid에 실패한 내용을 모두 리턴해주기 때문에, 모든 error 값을 수용하기 위해서입니다.
실행결과를 보시면 JSON 의 key 값으로 message 가, value 로는 @Pattern 에서 정의해놓은 메시지가 리턴되는 모습을 볼 수 있습니다.
🙈[SpringBoot] @Valid로 유효성 검사하기🐵2020. 1. 19. 15:08
@Valid 를 이용해 @RequestBody 객체 검증하기