GraphQL vs REST API: 언제 무엇을 써야 할까 - 코드픽 블로그
GraphQL vs REST API: 언제 무엇을 써야 할까
기술 가이드

GraphQL vs REST API: 언제 무엇을 써야 할까

2026년 3월 6일 35 views by 코드벤터

GraphQL vs REST API: 언제 무엇을 써야 할까

안녕하세요, 코드픽 독자 여러분! 🚀

오늘날 웹 및 모바일 애플리케이션 개발에서 API(Application Programming Interface)는 없어서는 안 될 핵심 요소입니다. 프론트엔드와 백엔드, 또는 여러 서비스 간의 통신을 담당하며 데이터 교환의 통로 역할을 하죠. 하지만 API를 설계할 때, 우리는 종종 중요한 기로에 서게 됩니다: 과연 어떤 API 스타일을 선택해야 할까? 특히 최근 몇 년간 GraphQL이 REST API의 강력한 대안으로 떠오르면서, 이 질문은 더욱 복잡해졌습니다.

REST(Representational State Transfer) API는 오랫동안 업계 표준으로 자리 잡으며 그 견고함과 유연성을 입증해왔습니다. 반면, GraphQL은 페이스북이 개발하여 오픈소스로 공개한 이후, 데이터 요청의 효율성과 개발 생산성 측면에서 많은 주목을 받고 있습니다.

그렇다면 우리 프로젝트에는 GraphQL과 REST API 중 어떤 것이 더 적합할까요? 이 글에서는 두 API 스타일의 핵심 개념부터 장단점, 그리고 실제 개발 환경에서 언제 무엇을 선택해야 할지에 대한 실질적인 가이드를 제시하고자 합니다. 실전 코드 예제와 함께 각 기술의 특징을 깊이 있게 파헤쳐보고, 여러분의 API 설계에 대한 고민을 덜어드리겠습니다.

1. REST API 깊이 들여다보기

REST는 웹 서비스를 위한 아키텍처 스타일로, 2000년 로이 필딩(Roy Fielding)의 박사 학위 논문에서 처음 소개되었습니다. HTTP 프로토콜의 표준 메서드를 사용하여 리소스 기반으로 데이터를 주고받는 방식이 특징입니다.

1.1. REST API의 핵심 원칙

REST API는 다음과 같은 원칙들을 기반으로 합니다.

  • 클라이언트-서버(Client-Server): 클라이언트와 서버는 독립적으로 개발되고 배포될 수 있으며, 서로 간의 의존성을 최소화합니다.
  • 스테이트리스(Stateless): 서버는 클라이언트의 요청 간에 어떤 상태도 저장하지 않습니다. 각 요청은 필요한 모든 정보를 포함해야 합니다.
  • 캐시 가능(Cacheable): 클라이언트는 서버 응답을 캐시할 수 있어야 합니다. 이는 네트워크 효율성을 높이는 데 기여합니다.
  • 계층화된 시스템(Layered System): 클라이언트는 중간 서버의 존재를 알지 못하며, 여러 계층을 거쳐 서버에 도달할 수 있습니다.
  • 균일한 인터페이스(Uniform Interface): 모든 리소스에 대한 접근 방식이 통일되어야 합니다. 이는 다음과 같은 세부 원칙을 포함합니다.
    • 리소스 식별(Identification of resources): 모든 리소스는 URI(Uniform Resource Identifier)로 식별됩니다.
    • 표현을 통한 리소스 조작(Manipulation of resources through representations): 클라이언트는 리소스의 표현(JSON, XML 등)을 통해 리소스를 조작합니다.
    • 자기 기술 메시지(Self-descriptive messages): 메시지 자체에 해당 메시지를 해석하는 데 필요한 모든 정보가 포함되어야 합니다.
    • 애플리케이션 상태에 대한 엔진으로서의 하이퍼미디어(Hypermedia as the Engine of Application State, HATEOAS): API는 다음 가능한 상태 전이를 나타내는 링크를 포함해야 합니다. (실제 구현에서는 잘 지켜지지 않는 경우가 많습니다.)

1.2. REST API의 장점

  • 단순성과 광범위한 채택: HTTP 표준을 따르므로 이해하고 구현하기 쉽습니다. 웹의 기본 아키텍처와 잘 맞습니다.
  • 캐싱: HTTP 캐싱 메커니즘을 활용하여 성능을 최적화하기 용이합니다.
  • 다양한 도구 및 에코시스템: 오랜 역사만큼이나 다양한 개발 도구, 라이브러리, 문서 및 커뮤니티 지원을 받을 수 있습니다.
  • 확장성: 클라이언트와 서버가 분리되어 있어 독립적인 확장이 가능합니다.

1.3. REST API의 단점

  • 오버페칭(Over-fetching) 및 언더페칭(Under-fetching):
    • 오버페칭: 클라이언트가 필요한 데이터보다 더 많은 데이터를 서버로부터 받는 현상입니다. 예를 들어, 사용자 목록에서 이름만 필요한데, 모든 사용자 정보(이메일, 주소, 전화번호 등)를 받는 경우입니다.
    • 언더페칭: 한 번의 요청으로 필요한 모든 데이터를 얻을 수 없어 여러 번의 요청을 보내야 하는 현상입니다. 예를 들어, 게시글과 함께 해당 게시글의 댓글, 작성자 정보를 모두 표시하려면 /posts/{id}, /posts/{id}/comments, /users/{id}와 같이 여러 엔드포인트에 요청해야 할 수 있습니다. 이는 네트워크 지연 시간을 증가시키고 클라이언트 측 로직을 복잡하게 만듭니다.
  • 버전 관리의 어려움: API가 변경될 때마다 새로운 버전(예: /v1/users, /v2/users)을 생성해야 할 수 있으며, 이는 클라이언트와의 호환성 유지에 어려움을 줍니다.
  • 고정된 응답 구조: 서버에서 정의한 응답 구조를 클라이언트가 그대로 사용해야 합니다. 다양한 클라이언트(웹, 모바일, 관리자 페이지 등)의 요구사항을 모두 충족시키기 어렵습니다.

1.4. REST API 코드 예제 (Python Flask)

간단한 사용자 관리 REST API를 Flask로 구현해 봅시다.

python
# app.py
from flask import Flask, jsonify, request

app = Flask(__name__)

# 간단한 인메모리 데이터베이스
users = {
    "1": {"id": "1", "name": "김철수", "email": "chulsoo.kim@example.com"},
    "2": {"id": "2", "name": "이영희", "email": "younghee.lee@example.com"}
}

# 모든 사용자 조회
@app.route(/users, methods=[GET])
def get_users():
    return jsonify(list(users.values()))

# 특정 사용자 조회
@app.route(/users/<string:user_id>, methods=[GET])
def get_user(user_id):
    user = users.get(user_id)
    if user:
        return jsonify(user)
    return jsonify({"message": "User not found"}), 404

# 새 사용자 생성
@app.route(/users, methods=[POST])
def create_user():
    new_user_data = request.json
    if not new_user_data or name not in new_user_data or email not in new_user_data:
        return jsonify({"message": "Missing name or email"}), 400

    new_id = str(len(users) + 1)
    new_user = {"id": new_id, "name": new_user_data[name], "email": new_user_data[email]}
    users[new_id] = new_user
    return jsonify(new_user), 201

# 사용자 정보 업데이트
@app.route(/users/<string:user_id>, methods=[PUT])
def update_user(user_id):
    if user_id not in users:
        return jsonify({"message": "User not found"}), 404

    update_data = request.json
    if not update_data:
        return jsonify({"message": "No data provided"}), 400

    users[user_id].update(update_data)
    return jsonify(users[user_id])

# 사용자 삭제
@app.route(/users/<string:user_id>, methods=[DELETE])
def delete_user(user_id):
    if user_id not in users:
        return jsonify({"message": "User not found"}), 404
    
    del users[user_id]
    return jsonify({"message": "User deleted"}), 204

if __name__ == __main__:
    app.run(debug=True)

이 예제에서 볼 수 있듯이, REST API는 각 리소스(여기서는 users)에 대해 고유한 URI를 사용하고, HTTP 메서드(GET, POST, PUT, DELETE)를 통해 해당 리소스에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행합니다.

클라이언트 요청 예시 (curl):

  • 모든 사용자 조회:

    bash
    curl http://127.0.0.1:5000/users

    응답: [{"id": "1", "name": "김철수", "email": "chulsoo.kim@example.com"}, {"id": "2", "name": "이영희", "email": "younghee.lee@example.com"}]

  • 특정 사용자 조회 (ID: 1):

    bash
    curl http://127.0.0.1:5000/users/1

    응답: {"id": "1", "name": "김철수", "email": "chulsoo.kim@example.com"}

  • 새 사용자 생성:

    bash
    curl -X POST -H "Content-Type: application/json" -d {"name": "박민준", "email": "minjun.park@example.com"} http://127.0.0.1:5000/users

    응답: {"id": "3", "name": "박민준", "email": "minjun.park@example.com"}

2. GraphQL API 깊이 들여다보기

GraphQL은 API를 위한 쿼리 언어(Query Language)이자, 기존 데이터로 해당 쿼리를 수행하기 위한 런타임입니다. 페이스북이 2012년에 내부적으로 개발하여 2015년에 오픈소스로 공개했습니다. 클라이언트가 필요한 데이터를 정확히 명시하여 요청할 수 있다는 점이 가장 큰 특징입니다.

2.1. GraphQL의 핵심 개념

  • 스키마(Schema): GraphQL API의 핵심입니다. 서버가 제공할 수 있는 모든 데이터의 타입과 필드를 정의합니다. 스키마 정의 언어(Schema Definition Language, SDL)를 사용하여 작성됩니다.
  • 타입(Types): 데이터 객체의 구조를 정의합니다. 예를 들어 User 타입은 id, name, email 필드를 가질 수 있습니다.
  • 쿼리(Query): 데이터를 읽어오는 작업입니다. 클라이언트는 필요한 필드만 명시하여 요청합니다.
  • 뮤테이션(Mutation): 데이터를 변경(생성, 업데이트, 삭제)하는 작업입니다.
  • 서브스크립션(Subscription): 실시간 데이터 업데이트를 위한 기능입니다. 클라이언트가 특정 이벤트에 구독하면, 해당 이벤트 발생 시 서버에서 클라이언트로 데이터를 푸시합니다.
  • 리졸버(Resolver): 스키마에 정의된 각 필드에 대한 데이터를 실제로 가져오는 함수입니다. 데이터베이스에서 데이터를 조회하거나, 다른 REST API를 호출하는 등의 로직을 포함합니다.
  • 단일 엔드포인트(Single Endpoint): REST API가 여러 엔드포인트를 가지는 것과 달리, GraphQL은 보통 /graphql과 같은 단일 엔드포인트를 통해 모든 요청을 처리합니다.

2.2. GraphQL의 장점

  • 정확한 데이터 페칭(No Over-fetching/Under-fetching): 클라이언트가 필요한 데이터 필드만 정확히 요청할 수 있으므로, 오버페칭과 언더페칭 문제가 발생하지 않습니다. 이는 모바일 환경과 같이 네트워크 대역폭이 제한적인 환경에서 특히 유리합니다.
  • 단일 요청으로 다양한 데이터 획득: 여러 리소스에서 필요한 데이터를 한 번의 요청으로 가져올 수 있어, 네트워크 왕복 횟수를 줄여줍니다.
  • 강력한 타입 시스템과 자동 완성: 스키마를 통해 데이터의 구조가 명확하게 정의되므로, 클라이언트 개발 시 자동 완성 및 유효성 검사가 가능해 개발 생산성을 높입니다.
  • 버전리스(Versionless) API: 필드를 추가하거나 제거하는 방식으로 API를 발전시킬 수 있어, REST API처럼 /v1, /v2와 같은 버전 관리가 필요 없는 경우가 많습니다.
  • 인트로스펙션(Introspection): 스키마를 쿼리하여 API의 구조를 동적으로 파악할 수 있습니다. 이는 문서화 도구나 개발자 도구에 활용됩니다.

2.3. GraphQL의 단점

  • 학습 곡선: REST API에 비해 새로운 개념(스키마, 리졸버, 쿼리 언어)이 많아 초기 학습 시간이 필요합니다.
  • 복잡한 캐싱: HTTP 캐싱을 직접 활용하기 어려운 구조입니다. 각 쿼리가 다르기 때문에, 클라이언트 측에서 캐싱 전략을 직접 구현하거나 Apollo Client와 같은 라이브러리의 도움을 받아야 합니다.
  • 파일 업로드 처리: GraphQL 자체는 파일 업로드에 대한 표준화된 메커니즘을 제공하지 않습니다. 일반적으로 REST API 엔드포인트를 사용하거나, GraphQL 멀티파트 요청 확장을 사용합니다.
  • N+1 문제: 리졸버에서 데이터를 효율적으로 가져오지 못하면, 하나의 쿼리 요청이 여러 번의 데이터베이스 쿼리를 유발할 수 있습니다. (DataLoader와 같은 기술로 해결 가능)
  • 서버 복잡성: 클라이언트의 유연한 요청을 처리하기 위해 서버 측에서 더 복잡한 로직(리졸버 구현, 권한 관리 등)이 필요할 수 있습니다.

2.4. GraphQL 코드 예제 (Python Graphene)

Python Graphene 라이브러리를 사용하여 간단한 사용자 관리 GraphQL API를 구현해 봅시다.

python
# app.py
import graphene
from flask import Flask
from flask_graphql import GraphQLView

# 간단한 인메모리 데이터베이스
users_db = {
    "1": {"id": "1", "name": "김철수", "email": "chulsoo.kim@example.com", "age": 30},
    "2": {"id": "2", "name": "이영희", "email": "younghee.lee@example.com", "age": 25}
}

# 1. User Type 정의
class User(graphene.ObjectType):
    id = graphene.ID()
    name = graphene.String()
    email = graphene.String()
    age = graphene.Int()

# 2. Query Type 정의 (데이터 읽기)
class Query(graphene.ObjectType):
    # 모든 사용자 조회
    users = graphene.List(User)
    # 특정 사용자 조회 (인자: id)
    user = graphene.Field(User, id=graphene.ID(required=True))

    def resolve_users(root, info):
        return list(users_db.values())

    def resolve_user(root, info, id):
        return users_db.get(id)

# 3. Mutation Type 정의 (데이터 변경)
class CreateUser(graphene.Mutation):
    class Arguments:
        name = graphene.String(required=True)
        email = graphene.String(required=True)
        age = graphene.Int()

    Output = User # 뮤테이션 성공 시 반환할 타입

    def mutate(root, info, name, email, age=None):
        new_id = str(len(users_db) + 1)
        new_user = {"id": new_id, "name": name, "email": email, "age": age}
        users_db[new_id] = new_user
        return new_user

class UpdateUser(graphene.Mutation):
    class Arguments:
        id = graphene.ID(required=True)
        name = graphene.String()
        email = graphene.String()
        age = graphene.Int()

    Output = User

    def mutate(root, info, id, name=None, email=None, age=None):
        if id not in users_db:
            raise Exception("User not found")
        
        user = users_db[id]
        if name is not None:
            user[name] = name
        if email is not None:
            user[email] = email
        if age is not None:
            user[age] = age
        
        return user

class DeleteUser(graphene.Mutation):
    class Arguments:
        id = graphene.ID(required=True)
    
    Output = graphene.String # 삭제 성공 메시지 반환

    def mutate(root, info, id):
        if id not in users_db:
            raise Exception("User not found")
        
        del users_db[id]
        return f"User with ID {id} deleted successfully."


class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()
    update_user = UpdateUser.Field()
    delete_user = DeleteUser.Field()

# 4. Schema 생성
schema = graphene.Schema(query=Query, mutation=Mutation)

# Flask 앱에 GraphQLView 연결
app = Flask(__name__)
app.add_url_rule(
    /graphql,
    view_func=GraphQLView.as_view(
        graphql,
        schema=schema,
        graphiql=True # GraphiQL 인터페이스 활성화 (개발용)
    )
)

if __name__ == __main__:
    app.run(debug=True)

이 코드를 실행하고 http://127.0.0.1:5000/graphql에 접속하면 GraphiQL이라는 개발자 도구가 나타납니다. 여기서 GraphQL 쿼리를 테스트할 수 있습니다.

클라이언트 쿼리 예시 (GraphiQL 또는 curl):

  • 모든 사용자의 이름과 이메일만 조회:

    graphql
    query {
      users {
        name
        email
      }
    }

    응답:

    json
    {
      "data": {
        "users": [
          { "name": "김철수", "email": "chulsoo.kim@example.com" },
          { "name": "이영희", "email": "younghee.lee@example.com" }
        ]
      }
    }
  • ID가 1인 사용자의 이름과 나이만 조회:

    graphql
    query {
      user(id: "1") {
        name
        age
      }
    }

    응답:

    json
    {
      "data": {
        "user": { "name": "김철수", "age": 30 }
      }
    }
  • 새 사용자 생성 (Mutation):

    graphql
    mutation {
      createUser(name: "최지훈", email: "jihun.choi@example.com", age: 28) {
        id
        name
        email
      }
    }

    응답:

    json
    {
      "data": {
        "createUser": {
          "id": "3",
          "name": "최지훈",
          "email": "jihun.choi@example.com"
        }
      }
    }

보시다시피 GraphQL은 클라이언트가 필요한 데이터의 구조를 직접 정의하여 요청할 수 있다는 점에서 REST API와 큰 차이를 보입니다.

3. GraphQL vs REST API: 핵심 비교

두 API 스타일의 주요 차이점을 표로 정리해 보았습니다. 이는 API설계 시 중요한 고려 사항이 됩니다.

항목REST APIGraphQL API
**데이터 페칭**고정된 리소스 기반 엔드포인트, 오버/언더페칭 가능클라이언트가 필요한 데이터 필드만 요청, 효율적
**엔드포인트**리소스별 다수의 엔드포인트단일 엔드포인트 (`/graphql`)
**스키마**명시적인 스키마 정의가 필수는 아님 (OpenAPI/Swagger 사용)SDL(Schema Definition Language)로 엄격하게 정의
**캐싱**HTTP 캐싱 메커니즘 활용 용이클라이언트 라이브러리(Apollo Client 등)를 통한 캐싱, 서버 측 캐싱 복잡
**에러 처리**HTTP 상태 코드(2xx, 4xx, 5xx) 및 응답 본문항상 HTTP 200 OK, 응답 본문에 에러 정보 포함
**버전 관리**URI 버전(e.g., `/v1/users`) 또는 헤더 사용스키마 진화(필드 추가/제거)를 통한 버전리스 운영 지향
**개발 복잡성**서버 측 구현이 비교적 간단스키마, 리졸버 등으로 서버 측 구현이 복잡할 수 있음
**유스케이스**간단한 CRUD, 파일 업로드/다운로드, 레거시 시스템복잡한 데이터 요구사항, 다양한 클라이언트, 마이크로서비스

4. 언제 무엇을 써야 할까? 실전 가이드

이제 가장 중요한 질문에 답할 차례입니다: 언제 GraphQL을 사용하고, 언제 REST API를 사용해야 할까요? 정답은 프로젝트의 특정 요구사항과 환경에 따라 다르다입니다.

4.1. GraphQL이 유리한 경우

  • 복잡하고 다양한 데이터 요구사항:
    • 모바일 애플리케이션: 네트워크 대역폭이 제한적이거나 지연 시간이 긴 모바일 환경에서 오버페칭을 줄여 성능을 최적화할 수 있습니다.
    • 다양한 클라이언트: 웹, 모바일, 관리자 페이지 등 여러 클라이언트가 각각 다른 데이터 구조를 필요로 할 때, GraphQL은 각 클라이언트가 필요한 데이터만 요청할 수 있게 하여 API 재사용성을 높입니다.
    • 데이터 그래프가 복잡할 때: 여러 리소스가 서로 복잡하게 연결되어 있고, 클라이언트가 이 관계를 탐색하며 데이터를 가져와야 할 때 GraphQL의 강점이 드러납니다. (예: 소셜 네트워크의 사용자-게시글-댓글-친구 관계)
  • 빠른 프로토타이핑 및 잦은 API 변경:
    • 프론트엔드 개발자가 백엔드 API 변경 없이 필요한 데이터를 직접 정의하여 빠르게 개발할 수 있습니다.
    • 제품 요구사항이 자주 변경되어 API 구조도 유동적으로 변해야 할 때 유연하게 대응할 수 있습니다.
  • 마이크로서비스 아키텍처:
    • 여러 마이크로서비스에서 데이터를 가져와 하나의 클라이언트 요청에 응답해야 할 때, GraphQL API Gateway 역할을 하여 클라이언트가 단일 엔드포인트에서 모든 데이터를 통합하여 가져올 수 있게 합니다. 이는 클라이언트가 여러 서비스의 존재를 알 필요 없게 만듭니다.
  • 강력한 타입 시스템의 이점:
    • API의 스키마가 명확하게 정의되어 있어, 프론트엔드 개발 시 타입 검사를 통해 오류를 줄이고 개발 생산성을 높일 수 있습니다.

4.2. REST API가 유리한 경우

  • 간단하고 예측 가능한 데이터 구조:
    • CRUD 작업이 명확하고, 각 리소스의 데이터 구조가 비교적 고정적이며 단순한 애플리케이션에는 REST API가 더 효율적일 수 있습니다. (예: 블로그 게시글, 단순 사용자 정보)
    • 오버페칭이나 언더페칭이 큰 문제가 되지 않는 경우.
  • 레거시 시스템과의 통합:
    • 기존에 REST API로 구축된 시스템이 많으므로, 새로운 시스템을 기존 시스템과 통합해야 할 때 REST API가 더 자연스러운 선택일 수 있습니다.
  • 캐싱이 매우 중요한 경우:
    • HTTP 캐싱 메커니즘을 적극적으로 활용하여 성능을 최적화해야 하는 경우, REST API가 더 직관적이고 구현하기 쉽습니다.
  • 작은 프로젝트 또는 학습 목적:
    • 개발 팀의 규모가 작거나, 새로운 기술 학습에 대한 부담을 줄이고자 할 때, 상대적으로 진입 장벽이 낮은 REST API가 좋은 선택입니다.
  • 파일 업로드/다운로드가 주된 기능:
    • GraphQL은 파일 업로드에 대한 기본 지원이 없으므로, 파일 처리가 핵심 기능인 경우 REST API가 더 적합합니다.

5. 실전 예제: 쇼핑몰 애플리케이션의 API 설계

가상의 쇼핑몰 애플리케이션을 통해 GraphQL과 REST API의 차이를 더 명확하게 이해해봅시다.

시나리오: 특정 상품의 상세 정보를 보여주는 페이지를 개발해야 합니다. 이 페이지에는 다음 정보가 필요합니다.

  • 상품 기본 정보 (이름, 가격, 설명, 이미지)
  • 상품 리뷰 목록 (작성자, 평점, 내용)
  • 판매자 정보 (이름, 연락처)

5.1. REST API로 구현하는 경우

REST API에서는 일반적으로 다음과 같이 여러 번의 요청을 보내야 할 수 있습니다.

  1. 상품 기본 정보 요청: GET /products/{productId}
    • 응답: { id, name, price, description, imageUrl, sellerId }
  2. 상품 리뷰 목록 요청: GET /products/{productId}/reviews
    • 응답: [ { id, userId, rating, comment }, ... ]
  3. 판매자 정보 요청: GET /sellers/{sellerId} (첫 번째 요청에서 얻은 sellerId 사용)
    • 응답: { id, name, contactEmail }

이 경우, 클라이언트는 총 3번의 네트워크 요청을 보내야 하며, 각 요청이 완료될 때까지 기다려야 합니다. 또한, GET /products/{productId}에서 sellerId만 필요하지만

개발 의뢰 상담

AI 서비스나 플랫폼 개발을
고민 중이신가요?

CodePick에서는
기획 → 개발 → 운영까지 함께합니다.
아이디어만 있어도 상담 가능합니다.

CodeVenter 개발팀이 직접 담당 · 1~2 영업일 내 회신

✓ 스타트업 MVP 개발✓ AI 서비스 개발✓ 웹 플랫폼 개발✓ 기업 시스템 구축✓ 모바일 앱 개발

AI Development Studio

코드픽 by 코드벤터

  • 대표: 윤승환 · 사업자등록번호: 121-57-64983
  • 대구광역시 중구 국채보상로 586, 16층 · info@codeventer.com

© 2025 코드벤터. All rights reserved.