Regex 를 활용한 필드 유효성 검사

msung99·2023년 2월 15일
1

시작에 앞서 : Regex 를 언제쓸까?

Regex 란 요청에 데이터가 어떤 특정 형태 및 조건을 충족하는지를 확인하는 방법입니다.

보통 회원가입과 같은 중요한 정보에 대한 API 를 개발할때 Regex 를 활용하곤합니다. 만일 Regex(정규 표현식) 을 사용하지 않는다면, 회원가입을 진행할때 "a@naver.com" 이라고 지정한 사용자가 나중에 계정을 읽어버려서 이메일로 임시 비밀번호를 발급받고 싶어도, 불가능하겠죠.


@Valid 어노테이션를 활용한 검사

스프링부트에서는 Regex (정규 표현식)을 쉽게 사용할 수 있도로 기능을 제공해줍니다. Dto 의 필드에 정규 표현식 조건을 작성해주면, @Valid 어노테이션과 함께 유효성 검사를 할 수 있습니다.

이 포스팅에서는 RestAPI 기반의 회원가입 진행시 어떻게 백엔드에서 검증 처리를 할 수 있을지 다루어보고자 합니다.


Entity 및 Request 설계

UserEntity

우선 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 : @Pattern 어노테이션

다음으로는 회원가입시 요청받는 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();
    }
}

Controller

다음으로는 컨트롤러입니다. @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 단에서는 보시듯이 간단한 예외처리를 진행하고 유저 데이터를 생성하는 모습을 볼 수 있습니다. 핵심적인 내용은 아니니 자세한 설명은 넘어가겠습니다.

@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);
        }
    }

ApiControllerAdvice

앞서 살펴본 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 객체 검증하기

0개의 댓글