【AWS Amplify 노하우 시리즈】 3. GraphQL 스키마의 type 에 @model 을 붙이지 않았을때 주의할 점

2020.07.24

안녕하세요!ㅎㅎ Classmethod 컨설팅부 소속 김태우입니다.

Amplify 시리즈 세번째 글입니다! 이번 글에서는 GraphQL 스키마의 type 에 @model 을 붙이지 않았을 때 어떤 일들이 벌어지는 지 설명하고 주의할 점들에 대해 적어보겠습니다.

그럼 시작하겠습니다! :)

@model 을 왜 안붙여요??

Amplify 를 처음 시작하시는 분들은 주로 튜토리얼을 따라해보면서 type 마다 전부 @model 디렉티브를 붙이고 있을텐데요, 사실 디렉티브 각각에 대해 정확히 이해하지 않으면 Amplify 를 통한 AppSync 의 효용성을 100% 발휘하기 어렵습니다. (어떤 기술이든 스펙을 제대로 확인하는것이 중요하죠!)

이번 글에서는 @model 디렉티브에 대해 알아볼건데요, 여러분들 모두 짐작하셨다시피 @model 디렉티브를 붙인 type 에 대해서는, AppSync API 에서 해당 type 의 데이터소스로 DynamoDB 테이블을 등록시켜주고, 이 데이터소스 (DynamoDB)에 쿼리하기 위한 VTL 기반의 GraphQL resolver (Request/Response Mapping Template) 까지도 자동으로 생성해주게 됩니다.

쉽게말해서, type 옆에 @model 을 적어두는 것만으로도

  1. 데이터소스(DynamoDB) 를 생성하고 AppSync API 와 연계시켜준다.
  2. Create/Read/Update/Delete/List 오퍼레이션을 수행하기 위한 Query 및 Mutation 과, 각각이 필요로 하는 input 등의 타입을 자동으로 생성해준다.
  3. 생성된 Query 및 Mutation, 타입 필드 리졸버 등을 위한 Request/Response Mapping Templates 를 자동으로 생성해준다.
  4. 이렇게 생성된 CRUD(L) API 를 클라이언트 소스코드에서 사용하게 하기 위한 API 라이브러리를 자동으로 생성해준다. (amplify codegen)

와 같은 혜택들을 누릴 수가 있게됩니다.

그런데, 이렇게 좋은 걸 일부러 안쓰고 싶은 경우가 있을까요? @model 을 안붙이다니...?!

@connection 의 도입

네, @model 을 붙여줌으로서 정말 귀찮고 반복적인 작업들이 상당히 많이 자동화되고 개선되는 것을 잘 알 수 있었습니다. 그런데, 보통 애플리케이션을 개발하고 서비스 하려고 하면 복수개의 type 을 사용하는 것이 일반적입니다. 복수개라고 해서 2~3개를 의미하는 건 아니라는 사실을 누구나 다 알고 있을겁니다. 적게는 20~30개의 타입, 많게는 100개 이상의 타입도, 혹은 그것보다 훨씬 많은 수의 타입을 선언하고 사용하게 될 텐데요.

문제를 심플하게 해서 아래와 같은 스키마를 생각해봅시다.

type Post @model {
  id: ID!
  authorID: ID!
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  addressID: ID!
}

type Address @model {
  id: ID!
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

Post, User, Address 타입이 있는 상황에서의 시나리오입니다. 게시글(Post)을 쓸 사용자(User)와 사용자가 서비스 이용중에 등록하게 될 주소정보(Address)로 이루어진 임의의 서비스가 있다고 가정해보겠습니다.

위 스키마 대로라면 Post 타입에 대해 쿼리할 때 User 타입인 author 를 쿼리해오지 못하는 상황이 되어, 일일이 클라이언트에서 author 정보를 다시 쿼리를 해와야하는 상황이 되어버립니다. 이런식으로 처리할 거면 GraphQL 을 쓰는 의미가 없죠. 따라서 아래와 같이 @connection 디렉티브를 활용하여 author 와 address 에 대해서도 쿼리시에 자동으로 데이터를 가져올 수 있도록 설정할 수 있습니다.

type Post @model {
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  addressID: ID!
  address: Address @connection(fields: ["addressID"])
}

type Address @model {
  id: ID!
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

이렇게 스키마를 작성함으로써, getPost(id: String!) 혹은 listPost() 등의 쿼리를 요청할 때 User 타입의 author 는 물론, depth 설정에 따라 Address 타입의 address 까지도 가져올 수 있게됩니다. 단 한번의 쿼리로 원하는 데이터를 전부 가져올 수 있게 되는거죠!

그런데, 이렇게 좋기만 할 줄 알았던 @connection 을 적절하지 못한 depth 와 함께 사용하게되면 퍼포먼스 혹은 금전적 비용의 문제가 생겨나게됩니다. 이게 무슨말이냐하면, Amplify 도 그렇지만 일반적으로 GraphQL 클라이언트를 사용할 때는 스키마를 기반으로 API 코드를 직접 작성하는 것이 아니라 codegen 등의 기능을 활용하여 자동으로 생성해서 사용하게 되는데요, 이 때, codegen 에서 얼마만큼의 depth 까지 데이터를 읽어올 것인지를 미리 설정을 해두어야합니다.

예를 들어 depth 를 2 까지로 설정해두면, getPost(id: String!) 쿼리는 아래와 같이 데이터를 읽어들여오게 됩니다.

{
  "data": {
    "getPost": {
      "title": "...",
      "content": "...",
      ...
      "author": {
        name: "...",
        addressID: "...",
        ...
      }
    }
  }
}

author 까지는 데이터를 잘 읽어왔는데, author 의 address 에 대해서는 depth 가 2 로 설정되었기 때문에 가져오지 않았습니다. 따라서 더 깊숙하게 데이터를 한번에 가져오기위해서는 depth 를 늘리는 것을 고민하시게 될 텐데요, 이 부분에 관해서는 또 상당히 주의할 점이 있습니다. 아래의 글에서 자세히 설명하고 있으니 관심있으신 분들은 참고해주세요 :)

즉, depth 를 무작정 늘려서는 안된다는 것이죠! 이런 경우 고민해볼 수 있는 어프로치 중 하나가 바로 아래에서 설명하는 @model 디렉티브를 붙이지 않은 type 정의입니다.

@model 디렉티브를 붙이지 않은 type 에 대하여

type Post @model {
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  address: Address 
}

type Address {
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

위와 같이 Address 타입에 @model 디렉티브를 없애고, User 타입에서 직접 Address 타입을 활용할 수 있도록 정의해두었습니다! User 타입과 Address 타입 간의 @connection 디렉티브가 더이상 필요로 하지 않게 되었고, 따라서 depth 를 2 로 설정해두어도 getPost(id: String!) 쿼리에서 address 필드까지 한번에 가져올 수 있게됨을 의미합니다!

AWS AppSync 콘솔의 Query 메뉴에서 아래와 같이 작성하고, 위에서부터 하나씩 createUser -> createPost -> getPost 순으로 실행해보겠습니다. 아래

mutation createUser {
  createUser(input: {
    name: "Taewoo",
    address: {
      zipcode: "122-122",
      country: "Korea",
      city: "Busan",
      building: "Busan Building",
      state: "",
      street: "Street 1"
    },
  }) {
    id
  }
}
mutation createPost {
  createPost(input: {
    authorID: "createUser 실행값으로 얻어온 ID 값",
    content: "hello world",
    title: "first post",
  }) {
    id
  }
}
query getPost {
  getPost(id: "createPost 실행값으로 얻어온 ID 값") {
    title
    content
    author {
      address {
        zipcode
      }
    }
  }
}

User 타입안의 Address 타입으로 선언된 address 필드의 내부 필드 (말이 어렵네요;;) 에 대해서도 데이터를 잘 읽어들여오네요! 이게 어떻게 가능한 지 실제로 데이터소스로 등록된 DynamoDB 에는 어떻게 저장되는지 살펴보겠습니다.

DynamoDB 내부에 JSON(Map) 타입으로 저장되어 있네요! 이런식으로 저장되면 address 타입의 각 필드별 리졸빙은 가능하나, Address 타입의 특정 필드로 "필터링(검색조건으로 활용)" 하는 것은 불가능해집니다. 실제로 amplify push 시에 생성된 amplify/backend/api/${API명}/build/schema.graphql 파일의 ModelUserFilterInput 을 살펴봐도 address 필드는 필터링 옵션으로 제공하고 있지 않습니다.

input ModelUserFilterInput {
  id: ModelIDInput
  name: ModelStringInput
  and: [ModelUserFilterInput]
  or: [ModelUserFilterInput]
  not: ModelUserFilterInput
}

즉, 커스텀 타입에 @model 을 붙이지 않는다는 것은, 커스텀타입을 DynamoDB 에 JSON 데이터로 저장을 하게 되고, 그 JSON 데이터를 그대로 읽어오는 것은 가능하나, JSON 데이터의 내부 필드를 검색 옵션으로 활용할 때는 제약이 생긴다는 것이죠!

이 사실을 미리 알지 못하면 Amplify 의 GraphQL 스키마를 설계할 때에 상당히 어려움을 겪게됩니다. 커스텀타입을 @model 디렉티브 없이 활용하려 할 때는 이 커스텀타입의 필드가 검색조건으로 활용되어야하는지에 대해서도 꼭 검토해보도록 합시다! :D

마치며

이번 글에서는 type 에 @model 디렉티브를 붙이지 않았을 경우 주의해야할 점에 대해 살펴보았습니다. 이 글의 내용이 도움이 되셨다면 본 시리즈의 다른 글들도 같이 읽어서, Amplify 의 GraphQL API 를 설계할 때 꼭 알아두어야하는 정보들을 미리미리 캐치업해두고 적재적소에 활용해봅시다!ㅎㅎ

이상, 컨설팅부의 김태우였습니다! :D