• 이 글은 StackOverflow에 게시된 What's the point of input type in GraphQL?을 번역한 글입니다.
  • 오역 또는 의역이 있을 수 있습니다. 양해 부탁드리며, 수정할 필요한 부분은 댓글로 요청해주세요.
  • 가독성을 위하여 마크다운을 일부 추가했습니다.

질문 - Dimitry

뮤테이션의 입력 인자가 객체라면, input 타입이어야 하는 이유가 무엇인가요? 단지 id를 제공하지 않는 상태에서 type을 재사용하는 것이 훨씬 단순한 것 같습니다.

예를 들어, 아래와 같은 코드처럼 말입니다.

type Sample {
  id: String
  name: String
}

input SampleInput {
  name: String
}

type RootMutation {
  addSample(sample: Sample): Sample # <-- 이것 대신 아래를 사용
  addSample(sample: SampleInput): Sample
}

객체가 단순하면 괜찮겠지만, 속성이 10개 넘는 객체가 스키마 내에 아주 많이 존재한다면 이는 꽤 부담스러울 것 같습니다.

질문에 대한 댓글

  • 입력 객체는 반드시 직렬화가 가능한 것이어야 합니다. 출력 객체는 순환이 포함될 수 있으므로, 입력으로 재사용될 수 없습니다. - Jesse Buchanan

답변 1 - LB2

Jesse의 댓글이 맞습니다. 보다 공식적인 답변으로, input 타입에 대한 GraphQL 문서에서 인용했습니다.

위에서 정의된 Object 타입은 재사용하기 부적합합니다. 왜냐하면 Object이 포함하는 필드는 순환 참조 또는 인터페이스 및 유니온에 대한 참조를 표현할 수 있는데, 둘 모두 입력 인자로 사용하기엔 부적합하기 때문입니다. 이런 이유로 input 객체는 별도의 타입을 가집니다.

UPDATE

답변을 게시한 뒤, nullable하다면 참조 순환이 입력에 사용될 수 있다는 것을 알았습니다(그렇지 않다면 무한 참조가 발생합니다). 하지만, 여전히 인터페이스와 같은, 입력을 위한 별도의 타입 시스템이 필요한 다른 제한 사항들이 존재합니다.

답변 1에 대한 댓글


답변 2 - Daniel Rearden

공식 명세에 따르면,

GraphQL Object 타입(ObjectTypeDefinition)은 … (입력으로서) 재사용이 부적절합니다. 왜냐하면 Object이 포함하는 필드는 순환 참조 또는 인터페이스 및 유니온에 대한 참조를 표현할 수 있는데, 둘 모두 입력 인자로 사용하기엔 부적합하기 때문입니다. 이런 이유로 input 객체는 별도의 타입을 가집니다.

이것이 "공식적인 이유"입니다. 하지만 Object 타입을 input 타입을 대신하여 사용하거나, 반대로 input 타입을 Object 타입을 대신하여 사용하면 안 되는 실무적인 이유가 몇 가지 더 존재합니다.

기능

Object 타입과 input 타입은 둘 다 필드를 가지지만, 이 필드들은 각 타입이 스키마 내에서 사용되는 방식에 따라 서로 다른 특성을 가집니다. 스키마에는 인자, 그리고 해당 Object 타입이 가지는 필드에 대한 리졸버 함수를 정의합니다. 그런데 이러한 특성은 input 타입의 맥락에서는 전혀 통하지 않습니다. 예를 들어, input 타입의 필드는 리졸브할 수 없습니다. 이미 명시적인 값을 가지기 때문입니다. 이와 비슷하게, 기본값이라는 개념 또한 input 타입에는 제공되지만, Object 타입의 필드에서는 사용할 수 없습니다.

다시 말해, 아래의 두 타입은 같은 역할을 같는 것처럼 보일 것입니다.

type Student {
  name: String
  graade: Grade
}

input StudentInput {
  name: String
  grade: Grade
}

하지만 각 타입에서만 사용되는 고유한 기능을 추가해보면, 두 타입이 서로 다르게 동작한다는 것이 확실해집니다.

type Student {
  name(preferred: Booleean): String
  graade: Grade
}

input StudentInput {
  name: String
  grade: Grade = F
}

타입 시스템의 제한

GraphQL의 타입은 출력 타입입력 타입으로 구분됩니다.

출력 타입은 GraphQL 서비스가 만들어내는 응답의 일부로서 반환되는 타입입니다. 반면 입력 타입은 필드 또는 지시자 인자에 대한 입력으로 유효하게 사용할 수 있는 타입입니다.

이 두 분류 간에 겹치는 부분이 존재합니다(예를 들어, 스칼라, 열거, 리스트, Non-null(!) 등). 하지만, 유니온과 인터페이스와 같은 추상 타입은 입력의 상황에서는 성립할 수 없으므로 입력으로 사용될 수 없습니다. Object 타입과 input 타입을 분리하면, input 타입이 사용되어야 할 곳에 추상 타입이 사용되는 일이 없도록 보장할 수 있습니다.

스키마 설계

스키마에 엔티티을 표현할 때, 입력 타입과 출력 타입 간에 실제로 "필드를 공유하는" 경우가 발생할 수도 있습니다. 아래의 경우를 보시죠.

type Student {
  firstName: String
  lastName: String
  grade: Grade
}

input StudentInput {
  firstName: String
  lastName: String
  grade: Grade
}

하지만, Object 타입은 아주 복잡한 데이터 구조를 모델링할 수 있습니다. 그리고 실제로도 자주 그렇게 사용되죠.

type Student {
  fullName: String!
  classes: [Class!]!
  address: Address!
  emergencyContact: Contact
  # etc
}

위와 같은 구조가 적절한 input으로 변환될 수도 있겠으나(그렇게 되면 Student를 생성할 때, 주소를 가지는 객체를 전달할 것입니다), 대부분의 경우 그렇지 못합니다. 아마도 Studentclasses에 대하여 class의 ID를 지정해야 하지, 객체를 전달하면 안 될 겁니다. 비슷하게, 일부 필드의 경우 반환만 필요하고 수정하면 안 되는 것이 존재할 것입니다. 또는 그 반대의 경우인, 수정은 가능하지만 반환해서는 안 되는 필드도 존재할 것입니다(password 필드처럼 말이죠).

더 나아가서 상대적으로 간단한 엔티티의 경우에도, Nullability를 다룰 때에도 Object 타입과 이에 "대응하는" type 객체 간에 요구 사항이 다른 경우가 많습니다. 예를 들어, 어떤 필드에 대하여 응답에서는 반환되지만 input으로는 반드시 필수로 전달할 필요는 없도록 보장해야만 하는 경우를 생각해봅시다.

type Student {
  firstName: String!
  lastName: String!
}

input StudentInput {
  firstName: String
  lastName: String
}

마지막으로, 많은 스키마에서는 엔티티에 대하여 Object 타입과 input 객체 타입 간에 일대일 대응이 이루어지지 않는 경우가 많습니다. 일반적인 패턴은 각각의 동작마다 별도의 input 객체 타입을 사용하여, 장기적으로 스키마 수준의 입력값 검사가 용이해지도록 코드를 작성하는 것입니다.

input CreateUserInput {
  firstName: String!
  lastName: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  email: String
  password: String
}

위의 예시들은 전부 중요한 점을 시사하고 있습니다. 즉, input 객체 타입은 Object 타입을 반영해야 하지만, 실제 서비스 상황에서는 비즈니스 요구 사항을 반영하기 위하여 그러지 못하는 경우가 많다는 것입니다.

참고