ID 중복확인 API의 응답 형식 정하기 { isDuplicate: true } vs. { errorCode: DUPLICATE_ID }

JoonyJoony
5 min read

서버에 검증을 요청하는 검증 API 중 누구나 떠올릴 수 있는 것으로 ID 중복여부 확인 API가 있다. 이 글에서는 ID 중복여부 확인 API를 예시로 검증 API의 응답형식을 디자인하는 2가지 방법과 각각의 특징을 설명한다.

ID 중복여부확인 API의 2가지 형식

ID의 중복여부를 확인하는 기능은 일반적으로 사용자가 가입과정에서 ID를 입력하고 [중복확인] 버튼을 클릭하거나 ID 입력상자에서 Focus out되는 시나리오에서 사용된다. 그리고 이를 다음 2가지 형식으로 구현할 수 있다.

1. 중복'여부' 요청 (isDuplicateXxx 형식)

RESTful URI: /users/is-duplicate-id?id=${id}

  • 클라이언트 관점

    클라이언트가 중복'여부'를 요청하는 행위는 '내가 이 ID를 사용하려고 하는데 중복되지는 않는지 알려주세요.'의 의미를 가지고 있고, 그 요청의 결과는 일반적으로 "사용할 수 있는 ID입니다." 또는 "이미 사용 중인 ID입니다."라는 메시지이다.

    사용자는 중복확인을 요청하면서 두 가지 결과(중복, 비중복)가 나올 수 있음을 인지하고 있다. 심지어 소위 레어닉(Rare Nickname)을 찾아내기 위해 의도적으로 이미 존재할 가능성이 높은 ID부터 확인을 요청할 수도 있다. 이때 사용자는 중복이 아니라는 응답을 받길 원하지만, 중복 응답을 받길 '기대(예상)'한다. 이때 서버로부터 Response Body로 { isDuplicateId: true/false }를 받으면 자연스럽다.

  • 서버 관점

    클라이언트의 요청을 받아 서버가 실질적으로 수행하는 작업은 SELECT 1 FROM user WHERE id = :id이다. 본질적으로 전달받은 ID를 단순히 검색하는 것이다. 그리고 그 결과는 '검색해봤는데 있었다' 또는 '검색해봤는데 없었다' 이다. 그런데 '검색해봤는데 있었다'를 예외로 취급하는 건 아무래도 어색하게 느껴진다. 이때 클라이언트에게 Response Body로 { isDuplicateId: true/false }를 응답하면 자연스럽다.

💡
클라이언트는 처리되길 기대했지만, 서버는 다양한 사유로 인해 처리할 수 없는 경우를 '예외'라고 정의하면 대체로 맞을 것이다.

2. 비중복'검증' 요청 (validateNonDuplicateXxx 형식)

RESTful URI: /users/validate-non-duplicate-id?id=${id}

필자는 처음 중복'여부' 요청 형식으로 API를 작성했다. 그게 자연스럽다고 생각했기 때문에. 그런데 중복검사에 실패할 경우는 예외응답을 줘야 Front-end에서 일관적으로 예외로 다룰 수 있어 좋지 않겠냐는 의견을 수렴하여 고민 끝에 Validate 동사를 사용하는 이 형식을 고안했다. (이 때는 Front-end의 코드가 어떤 모습일지 생각해보지 않았다.)

관습적으로 validate 동사는 검증을 수행하며1 검증이 성공할 경우 아무 변화를 유발하지 않고 종료되고, 검증이 실패할 경우만 예외를 던지는 상황에 사용한다. 이를 RESTful URI에 적용하면, 검증성공시 200 OK를 응답하고, 검증실패시 예외응답하는 API를 표현할 수 있다. 이 경우는 '중복이 아닌지 검증해주세요(Validate non-duplicate.).'로 해석하면 된다.

  • 서버 관점
    서버가 수행하는 작업은 중복'여부' 요청 때와 동일하다. 그런데 이번엔 '검색해봤는데 있었다(중복됐다)'면 예외를 던져 다음과 같은 Response Body를 만들어 응답해야 한다.

      {
          errorCode: "THIS_ID_IS_ALREADY_IN_USE",
          errorMessage: "요청한 ID는 이미 사용 중입니다."
      }
    

    이때 요청이 정상적으로 처리되지 않았음을 의미하는 HTTP Status code를 응답해야 하는데, 찾아보면 알겠지만 마땅히 사용할만한 게 없다. 400 Bad Request는 클라이언트가 잘못 요청한 것이 아니라서, 그리고 409 Conflict2 는 충돌을 유발한 것이 아니라 충돌여부를 확인만 한 거라서 어색하다. 그렇다고 500 Internal Server Error는 더 이상하다.

2. 409 Conflict: 이 응답은 요청이 현재 서버의 상태와 충돌될 때 보냅니다. (https://developer.mozilla.org/ko/docs/Web/HTTP/Status)

  • 클라이언트 관점

    Front-end에서는 예외응답(4XX, 5XX)을 받았을 때, 예외객체를 읽어 errorCodeTHIS_ID_IS_ALREADY_IN_USE일 때만 정상적인 비즈니스 로직을 태워야 한다. 이와 달리 클라이언트가 잘못 요청해서 받은 400 Bad Request, 서버에 정의된 예외가 발생해서 받은 5XX 등의 예외응답에 저마다의 예외처리 로직이 존재할 수 있는데 이들은 예외 핸들러에서 처리하는 것이 자연스러울 것 같다.

    200 OK일 때는 비즈니스 로직에서 "사용할 수 있는 ID입니다." 메시지를 보여주고, THIS_ID_IS_ALREADY_IN_USE일 때는 예외 핸들러에서 "이미 사용 중인 ID입니다." 메시지를 보여줄텐데, 개인적으로는 이 모두가 비즈니스 로직에 존재하는게 자연스럽게 느껴진다.

  • [참고] 회원가입 API의 형식

    회원가입을 요청할 때 클라이언트는 가입이 처리되길 '기대'한다. 회원가입 정책(필수값, ID제약조건, 비밀번호 제약조건 등)에 의해 Front-end가 요구한 모든 제약조건을 준수했을 것이기 때문이다. 그럼에도 불구하고 서버에서 ID 중복이라는 처리할 수 없는 사유가 발생한다. 이때는 Response Body로 { errorCode: THIS_ID_IS_ALREADY_IN_USE } 예외를 응답하면 자연스럽다. 그리고 비중복'검증' 요청과 달리 409 Conflict 를 사용하는 것도 자연스럽다.

[isDuplicateXxx] vs. [validateNonDuplicateXxx]

isDuplicateXxx 형식validateNonDuplicateXxx 형식
RESTful URI/users/is-duplicate-id?id=${id}/users/validate-non-duplicate-id?id=${id}
중복시 응답200 OK400 Bad Request(?)
{ isDuplicateId: true }{ errorCode: THIS_ID_IS_ALREADY_IN_USE, ... }
중복아닐시 응답200 OK200 OK
{ isDuplicateId: false }-

결론

RESTful URI Design 관점에서 보면 validateNonDuplicateXxx 형식은 성공적으로 처리한 요청에 대해 실패를 응답해야 하기 때문에 어색함이 있고, 의미상 결점이 없는 isDuplicateXxx 형식이 자연스럽게 느껴진다.

하지만 Front-end의 공통부 구현이나 프로젝트 표준이 앞서 언급한 것과 다른 경우엔, validate 동사를 사용하는게 상황에 맞는 URI Design이 될 수도 있다. 이제는 validate라는 개념을 활용하여 유연하게 Design 해보자. 그리고 당연한 얘기지만 선택의 근거가 단순히 'Front-end 개발이 편하기 때문에'가 이유가 돼서는 안 될 것이다.

사례 조사

네이버 회원가입 중 ID 중복확인 요청 (is 동사 사용)

  • 중복된 ID인 "sample"을 검증요청시 200 OK Status code에 검증실패를 의미하는 "NNNNN"이라는 Response body가 온다. (뒤에서 나오지만 "NNNNY"가 검증성공)

  • 중복되지 않은 ID인 "sample098765"를 검증요청시 200 OK Status code에 검증성공을 의미하는 "NNNNY"이라는 Response body가 온다.

GitHub 회원가입 중 ID 중복확인 요청 (validate 동사 사용)

  • 중복된 이메일인 "devil@gmail.com"을 검증요청시 422 Unprocessable Content3 Status code에 검증실패 메시지를 담은 Response body가 온다. (바로 렌더링 가능하도록 HTML 요소가 오는게 조금 특이하다.)

3. 422 Unprocessable Content: "요청은 잘 만들어졌지만, 문법 오류로 인하여 따를 수 없습니다." 이게 상황에 맞는 Status code 같진 않은데… (https://developer.mozilla.org/ko/docs/Web/HTTP/Status)

  • 중복되지 않은 이메일인 "somevalidmail@gmail.com"을 검증요청시 Response body 없이 200 OK Status code만 온다.


1. 관습적 Validate 동사 사용 예시:

@Spring Batch: org.springframework.batch.core.job.DefaultJobParametersValidator#validate()

public void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException {
    if (parameters == null) {
        throw new JobParametersInvalidException("The JobParameters can not be null");
    } else {
        Set<String> keys = parameters.getParameters().keySet();
        HashSet missingKeys;
        Iterator var4;
        String key;
        if (!this.optionalKeys.isEmpty()) {
            missingKeys = new HashSet();
            var4 = keys.iterator();

            while(var4.hasNext()) {
                key = (String)var4.next();
                if (!this.optionalKeys.contains(key) && !this.requiredKeys.contains(key)) {
                    missingKeys.add(key);
                }
            }

            if (!missingKeys.isEmpty()) {
                throw new JobParametersInvalidException("The JobParameters contains keys that are not explicitly optional or required: " + missingKeys);
            }
        }

        missingKeys = new HashSet();
        var4 = this.requiredKeys.iterator();

        while(var4.hasNext()) {
            key = (String)var4.next();
            if (!keys.contains(key)) {
                missingKeys.add(key);
            }
        }

        if (!missingKeys.isEmpty()) {
            throw new JobParametersInvalidException("The JobParameters do not contain required keys: " + missingKeys);
        }
    }
}
0
Subscribe to my newsletter

Read articles from Joony directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joony
Joony