[해결하기] 유효성 검증

이호인·2022년 6월 30일
0

문제해결

목록 보기
2/2

계정관련 서비스를 따로 분리하는 작업을 하면서 처음 만들 때보다 시간이 흘렀으니 더 나은 방식으로 코드를 짜보고 싶기도하고 계정 쪽은 처음이라 프론트 쪽만 가져오면서 다시 짜보고 있다.

지금까지 거의 게시판 위주의 작업만 해봤고 계정관련 서비스는 처음이었다. 게시판 쪽을 다룰 땐 최대 크기, xss 방어코드 정도만 생각했던 것 같은데 (물론 내가 모르는 부분이 더 많겠지만) 계정은 유효성 검증이 필수적이고 매우 중요한 부분이었다. (Spring Security는 나중에 도전😶)

유저 쪽을 맡았던 친구의 코드를 다시 살펴보면서

  • @Valid 어노테이션을 사용해서 @Requestbody로 들어온 객체의 유효성 검증을 하고 있다는 것
  • User라는 dto 각 필드에 @NotBlank @Size Pattern 등을 사용
  • 공백, 크기, 정규식에 맞는 값 등을 검사, 맞지 않는 항목들은 MethodArgumentNotValidException을 발생시킨다는 것
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable{

    private static final long serialVersionUID = 1L;

    @NotBlank(message = "아이디를 입력해주세요.")
    @Size(min = 4, max = 30, message = "아이디는 4글자에서 30글자 사이로 입력해주세요.")
    @Pattern(regexp = "^([a-z가-힣0-9]){4,30}$", message = "대문자, 특수문자는 입력할 수 없습니다.")
    private String userId;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    @Size(min = 8, max = 20, message = "최소 8자에서 최대 20자까지 입력 가능합니다.")
    @Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", message = "영문, 숫자, 특수문자가 적어도 1개 이상 조합되게 입력해주세요.")
    private String password;

    @Email(message = "이메일 형식에 맞게 입력해주세요.")
    @NotBlank(message = "이메일을 입력해주세요.")
    private String email;

이런 내용들을 알게되었고 구글링을 통해

  • MethodArgumentNotValidException에 대한 에러 내용들을 깔끔하게 커스텀해서 사용하는 법을 알게 되었다.
  • @ControllerAdvice@ExceptionHandler를 사용하여 error와 함께 body에 메세지를 담아 보내게 설정했다.

📌 문제점

  function checkUserId(){
    var userId = $('#userId').val();
    var userIdLength = $('#userId').val().length;

    $.ajax({
      url:'/users/check/id',
      type:'post',
      data:{userId:userId},
      success:function(cnt){
        if(4 > userIdLength) {
          $('.userId_fail').css("display", "inline-block");
          $('.userId_success').css("display", "none");
          $('.userId_fail').text("너무 짧습니다.");
        }
        if(userIdLength > 30) {
          $('.userId_fail').css("display", "inline-block");
          $('.userId_success').css("display", "none");
          $('.userId_fail').text("너무 깁니다.");
        }
        if(cnt!=0){
          $('.userId_fail').css("display", "inline-block");
          $('.userId_success').css("display", "none");
          $('.userId_fail').text("중복된 아이디입니다.");
        }
        if(idCheck.test(userId) === false) {
          $('.userId_fail').css("display", "inline-block");
          $('.userId_success').css("display", "none");
          $('.userId_fail').text("형식에 맞게 작성해주세요.");
        }
        if(cnt == 0 && 3 < userIdLength && userIdLength < 31 && idCheck.test(userId) === true) {
          $('.userId_success').css("display", "inline-block");
          $('.userId_fail').css("display", "none");
        }
      },
      error:function(){
      
      }
      .
      .

📌 첫번째 문제점

클라이언트 쪽에서 한번, 서버 쪽에서 한번. 총 2번 검사하는 건 OK.
서버 쪽 메세지를 body에 담았으나 그 메세지를 활용하지 못해서 클라이언트에게 전달해주지 못함.
메세지를 javascript로 일일이 다 설정해주어야함.

🔍 해결하고 싶은 것 : dto에 설정한 메세지를 활용할 수 있으면 좋겠다.

아까 @ControllerAdvice@ExceptionHandler를 사용하여 dto에 설정해둔 메세지를 body에 담아보냈다. 이걸 활용하는 방법은

error : function(xhr){
          alert(xhr.responseText);
        }

이런 식으로 써주면 모두 d라는 값을 입력해서 예외를 발생시킬 때

이렇게 뜬다.

❗❗❗ 개인적인 생각 ( 확실하지 않음) ❗❗❗
responseText는 body에 String으로 보냈을 때 사용하는 것 같고

error:function(xhr){
        alert(xhr.responseJSON.message);
      }

이런 식으로 사용하면 JSON 객체를 쓸 수 있는 것 같다.

📌 두번째 문제점

지금 중복확인하는 것을 DB에서 값이 있는지 (0개인지, 1개인지) 확인해서 count 값을 넘겨주고 그 값을 확인해서 처리를 하는데, 그렇게 하는 것보다 에러를 던져주는 것이 효율적일 것 같다는 코드 리뷰를 받았다.

@Service
@RequiredArgsConstructor
public class DuplicateChecker {

    private final UserMapper userMapper;

    @Transactional
    public int checkDuplicateId(String userId) {
      
      	int count = userMapper.checkDuplicateId(userId);
      
        return count;
    }
  • 원래는 아이디 개수를 리턴해주었지만 우리는 아이디 중복이라는 상황을 만나면 의도적으로 예외를 발생시켜 막으려고 한다. 이 때 사용할 수 있는 것이 RuntimeException이다.

  • RuntimeException은 unchecked exception으로 말 그대로 실행 중에 발생하며 의도적으로 개발자가 잡아내기 위한 조건 등에 부합할 때 발생 시키도록 만들 수 있다.

  • RuntimeException을 그대로 사용할 수도 있지만 좀 더 세밀하게 사용하기 위해 AccountApiException이라는 추상화 클래스를 사용하여 여러 상황에 부합한 메세지를 전달할 수 있도록 작성했다.

추상클래스를 사용하면

  1. 공통된 필드와 메서드를 통일할 수 있다.
  2. 실체클래스 구현 시, 시간절약을 할 수 있다.
  3. 규격에 맞는 실체클래스를 구현할 수 있다.

  • 상위 클래스에서 private으로 변수를 생성하면 다른 외부 클래스에서 일절 접근 불가하기 때문에 상속된 하위 클래스까지는 접근을 허용하는 protected 접근 제어자로 설정해준다.
  • AccountApiException을 상속하는 모든 exception들은 메세지를 갖고 있도록 설정했다.
@Getter
public abstract class AccountApiException extends RuntimeException {
    
    private final String message;

    protected AccountApiException(String message) {
        this.message = message;
    }
}

  • class가 인스턴스화 될 때 생성자가 실행되면서 객체 초기화를 하는데, 그 때 자신의 생성자만 실행되는 것이 아니라 부모의 생성자부터 실행된다.
  • 부모를 가리키는 키워드는 super (자신을 가리키는 키워드는 this)

⭐ 상속을 통해 같은 기능이지만 다른 결과를 도출하기 ---> 다형성

  • 현재 중복 가입을 막기 위해서 핸드폰 중복 검사도 하고 있는데 이럴 때마다 AccountApiException을 상속 받아서 오버라이딩하여 메세지만 넣어주면 된다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class DuplicatedIdException extends AccountApiException {

    public DuplicatedIdException() {
        super("중복된 아이디입니다.");
    }
}

  • 0보다 크면 중복이라는 말이니까 DuplicatedIdException이 발생, super를 사용했기 때문에 부모 생성자에 DuplicatedIdException의 내용이 매개변수로 설정됨
	if(count > 0) 
    	throw new DuplicatedIdException();

@RestControllerAdvice@ExceptionHandler를 사용한다.

  1. super()를 통해 DuplicatedIdException의 message를 AccountApiException message에 넣어서
  2. 미리 만들어둔 ErrorDto에 getMessage() 메서드를 사용하여 리턴한다.
  3. AccountApiException이 발생하면 BAD_REQUEST 400 에러로 뿌려준다.
@RestControllerAdvice
@RestController
public class ExceptionController {

.
.
.

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(AccountApiException.class)
    public ErrorDto invalidIdError(AccountApiException ex) {
        return new ErrorDto(ex.getMessage());
    }
}

이렇게 하면 아이디가 중복되었을 때 예외 발생하여 메세지를 클라이언트에 전달할 수 있다.

error:function(xhr){
        alert(xhr.responseJSON.message);
      }


마치며

  • 코드 리뷰 받고 예시를 들어주며 설명을 들었는데 바로 따라 작업하기에는 이해가 부족한 것 같아서 한번 정리해두고 진행하려고 한다.
  • 추상화, 상속, 접근 제어자 개념들은 한번 따로 정리해두어야겠다.
  • 익숙한 model, view를 사용하지 않아서 좀 더 어렵게 느낀 것 같다.
  • 구글링 늘었다고 생각했는데 xhr.respsonse 저 부분을 해결 못하고 한참 헤맸다. 멘토도 원래 잘 모르는 부분이었는데 5분 컷 ㅋㅋ
  • 간만에 재밌었다.

reference!

https://comboong.com/entry/JAVA-Java-%EC%83%81%EC%86%8D%EA%B3%BC-Protected-%EC%A0%91%EA%B7%BC%EC%A0%9C%EC%96%B4%EC%9E%90
https://programmers.co.kr/learn/courses/5/lessons/192

profile
공부 기록

0개의 댓글