Skip to content

필사 모드: GraphQL 완전 가이드 2025: REST를 넘어서 — 스키마 설계부터 Federation, 성능 최적화까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며: 왜 GraphQL인가?

2015년 Facebook이 오픈소스로 공개한 GraphQL은 이제 API 설계의 표준 선택지가 되었습니다. GitHub, Shopify, Netflix, Airbnb 등 주요 테크 기업들이 채택하며, REST의 한계를 넘어서는 새로운 패러다임을 보여주고 있습니다.

이 글에서는 GraphQL의 기초부터 Federation을 활용한 마이크로서비스 통합, 성능 최적화까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.

1. REST vs GraphQL: 근본적인 차이

REST의 한계

REST API는 오랫동안 웹 API의 표준이었지만, 몇 가지 고질적인 문제가 있습니다.

**Over-fetching**: 필요하지 않은 데이터까지 응답에 포함됩니다. 사용자 이름만 필요한데 전체 프로필 정보가 옵니다.

**Under-fetching**: 하나의 화면을 그리기 위해 여러 엔드포인트를 호출해야 합니다. 사용자 정보 + 게시글 + 댓글 = 3번의 요청.

**엔드포인트 관리**: 클라이언트 요구사항이 변할 때마다 새 엔드포인트를 만들거나 기존 것을 수정해야 합니다.

GraphQL의 해결책

한 번의 요청으로 필요한 데이터만 정확히 가져옵니다

query GetUserWithPosts {

user(id: "123") {

name

email

posts(first: 5) {

title

commentCount

}

}

}

| 구분 | REST | GraphQL |

|------|------|---------|

| 엔드포인트 | 리소스마다 별도 URL | 단일 엔드포인트 |

| 데이터 양 | 서버가 결정 | 클라이언트가 결정 |

| 버전 관리 | v1, v2 URL 분기 | 스키마 진화(deprecation) |

| 타입 시스템 | Swagger/OpenAPI로 보강 | 내장 타입 시스템(SDL) |

| 실시간 | 별도 WebSocket 구현 | Subscription 내장 |

| 캐싱 | HTTP 캐시 활용 용이 | 별도 전략 필요 |

| 학습 곡선 | 낮음 | 중간~높음 |

핵심 차이: 선언적 데이터 요청

REST는 **어디서(WHERE)** 데이터를 가져올지 지정하고, GraphQL은 **무엇을(WHAT)** 가져올지 선언합니다.

2. 스키마 설계 (Schema Definition Language)

기본 타입 시스템

GraphQL의 스키마는 API의 계약서입니다. SDL을 사용하여 타입을 정의합니다.

스칼라 타입: String, Int, Float, Boolean, ID

! 는 Non-null을 의미합니다

type User {

id: ID!

name: String!

email: String!

age: Int

posts: [Post!]!

createdAt: DateTime!

}

type Post {

id: ID!

title: String!

content: String!

author: User!

comments: [Comment!]!

tags: [String!]

publishedAt: DateTime

}

type Comment {

id: ID!

body: String!

author: User!

post: Post!

createdAt: DateTime!

}

Query, Mutation, Subscription

GraphQL에는 세 가지 작업 타입이 있습니다.

type Query {

데이터 조회

user(id: ID!): User

users(filter: UserFilter, pagination: PaginationInput): UserConnection!

post(id: ID!): Post

searchPosts(query: String!): [Post!]!

}

type Mutation {

데이터 변경

createUser(input: CreateUserInput!): User!

updateUser(id: ID!, input: UpdateUserInput!): User!

deleteUser(id: ID!): Boolean!

createPost(input: CreatePostInput!): Post!

likePost(postId: ID!): Post!

}

type Subscription {

실시간 업데이트

postCreated: Post!

commentAdded(postId: ID!): Comment!

userStatusChanged(userId: ID!): User!

}

Input 타입과 Enum

input CreateUserInput {

name: String!

email: String!

age: Int

role: UserRole = USER

}

input UserFilter {

role: UserRole

nameContains: String

createdAfter: DateTime

}

input PaginationInput {

first: Int = 20

after: String

}

enum UserRole {

ADMIN

EDITOR

USER

GUEST

}

enum PostStatus {

DRAFT

PUBLISHED

ARCHIVED

}

Relay 스타일 커넥션 (Cursor-based Pagination)

type UserConnection {

edges: [UserEdge!]!

pageInfo: PageInfo!

totalCount: Int!

}

type UserEdge {

node: User!

cursor: String!

}

type PageInfo {

hasNextPage: Boolean!

hasPreviousPage: Boolean!

startCursor: String

endCursor: String

}

3. Resolver 패턴

기본 Resolver 구조

Resolver는 스키마의 각 필드를 실제 데이터와 연결합니다.

// resolver의 4가지 인자: parent, args, context, info

const resolvers = {

Query: {

user: async (parent, args, context, info) => {

const { id } = args;

const { dataSources, currentUser } = context;

return dataSources.userAPI.getUser(id);

},

users: async (_, { filter, pagination }, { dataSources }) => {

return dataSources.userAPI.getUsers(filter, pagination);

},

searchPosts: async (_, { query }, { dataSources }) => {

return dataSources.postAPI.search(query);

},

},

Mutation: {

createUser: async (_, { input }, { dataSources, currentUser }) => {

if (!currentUser?.isAdmin) {

throw new ForbiddenError('Admin access required');

}

return dataSources.userAPI.createUser(input);

},

likePost: async (_, { postId }, { dataSources, currentUser }) => {

if (!currentUser) {

throw new AuthenticationError('Login required');

}

return dataSources.postAPI.likePost(postId, currentUser.id);

},

},

// 타입 resolver - 관계 필드 해결

User: {

posts: async (user, _, { dataSources }) => {

return dataSources.postAPI.getPostsByAuthor(user.id);

},

},

Post: {

author: async (post, _, { dataSources }) => {

return dataSources.userAPI.getUser(post.authorId);

},

comments: async (post, _, { dataSources }) => {

return dataSources.commentAPI.getByPost(post.id);

},

},

};

Context 설정

const server = new ApolloServer({

typeDefs,

resolvers,

});

await server.start();

app.use(

'/graphql',

expressMiddleware(server, {

context: async ({ req }) => {

const token = req.headers.authorization || '';

const currentUser = await getUserFromToken(token);

return {

currentUser,

dataSources: {

userAPI: new UserDataSource(),

postAPI: new PostDataSource(),

commentAPI: new CommentDataSource(),

},

};

},

})

);

4. N+1 문제와 DataLoader

N+1 문제란?

게시글 목록을 가져온 뒤, 각 게시글의 작성자를 개별 쿼리로 가져오면 N+1 문제가 발생합니다.

Query: posts(first: 10) → 1번 쿼리 (게시글 10개)

Post.author (post 1) → 2번 쿼리

Post.author (post 2) → 3번 쿼리

...

Post.author (post 10) → 11번 쿼리

총 11번의 DB 쿼리가 실행됩니다.

DataLoader로 해결

DataLoader는 같은 이벤트 루프(tick) 내의 요청을 모아 배치 처리합니다.

// 배치 함수: ID 배열을 받아 같은 순서로 결과 반환

const userLoader = new DataLoader(async (userIds: string[]) => {

const users = await db.users.findMany({

where: { id: { in: userIds } },

});

// 요청된 ID 순서대로 결과를 매핑

const userMap = new Map(users.map((u) => [u.id, u]));

return userIds.map((id) => userMap.get(id) || null);

});

// Resolver에서 사용

const resolvers = {

Post: {

author: (post, _, { loaders }) => {

return loaders.userLoader.load(post.authorId);

},

},

};

**결과**: 11번의 쿼리가 2번으로 줄어듭니다.

Query: posts(first: 10) → 1번 쿼리

DataLoader batch: users([1,2,3,...,10]) → 2번 쿼리 (배치)

DataLoader 컨텍스트 패턴

// 요청마다 새로운 DataLoader 인스턴스를 생성합니다

function createLoaders() {

return {

userLoader: new DataLoader((ids) => batchGetUsers(ids)),

postLoader: new DataLoader((ids) => batchGetPosts(ids)),

commentLoader: new DataLoader((ids) => batchGetComments(ids)),

// 1:N 관계용 loader

postsByAuthorLoader: new DataLoader(async (authorIds) => {

const posts = await db.posts.findMany({

where: { authorId: { in: authorIds } },

});

const grouped = groupBy(posts, 'authorId');

return authorIds.map((id) => grouped[id] || []);

}),

};

}

// context에서 매 요청마다 새로 생성

context: async ({ req }) => ({

currentUser: await getUser(req),

loaders: createLoaders(),

}),

5. Apollo Server와 Client

Apollo Server 설정

const schema = makeExecutableSchema({

typeDefs,

resolvers,

});

const server = new ApolloServer({

schema,

plugins: [

// 인트로스펙션 제한 (프로덕션)

ApolloServerPluginLandingPageDisabled(),

// 사용량 리포팅

ApolloServerPluginUsageReporting(),

],

formatError: (formattedError, error) => {

// 에러 포맷팅 - 내부 정보 숨기기

if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {

return { message: 'Internal server error' };

}

return formattedError;

},

});

const { url } = await startStandaloneServer(server, {

listen: { port: 4000 },

context: async ({ req }) => ({

currentUser: await authenticateUser(req),

loaders: createLoaders(),

}),

});

console.log(`Server ready at ${url}`);

Apollo Client (React)

ApolloClient,

InMemoryCache,

ApolloProvider,

useQuery,

useMutation,

gql,

} from '@apollo/client';

const client = new ApolloClient({

uri: 'https://api.example.com/graphql',

cache: new InMemoryCache({

typePolicies: {

Query: {

fields: {

posts: {

// Relay 스타일 커서 페이지네이션 머지

keyArgs: ['filter'],

merge(existing, incoming, { args }) {

if (!args?.after) return incoming;

return {

...incoming,

edges: [...(existing?.edges || []), ...incoming.edges],

};

},

},

},

},

},

}),

defaultOptions: {

watchQuery: {

fetchPolicy: 'cache-and-network',

},

},

});

// React 컴포넌트에서 사용

const GET_POSTS = gql`

query GetPosts($first: Int!, $after: String) {

posts(first: $first, after: $after) {

edges {

node {

id

title

author {

name

}

}

}

pageInfo {

hasNextPage

endCursor

}

}

}

`;

function PostList() {

const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {

variables: { first: 10 },

});

if (loading) return <p>Loading...</p>;

if (error) return <p>Error occurred</p>;

return (

{data.posts.edges.map(({ node }) => (

))}

{data.posts.pageInfo.hasNextPage && (

onClick={() =>

fetchMore({

variables: { after: data.posts.pageInfo.endCursor },

})

}

>

Load More

)}

);

}

6. Apollo Federation v2

Federation이란?

Apollo Federation은 여러 개의 GraphQL 서비스(Subgraph)를 하나의 통합 API(Supergraph)로 합칩니다. 각 팀이 독립적으로 서비스를 개발하면서도 클라이언트에게는 하나의 GraphQL 엔드포인트를 제공합니다.

Subgraph 설계

--- Users Subgraph ---

type User @key(fields: "id") {

id: ID!

name: String!

email: String!

role: UserRole!

}

type Query {

user(id: ID!): User

me: User

}

--- Posts Subgraph ---

type Post @key(fields: "id") {

id: ID!

title: String!

content: String!

author: User!

publishedAt: DateTime

}

다른 subgraph의 User 타입 확장

type User @key(fields: "id") {

id: ID!

posts: [Post!]! # Posts 서비스에서 추가하는 필드

postCount: Int!

}

type Query {

post(id: ID!): Post

feed(first: Int = 20, after: String): PostConnection!

}

--- Reviews Subgraph ---

type Review @key(fields: "id") {

id: ID!

rating: Int!

comment: String

author: User!

post: Post!

}

type Post @key(fields: "id") {

id: ID!

reviews: [Review!]! # Reviews 서비스에서 추가

averageRating: Float

}

Router 설정 (Apollo Router)

router.yaml

supergraph:

listen: 0.0.0.0:4000

subgraphs:

users:

routing_url: http://users-service:4001/graphql

posts:

routing_url: http://posts-service:4002/graphql

reviews:

routing_url: http://reviews-service:4003/graphql

headers:

all:

request:

- propagate:

named: authorization

Subgraph의 Reference Resolver

// Posts Subgraph

const resolvers = {

User: {

// Federation이 User를 resolve할 때 호출

__resolveReference: async (user, { loaders }) => {

// user.id만 전달받음

return loaders.postsByUserLoader.load(user.id);

},

posts: async (user, _, { dataSources }) => {

return dataSources.postAPI.getByAuthor(user.id);

},

postCount: async (user, _, { dataSources }) => {

return dataSources.postAPI.countByAuthor(user.id);

},

},

};

7. Subscription (실시간 기능)

서버 설정

const pubsub = new PubSub();

const httpServer = createServer(app);

const wsServer = new WebSocketServer({

server: httpServer,

path: '/graphql',

});

useServer(

{

schema,

context: async (ctx) => {

const token = ctx.connectionParams?.authorization;

const user = await authenticateUser(token);

return { currentUser: user, pubsub };

},

},

wsServer

);

// Resolver

const resolvers = {

Subscription: {

commentAdded: {

subscribe: (_, { postId }) => {

return pubsub.asyncIterator(`COMMENT_ADDED_${postId}`);

},

},

postCreated: {

subscribe: () => pubsub.asyncIterator('POST_CREATED'),

},

},

Mutation: {

createComment: async (_, { input }, { pubsub, currentUser }) => {

const comment = await db.comments.create({

data: { ...input, authorId: currentUser.id },

});

// 구독자에게 알림

pubsub.publish(`COMMENT_ADDED_${input.postId}`, {

commentAdded: comment,

});

return comment;

},

},

};

클라이언트 Subscription

const COMMENT_SUBSCRIPTION = gql`

subscription OnCommentAdded($postId: ID!) {

commentAdded(postId: $postId) {

id

body

author {

name

}

createdAt

}

}

`;

function CommentFeed({ postId }) {

const { data, loading } = useSubscription(COMMENT_SUBSCRIPTION, {

variables: { postId },

});

// 새 댓글이 올 때마다 자동으로 data가 업데이트됩니다

if (data?.commentAdded) {

return <NewComment comment={data.commentAdded} />;

}

return null;

}

8. 인증(Authentication)과 인가(Authorization)

Directive 기반 인가

커스텀 directive 정의

directive @auth(requires: Role = USER) on FIELD_DEFINITION | OBJECT

enum Role {

ADMIN

EDITOR

USER

GUEST

}

type Query {

publicPosts: [Post!]!

me: User! @auth

adminDashboard: Dashboard! @auth(requires: ADMIN)

userManagement: [User!]! @auth(requires: ADMIN)

}

type Mutation {

createPost(input: CreatePostInput!): Post! @auth

deletePost(id: ID!): Boolean! @auth(requires: ADMIN)

updateProfile(input: UpdateProfileInput!): User! @auth

}

Directive 구현

function authDirectiveTransformer(schema) {

return mapSchema(schema, {

[MapperKind.OBJECT_FIELD]: (fieldConfig) => {

const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];

if (authDirective) {

const requiredRole = authDirective.requires || 'USER';

const originalResolve = fieldConfig.resolve || defaultFieldResolver;

fieldConfig.resolve = async (source, args, context, info) => {

const { currentUser } = context;

if (!currentUser) {

throw new AuthenticationError('Not authenticated');

}

if (!hasRole(currentUser, requiredRole)) {

throw new ForbiddenError('Insufficient permissions');

}

return originalResolve(source, args, context, info);

};

}

return fieldConfig;

},

});

}

9. 성능 최적화

Query Complexity 제한

const server = new ApolloServer({

schema,

validationRules: [

createComplexityLimitRule(1000, {

scalarCost: 1,

objectCost: 2,

listFactor: 10,

formatErrorMessage: (cost) =>

`Query too complex: cost ${cost} exceeds maximum 1000`,

}),

],

});

Depth Limiting

const server = new ApolloServer({

schema,

validationRules: [depthLimit(7)],

});

Persisted Queries (APQ)

// 서버

const server = new ApolloServer({

schema,

plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 60 })],

persistedQueries: {

cache: new KeyValueCache(), // Redis 등

},

});

// 클라이언트

const link = createPersistedQueryLink({ sha256 });

const client = new ApolloClient({

link: link.concat(httpLink),

cache: new InMemoryCache(),

});

캐시 제어

type Query {

posts: [Post!]! @cacheControl(maxAge: 300)

user(id: ID!): User @cacheControl(maxAge: 60)

me: User @cacheControl(maxAge: 0, scope: PRIVATE)

}

type Post @cacheControl(maxAge: 600) {

id: ID!

title: String!

author: User! @cacheControl(inheritMaxAge: true)

viewCount: Int! @cacheControl(maxAge: 30)

}

10. 캐싱 전략

서버 사이드 캐싱

const server = new ApolloServer({

schema,

cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis('redis://localhost:6379') })),

});

// DataSource 레벨 캐싱

class CachedUserAPI {

private cache: RedisClient;

private ttl = 300; // 5분

async getUser(id: string): Promise<User> {

const cacheKey = `user:${id}`;

const cached = await this.cache.get(cacheKey);

if (cached) return JSON.parse(cached);

const user = await db.users.findUnique({ where: { id } });

await this.cache.set(cacheKey, JSON.stringify(user), 'EX', this.ttl);

return user;

}

}

클라이언트 사이드 캐싱 (Apollo Cache)

const cache = new InMemoryCache({

typePolicies: {

User: {

keyFields: ['id'],

fields: {

fullName: {

read(_, { readField }) {

const first = readField('firstName');

const last = readField('lastName');

return `${first} ${last}`;

},

},

},

},

Post: {

keyFields: ['id'],

},

Query: {

fields: {

post(_, { args, toReference }) {

return toReference({ __typename: 'Post', id: args?.id });

},

},

},

},

});

11. 테스팅

Resolver 단위 테스트

describe('User Resolvers', () => {

it('should return user by id', async () => {

const mockUser = { id: '1', name: 'Test User', email: 'test@example.com' };

const context = {

dataSources: {

userAPI: { getUser: vi.fn().mockResolvedValue(mockUser) },

},

};

const result = await resolvers.Query.user(null, { id: '1' }, context, null);

expect(result).toEqual(mockUser);

expect(context.dataSources.userAPI.getUser).toHaveBeenCalledWith('1');

});

it('should throw if not admin for createUser', async () => {

const context = {

currentUser: { role: 'USER' },

dataSources: { userAPI: { createUser: vi.fn() } },

};

await expect(

resolvers.Mutation.createUser(null, { input: {} }, context, null)

).rejects.toThrow('Admin access required');

});

});

통합 테스트

describe('GraphQL Integration', () => {

let server: ApolloServer;

beforeAll(async () => {

server = new ApolloServer({ typeDefs, resolvers });

});

it('should execute GetUser query', async () => {

const response = await server.executeOperation({

query: `

query GetUser($id: ID!) {

user(id: $id) {

id

name

email

}

}

`,

variables: { id: '1' },

});

expect(response.body.kind).toBe('single');

expect(response.body.singleResult.errors).toBeUndefined();

expect(response.body.singleResult.data?.user).toBeDefined();

});

});

12. GraphQL을 쓰지 말아야 할 때

모든 상황에 GraphQL이 적합하지는 않습니다.

**REST가 나은 경우:**

- 단순 CRUD API (리소스 구조가 명확할 때)

- 파일 업로드/다운로드가 주 기능인 서비스

- HTTP 캐싱이 핵심인 공개 API

- 팀 규모가 작고 러닝 커브를 감당하기 어려울 때

- 서버 간 내부 통신 (gRPC가 더 적합)

**GraphQL이 빛나는 경우:**

- 다양한 클라이언트(웹, 모바일, TV)가 같은 API를 사용

- 프론트엔드 팀이 빠르게 이터레이션해야 할 때

- 데이터 관계가 복잡한 도메인 (SNS, 이커머스)

- 마이크로서비스를 통합 API로 제공해야 할 때

- 실시간 기능이 필요한 애플리케이션

13. 실무 면접 질문 15선

기초 (1-5)

**Q1. GraphQL과 REST의 핵심 차이를 설명하세요.**

GraphQL은 클라이언트가 필요한 데이터의 구조를 직접 명시하는 쿼리 언어입니다. REST는 서버가 정의한 엔드포인트별로 고정된 응답을 반환하는 반면, GraphQL은 단일 엔드포인트에서 클라이언트가 원하는 필드만 선택적으로 요청합니다.

**Q2. Over-fetching과 Under-fetching을 설명하세요.**

Over-fetching은 API가 클라이언트에게 필요한 것 이상의 데이터를 반환하는 것이고, Under-fetching은 필요한 데이터를 얻기 위해 여러 번 API를 호출해야 하는 것입니다.

**Q3. SDL(Schema Definition Language)의 주요 타입들을 설명하세요.**

Object 타입(커스텀 필드 정의), Scalar 타입(String, Int, Float, Boolean, ID), Enum(열거형), Input(입력용 객체), Interface(공통 필드 정의), Union(여러 타입 중 하나)이 있습니다.

**Q4. Query, Mutation, Subscription의 차이를 설명하세요.**

Query는 데이터 조회(GET과 유사), Mutation은 데이터 변경(POST/PUT/DELETE와 유사), Subscription은 WebSocket을 통한 실시간 이벤트 스트림입니다.

**Q5. Non-null(!)과 리스트 타입의 조합을 설명하세요.**

`[String]`은 null 가능한 문자열의 null 가능한 배열, `[String!]`은 null 불가 문자열의 null 가능한 배열, `[String!]!`은 null 불가 문자열의 null 불가 배열입니다.

중급 (6-10)

**Q6. N+1 문제가 무엇이며 어떻게 해결하나요?**

부모 목록을 조회한 뒤 각 항목의 관계 데이터를 개별 쿼리로 가져오는 문제입니다. DataLoader로 같은 틱의 요청을 모아 한 번의 배치 쿼리로 실행합니다.

**Q7. Apollo Federation의 핵심 개념을 설명하세요.**

여러 Subgraph를 Router가 하나의 Supergraph로 합칩니다. @key로 엔티티를 식별하고, __resolveReference로 다른 서비스의 엔티티를 resolve합니다.

**Q8. GraphQL에서 인증과 인가를 어떻게 구현하나요?**

인증은 context에서 토큰을 검증하여 currentUser를 설정합니다. 인가는 커스텀 directive(@auth)나 resolver 내부에서 역할 기반 검사를 수행합니다.

**Q9. Persisted Queries의 장점은 무엇인가요?**

쿼리 문자열 대신 해시값을 전송하여 네트워크 페이로드를 줄이고, 서버에서 허용된 쿼리만 실행하여 보안을 강화합니다.

**Q10. Cursor-based vs Offset-based Pagination의 차이는?**

Offset은 페이지 번호 기반으로 중간 삽입/삭제 시 데이터가 중복/누락됩니다. Cursor는 특정 위치(opaque cursor) 기반으로 실시간 데이터에서도 안정적입니다.

고급 (11-15)

**Q11. Query Complexity와 Depth Limit을 설명하세요.**

Query Complexity는 각 필드에 비용을 부여해 총 비용이 한도를 초과하면 거부합니다. Depth Limit은 중첩 깊이를 제한하여 악의적인 재귀 쿼리를 방지합니다.

**Q12. Federation에서 Entity와 Reference Resolver의 동작을 설명하세요.**

Entity는 @key로 식별되는 타입입니다. Router가 다른 subgraph의 데이터가 필요할 때 __resolveReference를 호출하여 key 필드만으로 전체 엔티티를 resolve합니다.

**Q13. GraphQL의 캐싱이 REST보다 어려운 이유는?**

REST는 URL 기반 HTTP 캐시를 직접 활용하지만, GraphQL은 단일 엔드포인트에 POST 요청을 사용하므로 HTTP 캐시를 쓸 수 없습니다. APQ, CDN 캐시 키 설정, 정규화된 클라이언트 캐시 등 별도 전략이 필요합니다.

**Q14. Subscription의 프로덕션 스케일링은 어떻게 하나요?**

인메모리 PubSub 대신 Redis PubSub이나 Kafka를 사용합니다. 여러 서버 인스턴스 간 메시지를 공유하며, WebSocket 연결의 로드밸런싱에는 sticky session이나 별도 WebSocket 게이트웨이를 구성합니다.

**Q15. 스키마 진화(Evolution) 전략을 설명하세요.**

@deprecated directive로 필드를 폐기 예고하고, 새 필드를 추가합니다. 필드 제거 전 클라이언트 사용량을 모니터링하고, 스키마 레지스트리로 변경 호환성을 검증합니다. REST의 URL 버전 관리보다 유연합니다.

14. 실전 퀴즈 5문제

**정답: `[String!]!`**

`[String!]!` — 배열 자체도 null이 아니고(!), 배열 내 각 요소도 null이 아닙니다(String!). `[String]`은 배열도 null 가능, 요소도 null 가능합니다.

**정답: 같은 이벤트 루프 틱 내의 .load() 호출을 모아 배치 함수를 한 번 실행합니다.**

DataLoader는 요청을 큐에 쌓다가 현재 틱이 끝나면 모든 ID를 하나의 배치 함수에 전달합니다. 이를 통해 N+1 쿼리를 1+1 쿼리로 줄입니다.

**정답: 엔티티(Entity)를 고유하게 식별하는 필드를 지정합니다.**

`@key(fields: "id")`는 해당 타입이 Federation의 엔티티임을 선언하고, Router가 다른 subgraph에서 이 엔티티를 참조할 때 id 필드를 사용하여 resolve할 수 있게 합니다.

**정답: WebSocket**

Subscription은 HTTP 요청-응답이 아닌 WebSocket을 통한 양방향 연결을 사용합니다. graphql-ws 프로토콜로 서버에서 클라이언트로 실시간 이벤트를 push합니다.

**정답: 서버에 등록된 쿼리만 실행하여 임의의 쿼리(악의적 쿼리) 실행을 방지합니다.**

APQ(Automatic Persisted Queries)는 쿼리의 SHA256 해시를 전송합니다. 서버는 해시와 매핑된 쿼리만 실행하므로 공격자가 임의의 복잡한 쿼리를 보내 서버 자원을 소모하는 것을 막습니다.

참고 자료 (References)

1. [GraphQL 공식 문서](https://graphql.org/learn/)

2. [Apollo GraphQL 문서](https://www.apollographql.com/docs/)

3. [Apollo Federation 가이드](https://www.apollographql.com/docs/federation/)

4. [GraphQL Spec (2021)](https://spec.graphql.org/October2021/)

5. [DataLoader GitHub](https://github.com/graphql/dataloader)

6. [graphql-ws 프로토콜](https://github.com/enisdenjo/graphql-ws)

7. [How to GraphQL - 튜토리얼](https://www.howtographql.com/)

8. [Relay 공식 문서](https://relay.dev/)

9. [GraphQL Best Practices](https://graphql.org/learn/best-practices/)

10. [Production-Ready GraphQL (Marc-Andre Giroux)](https://book.productionreadygraphql.com/)

11. [The Guild - GraphQL Tools](https://the-guild.dev/graphql/tools)

12. [GraphQL Security (OWASP)](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)

13. [Apollo Client React 가이드](https://www.apollographql.com/docs/react/)

14. [Netflix: GraphQL at Scale](https://netflixtechblog.com/our-learnings-from-adopting-graphql-f712692c381)

15. [Shopify: GraphQL Design Tutorial](https://github.com/Shopify/graphql-design-tutorial)

16. [GitHub GraphQL API](https://docs.github.com/en/graphql)

현재 단락 (1/699)

2015년 Facebook이 오픈소스로 공개한 GraphQL은 이제 API 설계의 표준 선택지가 되었습니다. GitHub, Shopify, Netflix, Airbnb 등 주요 테...

작성 글자: 0원문 글자: 17,615작성 단락: 0/699