• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

Validation 관련해서 질문드립니다.

23.09.11 23:43 작성 조회수 399

1

무료 강의임에도 굉장히 강의 퀄리티가 높은 것에 감탄을 하며 보고 있는 중입니다. 좋은 감사드립니다.

Validation 처리와 관련해서 질문드릴 것이 있는데요. 강의에서 DTO에 해당하는 클래스의 프로퍼티에 백킹필드를 사용하시고 Custom Getter를 만들어서 처리를 하셨는데, 혹시 이렇게 하는 방법 말고 바로 프로퍼티만으로 하는 방법도 있을까요?

답변 2

·

답변을 작성해보세요.

2

JUNN님 추가로 질문주셔서 감사합니다.

JUNN님 질문을 읽고 다시 한번 DTO를 어떤식으로 활용하는게 좋을지 생각해봤습니다.

  1. RequestDto의 경우 모든 값이 다 담겨져 있어야 한다면 왜 꼭 null 또는 "" 을 허용해줘야 하는가? 이걸 허용 안 해준다면 String? 이 아닌 String을 사용하면 안 되는 것일까?

=> 이 부분은 프론트쪽과 값을 주고 받는 부분이 맞아떨어진다면 String으로 선언하셔도 됩니다. 애초에 프론트에서 null이나 ""를 넘기지 않을테니까요.

그런데 String?으로 했던 이유를 조금 말씀드리자면 저는 클라이언트(프론트화면일 수도 있고 postman일수도 있는)에서 오는 Request는 언제든지 틀릴 수 있다는 전제가 있습니다. 그래서 그것을 검증하는 일환으로 Validation을 사용합니다.

 

예를 들어보겠습니다.

Request body에 아래의 값을 전달한다고 가정해보겠습니다.(loginId, password 필수값)

{ "loginId":null, "password":null }

 

  1. String 으로 선언

class LoginDto(
    @field:NotBlank
    val loginId: String,

    @field:NotBlank
    val password: String,
)

String으로 선언하고 값을 전달하면 request에 아래와 같이 나오게 됩니다.

{
    "resultCode": "ERROR",
    "data": {
        "미처리 에러": "JSON parse error: Instantiation of [simple type, class com.example.auth.member.dto.LoginDto] value failed for JSON property loginId due to missing (therefore NULL) value for creator parameter loginId which is a non-nullable type"
    },
    "message": "에러가 발생했습니다."
}

그러면 loginId에 null을 넣을 수 없다고 error메세지를 출력하게 됩니다.

 

  1. String? 으로 선언

class LoginDto(
    @field:NotBlank
    val loginId: String?,

    @field:NotBlank
    val password: String?,
)

String?으로 선언하고 값을 전달하면 request에 아래와 같이 나오게 됩니다.

{
    "resultCode": "ERROR",
    "data": {
        "loginId": "공백일 수 없습니다",
        "password": "공백일 수 없습니다"
    },
    "message": "에러가 발생했습니다."
}

그러면 loginId, password 둘 다 안되는 이유를 Response에 담아서 보내줄 수 있습니다.

 

이것으로 보면 String으로 선언하면 Validation 체크를 하기 전에 DTO에 담는 과정에서 error를 발생시키는 것을 알 수 있는데요. 예를 들어 항목이 10개인데 그 항목들 모두 문제가 있다면 상황에 따라 문제가 있는 1개의 항목만 Request 보낸 쪽에서 알 수 있습니다. String?로 선언해서 DTO에 데이터를 담는 과정에서 문제를 발생시키지 않는다면 Validation으로 모든 항목을 검사하고 모든 문제를 Request 보낸 쪽에 알려 줄 수 있습니다.

{
    "resultCode": "ERROR",
    "data": {
        "loginId": "공백일 수 없습니다",
        "name": "공백일 수 없습니다",
        "birthDate": "날짜형식(YYYY-MM-DD)을 확인해주세요",
        "password": "영문, 숫자, 특수문자를 포함한 8~20자리로 입력해주세요",
        "gender": "MAN 이나 WOMAN 중 하나를 선택해주세요",
        "email": "올바른 형식의 이메일 주소여야 합니다"
    },
    "message": "에러가 발생했습니다."
}

그래서 회원가입쪽을 보시면 Validation으로 처리했을때의 장점을 보실 수 있습니다.

 

  1. 1번과 같은 접근이라면 UpdateDto의 경우에 만약 모든 값을 수정하는 것이 아니라 일부 프로퍼티에만 값이 담겨져서 오면 이럴 때는 String? 타입을 사용해야 되는 것일까?

=> 저의 서비스 운영 관점에서 본다면 update를 해야하는 경우 일부 값이 변경이 되어도 항목들 전체를 받아서 전체를 update치는게 좋다고 생각합니다.

일부를 update하게 되면 더 효율적으로 보일 수 있겠으나 각 항목의 변경시점과 변경자가 달라져 문제가 발생했을때 원인을 찾아가는 것에 추가적인 리소스가 발생한다고 생각합니다.

일부 프로퍼티만 값을 업데이트 치는 것으로 예를 들어 보겠습니다.

name : "홍길동", age : 30 라는 정보를

A유저는 name = "임꺽정"

B유저는 age = 40으로 업데이트치는 Request를 같은 시점에 보냈다고 하면

A의 입장에서는 name만 변경했는데 age까지 변경이 된 것으로 보일겁니다. B도 마찬가지 입장이겠죠.

 

상황에 따라 다르지만 2가지 update 가 넘어올때 최종 변경 데이터를 가지고 있도록 만드는 경우가 많습니다.

그래서 전체 항목을 받아서 현재 데이터에 업데이트를 치는 것이죠

A유저의 업데이트가 먼저 실행됐다면 B유저의 name = "홍길동", age = 40으로 덮어씌워 최종적으론 B유저의 정보만 남게 됩니다.

예시가 적절하지 않았을 수 있으나 이런 관점에서 본다면 전체 프로퍼티를 받게 되는 것이고 각 프로퍼티의 성격에 따라서 타입과 Validation을 지정하는 것이 좋은 방법이라고 생각됩니다.

 

질문에 대한 적절한 답이 되셨으면 좋겠습니다.

저도 JUNN님 덕분에 해왔던대로 그냥 개발을 하지 않았는지 이 부분은 왜 이렇게 사용했었는지 다시 한번 생각하게 됐습니다. 감사합니다.

 

앞으로 또 궁금하신점 생기시면 언제든지 질문 부탁드립니다.

감사합니다 :)

 

 

친절하고 자세한 답변 정말 감사드립니다. 덕분에 도움이 많이 되었습니다! 저도 이 부분에 대해서는 계속해서 고민을 해봐야겠습니다. 다음 번에 또 좋은 강의 기대하겠습니다 ^_^

1

안녕하세요 JUNN님 칭찬과 함께 질문까지 주셔서 감사합니다.

저도 강의 자료 만들면서 DTO에서 Validation 처리를 어떻게 하는 것이 합리적일지 고민을 했는데요. 그래서 그런지 질문이 더욱 와닿았습니다.

 

강의시 만들었던 LoginDto 기준으로 다른 방안을 말씀 드려보겠습니다.

data class LoginDto(
    @field:NotBlank
    @JsonProperty("loginId")
    private val _loginId: String?,

    @field:NotBlank
    @JsonProperty("password")
    private val _password: String?,
) {
    val loginId: String
        get() = _loginId!!
    val password: String
        get() = _password!!
}

제가 DTO에 이런식으로 만들었던 몇가지 이유가 있었습니다.

  1. DTO에서 Validation처리를 한다.

  2. Validation 체크시 빈값이 아닌게 확인되면 Service에서 해당 DTO를 쓸때 null이 아님을 보장한다.

  3. Request의 body에 아래와 같이 넘어오는 모든 데이터는 빈값으로 여긴다.

    1. 해당 key의 값이 null로 넘어오는 경우

      { "loginId":null, "password":null }
    2. 해당 key의 값이 ""로 넘어오는 경우

      { "loginId":"", "password":" " }
    3. 해당 key가 안넘어오는 경우

      {}

       

그래서 처리 순서를 아래와 같이 잡았습니다.

  1. DTO에 데이터 담기(값이 어떻게 넘어올지 모르니 null을 허용)

  2. Validation 체크

  3. 뽑아쓸때 null 불가 custom getter

 

그래서 이번에 data class가 아닌 class를 사용해서 DTO를 만들었습니다.

변경한 코드는 아래와 같습니다.

class LoginDto(
    loginId: String?,    // 1
    password: String?,
) {
    @field:NotBlank    // 3
    val loginId: String = loginId ?: ""    // 2

    @field:NotBlank
    val password: String = password ?: ""
}
  1. 생성자에서 null 허용타입으로 값을 받아오기

  2. 그것을 프로퍼티에 넣어줄때 elvis operator( ?: ) 사용해서 default 값을 ""으로 넣어주기

  3. Validation 체크

 

이렇게 하면 해당 DTO의 값을 뽑아쓸때 null 불가 타입이여서 null이 아님을 보장하게 됩니다.

이 방법은 data class를 사용하기는 어렵습니다.

data class는 기본적으로 생성자에 선언된 프로퍼티 기준으로 equals, hashCode, toString 등의 함수를 생성하는데요. 지금 제시한 방법은 본문에 프로퍼티를 선언하면서 처리를 하게 한 것이여서 data class를 사용해서 하는 방식으로는 어려울듯 합니다.

 

<추가>

  • 해결 방법은 아니고 이것저것 해보면서 나온 과정 중에 아직 풀지 못한 부분이지만 공유 드립니다.

 

제가 처음에 생성자에서 바로 프로퍼티를 선언하면서 처리할 수 있는 방법이 없을지 이것저것 해봤었는데요.

class LoginDto(
    @field:NotBlank
    val loginId: String,

    @field:NotBlank
    val password: String,
)

위 코드를 JAVA로 바꿔서 생성자 부분을 보면 아래와 같습니다.

public LoginDto(@NotNull String loginId, @NotNull String password) {
   Intrinsics.checkNotNullParameter(loginId, "loginId");
   Intrinsics.checkNotNullParameter(password, "password");
   super();
   this.loginId = loginId;
   this.password = password;
}

여기서 필드에 데이터를 담아야 Validation 체크를 할텐데

그 전에 null 체크가 실행되면서 Validation 체크 전에 error를 떨어뜨립니다.

Service에서 해당 DTO의 값을 불러올때 null 허용되지 않는 타입으로 가져오기위해 String을 쓰니 생성자에서 이미 체크를 해버리고 error를 발생시키더라구요.

그래서 이렇게 하니 애초에 DTO에 데이터를 담는 과정에서 문제가 발생했습니다.

 

그래서 말씀드린 다른 방법처럼 전달받는 값은 null 허용으로 받으면서 프로퍼티에 넣어줄때 null인 경우 default 값으로 ""을 넣었습니다.

그렇게 하면 JAVA로 변경한 생성자가 아래와 같이 만들어집니다.

public LoginDto(@Nullable String loginId, @Nullable String password) {
      String var10001 = loginId;
      if (loginId == null) {
         var10001 = "";
      }

      this.loginId = var10001;
      var10001 = password;
      if (password == null) {
         var10001 = "";
      }

      this.password = var10001;
   }

 

제 답변이 도움이 되셨으면 좋겠습니다. 이후 더 괜찮은 방법 찾게되면 공유 드리겠습니다.

강의 봐주시고 이렇게 질문까지 주셔서 다시 한번 감사드립니다 :)

친절한 답변 감사드립니다! 자세하게 답변을 주셔서 보는데 이해가 잘 되었습니다.

한 가지 궁금한 걸 질문을 드릴까 하는데요. 자바에서 Validation을 처리할 때 String 타입 필드의 경우 @NotBlank를 자주 사용했습니다. 그러면 null 이거나 "" 의 경우에는 에러를 유발한다는 것은 선생님도 잘 아실 것이라 생각합니다.

저의 경우에는 Request 요청을 처리하는 DTO의 경우 null이나 "" 값이 오면 에러를 뱉어내는게 조금 당연하다고 생각합니다. 왜냐하면 요청을 처리하는 데이터가 제대로 담겨있지 않다는 것이니까요.

이런 관점에서 생각해보면 선생님이 알려주신 이 방법에 대해 조금 더 질문을 드리고자 합니다.

class LoginDto(
    @field:NotBlank
    val loginId: String,

    @field:NotBlank
    val password: String,
)

이 부분을 설명해주실 때 에러를 뱉어낼 때 Validation 체크 하기도 전에 에러를 뱉어낸다고 하셨는데, 이게 NotBlank에 걸리지 않고 다른 에러를 뱉어낸다는 것인지 그래서 Validation으로서의 역할을 제대로 수행하지 못 하고 있다는 말씀이신지 궁금합니다.

또 하나 제가 생각하는 이 관점으로는 RequestDto의 경우는 모든 값이 담겨져 있어야 하기 때문에 null을 꼭 허용해주지 않아도 되지 않다! 라는 접근이 가능하지만 UpdateDto의 경우는 또 다를 것 같기도 해서 올바른 접근인지 조금 확신이 잘 안 서기도 합니다. UpdateDto의 경우는 수정하려고 하는 부분에만 값이 담겨져 올 수 있으니까요..! (프론트엔드에 쪽에서 수정되지 않는 필드도 기존 값을 넣어서 보내준다면 상관이 없지 않을까? 하는 생각을 해보기도 했습니다.)

글이 길어졌는데 요약을 해서 다시 말씀을 드려보자면 이렇게 될 것 같습니다.

  1. RequestDto의 경우 모든 값이 다 담겨져 있어야 한다면 왜 꼭 null 또는 "" 을 허용해줘야 하는가? 이걸 허용 안 해준다면 String? 이 아닌 String을 사용하면 안 되는 것일까?

  2. 1번과 같은 접근이라면 UpdateDto의 경우에 만약 모든 값을 수정하는 것이 아니라 일부 프로퍼티에만 값이 담겨져서 오면 이럴 때는 String? 타입을 사용해야 되는 것일까?

 

아직 프로그래밍 자체도 초보이고 코틀린도 처음이라 낯서네요. 답변 해주시면 도움 많이 될 것 같습니다! 감사합니다 (꾸벅)