웹 통신 기법 비교

웹 개발을 하다 보면 클라이언트와 서버 간의 통신 방식을 선택해야 하는 순간이 온다. 오늘은 가장 널리 사용되는 네 가지 통신 기법인 REST API, GraphQL, gRPC, WebSocket에 대해 알아보자.

본 포스트는 Claude 4로 작성 후, 수정하였습니다.


REST API

REST(Representational State Transfer)는 웹에서 가장 일반적으로 사용되는 통신 방식이다. HTTP 메서드(GET, POST, PUT, DELETE)를 사용해 리소스를 조작한다.

REST API의 핵심 특징은 다음과 같다:

  • GET: 서버에서 데이터를 조회할 때 사용.
  • POST: 서버에 새 데이터를 생성할 때 사용.
  • PUT: 서버에 기존 데이터를 전체 수정할 때 사용.
  • DELETE: 서버에서 데이터를 삭제할 때 사용.

FastAPI를 사용한 간단한 REST API 예시를 보자:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    email: str

users = []

@app.get("/users")
def get_users():
    return users

@app.post("/users")
def create_user(user: User):
    users.append(user)
    return {"message": "User created successfully"}

@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id < len(users):
        return users[user_id]
    return {"error": "User not found"}

REST API는 단순하고 직관적이지만, over-fetching(필요 이상의 데이터 전송)이나 under-fetching(여러 번의 요청 필요) 문제가 있을 수 있다.


GraphQL

GraphQL은 Facebook에서 개발한 쿼리 언어이자 런타임이다. 클라이언트가 필요한 데이터의 구조를 정확히 명시할 수 있어 효율적인 데이터 fetching이 가능하다.

GraphQL의 주요 장점:

  • 단일 엔드포인트: 모든 요청이 하나의 URL로 이루어진다
  • 타입 시스템: 강력한 타입 시스템으로 API 스키마를 정의한다
  • 실시간 업데이트: 구독(Subscription)을 통해 실시간 데이터 업데이트가 가능하다

FastAPI와 Strawberry를 사용한 GraphQL 예시:

import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

@strawberry.type
class User:
    name: str
    email: str
    age: int

@strawberry.type
class Query:
    @strawberry.field
    def users(self) -> List[User]:
        return [
            User(name="Alice", email="alice@example.com", age=30),
            User(name="Bob", email="bob@example.com", age=25)
        ]

    @strawberry.field
    def user(self, name: str) -> User:
        # 실제로는 데이터베이스에서 조회
        return User(name=name, email=f"{name}@example.com", age=25)

schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

클라이언트는 다음과 같은 쿼리로 필요한 데이터만 요청할 수 있다:

query {
  users {
    name
    email
  }
}

이 쿼리는 사용자의 이름과 이메일만 반환하며, 나이는 전송되지 않는다.


gRPC

gRPC는 Google에서 개발한 고성능 RPC(Remote Procedure Call) 프레임워크다. Protocol Buffers를 사용해 데이터를 직렬화하며, HTTP/2를 기반으로 동작한다.

gRPC의 특징:

  • 고성능: 바이너리 프로토콜로 빠른 통신이 가능하다
  • 스트리밍: 단방향, 양방향 스트리밍을 지원한다
  • 언어 중립적: 다양한 프로그래밍 언어를 지원한다

먼저 Protocol Buffer 파일을 정의한다:

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}

message User {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message GetUserRequest {
  string user_id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message CreateUserResponse {
  string message = 1;
}

Python으로 gRPC 서버를 구현하면:

import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc

class UserService(user_pb2_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        # 실제로는 데이터베이스에서 조회
        return user_pb2.User(
            name="Alice",
            email="alice@example.com",
            age=30
        )

    def CreateUser(self, request, context):
        # 실제로는 데이터베이스에 저장
        return user_pb2.CreateUserResponse(
            message="User created successfully"
        )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Golang으로 구현한 예시는 다음과 같다:

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "your-module/user" // 실제 모듈명으로 변경
)

type server struct {
    pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    // 실제로는 데이터베이스에서 조회
    return &pb.User{
        Name:  "Alice",
        Email: "alice@example.com",
        Age:   30,
    }, nil
}

func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    // 실제로는 데이터베이스에 저장
    log.Printf("Created user: %s", req.Name)
    return &pb.CreateUserResponse{
        Message: "User created successfully",
    }, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, &server{})

    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

gRPC는 마이크로서비스 간 통신에 특히 적합하다. 하지만 브라우저에서 직접 사용하기 어렵고, 디버깅이 REST API보다 복잡할 수 있다.


WebSocket

WebSocket은 클라이언트와 서버 간 실시간 양방향 통신을 가능하게 한다. HTTP와 달리 연결이 지속되며, 어느 쪽에서든 언제든지 데이터를 보낼 수 있다.

WebSocket의 활용 사례:

  • 실시간 채팅 애플리케이션
  • 온라인 게임
  • 실시간 알림 시스템
  • 실시간 데이터 대시보드

FastAPI를 사용한 WebSocket 예시:

from fastapi import FastAPI, WebSocket
from fastapi.websockets import WebSocketDisconnect
import json

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            message = json.loads(data)
            await manager.broadcast(f"사용자: {message['text']}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)

WebSocket은 실시간 통신에는 탁월하지만, 연결 상태를 관리해야 하고 서버 리소스를 지속적으로 사용한다는 단점이 있다.


어떤 통신 방식을 선택할까?

각 통신 방식은 고유한 장단점이 있다:

  • REST API간단하고 캐싱이 가능해 일반적인 CRUD 작업에 적합하다. 하지만 복잡한 데이터 요구사항에는 비효율적일 수 있다.
  • GraphQL유연한 데이터 페칭이 가능해 모바일 앱이나 복잡한 프론트엔드에 유리하다. 하지만 학습 곡선이 있고 캐싱이 복잡하다.
  • gRPC는 고성능과 타입 안정성이 필요한 마이크로서비스 환경에 최적이다. 하지만 브라우저 지원이 제한적이다.
  • WebSocket실시간 통신이 필요한 애플리케이션에 필수적이다. 하지만 연결 관리와 확장성 고려사항이 있다.

프로젝트의 요구사항에 따라 적절한 통신 방식을 선택하거나, 필요에 따라 여러 방식을 조합해서 사용하는 것이 좋다. 예를 들어, 일반적인 API는 REST나 GraphQL로 구현하고, 실시간 알림은 WebSocket으로 처리하는 식으로 말이다.

각 기술의 특성을 이해하고 적절히 활용한다면, 더 나은 사용자 경험과 시스템 성능을 제공할 수 있을 것이다.