AWS AppSync란?

AWS AppSync는 서버리스 백엔드에서 사용할 수 있는 관리형 API 서비스입니다. 이번 장에서는 이에 대해 자세히 알아보겠습니다:

시작해 봅시다!

배경

API Gateway에 이미 익숙하다면, 왜 또 다른 서비스를 배워야 하는지 궁금할 수 있습니다. 이는 여러분이 사용하려는 프로토콜, 즉 REST와 GraphQL에 따라 결정됩니다.

GraphQL

GraphQL은 2012년 페이스북에서 모바일 앱의 뉴스 피드에 필요한 네트워크 트래픽을 줄이기 위해 만들어졌습니다. 페이스북은 몇 년 후 GraphQL을 오픈소스 프로젝트로 공개했고, 점차 인기를 얻기 시작했습니다. 2016년에는 GitHub가 공개 API를 GraphQL로 전환했습니다.

GraphQL, 또는 그래프 쿼리 언어는 REST(Representational State Transfer) 위에 구축된 API 프로토콜입니다. 이를 통해 응답에 포함될 데이터를 필터링하거나 필요한 속성을 지정할 수 있습니다.

Smartbear의 2020년 API 현황 보고서에 따르면, 응답 기관 중 약 19%만이 GraphQL을 사용하고 있습니다. 대부분의 기관(최소 82%)은 어떤 형태로든 REST를 사용하고 있습니다.

GraphQL은 서버로 보내는 요청 수와 응답 크기를 제한하고 싶을 때 빛을 발합니다. 이를 통해 백엔드에서 여러 소스의 데이터를 조합할 수 있습니다. 또한 중첩된 데이터를 처리할 수 있어, ID 목록을 가져오기 위해 요청을 보내고 각 ID에 대한 데이터를 가져오기 위해 또 다른 요청을 보낼 필요가 없습니다. 마지막으로, GraphQL은 응답에 포함할 속성을 지정할 수 있게 해줍니다. 이는 원하지 않거나 필요하지 않은 데이터를 다운로드하는 대역폭 낭비를 방지하는 데 도움이 됩니다.

AWS Lambda에서 GraphQL 사용하기

서버리스 애플리케이션에서 GraphQL을 사용하기로 결정했다면, 서버를 어떻게 실행할까요? AWS에서 서버리스 컴퓨팅 서비스는 AWS Lambda입니다. 몇 가지 제한 사항이 있지만, Lambda에서 GraphQL 서버를 실행하는 것은 그리 어렵지 않습니다.

AWS Lambda에서 실행할 수 있는 주요 GraphQL 서버 라이브러리는 두 가지가 있습니다. Apollo Server는 Lambda에서 실행할 수 있도록 지원 패키지를 제공합니다. 다른 옵션은 Express 미들웨어인 Express GraphQL로, REST 엔드포인트와 함께 GraphQL을 실행할 수 있게 해줍니다.

Apollo Server는 두 옵션 중 더 인기 있는 선택지이며, 대부분의 경우 이 옵션을 선택하는 것이 좋습니다. 그러나 이미 Lambda에서 실행 중인 REST API가 있고 이를 GraphQL로 전환하려는 경우, 서버에 미들웨어 플러그인을 추가하는 간단함이 큰 장점입니다. 프로젝트에 express-graphql을 추가하기만 하면 REST 경로를 GraphQL 쿼리와 뮤테이션으로 점차 대체할 수 있습니다.

미들웨어 플러그인에 대해 말하자면, 이것이 Express를 고수해야 하는 또 다른 이유입니다. Express는 많은 플러그인을 가지고 있습니다. Apollo Server가 여러분이 원하는 인증 방식을 아직 지원하지 않는다면, 누군가가 이미 Express 미들웨어를 작성했을 가능성이 큽니다.

AWS AppSync란?

람다(Lambda)는 많은 작업에 유용하지만, GraphQL 서버를 람다에서 실행하는 것은 까다로울 수 있습니다. 특히 구독(subscription) 같은 기능을 사용하려고 할 때 더욱 그렇습니다. AppSync는 AWS가 이 문제를 해결하기 위해 제공하는 서비스입니다.

AWS AppSync는 서버리스 형태로 제공되는 관리형 GraphQL API 서비스로, 호스팅 방법을 걱정하지 않고도 API를 생성할 수 있습니다. 구독 기능을 완벽하게 지원하며, 다른 AWS 서비스에서 데이터를 가져오는 작업을 단순화하는 몇 가지 기능도 제공합니다.

AppSync API는 정의해야 하는 세 가지 구성 요소로 이루어져 있습니다: 스키마(schema), 리졸버(resolver), 데이터 소스(data source). 이 세 가지를 조합하면 API가 리소스와 상호작용하고 응답을 원하는 형식으로 변환할 수 있습니다.

AWS AppSync 아키텍처

요청이 처음 들어오면, AppSync는 스키마를 사용해 요청을 검증합니다. 요청의 권한은 선택적인 타입 데코레이터(type decorator)를 통해 확인되며, 요청된 속성이 유효한지 검사됩니다.

AppSync가 요청된 객체 타입을 확인하고 사용자가 해당 타입을 요청할 수 있는 권한이 있는지 확인한 후, 해당 타입과 연결된 리졸버를 찾습니다. 리졸버는 GraphQL과 데이터 소스 간의 데이터를 변환하기 위한 간단한 로직을 사용하는 요청 및 응답 템플릿을 정의합니다.

GraphQL 쿼리가 리졸버에 의해 변환된 후, 데이터 소스로 전송됩니다. 데이터 소스는 데이터베이스 연결, 람다 ARN, 또는 요청이 전송될 다른 목적지를 정의합니다. 리소스가 요청된 작업을 실행한 후, 결과는 응답 템플릿으로 반환되어 스키마와 호환되는 형식으로 다시 변환됩니다.

현재 AppSync에서 지원하는 데이터 소스 유형은 총 6가지입니다:

  • DynamoDB
  • ElasticSearch
  • Lambda
  • None
  • Http
  • RDS

목록에 없는 AWS 서비스의 데이터를 사용해야 한다면, Http 데이터 소스를 사용할 수 있습니다.

AppSync의 가장 큰 단점은 개발 경험입니다. 리졸버 템플릿 개발은 Apache VTL을 사용하기 때문에 매우 어렵기로 유명합니다.

AppSync 요금

AppSync를 사용할 때 다음과 같은 항목에 대해 요금이 부과됩니다:

  • 쿼리/뮤테이션
  • 구독 업데이트
  • 클라이언트가 구독을 수신하는 시간(분 단위)
  • 외부로 전송된 데이터
  • 선택적으로 캐싱

아래 표는 AppSync API를 실행할 때 현재 부과되는 요금을 상세히 설명합니다. 최신 요금 정보는 AWS AppSync 요금 페이지를 참조하세요.

설명 요금
100만 건의 쿼리/뮤테이션 $4.00
100만 건의 5KB 업데이트 $2.00
100만 분 동안 구독 연결 $0.08
인터넷으로 전송된 1GB 데이터 $0.09

AppSync API에 대해 선택적으로 캐싱을 활성화할 수 있습니다. 몇 가지 옵션 중에서 인스턴스 타입을 선택할 수 있으며, 각 인스턴스 타입에는 현재 시간당 $0.044부터 $6.775까지의 요금이 부과됩니다. 최신 요금 정보는 AWS AppSync 요금 페이지를 참조하세요.

AppSync 개념

이제 AppSync의 주요 개념을 살펴보겠습니다. 데이터 소스부터 시작하겠습니다.

데이터 소스

AWS AppSync에서 데이터 소스는 여러분의 GraphQL API가 쿼리하고 스키마를 채우는 데 사용하는 데이터를 보유한 서비스, 데이터베이스 또는 API입니다.

AWS AppSync가 지원하는 데이터 소스는 몇 가지가 있습니다. 예를 들어 Amazon DynamoDB, AWS Lambda (Lambda를 사용하면 RDS나 ElastiCache와 같은 다른 옵션을 사용할 수 있음), 그리고 Amazon Elasticsearch Service 등이 있습니다.

SST의 AppSyncApi construct를 사용하면 데이터 소스를 훨씬 쉽게 생성할 수 있습니다. 아래에서 이를 어떻게 하는지 몇 가지 예제를 살펴보겠습니다.

데이터를 가져오고 조작해야 할 때마다 데이터 소스가 필요합니다. 그러나 경우에 따라 리졸버를 사용해 데이터 변환만 수행하고 뮤테이션에 의해 호출되는 구독만 필요하다면 데이터 소스가 필요하지 않을 수도 있습니다.

SST AppSyncApi를 사용하면 AWS 콘솔에 로그인하지 않고도 GraphQL API에 데이터 소스를 쉽게 추가할 수 있습니다.

import { AppSyncApi } from "@serverless-stack/resources";

new AppSyncApi(this, "GraphqlApi", {
  graphqlApi: {
    //...
  },
  dataSources: {
    notesDS: "functions/notes.main",
  },
  resolvers: {
    //...
  },
}

리졸버

GraphQL에서 리졸버는 GraphQL API를 쿼리할 때 응답을 반환하는 함수입니다. 이 함수는 GraphQL 스키마의 필드에 매핑되며, 해당 필드에 대한 결과를 반환하는 역할을 합니다.

리졸버 함수는 일반적으로 네 가지 인자를 포함합니다:

  • parent
  • arguments
  • context
  • info

함수 정의는 다음과 같습니다:

fieldName: (parent, args, context, info) => data;

각 인자의 의미를 살펴보겠습니다:

  • parent: parent는 때로 root라고도 불리며, 참조 필드의 반환 값을 담고 있는 객체입니다. 이는 항상 해당 필드의 자식 리졸버보다 먼저 실행됩니다. 선택적 매개변수입니다.
  • args: 특정 필드에 제공된 모든 GraphQL 인자는 args에서 접근할 수 있습니다. 예를 들어, Query{ todo(id: "2") }를 실행할 때, argstodo 리졸버에 전달된 객체 { "id": "2" }입니다.
  • context: 특정 작업에 대해 실행되는 모든 리졸버는 동일한 context를 공유하며, 리졸버 내에서 동일한 인자로 접근할 수 있습니다.
  • info: 이 인자는 쿼리 실행에 대한 정보를 포함하며, 필드의 이름과 경로 등을 담고 있습니다.

다음과 같은 스키마가 있다고 가정해 보겠습니다:

type Todo {
  id: ID!
  description: String!
  checked: Boolean!
}

type Query {
  todos: [Todo]
  todo(id: ID!): Todo
}

그러면 todos 스키마 타입을 쿼리하기 위한 리졸버를 작성할 수 있습니다.

Query: {
  todos: () => Todo.find(),
  todo: (_, { id }) => Todo.findById(id),
},

여기서는 Mongoose를 사용하여 MongoDB 데이터베이스를 쿼리합니다.

todos 리졸버 함수에서는 어떤 인자도 전달할 필요가 없습니다. 그러나 todo 함수에서는 todoid와 같은 컨텍스트를 전달해야 합니다.

이제 SST의 AppSyncApi 구성을 사용하여 리졸버를 설정할 수 있습니다:

import { AppSyncApi } from "@serverless-stack/resources";

new AppSyncApi(this, "GraphqlApi", {
  graphqlApi: {
    schema: "graphql/schema.graphql",
  },
  dataSources: {
    todoDS: "src/todos.main",
  },
  resolvers: {
    "Query todos": "todoDS",
  },
});

뮤테이션

뮤테이션은 데이터 저장소를 수정하고 값을 반환하는 리졸버 함수입니다. 데이터를 삽입, 업데이트, 삭제하는 데 사용할 수 있습니다. 쿼리 리졸버 함수와 뮤테이션의 유일한 차이점은 리졸버 맵에서 뮤테이션을 사용한다는 점입니다:

import Todo from "./Todo";

export default {
  Query: {
    todos: () => Todo.find(),
    todo: (_, { id }) => Todo.findById(id),
  },
  Mutation: {
    createTodo: (_, { description }) => {
      const todo = Todo.create({ description, checked: false });
      return todo;
    },
    checkedTodo: (_, { id, checked }) =>
      Todo.findByIdAndUpdate(id, { checked: checked }),
    updateTodo: (_, { id, description }) =>
      Todo.findByIdAndUpdate(id, { description }),
    deleteTodo: (_, { id }) => Todo.findByIdAndDelete(id),
  },
};

데이터 저장소의 데이터를 수정해야 할 때는 리졸버에서 뮤테이션을 사용합니다.

쿼리와 마찬가지로, SST AppSyncApi에 뮤테이션을 쉽게 추가할 수 있습니다:

import { AppSyncApi } from "@serverless-stack/resources";

new AppSyncApi(this, "GraphqlApi", {
  graphqlApi: {
    schema: "graphql/schema.graphql",
  },
  dataSources: {
    todoDS: "src/todos.main",
  },
  resolvers: {
    "Query todos": "todoDS",
    "Mutation createNote": "todoDS",
    "Mutation updateNote": "todoDS",
    "Mutation deleteNote": "todoDS",
  },
});

구독(Subscriptions)

GraphQL 구독은 서버와 활성 연결을 유지하며(보통 WebSockets를 통해), 서버로부터 실시간 메시지를 수신하고 클라이언트와 서버 간의 양방향 통신을 가능하게 합니다. 간단히 말해, 서버의 활동을 감지하고 클라이언트에게 실시간으로 업데이트를 보냅니다.

대부분의 경우, 쿼리를 통해 필요할 때마다 데이터를 가져오는 방식(예: 버튼 클릭 또는 페이지 새로고침)을 사용하지만, 때로는 큰 객체의 작은 증분 변경 사항을 클라이언트에게 알리기 위해 구독을 사용해야 할 수도 있습니다. 또한 낮은 지연 시간과 실시간 업데이트가 필요한 상황에서도 구독을 사용할 수 있습니다.

구독을 생성하려면 먼저 구독 스키마 타입을 만들고, 여기에 AWS AppSync 어노테이션 @aws_subscribe()를 추가해야 합니다.

type Subscription {
  newTodo: Todo
  @aws_subscribe(mutations: ["newTodo"])
}

권한 설정

대부분의 경우, 여러분은 Lambda 함수가 S3와 같은 AWS 서비스 중 일부 또는 전체에만 접근할 수 있도록 설정하고 싶을 것입니다.

예를 들어, SST의 AppSyncApi를 사용하면 API 내의 모든 Lambda 함수가 S3(또는 다른 리소스)에 접근할 수 있도록 허용할 수 있습니다.

import { AppSyncApi } from "@serverless-stack/resources";

new AppSyncApi(this, "GraphqlApi", {
  graphqlApi: {
    schema: "graphql/schema.graphql",
  },
  dataSources: {
    todoDS: "src/todos.main",
  },
  resolvers: {
    "Query todos": "todoDS",
    "Mutation createNote": "todoDS",
    "Mutation updateNote": "todoDS",
    "Mutation deleteNote": "todoDS",
  },
});

// AppSync API가 S3에 접근할 수 있도록 허용
api.attachPermissions(["s3"]);

또는 특정 Lambda 함수에만 권한을 추가할 수도 있습니다. 예를 들어, 특정 데이터 소스에 대한 함수가 AWS S3에 접근할 수 있도록 설정할 수 있습니다.

api.attachPermissionsToDataSource("todoDS", ["s3"]);

여기서는 데이터 소스를 키인 todoDS로 참조합니다. 이렇게 하면 해당 함수만 여러분의 서비스에 접근할 수 있는 권한을 가지게 됩니다.

마무리

여기까지입니다. 이제 여러분은 AWS AppSync가 무엇인지, 그리고 이를 사용해 GraphQL 백엔드를 구축하는 방법에 대해 잘 이해했을 것입니다. 첫 번째 AppSync 애플리케이션을 시작하려면 예제를 확인해 보세요 — AWS AppSync로 서버리스 GraphQL API 만들기.