내부 서비스 및 외부 소비자용 API 맞춤 설정
Olivia Novak
Dev Intern · Leapcell

서론
백엔드 개발의 복잡한 세계에서 효과적인 API 설계는 매우 중요합니다. 그러나 "만능" 접근 방식은 서로 다른 API 소비자의 다양한 요구를 충족시킬 때 종종 부족합니다. 특히 gRPC 또는 RPC와 같은 고성능 프로토콜을 통해 통신하는 내부 서비스의 요구 사항은 REST 또는 GraphQL과 같은 보다 표준화된 인터페이스를 통해 상호 작용하는 외부 클라이언트의 요구 사항과 크게 다릅니다. 이러한 불일치로 인해 각 사용 사례에 최적화된 별도의 API 설계 전략이 필요합니다. 이러한 차이점을 이해하고 올바른 접근 방식을 의식적으로 선택하면 더 성능이 뛰어나고 유지 관리하기 쉬우며 확장 가능한 시스템을 구축할 수 있으며, 궁극적으로 전반적인 개발자 경험을 향상시키고 제품 출시를 가속화할 수 있습니다. 이 문서는 이러한 다양한 전략을 탐구하고 대상 고객에게 진정으로 서비스를 제공하는 API를 설계하기 위한 포괄적인 가이드를 제공합니다.
핵심 개념 이해
설계 전략을 자세히 살펴보기 전에 이 논의의 기초가 되는 핵심 용어를 명확히 해 보겠습니다.
- gRPC (gRPC 원격 프로시저 호출): Google에서 개발한 고성능 오픈 소스 범용 RPC 프레임워크입니다. 인터페이스 정의 언어(IDL) 및 메시지 교환 형식으로 Protocol Buffers(protobuf)를 사용하며 효율적인 데이터 직렬화 및 역직렬화를 가능하게 합니다. gRPC는 다양한 프로그래밍 언어를 지원하며 HTTP/2를 통해 작동하고 양방향 스트리밍, 흐름 제어 및 헤더 압축과 같은 기능을 제공합니다.
- RPC (원격 프로시저 호출): 프로그램이 다른 주소 공간(일반적으로 공유 네트워크의 다른 컴퓨터)에서 프로시저(서브루틴 또는 함수)를 실행하도록 하는 기본 통신 패러다임이며, 프로그래머는 이 원격 상호 작용에 대한 세부 정보를 명시적으로 코딩하지 않습니다.
- REST (Representational State Transfer): 분산 하이퍼미디어 시스템을 설계하기 위한 아키텍처 스타일입니다. REST API는 표준 HTTP 메서드(GET, POST, PUT, DELETE)와 리소스와 같은 개념을 활용하며 종종 JSON 또는 XML을 데이터 교환에 사용합니다. 상태 비저장이며 단순성, 확장성 및 광범위한 클라이언트 호환성을 강조합니다.
- GraphQL: API용 쿼리 언어이며 기존 데이터로 해당 쿼리를 충족하기 위한 런타임입니다. Facebook에서 개발한 GraphQL은 클라이언트가 필요한 데이터를 정확히 요청할 수 있도록 하며, 그 이상도 이하도 아닙니다. 일반적으로 단일 엔드포인트를 사용하며 클라이언트가 응답 구조를 정의할 수 있도록 하여 과다 가져오기 및 과소 가져오기를 줄입니다.
내부 서비스(gRPC/RPC)용 API 설계
내부 서비스는 종종 성능, 효율성 및 유형 안전성을 우선시합니다. 제어된 생태계 내에서 작동하므로 광범위한 호환성보다는 최적화된 서비스 간 통신에 중점을 Shift합니다.
원칙
- 엄격하게 정의된 계약: Protocol Buffers(gRPC용) 또는 Avro(일부 RPC 구현용)와 같은 IDL을 활용하여 서비스 인터페이스 및 메시지 구조를 정의합니다. 이렇게 하면 서비스 전반에 걸쳐 강력한 유형 안전성과 일관성이 보장됩니다.
- 성능 최적화: 효율적인 데이터 직렬화(이진 형식)를 강조하고 오버헤드를 최소화합니다. gRPC의 HTTP/2 기반 및 스트리밍 기능은 이를 위해 훌륭합니다.
- 도메인 주도 설계 (DDD): 내부 서비스용 API는 종종 내부 도메인 모델을 더 직접적으로 반영합니다. 이렇게 하면 더 세분화되고 작업 중심적인 API를 만들 수 있습니다.
- 오류 처리: 상세하고 프로그래밍 가능한 오류 코드와 메시지는 일반적인 HTTP 상태 코드보다 더 유용합니다.
구현 및 예제 (Go를 사용한 gRPC)
간단한 내부 사용자 관리 서비스를 가정해 보겠습니다.
먼저 .proto 파일에서 서비스와 메시지를 정의합니다.
// api/user_management_service.proto syntax = "proto3"; package usermanagement; option go_package = "./usermanagement"; service UserManagementService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); rpc UpdateUser(UpdateUserRequest) returns (UpdateResponse); } message GetUserRequest { string user_id = 1; } message GetUserResponse { User user = 1; } message CreateUserRequest { string username = 1; string email = 2; } message CreateUserResponse { string user_id = 1; } message UpdateUserRequest { string user_id = 1; string username = 2; string email = 3; } message UpdateResponse { bool success = 1; string message = 2; } message User { string id = 1; string username = 2; string email = 3; string created_at = 4; }
이 proto 파일은 정확한 계약을 정의합니다. protoc과 같은 도구는 다양한 언어에 대한 코드를 생성합니다.
다음은 Go 서버 구현의 일부입니다.
// internal/server/user_server.go package server import ( "context" "fmt" // 오류 예제용 pb "your-project/pkg/usermanagement" // 생성된 proto 패키지 ) type UserManagementServer struct { pb.UnimplementedUserManagementServiceServer // 설계에 따라 여기에 리포지토리 또는 서비스 계층이 있을 수 있습니다. // userRepo repository.UserRepository } // GetUser는 ID별로 사용자를 검색하는 요청을 처리합니다. func (s *UserManagementServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { fmt.Printf("Received GetUser request for user_id: %s\n", req.GetUserId()) // 실제 애플리케이션에서는 데이터베이스에서 가져올 것입니다. if req.GetUserId() == "123" { return &pb.GetUserResponse{ User: &pb.User{ Id: "123", Username: "johndoe", Email: "john@example.com", CreatedAt: "2023-01-01T10:00:00Z", }, }, nil } return nil, fmt.Errorf("user not found: %s", req.GetUserId()) } // CreateUser는 새 사용자를 생성하는 요청을 처리합니다. func (s *UserManagementServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { fmt.Printf("Received CreateUser request for username: %s, email: %s\n", req.GetUsername(), req.GetEmail()) // DB에 사용자 생성, ID 생성 로직 newUserID := "456" // 모의 ID return &pb.CreateUserResponse{UserId: newUserID}, nil } // UpdateUser는 기존 사용자를 업데이트하는 요청을 처리합니다. func (s *UserManagementServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateResponse, error) { fmt.Printf("Received UpdateUser request for user_id: %s, new username: %s\n", req.GetUserId(), req.GetUsername()) // DB에 사용자 업데이트 로직 return &pb.UpdateResponse{Success: true, Message: "User updated successfully"}, nil }
그리고 이 내부 서비스를 호출하는 클라이언트입니다.
// internal/client/user_client.go package client import ( "context" "log" pb "your-project/pkg/usermanagement" // 생성된 proto 패키지 "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func CallUserManagementService() { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewUserManagementServiceClient(conn) // 사용자 가져오기 res, err := c.GetUser(context.Background(), &pb.GetUserRequest{UserId: "123"}) if err != nil { log.Printf("could not get user: %v", err) } else { log.Printf("User: %v", res.GetUser()) } // 사용자 생성 createRes, err := c.CreateUser(context.Background(), &pb.CreateUserRequest{Username: "alice", Email: "alice@example.com"}) if err != nil { log.Printf("could not create user: %v", err) } else { log.Printf("Created User ID: %s", createRes.GetUserId()) } // 사용자 업데이트 updateRes, err := c.UpdateUser(context.Background(), &pb.UpdateUserRequest{UserId: "456", Username: "alice_updated"}) if err != nil { log.Printf("could not update user: %v", err) } else { log.Printf("Update successful: %v", updateRes.GetSuccess()) } }
이 예제는 gRPC의 강력한 타이핑과 직접적인 메서드 호출을 보여주며, 이는 내부 서비스 통신에 이상적입니다.
외부 클라이언트(REST/GraphQL)용 API 설계
웹 브라우저, 모바일 앱, 타사 통합에 이르기까지 외부 클라이언트는 다른 품질을 요구합니다. 사용 편의성, 검색 가능성, 광범위한 언어 지원 및 유연성입니다.
원칙
- 리소스 중심(REST): 특정 작업보다는 비즈니스 리소스를 중심으로 API를 구조화합니다. 이러한 리소스에 대한 작업을 수행하기 위해 표준 HTTP 메서드를 사용합니다.
- 유연한 데이터 가져오기(GraphQL): 클라이언트가 과다 가져오기 또는 과소 가져오기를 피하기 위해 데이터 요구 사항을 정의할 수 있도록 합니다.
- 자가 설명: OpenAPI/Swagger(REST용) 또는 내성 스키마(GraphQL용)를 통해 명확한 문서를 제공합니다.
- 오류 처리: 문제를 전달하기 위해 표준 HTTP 상태 코드(REST용) 또는 잘 정의된 오류 개체 구조(GraphQL용)를 사용합니다.
- 버전 관리: 기존 클라이언트에 대한 호환성 중단을 방지하기 위해 API 진화를 계획합니다.
- 보안: 강력한 인증(OAuth2, JWT) 및 권한 부여 메커니즘을 구현합니다.
구현 및 예제 (Go를 사용한 REST, GraphQL 개념)
사용자 정보에 대한 공개 REST API를 노출해 보겠습니다. 이 REST API는 내부 gRPC 서비스를 사용할 수 있습니다.
REST API (Go의 net/http 사용)
// external/api/rest_user_handler.go package api import ( "encoding/json" "fmt" "log" "net/http" "github.com/gorilla/mux" // Go에서 REST API에 대한 인기 있는 라우터 pb "your-project/pkg/usermanagement" // 생성된 proto 패키지 "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "context" ) // REST 핸들러는 일반적으로 서비스 계층과 상호 작용하며, // 이 서비스 계층은 내부 gRPC 서비스를 호출합니다. type UserRESTHandler struct { // 내부 gRPC 서비스용 클라이언트 grpcClient pb.UserManagementServiceClient } func NewUserRESTHandler() *UserRESTHandler { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("REST handler can not connect to gRPC server: %v", err) } // 참고: 프로덕션 시스템에서는 이 연결을 싱글톤 또는 종속성 주입 등을 사용하여 신중하게 관리하십시오. return &UserRESTHandler{ grpcClient: pb.NewUserManagementServiceClient(conn), } } // GetUser는 GET /users/{id}를 처리합니다. func (h *UserRESTHandler) GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userID := vars["id"] grpcReq := &pb.GetUserRequest{UserId: userID} grpcRes, err := h.grpcClient.GetUser(context.Background(), grpcReq) if err != nil { http.Error(w, fmt.Sprintf("Failed to fetch user from internal service: %v", err), http.StatusInternalServerError) return } if grpcRes.GetUser() == nil { // 사용자가 실제로 반환되었는지 확인, gRPC 오류 처리에 기반 http.Error(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ // proto 사용자를 간단한 JSON 개체로 매핑 "id": grpcRes.GetUser().GetId(), "username": grpcRes.GetUser().GetUsername(), "email": grpcRes.GetUser().GetEmail(), "createdAt": grpcRes.GetUser().GetCreatedAt(), }) } // CreateUser는 POST /users를 처리합니다. func (h *UserRESTHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var requestBody struct { Username string `json:"username"` Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } grpcReq := &pb.CreateUserRequest{ Username: requestBody.Username, Email: requestBody.Email, } grpcRes, err := h.grpcClient.CreateUser(context.Background(), grpcReq) if err != nil { http.Error(w, fmt.Sprintf("Failed to create user in internal service: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"id": grpcRes.GetUserId()}) } // 라우터 설정 // func main() { // router := mux.NewRouter() // handler := NewUserRESTHandler() // router.HandleFunc("/users/{id}", handler.GetUser).Methods("GET") // router.HandleFunc("/users", handler.CreateUser).Methods("POST") // log.Fatal(http.ListenAndServe(":8080", router)) // }
이 REST API는 리소스 중심 보기(예: /users/{id})를 나타내고 표준 HTTP 동사를 사용합니다. API 게이트웨이 역할을 하여 외부 REST 요청을 내부 gRPC 호출로 변환합니다.
GraphQL (개념)
GraphQL의 경우 클라이언트가 특정 사용자 필드를 쿼리할 수 있도록 하는 스키마를 정의합니다.
# schema.graphql type User { id: ID! username: String! email: String createdAt: String } type Query { user(id: ID!): User } type Mutation { createUser(username: String!, email: String): User }
GraphQL 리졸버(다시 Go, Node.js 등에서)는 이러한 GraphQL 쿼리 및 뮤테이션을 내부 gRPC 서비스 호출에 매핑하며, REST 핸들러와 유사합니다.
GraphQL 리졸버 스니펫 (개념적 Go):
// external/api/graphql_resolvers.go package api import ( "context" pb "your-project/pkg/usermanagement" // 생성된 proto 패키지 ) type Resolver struct { grpcClient pb.UserManagementServiceClient } func (r *Resolver) Query_user(ctx context.Context, args struct{ ID string }) (*User, error) { grpcReq := &pb.GetUserRequest{UserId: args.ID} grpcRes, err := r.grpcClient.GetUser(ctx, grpcReq) if err != nil { // gRPC 오류 처리 및 GraphQL 오류로 매핑 return nil, err } if grpcRes.GetUser() == nil { return nil, nil // GraphQL 클라이언트는 없는 경우 null을 기대합니다. } return &User{ ID: grpcRes.GetUser().GetId(), Username: grpcRes.GetUser().GetUsername(), Email: grpcRes.GetUser().GetEmail(), CreatedAt: grpcRes.GetUser().GetCreatedAt(), }, nil } // 마찬가지로 뮤테이션의 경우
이것은 REST 또는 GraphQL과 같은 외부 API가 내부 통신 세부 정보를 추상화하여 클라이언트 친화적인 인터페이스를 제공하는 방법을 보여줍니다.
애플리케이션 시나리오
- 
내부 서비스 (gRPC/RPC): - 대규모 분산 시스템 내의 마이크로 서비스 통신.
- 직렬화 및 역직렬화 오버헤드를 최소화해야 하는 고처리량 데이터 파이프라인.
- 백엔드 구성 요소 간의 효율적이고 유형 안전한 통신 구축.
- 서비스 간 데이터 스트리밍 (예: 실시간 분석).
 
- 
외부 클라이언트 (REST/GraphQL): - 웹 및 모바일 애플리케이션을 위한 공개 API.
- 광범위한 호환성이 중요한 타사 통합 지점.
- 데이터 요구 사항을 정확하게 정의할 수 있는 유연한 프런트엔드 개발.
- 표준 엔터프라이즈 애플리케이션 통합 (REST).
 
결론
효과적인 API 설계는 내부 서비스 통신과 외부 클라이언트 상호 작용을 구별하는 신중한 접근 방식이 필요합니다. 내부 서비스의 경우 gRPC/RPC는 이진 프로토콜과 강력한 계약을 활용하여 탁월한 성능, 유형 안전성 및 효율성을 제공합니다. 외부 소비자의 경우 REST는 광범위한 채택과 리소스 중심의 단순성을 제공하며, GraphQL은 클라이언트 기반 데이터 가져오기 유연성을 제공합니다. 이러한 개별 전략을 의식적으로 적용함으로써 개발자는 각 유형의 소비자의 요구 사항을 정확하게 충족하는 견고하고 최적화되며 유지 관리 가능한 백엔드 시스템을 구축하여 더 효율적인 개발과 전반적인 시스템 성능 향상을 가져올 수 있습니다. 핵심은 올바른 잠재 고객에게 올바른 인터페이스를 제공하는 것입니다.