Node.js 인터뷰 질문 61
질문: Node.js에서 GraphQL API를 구현하는 방법과 RESTful API와 비교했을 때의 장단점은 무엇인가요?
답변:
GraphQL은 Facebook에서 개발한 쿼리 언어 및 API 런타임으로, 클라이언트가 필요한 데이터를 정확히 요청할 수 있는 유연성을 제공합니다. Node.js에서는 GraphQL API를 쉽게 구현할 수 있으며, RESTful API와 비교하여 각각 장단점을 가지고 있습니다.
1. Node.js에서 GraphQL 구현하기
1.1 Apollo Server를 사용한 기본 구현
const { ApolloServer, gql } = require("apollo-server");
// 스키마 정의
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: Author!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}
type Query {
books: [Book!]!
book(id: ID!): Book
authors: [Author!]!
}
type Mutation {
addBook(title: String!, authorId: ID!): Book!
}
`;
// 리졸버 함수 구현
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find((book) => book.id === id),
authors: () => authors,
},
Mutation: {
addBook: (_, { title, authorId }) => {
const newBook = {
id: String(books.length + 1),
title,
authorId,
};
books.push(newBook);
return newBook;
},
},
Book: {
author: (book) => authors.find((author) => author.id === book.authorId),
},
Author: {
books: (author) => books.filter((book) => book.authorId === author.id),
},
};
// 샘플 데이터
const books = [
/* ... */
];
const authors = [
/* ... */
];
// 서버 생성 및 시작
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 GraphQL 서버가 ${url}에서 실행 중입니다`);
});
1.2 Express와 통합하기
const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const http = require("http");
async function startApolloServer(typeDefs, resolvers) {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app });
// Express 라우트 추가
app.get("/", (req, res) => {
res.send("GraphQL과 통합된 Express 서버");
});
await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(
`🚀 서버가 http://localhost:4000${server.graphqlPath}에서 실행 중입니다`
);
}
1.3 데이터베이스 통합 (MongoDB 예시)
const mongoose = require("mongoose");
const { ApolloServer } = require("apollo-server");
// MongoDB 연결
mongoose.connect("mongodb://localhost:27017/graphql-db");
// Mongoose 모델 정의
const Book = mongoose.model("Book", {
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: "Author" },
});
const Author = mongoose.model("Author", {
name: String,
});
// 리졸버 함수 구현
const resolvers = {
Query: {
books: async () => await Book.find(),
book: async (_, { id }) => await Book.findById(id),
// ...
},
// ...
};
2. GraphQL과 RESTful API 비교
2.1 GraphQL의 장점
- 단일 요청으로 필요한 모든 데이터 조회
# 한 번의 요청으로 책과 저자 정보 조회
query {
book(id: "1") {
title
author {
name
books {
title
}
}
}
}
강력한 타입 시스템: API 인터페이스를 명확하게 정의
버전 관리 없는 API 진화: 기존 필드를 유지하며 새 필드 추가 가능
자체 문서화: 인트로스펙션을 통한 API 스키마 탐색 가능
프론트엔드 개발 가속화: 백엔드 변경 없이 필요한 데이터만 요청 가능
2.2 GraphQL의 단점
학습 곡선: 새로운 쿼리 언어와 스키마 정의 학습 필요
캐싱 복잡성: URL 기반 캐싱이 어려워 별도 캐싱 전략 필요
파일 업로드: 기본적으로 파일 업로드 지원하지 않음
성능 고려사항: N+1 쿼리 문제, 복잡한 쿼리 처리 비용
// dataloader를 사용한 N+1 쿼리 문제 해결
const DataLoader = require("dataloader");
const authorLoader = new DataLoader(async (authorIds) => {
const authors = await Author.find({ _id: { $in: authorIds } });
return authorIds.map(
(id) =>
authors.find((author) => author.id.toString() === id.toString()) || null
);
});
// 리졸버에서 사용
const resolvers = {
Book: {
author: async (book) => authorLoader.load(book.authorId),
},
};
- 에러 처리: 항상 200 상태 코드 반환, 응답 내에 오류 포함
2.3 RESTful API의 장점
성숙한 생태계: 널리 사용되는 표준, 풍부한 도구와 지식
HTTP 캐싱: 표준 HTTP 캐싱 메커니즘 활용 용이
단순성: 개념 이해가 쉽고, 진입 장벽이 낮음
상태 코드 표준화: HTTP 상태 코드로 명확한 오류 처리
파일 업로드: 기본적으로 지원
2.4 RESTful API의 단점
- 오버페칭/언더페칭: 필요한 데이터만 정확히 가져오기 어려움
// REST: 여러 관련 리소스를 가져오려면 여러 요청 필요
GET /users/123
GET /users/123/posts
// 또는 모든 필드를 포함한 과도한 데이터 수신
GET /users/123?include=posts
엔드포인트 증가: 기능이 추가될수록 관리할 엔드포인트 증가
버전 관리 필요: API 변경 시 버전 관리 복잡성 증가
문서화 추가 비용: API 문서를 별도로 작성하고 유지 필요
3. 실제 사용 사례 및 모범 사례
3.1 GraphQL이 적합한 경우
- 복잡한 데이터 요구사항: 여러 리소스 간 복잡한 관계 조회 필요
- 다양한 클라이언트: 다양한 기기에서 서로 다른 데이터 요구사항
- 빠른 반복: 백엔드 변경 없이 프론트엔드 요구사항 변경 필요
- 마이크로서비스 통합: 여러 서비스의 데이터 통합 제공
3.2 RESTful API가 적합한 경우
- 단순한 CRUD 작업: 기본적인 리소스 조작이 주된 작업
- 공개 API: 외부 개발자가 사용할 공개 API
- 파일 업로드가 중요한 경우: 대용량 파일 처리 필요
- 캐싱 중요: HTTP 캐싱 최대한 활용 필요
3.3 GraphQL 모범 사례
- N+1 쿼리 문제 해결: DataLoader 사용
- 쿼리 복잡성 제한: 쿼리 복잡도 검증 규칙 적용
- 인증 및 권한 처리: context를 활용한 인증 구현
- 페이지네이션 구현: 커서 기반 페이지네이션 적용
4. GraphQL과 REST의 공존
많은 실제 애플리케이션에서는 각 접근 방식의 장점을 활용하는 하이브리드 접근법을 사용합니다.
const app = express();
// REST 엔드포인트 구현
app.get("/api/health", (req, res) => {
res.json({ status: "healthy" });
});
// 파일 업로드는 REST로 처리
app.post("/api/upload", express.json(), (req, res) => {
// 파일 업로드 처리
res.json({ success: true });
});
// GraphQL 서버 설정
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app });
요약
Node.js에서 GraphQL은 Apollo Server를 통해 쉽게 구현할 수 있으며, 복잡한 데이터 요구사항이 있는 애플리케이션에 특히 적합합니다. GraphQL의 주요 장점은 정확한 데이터 요청, 강력한 타입 시스템, 자체 문서화입니다. 반면 REST는 단순성, HTTP 캐싱, 파일 업로드에 강점이 있습니다.
실제 프로젝트에서는 요구사항과 팀의 전문성을 고려하여 GraphQL과 REST 중 선택하거나, 두 접근 방식을 함께 사용하는 하이브리드 전략을 채택할 수 있습니다. 중요한 것은 애플리케이션의 구체적인 요구사항에 맞는 최적의 솔루션을 선택하는 것입니다.