개념, 아이디어, 전략
개념, 아이디어, 전략중첩 뮤테이션 설명

중첩 뮤테이션 설명

뮤테이션은 게시물 작성, 사용자 이름 업데이트, 게시물에 댓글 추가 등과 같이 GraphQL 서버의 데이터를 변경할 수 있는 작업입니다.

GraphQL에서 뮤테이션은 다음과 같이 MutationRoot 타입에만 공개됩니다.

type MutationRoot {
  createPost(id: ID!, title: String!, content: String): Post!
  updateUserName(userID: ID!, newName: String!): User!
  addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}

(이 가이드의 GraphQL 스키마는 예시를 설명하기 위한 것이며, 플러그인이 제공하는 스키마와는 다릅니다.)

이 스키마를 사용하면 사용자 이름 변경은 다음과 같이 이루어집니다.

mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

뮤테이션이 mutation root object type에만 공개되는 것은, GraphQL 사양에서 설명된 바와 같이 직렬 실행을 강제하기 위해서입니다.

It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.

"직렬 실행"이라는 용어는 "병렬 실행"의 반대말이며, 병렬 실행은 필드를 해결할 때 권장되는 동작입니다.

예를 들어, 아래 쿼리에서 GraphQL 서버가 어느 필드(name 또는 email)를 먼저 해결하는지는 중요하지 않으며, 이들은 병렬로 해결될 수 있습니다.

query {
  user(by: { id: 37 }) {
    name
    email
  }
}

그러나 뮤테이션은 데이터를 변경하기 때문에 필드가 해결되는 순서가 중요하며, 직렬 실행이 필요합니다(그렇지 않으면 경쟁 조건이 발생할 수 있습니다).

예를 들어, 아래 두 쿼리는 서로 다른 결과를 생성합니다.

# 쿼리 1: 실행 후 사용자 이름은 "John"이 됩니다
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# 쿼리 2: 실행 후 사용자 이름은 "Peter"가 됩니다
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

뮤테이션을 MutationRoot를 통해서만 공개하면 이 타입이 비대해지는 결과를 초래합니다. 직렬 실행이 필요하다는 기술적인 이유 외에는 공통점이 없는 필드들이 혼재하게 되며, 이는 인터페이스 설계 결정이 아닌 기술적인 문제입니다.

중첩 뮤테이션의 필요성

위의 뮤테이션 중에서 createPost만이 진정으로 MutationRoot 타입에 속합니다. 왜냐하면 이것은 아무것도 없는 상태에서 새로운 요소를 생성하기 때문입니다. 반면 updateUserNameaddCommentToPost는 다른 타입의 기존 엔티티에 적용되는 동등한 작업을 충분히 가질 수 있습니다.

type User {
  updateName(newName: String!): User!
}
 
type Post {
  addComment(comment: String!, userID: ID): Comment!
}

이 스키마를 사용하면 사용자 이름 변경은 다음과 같이 이루어질 수 있습니다.

mutation {
  user(ID: 37) {
    updateName(newName: "Peter") {
      name
    }
  }
}

이 기능을 "중첩 뮤테이션"이라고 합니다. 쿼리 또는 뮤테이션인 다른 작업의 결과에 뮤테이션을 적용하는 것입니다.

중첩 뮤테이션을 사용하면 GraphQL 스키마가 얼마나 우아해지는지 주목해 주세요.

  • MutationRoot.updateUserName 작업은 사용자의 ID를 받아야 하지만, 동등한 User.updateName 작업은 사용자 엔티티에서 실행되므로 ID를 받을 필요가 없습니다
  • 필드 이름이 updateUserName에서 updateName으로 축약됩니다

또한, 그래프 내의 엔티티 간을 탐색하여 데이터를 쿼리하는 것과 동일한 방식으로 데이터를 수정할 수 있으므로, GraphQL 서비스가 더 단순하고 이해하기 쉬워집니다.

중첩 뮤테이션은 여러 레벨에 걸쳐 이루어질 수 있습니다. 예를 들어, 단일 쿼리 내에서 새로 작성된 게시물에 댓글을 추가할 수 있습니다.

mutation {
  createPost(ID: 37, title: "Hello world!", content: "Just another post") {
    id
    addComment(comment: "Lovely post") {
      id
    }
  }
}

이로 인해 중첩 뮤테이션은 여러 요소를 뮤테이션하기 위해 여러 쿼리를 실행하는 것에서 단일 쿼리를 실행하는 것으로 왕복 지연 시간을 줄임으로써 성능을 향상시킬 수도 있습니다.

중첩 뮤테이션이 사양에 포함되지 않은 이유

GraphQL 사양은 모든 언어의 GraphQL 서버 구현 모두에서 작동하도록 설계되었습니다. 그러나 그 추진력은 참조 구현인 graphql-js를 통한 JavaScript입니다.

다시 말해, graphql-js에서 지원할 수 없는 기능은 사양의 일부가 되지 않습니다.

JavaScript가 promises를 지원하기 때문에 필드의 병렬 해결이 가능해졌고, 병렬 처리는 graphql-js를 처음 설계할 때의 근본 원칙 중 하나가 되었습니다. 이는 배치 함수가 JavaScript promises를 반환하는 데이터 페칭 레이어인 DataLoader에서 잘 나타납니다.

병렬 실행의 성능상 이점은 매우 많으며, 중첩 뮤테이션은 병렬 처리와 함께 작동할 수 없습니다. 병렬 실행을 중첩 뮤테이션과 맞바꾸는 것은 가치가 없다고 판단되었습니다.

중첩 뮤테이션과 성능

Gato GraphQL 플러그인에서 필드는 항상 직렬로 해결되며, 해결되는 순서는 결정론적입니다. (이 특성은 쿼리 해결 성능에 영향을 주지 않습니다. 서버가 먼저 쿼리의 그래프를 컴포넌트 모델로 변환하고, 이를 최적의 선형 시간으로 해결하기 때문입니다.)

이는 플러그인이 중첩 뮤테이션을 지원하여 모든 이점을 제공하면서도 그 부작용을 겪지 않음을 의미합니다.

GraphQL 사양

이 기능은 현재 GraphQL 사양의 일부가 아니지만, 다음에서 요청되었습니다.