GraphQL 명세 ⑤: 실행 전에 쿼리를 거르는 검증(Validation)

GraphQL 명세 ⑤: 실행 전에 쿼리를 거르는 검증(Validation)

GraphiQL에서 존재하지 않는 필드를 입력하면, 실행하기도 전에 빨간 밑줄이 그어지면서 “그런 필드는 없다”고 알려주는 걸 보셨을 텐데요. 서버에 요청을 보내지도 않았는데 어떻게 틀렸다는 걸 알았을까요? 바로 GraphQL이 쿼리를 실행하기 전에 한 단계를 더 거치기 때문입니다. 그 단계가 이번 편의 주제인 검증(Validation) 입니다.

지난 편에서 본 인트로스펙션으로 스키마 정보를 손에 쥐면, 어떤 쿼리가 그 스키마에 맞는지 안 맞는지를 실행 없이도 판단할 수 있습니다. 명세 제5장은 바로 이 “맞는 쿼리”의 조건을 규칙으로 정리해둔 장인데요. 이번 편에서는 대표적인 검증 규칙들을 일부러 틀린 쿼리와 함께 살펴보겠습니다.

왜 실행 전에 검증할까요

GraphQL은 쿼리를 받으면 곧장 실행하지 않습니다. 먼저 문법에 맞는지 파싱하고(2편의 Language), 그다음 스키마에 비추어 의미가 올바른지 검증하고(이번 편), 그러고 나서야 실행합니다(다음 편). 이 검증 단계의 핵심은 정적(static) 이라는 점인데요. 실제 데이터를 건드리지 않고, 오직 쿼리와 스키마만 비교해서 옳고 그름을 판단합니다.

이렇게 실행 전에 거르는 데는 분명한 이점이 있습니다. 잘못된 쿼리를 끝까지 실행하다 중간에 터지는 대신, 시작도 하기 전에 명확한 에러로 막을 수 있으니까요. 게다가 검증은 데이터와 무관하므로, 같은 쿼리에 대한 검증 결과를 캐싱해두고 재사용할 수도 있습니다. 무엇보다 GraphiQL 같은 도구가 우리가 타이핑하는 즉시 오류를 잡아주는 것도, 검증이 서버 없이 스키마만으로 가능하기 때문입니다.

필드는 그 타입에 실제로 있어야 합니다

가장 기본적인 규칙은 “요청한 필드가 그 타입에 정의되어 있어야 한다”입니다. 너무 당연해 보이지만, 검증 규칙의 절반은 이 한 문장에서 파생됩니다.

{
  user(id: "1") {
    name
    age
  }
}

만약 3편에서 정의한 User 타입에 age 필드가 없다면, 이 쿼리는 검증을 통과하지 못합니다.

에러 응답
{
  "errors": [
    {
      "message": "Cannot query field \"age\" on type \"User\"."
    }
  ]
}

비슷한 규칙으로, 객체 타입을 반환하는 필드는 반드시 하위 필드를 가져야 하고(예: user만 쓰고 끝내면 안 됨), 반대로 스칼라 필드는 하위 필드를 가질 수 없습니다(예: name { something }은 안 됨). 2편에서 잠깐 언급했던 그 규칙이 바로 여기 검증 단계에서 실제로 작동하는 거죠.

인자와 변수도 타입이 맞아야 합니다

필드에 넘기는 인자도 검증 대상입니다. 인자 이름이 실제로 존재해야 하고, 넘긴 값의 타입이 스키마가 기대하는 타입과 호환되어야 합니다.

{
  user(id: 1) {
    name
  }
}

Userid 인자가 ID! 타입인데 정수 1을 넘기면, 타입이 맞지 않아 걸립니다. 또 2편에서 본 변수에도 비슷한 규칙이 적용되는데요. 선언한 변수는 반드시 어딘가에서 쓰여야 하고, 거꾸로 쓰는 변수는 반드시 선언되어 있어야 합니다.

query GetUser($id: ID!) {
  user(id: $userId) {
    name
  }
}

이 쿼리는 두 가지 규칙을 동시에 어깁니다. 선언한 $id는 어디서도 쓰이지 않고, 사용한 $userId는 선언된 적이 없으니까요. 또 변수의 타입도 그 변수가 쓰이는 자리와 호환되어야 합니다. ID! 자리에 Int 변수를 넘기면 검증에서 막힙니다. 이런 규칙들 덕분에, 우리는 실행 전에 “변수를 빠뜨렸다”거나 “타입을 잘못 맞췄다” 같은 실수를 미리 잡아낼 수 있습니다.

프래그먼트에도 규칙이 있습니다

2편에서 본 프래그먼트는 편리한 만큼 따라오는 규칙도 여럿입니다. 우선 정의한 프래그먼트는 반드시 한 번은 쓰여야 하고, 쓰는 프래그먼트는 반드시 정의되어 있어야 합니다. 변수 규칙과 똑 닮았죠.

특히 까다로운 건 순환 참조 금지입니다. 프래그먼트가 직접이든 간접이든 자기 자신을 다시 펼치면 안 됩니다.

fragment userFields on User {
  name
  friends {
    ...userFields
  }
}

언뜻 “친구의 친구의 친구…”를 표현한 것처럼 보이지만, 이건 무한히 펼쳐지는 순환이라 검증에서 거부됩니다. 만약 이런 쿼리가 실행 단계까지 갔다면 서버가 무한 루프에 빠질 수도 있는데, 검증이 그 전에 막아주는 셈입니다.

또 프래그먼트는 적용 대상 타입이 맞아야 합니다. fragment userFields on UserPost 타입 필드 안에서 펼치려 하면, 타입이 맞지 않아 걸립니다. 이런 규칙들은 결국 “펼쳐진 결과가 항상 말이 되는 쿼리여야 한다”는 한 가지 원칙에서 나옵니다.

같은 자리의 필드는 서로 충돌하면 안 됩니다

조금 미묘하지만 중요한 규칙이 하나 더 있습니다. 응답의 같은 위치에 모이게 될 필드들은 서로 모순되면 안 된다는 규칙인데요. 명세에서는 이를 “필드 병합 가능성(field merging)“이라고 부릅니다.

{
  user(id: "1") {
    name: nickname
    name: fullName
  }
}

2편에서 본 별칭을 써서 name이라는 같은 응답 키에 nicknamefullName이라는 서로 다른 필드를 담으려 하고 있습니다. 응답 객체에서 name 키 하나가 두 값을 동시에 가질 수는 없으니, 이런 충돌은 검증에서 막힙니다. 별칭이나 프래그먼트를 조합하다 보면 의외로 마주치기 쉬운 규칙이라 알아두면 좋습니다.

디렉티브는 정해진 자리에만 붙습니다

마지막으로 2편에서 본 디렉티브에도 규칙이 있습니다. 디렉티브는 정의된 위치에만 붙을 수 있고, 같은 위치에 같은 디렉티브를 중복해서 붙일 수 없습니다. 예를 들어 @skip@include는 필드나 프래그먼트에 붙도록 정의되어 있어서, 엉뚱한 위치에 쓰면 검증에서 걸립니다.

이처럼 검증 규칙들은 제각각 달라 보여도, 결국 “이 쿼리를 실행하면 스키마와 모순 없이 의미 있는 결과가 나오는가”라는 한 가지 질문을 여러 각도에서 던지는 것입니다. 그래서 검증을 통과한 쿼리는 실행 단계에서 “이런 필드가 없네”, “타입이 안 맞네” 같은 구조적 문제로 실패할 일이 없습니다. 실행기는 안심하고 데이터를 채우는 데만 집중하면 되는 거죠.

검증이 책임지지 않는 것

여기서 한 가지 오해를 짚고 넘어가야 합니다. 검증은 만능 방어막이 아닙니다. 검증은 어디까지나 정적, 즉 쿼리와 스키마만 비교하는 단계라서 실제 데이터나 실행 환경이 필요한 문제는 다루지 못하는데요.

예를 들어 user(id: "999")라는 쿼리는 문법도 맞고 타입도 맞으니 검증을 가뿐히 통과합니다. 하지만 id가 999인 사용자가 실제로 있는지는 데이터를 뒤져봐야 알 수 있으니, 검증이 아니라 실행(Execution) 단계에서야 판가름 납니다. 로그인한 사용자에게 이 필드를 보여줘도 되는지 같은 권한 문제도 마찬가지로 검증의 몫이 아닙니다. 검증은 “쿼리가 스키마에 맞는가”만 판단하지, “이 요청이 지금 허용되는가”까지는 판단하지 않기 때문인데요. 그래서 인증이나 권한, 존재 여부 같은 검사는 리졸버 안에서 따로 처리해야 합니다.

명세에는 이 밖에도 자잘한 규칙이 더 있습니다. 이름 없는 익명 연산은 한 문서에 하나만 있을 수 있고({ ... }를 두 개 나란히 둘 수 없습니다), 구독 연산은 최상위 필드를 정확히 하나만 가져야 한다는 규칙 같은 것들인데요. 모두 “실행할 때 모호함이 없도록” 미리 못 박아두는 장치라는 점에서 결이 같습니다.

마치며

이번 편에서는 GraphQL 명세 제5장 Validation을 따라, 쿼리가 실행되기 전에 거치는 정적 검증 규칙들을 살펴봤습니다. 필드는 그 타입에 실제로 있어야 하고, 인자와 변수는 타입이 맞아야 하며, 프래그먼트는 순환하면 안 되고, 같은 자리의 필드는 충돌하면 안 되고, 디렉티브는 정해진 위치에만 붙는다는 규칙들을 잘못된 쿼리와 함께 확인했는데요. 이 모든 규칙이 “실행하면 말이 되는 쿼리인가”라는 한 가지 원칙으로 수렴한다는 점도 짚어봤습니다.

검증을 통과한 깨끗한 쿼리는 이제 드디어 실행될 차례입니다. 다음 편 실행(Execution)에서는 서버가 이 쿼리를 어떻게 한 필드씩 처리해 데이터를 채워 넣는지, 그리고 그 과정에서 에러가 나면 어떻게 다루는지를 살펴보겠습니다. 특히 3편에서 예고했던 Non-Null의 흥미로운 동작이 거기서 등장합니다.

검증 규칙의 전체 목록과 형식적 정의는 GraphQL September 2025 명세의 Validation 장에 정리되어 있습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord