[Daily morning study] GraphQL vs REST API ๋น„๊ต

#daily morning study

Image


REST API ๊ธฐ๋ณธ ๊ฐœ๋…

REST(Representational State Transfer)๋Š” HTTP ํ”„๋กœํ† ์ฝœ ๊ธฐ๋ฐ˜์˜ ์•„ํ‚คํ…์ฒ˜ ์Šคํƒ€์ผ์ด๋‹ค. ๊ฐ ๋ฆฌ์†Œ์Šค๋Š” ๊ณ ์œ ํ•œ URL๋กœ ํ‘œํ˜„๋˜๊ณ , HTTP ๋ฉ”์„œ๋“œ๋กœ CRUD ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

GET    /users       โ†’ ์‚ฌ์šฉ์ž ๋ชฉ๋ก
GET    /users/1     โ†’ ํŠน์ • ์‚ฌ์šฉ์ž
POST   /users       โ†’ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
PUT    /users/1     โ†’ ์‚ฌ์šฉ์ž ์ˆ˜์ •
DELETE /users/1     โ†’ ์‚ฌ์šฉ์ž ์‚ญ์ œ

GraphQL ๊ธฐ๋ณธ ๊ฐœ๋…

GraphQL์€ Facebook(ํ˜„ Meta)์ด 2015๋…„์— ๊ณต๊ฐœํ•œ ์ฟผ๋ฆฌ ์–ธ์–ด์ด์ž ๋Ÿฐํƒ€์ž„์ด๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ๋ฅผ ์ง์ ‘ ์ •์˜ํ•ด์„œ ์š”์ฒญํ•œ๋‹ค.

query {
  user(id: "1") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ(/graphql)๋กœ ๋ชจ๋“  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ์ ์ด REST์™€ ๊ฐ€์žฅ ํฐ ๊ตฌ์กฐ์  ์ฐจ์ด๋‹ค.


ํ•ต์‹ฌ ์ฐจ์ด์  ๋น„๊ต

ํ•ญ๋ชฉRESTGraphQL
์—”๋“œํฌ์ธํŠธ๋ฆฌ์†Œ์Šค๋งˆ๋‹ค ๋‹ค๋ฆ„๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ
์‘๋‹ต ํ˜•ํƒœ ๊ฒฐ์ •๊ถŒ์„œ๋ฒ„ํด๋ผ์ด์–ธํŠธ
Over-fetching์ž์ฃผ ๋ฐœ์ƒํ•ด๊ฒฐ๋จ
Under-fetching์ž์ฃผ ๋ฐœ์ƒํ•ด๊ฒฐ๋จ
HTTP ์บ์‹ฑ๊ธฐ๋ณธ ์ง€์›๋ณ„๋„ ๊ตฌํ˜„ ํ•„์š”
ํŒŒ์ผ ์—…๋กœ๋“œ๊ฐ„๋‹จ์ถ”๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•„์š”
ํ•™์Šต ๊ณก์„ ๋‚ฎ์Œ์ƒ๋Œ€์ ์œผ๋กœ ๋†’์Œ
๋ฒ„์ „ ๊ด€๋ฆฌ/v1/, /v2/ ๋ฐฉ์‹์Šคํ‚ค๋งˆ ์ง„ํ™”๋กœ ์ฒ˜๋ฆฌ

Over-fetching๊ณผ Under-fetching

REST API์—์„œ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋น„ํšจ์œจ์ด๋‹ค.

Over-fetching: ํ•„์š” ์ด์ƒ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๋Š” ๊ฒƒ

์‚ฌ์šฉ์ž ์ด๋ฆ„๋งŒ ํ•„์š”ํ•œ๋ฐ /users/1์„ ํ˜ธ์ถœํ•˜๋ฉด name, email, age, address, createdAt ๋“ฑ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์‘๋‹ต์œผ๋กœ ์˜จ๋‹ค.

Under-fetching: ํ•œ ๋ฒˆ์˜ ์š”์ฒญ์œผ๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ๋ชป ๋ฐ›๋Š” ๊ฒƒ

์‚ฌ์šฉ์ž ์ •๋ณด์™€ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ํ•จ๊ป˜ ํ‘œ์‹œํ•˜๋ ค๋ฉด ๋‘ ๋ฒˆ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

GET /users/1       โ†’ ์‚ฌ์šฉ์ž ์ •๋ณด
GET /users/1/posts โ†’ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก

์ด ํŒจํ„ด์ด ๋ฐ˜๋ณต๋˜๋ฉด N+1 ๋ฌธ์ œ๋กœ ์ด์–ด์ง„๋‹ค. GraphQL์€ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์ •ํ™•ํžˆ ๊ฐ€์ ธ์™€ ๋‘ ๋ฌธ์ œ๋ฅผ ๋™์‹œ์— ํ•ด๊ฒฐํ•œ๋‹ค.


GraphQL ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ

Schema์™€ Type System

GraphQL์€ ๊ฐ•ํƒ€์ž… ์‹œ์Šคํ…œ์„ ๊ฐ–๋Š”๋‹ค. ์„œ๋ฒ„๋Š” ์Šคํ‚ค๋งˆ๋กœ ๋ฐ์ดํ„ฐ ํ˜•ํƒœ๋ฅผ ๋ฏธ๋ฆฌ ์ •์˜ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๋Š” ๊ทธ ๋ฒ”์œ„ ์•ˆ์—์„œ ์ž์œ ๋กญ๊ฒŒ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

type Mutation {
  createUser(name: String!, email: String!): User!
  deleteUser(id: ID!): Boolean!
}

!๋Š” null ๋ถˆ๊ฐ€๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

Query, Mutation, Subscription

  • Query: ๋ฐ์ดํ„ฐ ์กฐํšŒ (REST์˜ GET์— ํ•ด๋‹น)
  • Mutation: ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ (POST, PUT, DELETE์— ํ•ด๋‹น)
  • Subscription: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ตฌ๋… (WebSocket ๊ธฐ๋ฐ˜)
subscription {
  messageAdded(roomId: "1") {
    content
    author {
      name
    }
  }
}

Subscription์€ REST๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ์„ ์Šคํ‚ค๋งˆ ์ˆ˜์ค€์—์„œ ์ •์˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

Resolver

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•œ ๊ฐ ํ•„๋“œ์— ๋Œ€ํ•ด ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜๋‹ค.

const resolvers = {
  Query: {
    user: (_, { id }) => User.findById(id),
    users: () => User.findAll(),
  },
  User: {
    posts: (user) => Post.findByAuthorId(user.id),
  },
};

๊ฐ ํ•„๋“œ๋งˆ๋‹ค resolver๊ฐ€ ์‹คํ–‰๋˜๋ฏ€๋กœ, ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์—์„œ๋Š” N+1 ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค.


N+1 ๋ฌธ์ œ์™€ DataLoader

์‚ฌ์šฉ์ž 10๋ช…์˜ ๊ฒŒ์‹œ๊ธ€์„ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๋Š” ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด:

  1. users ์ฟผ๋ฆฌ ์‹คํ–‰ โ†’ 1๋ฒˆ DB ํ˜ธ์ถœ
  2. ๊ฐ User์˜ posts resolver ์‹คํ–‰ โ†’ 10๋ฒˆ DB ํ˜ธ์ถœ
  3. ์ด 11๋ฒˆ DB ์ฟผ๋ฆฌ ๋ฐœ์ƒ

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด DataLoader๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์š”์ฒญ์„ ๋ฐฐ์น˜(batch)๋กœ ๋ฌถ์–ด ๋‹จ 1๋ฒˆ์˜ DB ์ฟผ๋ฆฌ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

const postLoader = new DataLoader(async (userIds) => {
  const posts = await Post.findByAuthorIds(userIds); // IN ์ฟผ๋ฆฌ 1๋ฒˆ
  return userIds.map(id => posts.filter(p => p.authorId === id));
});

// User resolver์—์„œ
User: {
  posts: (user) => postLoader.load(user.id),
}

DataLoader๋Š” ๊ฐ™์€ ์ด๋ฒคํŠธ ๋ฃจํ”„ ๋‚ด์—์„œ ๋ฐœ์ƒํ•˜๋Š” load() ํ˜ธ์ถœ์„ ์ž๋™์œผ๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.


REST๊ฐ€ ๋” ๋‚˜์€ ๊ฒฝ์šฐ

  • ๋‹จ์ˆœํ•œ CRUD API: ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
  • ํŒŒ์ผ ์—…๋กœ๋“œ: multipart/form-data ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ„๋‹จ
  • HTTP ์บ์‹ฑ ์ ๊ทน ํ™œ์šฉ: CDN, ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
  • ๊ณต๊ฐœ API: ๋‹ค์–‘ํ•œ ์™ธ๋ถ€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํผ๋ธ”๋ฆญ API
  • ํŒ€์ด REST์— ์ต์ˆ™ํ•˜๊ณ  ํ•™์Šต ๋น„์šฉ์„ ์ค„์—ฌ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ

GraphQL์ด ๋” ๋‚˜์€ ๊ฒฝ์šฐ

  • ๋ชจ๋ฐ”์ผ ์•ฑ: ๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ ˆ๊ฐ, ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์ˆ˜์‹ 
  • ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„: ์—ฌ๋Ÿฌ ๋ฆฌ์†Œ์Šค๋ฅผ ํ•œ ์ฟผ๋ฆฌ๋กœ ๊ฐ€์ ธ์™€์•ผ ํ•  ๋•Œ
  • ๋‹ค์–‘ํ•œ ํด๋ผ์ด์–ธํŠธ: ์›น, ๋ชจ๋ฐ”์ผ, IoT๊ฐ€ ๊ฐ๊ฐ ๋‹ค๋ฅธ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„์š”๋กœ ํ•  ๋•Œ
  • ๋น ๋ฅธ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ: ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ ์—†์ด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ง์ ‘ ์ •์˜
  • ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ: Subscription์œผ๋กœ WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

๋‘ ๋ฐฉ์‹์„ ํ•จ๊ป˜ ์“ฐ๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ํŒจํ„ด

์‹ค๋ฌด์—์„œ๋Š” REST์™€ GraphQL์„ ๋™์‹œ์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ๋‹ค.

  • ์ธ์ฆ, ํŒŒ์ผ ์—…๋กœ๋“œ โ†’ REST (/auth/login, /upload)
  • ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฐ ๋ณ€๊ฒฝ โ†’ GraphQL (/graphql)

์บ์‹ฑ์ด ์ค‘์š”ํ•œ ํผ๋ธ”๋ฆญ ์ฝ˜ํ…์ธ ๋Š” REST๋กœ, ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๋‚ด๋ถ€ API๋Š” GraphQL๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.


์ •๋ฆฌ

REST์™€ GraphQL์€ ๊ฐ์ž ์ž˜ ๋งž๋Š” ์ƒํ™ฉ์ด ๋‹ค๋ฅด๋‹ค. REST๋Š” ๋‹จ์ˆœํ•˜๊ณ  HTTP ํ‘œ์ค€๊ณผ ์ž˜ ๋งž๋Š” ๋ฐ˜๋ฉด, GraphQL์€ ํด๋ผ์ด์–ธํŠธ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋‹ค์–‘ํ•˜๊ณ  ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๊ฐ€ ๋ณต์žกํ•  ๋•Œ ๊ฐ•์ ์„ ๋ฐœํœ˜ํ•œ๋‹ค. ์„ ํƒ ๊ธฐ์ค€์€ ํ”„๋กœ์ ํŠธ ๋ณต์žก๋„, ํŒ€ ์—ญ๋Ÿ‰, ํด๋ผ์ด์–ธํŠธ ๋‹ค์–‘์„ฑ์ด๋‹ค.