개발자의 나침반: 최신 API 기초 완벽 가이드

Sue·2025년 8월 27일
0

개발자의 나침반: 최신 API 기초 완벽 가이드

서론

오늘날 우리가 사용하는 거의 모든 최신 애플리케이션은 API(Application Programming Interface)에 의해 구동됩니다. 스마트폰의 날씨 앱부터 복잡한 기업용 대시보드에 이르기까지, API는 우리 디지털 세계를 함께 엮는 보이지 않는 실과 같습니다. API는 서로 다른 소프트웨어 시스템이 통신할 수 있도록 하는 규칙과 프로토콜의 집합입니다. 이는 클라이언트가 서버에 정보나 작업을 어떻게 요청할 수 있는지를 정의하는 계약서와 같습니다.

수많은 API 구축 방식 중에서, REST(REpresentational State Transfer)는 오늘날 웹 API를 구축하는 데 있어 지배적인 아키텍처 스타일로 자리 잡았습니다. 그 이유는 단순성, 확장성, 그리고 웹 자체의 아키텍처와 잘 부합하기 때문입니다.

이 가이드는 API 개발의 여정을 체계적으로 안내할 것입니다. 먼저 '왜'에 해당하는 REST의 핵심 원칙부터 시작하여, '무엇을' 의미하는 HTTP 메서드와 상태 코드를 살펴볼 것입니다. 그다음 '어떻게'에 해당하는 실제 코드 예제를 통해 이론을 실습으로 옮기고, 마지막으로 '어떻게 잘할 것인가'에 대한 고급 설계 및 보안 모범 사례를 다루며 마무리할 것입니다. 이 글을 통해 API의 기초를 다지고, 더 나아가 견고하고 효율적인 API를 설계하고 개발하는 데 필요한 깊이 있는 지식을 얻게 될 것입니다.


1장: 통신의 청사진 - RESTful API 이해하기

API 개발의 세계로 뛰어들기 전에, 가장 중요한 개념 중 하나를 명확히 해야 합니다. 바로 REST는 프로토콜이 아닌 아키텍처 스타일이라는 점입니다. 이는 매우 중요한 구별입니다. HTTP는 클라이언트와 서버가 데이터를 주고받는 데 사용하는 통신 프로토콜, 즉 '언어와 전달 메커니즘'입니다. 반면, REST는 우리가 HTTP를 효과적으로 사용하도록 안내하는 아키텍처 스타일, 즉 '문법과 대화의 규칙'입니다. 이러한 분리 덕분에 REST는 웹의 기존 인프라를 최대한 활용하여 단순하고 확장 가능한 시스템을 구축할 수 있습니다.

REST의 여섯 가지 핵심 원칙 (The "Why")

REST 아키텍처는 견고하고 확장 가능한 시스템을 구축하기 위한 여섯 가지 핵심 원칙 또는 제약 조건에 기반합니다. 이 원칙들은 개별적인 규칙이 아니라 서로 긴밀하게 연결된 시스템으로, 하나를 위반하면 다른 원칙들의 이점까지 약화될 수 있습니다..

클라이언트-서버 분리 (Client-Server Separation)

이 원칙은 클라이언트(사용자 인터페이스, 프론트엔드)와 서버(데이터 저장 및 비즈니스 로직, 백엔드)가 완전히 독립적인 개체로 존재해야 함을 의미합니다. 둘 사이의 유일한 소통 창구는 네트워크를 통해 주고받는 요청과 응답뿐입니다.

이러한 관심사의 분리가 중요한 이유는 각 구성 요소가 독립적으로 발전하고, 배포되고, 확장될 수 있기 때문입니다. 예를 들어, 서버의 로직이 특정 사용자 인터페이스에 얽매여 있지 않기 때문에 하나의 잘 설계된 API를 통해 웹 애플리케이션, 모바일 앱, 데스크톱 앱이 모두 동일한 데이터와 기능에 접근할 수 있습니다. 클라이언트-서버 분리는 '균일 인터페이스(Uniform Interface)'라는 안정적이고 예측 가능한 계약이 존재하기에 실질적으로 작동할 수 있습니다. 이 계약이 없다면 클라이언트와 서버는 서로의 구현에 대해 너무 많은 것을 알아야 하므로 긴밀하게 결합되어 분리의 목적을 상실하게 됩니다.

무상태성 (Statelessness)

무상태성은 REST의 가장 중요한 특징 중 하나입니다. 이는 클라이언트가 서버로 보내는 모든 요청이 해당 요청을 이해하고 처리하는 데 필요한 모든 정보를 포함해야 한다는 것을 의미합니다. 서버는 요청과 요청 사이에 클라이언트의 세션 상태(컨텍스트)를 저장하지 않습니다.

이 원칙은 신뢰성과 확장성을 극적으로 향상시킵니다. 서버는 이전 요청에 대한 기록을 유지할 필요가 없으므로, 어떤 서버 인스턴스라도 들어오는 모든 요청을 처리할 수 있습니다. 이는 서버 설계를 단순화하고 로드 밸런싱을 매우 용이하게 만듭니다. 무상태성은 또한 캐시 가능성(Cacheability) 원칙이 효과적으로 작동하는 기반이 됩니다. 각 요청이 독립적이고 완전하기 때문에, 중간 캐시는 이전 세션 상태를 확인할 필요 없이 동일한 요청에 대해 캐시된 응답을 안전하게 제공할 수 있습니다.

균일 인터페이스 (Uniform Interface)

균일 인터페이스는 REST 아키텍처의 핵심이며, API와 상호작용하는 일관되고 예측 가능한 방식을 보장합니다. 이 원칙은 네 가지 하위 제약 조건으로 구성됩니다.

  1. 리소스 식별 (Identification of Resources via URIs): REST에서 모든 것은 '리소스'로 간주됩니다. 사용자가 될 수도 있고, 제품, 주문 등 모든 개념이 리소스가 될 수 있습니다. 각 리소스는 /users/123이나 /products/456과 같은 고유한 URI(Uniform Resource Identifier)를 통해 식별됩니다. URI는 API의 '명사' 역할을 합니다.
  2. 표현을 통한 리소스 조작 (Manipulation of Resources Through Representations): 클라이언트는 리소스 자체를 직접 조작하는 것이 아니라, 리소스의 '표현'을 통해 상호작용합니다. 이 표현은 일반적으로 JSON 또는 XML 형식입니다. 예를 들어, 서버는 특정 사용자를 나타내는 JSON 객체를 클라이언트에 보내고, 클라이언트는 이 JSON 객체를 수정하여 서버에 다시 보내 사용자를 업데이트합니다.
  3. 자기 서술적 메시지 (Self-descriptive Messages): 각 메시지(요청과 응답)는 수신자가 해당 메시지를 이해하는 데 충분한 정보를 담고 있어야 합니다. 예를 들어, HTTP 메서드(GET, POST)는 서버에게 무엇을 해야 할지 알려주고, HTTP 상태 코드(200, 404)는 클라이언트에게 요청의 결과를 알려줍니다. 또한 Content-Type 헤더는 메시지 본문이 JSON인지 XML인지를 명시합니다.
  4. HATEOAS (Hypermedia as the Engine of Application State): 이는 REST의 가장 성숙한 단계로, 응답에 관련된 다른 작업이나 리소스로 연결되는 링크(하이퍼미디어)를 포함해야 한다는 원칙입니다. 예를 들어, 특정 주문에 대한 응답에는 해당 주문을 취소하거나 배송 상태를 조회할 수 있는 URL 링크가 포함될 수 있습니다. 이를 통해 클라이언트는 마치 사용자가 웹사이트를 탐색하듯이 동적으로 API를 탐색할 수 있게 되어 클라이언트와 서버 간의 결합도를 낮춥니다.

캐시 가능성 (Cacheability)

RESTful API의 응답은 스스로 캐시가 가능한지 여부를 명시해야 합니다. Cache-Control과 같은 HTTP 헤더를 통해 응답이 캐시될 수 있는지, 그리고 얼마나 오랫동안 유효한지를 클라이언트에게 알려줍니다.

캐시 가능성은 성능 향상에 지대한 영향을 미칩니다. 자주 접근되지만 내용이 자주 바뀌지 않는 데이터의 경우, 클라이언트나 중간 프록시 서버가 응답을 캐시에 저장해두고 동일한 요청이 다시 들어왔을 때 서버까지 가지 않고 캐시된 데이터를 반환할 수 있습니다. 이는 서버의 부하를 줄이고 사용자에게 더 빠른 응답 시간을 제공합니다.

계층화된 시스템 (Layered System)

REST 아키텍처는 여러 계층으로 구성될 수 있습니다. 클라이언트는 API 서버와 직접 통신하는지, 아니면 로드 밸런서, API 게이트웨이, 보안 프록시와 같은 중간 계층을 통해 통신하는지 알 수 없으며 알 필요도 없습니다. 클라이언트는 오직 최종 목적지의 URI와만 상호작용합니다.

이러한 구조는 시스템 아키텍처에 엄청난 유연성을 부여합니다. 클라이언트나 서버 코드에 영향을 주지 않으면서 보안, 로깅, 캐싱을 위한 새로운 계층을 시스템에 추가할 수 있습니다.

주문형 코드 (Code on Demand) (선택 사항)

이는 REST의 유일한 선택적 제약 조건입니다. 서버가 클라이언트의 기능을 일시적으로 확장하기 위해 JavaScript와 같은 실행 가능한 코드를 전송할 수 있음을 의미합니다. 이 원칙은 널리 사용되지는 않지만, 특정 시나리오에서 클라이언트의 복잡성을 줄이고 유연성을 높일 수 있습니다.


2장: API의 언어 - HTTP 메서드 심층 분석

RESTful API의 상호작용은 HTTP 메서드(동사)를 사용하여 리소스(명사)에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행하는 것을 기본으로 합니다. 각 메서드는 고유한 의미와 특성을 가지며, 이를 올바르게 사용하는 것이 잘 설계된 API의 첫걸음입니다.

주요 HTTP 메서드

GET

  • 목적: 리소스 조회 (Read)
  • 설명: GET 메서드는 특정 리소스의 표현을 검색하는 데 사용됩니다. 예를 들어, /users/123으로 GET 요청을 보내면 ID가 123인 사용자의 정보를 가져옵니다.
  • 특성:
    • 안전(Safe): GET 요청은 서버의 상태를 변경하지 않아야 합니다. 즉, 리소스를 조회하는 것 외에 다른 부수 효과(side effect)를 일으키지 않습니다.
    • 멱등성(Idempotent): 동일한 GET 요청을 한 번 보내든 여러 번 보내든 결과는 항상 동일합니다.
  • 데이터 전달: 데이터는 URL의 쿼리 문자열(query string)을 통해 전달됩니다 (예: /users?status=active). 이 때문에 URL이 서버 로그에 남거나 브라우저 히스토리에 저장될 수 있어 민감한 정보를 전달하는 데는 적합하지 않습니다.

POST

  • 목적: 새 리소스 생성 (Create)
  • 설명: POST 메서드는 서버에 데이터를 제출하여 새로운 리소스를 생성하는 데 사용됩니다. 예를 들어, /users로 사용자 정보를 담아 POST 요청을 보내면 새로운 사용자가 생성됩니다.
  • 특성:
    • 안전하지 않음(Not Safe): 서버에 새로운 리소스를 생성하므로 서버의 상태를 변경합니다.
    • 멱등성 없음(Not Idempotent): 동일한 POST 요청을 두 번 보내면 두 개의 다른 리소스가 생성될 수 있습니다.
  • 데이터 전달: 데이터는 요청 본문(request body)에 담겨 전송됩니다. 따라서 크기가 크거나 복잡한 데이터, 또는 민감한 정보를 보내는 데 적합합니다.

PUT

  • 목적: 리소스 전체 교체 또는 생성 (Update/Replace)
  • 설명: PUT 메서드는 지정된 URI의 리소스를 요청 본문에 담긴 내용으로 완전히 교체합니다. 만약 해당 URI에 리소스가 존재하지 않으면 새로 생성할 수도 있습니다.
  • 특성:
    • 안전하지 않음(Not Safe): 리소스를 수정하거나 생성하므로 서버 상태를 변경합니다.
    • 멱등성(Idempotent): 동일한 PUT 요청을 여러 번 보내도 결과는 항상 동일합니다. 즉, 리소스는 항상 요청 본문의 내용과 같은 최종 상태를 갖게 됩니다.
  • 데이터 전달: 요청 본문에 리소스의 전체 표현을 담아 보내야 합니다. 만약 특정 필드를 누락하면 해당 필드는 서버에서 삭제되거나 기본값으로 설정될 수 있습니다.

DELETE

  • 목적: 리소스 삭제 (Delete)
  • 설명: DELETE 메서드는 지정된 URI의 리소스를 삭제합니다.
  • 특성:
    • 안전하지 않음(Not Safe): 리소스를 삭제하므로 서버 상태를 변경합니다.
    • 멱등성(Idempotent): 동일한 DELETE 요청을 여러 번 보내도 결과는 동일합니다. 첫 번째 요청에서 리소스가 삭제되고, 이후의 요청은 '리소스 없음'(예: 404 Not Found)을 반환하겠지만, 시스템의 최종 상태는 동일하게 유지됩니다.

PATCH

  • 목적: 리소스 부분 수정 (Partial Update)
  • 설명: PATCH 메서드는 리소스의 일부만 수정할 때 사용됩니다. PUT이 리소스 전체를 교체하는 반면, PATCH는 변경하려는 필드만 요청 본문에 담아 보냅니다.
  • 특성:
    • 안전하지 않음(Not Safe): 리소스를 수정하므로 서버 상태를 변경합니다.
    • 멱등성 없음(Not Idempotent, 일반적으로): PATCH 연산의 결과는 리소스의 현재 상태에 따라 달라질 수 있으므로 일반적으로 멱등하지 않습니다. 예를 들어, '카운터를 1 증가시켜라'는 PATCH 요청은 보낼 때마다 결과가 달라집니다.
  • 데이터 전달: 변경할 필드만 요청 본문에 담아 보내므로, 작은 변경에 대해 PUT보다 네트워크 효율성이 높습니다.

기타 메서드

  • HEAD: GET과 동일하지만, 응답 본문(body) 없이 헤더(header)만 반환합니다. 리소스의 존재 여부나 메타데이터(예: Content-Length, Last-Modified)를 확인하는 데 유용합니다.
  • OPTIONS: 대상 리소스에 대해 수행할 수 있는 통신 옵션(예: 허용되는 HTTP 메서드)을 설명하는 데 사용됩니다. 주로 CORS(Cross-Origin Resource Sharing)에서 preflight 요청에 사용됩니다.

심층 분석: PUT과 PATCH의 끝나지 않는 논쟁

PUTPATCH의 차이점은 API 설계의 미묘한 트레이드오프를 보여주는 좋은 예시입니다.

PUT은 리소스의 상태를 '대체'하는 것이고, PATCH는 현재 상태를 '수정'하는 것입니다.

이를 문서 편집에 비유할 수 있습니다. PUT 요청은 문서의 완전히 새로운 버전을 업로드하여 기존 버전을 덮어쓰는 것과 같습니다. 반면, PATCH 요청은 "5번 줄을 변경하고 3번 단락을 삭제하라"와 같은 편집 지시 목록을 보내는 것과 유사합니다.

실제 사용 사례: 사용자 프로필 업데이트

  • 기존 리소스:
    {
      "username": "alex",
      "email": "alex@example.com",
      "status": "active"
    }
  • 목표: 이메일 주소만 변경하기
  • PUT 요청: 리소스의 전체 표현을 보내야 합니다. 만약 status 필드를 누락하면 서버 구현에 따라 사용자의 계정이 비활성화될 수도 있습니다. JSON
    PUT /users/123
    Content-Type: application/json
    
    {
      "username": "alex",
      "email": "alex.new@example.com",
      "status": "active"
    }
  • PATCH 요청: 변경할 필드만 보내므로 훨씬 효율적이고 의도치 않은 수정을 방지할 수 있습니다.
    PATCH /users/123
    Content-Type: application/json
    
    {
      "email": "alex.new@example.com"
    }
기능PUTPATCH
업데이트 범위리소스 전체를 교체합니다.리소스의 일부를 업데이트합니다.
멱등성보장됨. 동일한 요청은 항상 동일한 최종 상태를 만듭니다.보장되지 않음 (일반적). 결과가 리소스의 현재 상태에 따라 달라질 수 있습니다.
요청 페이로드리소스의 완전한 표현을 보내야 합니다.변경이 필요한 필드만 보냅니다.
네트워크 효율성큰 리소스의 작은 변경 시 비효율적일 수 있습니다.데이터 전송을 최소화하여 매우 효율적입니다.
주요 사용 사례전체 레코드 동기화, 설정 파일 교체.단일 필드 업데이트 (예: 사용자 이메일, 제품 가격).

신뢰성 있는 API의 초석: 멱등성(Idempotency)의 중요성

GET, PUT, DELETE 메서드에 대해서는 멱등성이 반복적으로 언급되지만 POSTPATCH에 대해서는 그렇지 않습니다. 이는 단순히 학술적인 구분이 아니라, 장애에 강한 클라이언트를 구축하는 데 있어 근본적인 차이를 만듭니다.

네트워크는 본질적으로 불안정합니다. 클라이언트는 요청을 보냈지만, 서버의 응답을 받기 전에 연결이 끊어질 수 있습니다. 이 경우 클라이언트는 요청이 성공적으로 처리되었는지 알 수 없습니다. "요청을 재시도해야 할까?"라는 딜레마에 빠지게 됩니다.

이때 연산의 멱등성이 중요해집니다. 만약 PUT이나 DELETE처럼 연산이 멱등하다면, 클라이언트는 의도치 않은 부수 효과(예: 중복 리소스 생성)에 대한 걱정 없이 동일한 요청을 안전하게 다시 보낼 수 있습니다. 시스템의 최종 상태는 요청이 첫 번째에 성공했든, 재시도 끝에 성공했든 동일할 것이기 때문입니다.

반면, POST와 같이 멱등하지 않은 연산을 재시도하면, 두 개의 중복된 리소스가 생성될 수 있습니다. 이는 신용카드 결제를 두 번 처리하거나, 동일한 게시물을 두 번 등록하는 것과 같은 심각한 문제를 야기할 수 있습니다. 따라서 멱등성을 이해하는 것은 클라이언트의 오류 처리 및 재시도 로직을 어떻게 구현해야 하는지를 직접적으로 결정하는 중요한 요소입니다. 멱등한 메서드를 올바르게 사용하는 API는 본질적으로 더 견고하며 개발자가 더 안정적으로 사용하기 쉽습니다.


3장: 서버의 응답 해석하기 - 필수 HTTP 상태 코드

클라이언트가 서버에 요청을 보내면, 서버는 요청의 처리 결과를 나타내는 HTTP 상태 코드를 포함한 응답을 반환합니다. 이 상태 코드는 API 통신의 핵심적인 부분으로, 클라이언트가 다음 행동을 결정하는 데 중요한 단서를 제공합니다. 상태 코드는 다섯 가지 클래스로 분류되어 응답의 성격을 대략적으로 알려줍니다.

  • 1xx (Informational): 요청을 받았으며, 프로세스를 계속 진행합니다.
  • 2xx (Successful): 요청을 성공적으로 수신, 이해, 수락했습니다.
  • 3xx (Redirection): 요청을 완료하기 위해 추가 조치가 필요합니다.
  • 4xx (Client Error): 클라이언트의 요청에 오류가 있습니다 (예: 잘못된 문법).
  • 5xx (Server Error): 서버가 유효한 요청을 처리하는 데 실패했습니다.

API 개발자를 위한 핵심 HTTP 상태 코드

모든 상태 코드를 알 필요는 없지만, API 개발자라면 반드시 알아야 할 핵심적인 코드들이 있습니다. 다음 표는 클라이언트 측 로직에서 자주 처리해야 하는 주요 상태 코드와 그 사용 시나리오를 요약한 것입니다.

코드의미일반적인 API 시나리오
200 OK성공GET 요청에 대한 표준적인 성공 응답. 응답 본문에 요청한 데이터가 포함됩니다.
201 Created리소스 생성됨POST 요청이 성공적으로 완료되어 새로운 리소스가 생성되었을 때 반환됩니다. 응답 헤더에 새로 생성된 리소스의 URI를 가리키는 Location 헤더가 포함되는 경우가 많습니다.
204 No Content성공, 내용 없음DELETE 요청이 성공적으로 처리되었거나, 데이터를 반환할 필요가 없는 PUT/PATCH 요청에 대한 성공 응답입니다. 응답 본문은 비어 있습니다.
400 Bad Request잘못된 요청요청의 문법이 잘못되었거나 유효하지 않은 데이터를 포함할 때 반환됩니다 (예: JSON 형식 오류, 필수 필드 누락).
401 Unauthorized인증 필요사용자가 로그인하지 않았거나 유효하지 않은 API 키/토큰을 제공하여 인증에 실패했을 때 반환됩니다.
403 Forbidden접근 금지사용자는 성공적으로 인증되었지만(로그인 상태), 요청한 리소스에 접근할 권한이 없을 때 반환됩니다.
404 Not Found리소스 없음요청한 URI에 해당하는 리소스가 존재하지 않을 때 반환됩니다 (예: /users/9999).
500 Internal Server Error서버 내부 오류서버 측에서 예기치 않은 오류가 발생했을 때 반환되는 일반적인 오류 코드입니다 (예: 데이터베이스 연결 실패, 처리되지 않은 예외 발생).

심층 분석: 문지기의 수수께끼 - 401 Unauthorized vs. 403 Forbidden

401403의 차이는 API 보안과 사용자 경험 설계에 있어 매우 중요하지만, 종종 혼동되는 개념입니다.

  • 인증(Authentication) 오류: 401 Unauthorized
    • 질문: "당신은 누구십니까?"
    • 상황: 서버가 요청을 보낸 클라이언트의 신원을 확인하지 못했거나, 제공된 신원 정보(API 키, 토큰 등)가 유효하지 않은 경우입니다. 문제는 '유효한 자격 증명의 부재'입니다.
    • 해결: 클라이언트는 로그인을 하거나 유효한 토큰을 발급받아 다시 요청함으로써 이 문제를 해결할 수 있습니다. 좋은 API는 401 응답 시 클라이언트가 어떻게 인증해야 하는지에 대한 정보를 담은 WWW-Authenticate 헤더를 함께 보내야 합니다.
  • 인가(Authorization) 오류: 403 Forbidden
    • 질문: "당신은 들어올 수 없습니다."
    • 상황: 서버는 클라이언트의 신원을 성공적으로 확인했습니다(즉, 인증은 통과했습니다). 하지만 해당 클라이언트가 가진 권한으로는 요청한 작업이나 리소스에 접근할 수 없는 경우입니다. 문제는 '권한 부족'입니다.
    • 해결: 단순히 다시 인증하는 것으로는 이 문제가 해결되지 않습니다. 사용자는 관리자에게 권한을 요청하는 등의 별도 조치가 필요합니다.

실제 시나리오: HR 포털 API

  • 사례 1 (401 Unauthorized): 한 직원이 로그인하지 않은 상태에서 자신의 급여 정보를 보기 위해 https://hr.portal/api/salary/123에 접근을 시도합니다. API 서버는 이 요청이 누구로부터 온 것인지 알 수 없습니다.
    • 응답: 401 Unauthorized
  • 사례 2 (403 Forbidden): 직원 'Alice'(ID: 123)가 성공적으로 로그인했습니다. 그녀는 자신의 급여 정보(https://hr.portal/api/salary/123)에 접근합니다.
    • 응답: 200 OK
    • 이제 Alice는 호기심에 동료 'Bob'(ID: 456)의 급여 정보(https://hr.portal/api/salary/456)에 접근을 시도합니다. API 서버는 요청자가 'Alice'임을 알고 있지만, 일반 직원 역할(role)을 가진 Alice에게는 다른 직원의 급여 정보를 볼 권한이 없습니다.
    • 응답: 403 Forbidden

API 계약의 일부로서의 상태 코드

상태 코드의 선택은 단순한 오류 신호를 넘어, 때로는 의도적인 보안 결정이 될 수 있습니다. 특히 403 Forbidden404 Not Found 사이의 선택이 그렇습니다.

권한이 없는 사용자가 /admin/dashboard와 같이 민감한 리소스에 접근을 시도했다고 가정해 봅시다. 이때 서버가 403 Forbidden을 반환하면, 이는 두 가지 정보를 암묵적으로 확인해 주는 셈입니다. 첫째, 사용자는 인증되었다. 둘째, /admin/dashboard라는 리소스가 실제로 존재한다.

이러한 정보의 존재 확인 자체만으로도 공격자에게는 유용한 단서가 될 수 있습니다. 공격자는 이 반응을 이용해 시스템에 존재하는 유효한 사용자 ID나 민감한 엔드포인트를 추측하고 열거하는 공격을 시도할 수 있습니다.

이러한 정보 유출을 방지하기 위해, 서버는 403 대신 404 Not Found를 반환하도록 설계될 수 있습니다. 이렇게 하면 권한 없는 사용자의 입장에서는 해당 리소스가 단순히 존재하지 않는 것처럼 보입니다. 즉, "권한 없는 클라이언트로부터 리소스의 존재를 숨기는 것"입니다. 이는 API 보안이 얼마나 미묘한지를 보여줍니다. 상태 코드는 단순한 성공/실패 신호가 아니라, 효과적으로 소통하면서도 보안 위험을 최소화하기 위해 신중하게 사용되어야 하는 API 계약의 중요한 일부입니다.


4장: 이론을 현실로 - API 상호작용 예제

이론적인 개념을 이해했다면, 이제 실제 코드를 통해 어떻게 API와 상호작용하는지 살펴볼 차례입니다. 이 장에서는 프론트엔드 개발자들이 주로 사용하는 JavaScript의 fetch() API와, 백엔드 및 데브옵스 엔지니어들이 애용하는 cURL 커맨드 라인 도구를 사용하여 API 요청을 보내는 구체적인 예제를 제공합니다.

프론트엔드 개발자를 위한 예제: JavaScript fetch() API

fetch()는 최신 브라우저에 내장된 강력하고 유연한 네트워크 요청 API입니다. Promise 기반으로 작동하여 비동기 통신을 깔끔하게 처리할 수 있습니다.

기본 문법은 fetch(url, options) 형태이며, async/await 또는 .then() 체이닝을 사용하여 응답을 처리합니다.

중요: fetch()의 오류 처리fetch()는 네트워크 오류(예: DNS 조회 실패, 인터넷 연결 없음)가 발생한 경우에만 Promise를 reject합니다. 서버가 404 Not Found500 Internal Server Error 같은 HTTP 오류 상태 코드를 반환하더라도, fetch()는 이를 성공적인 요청으로 간주하고 Promise를 resolve합니다. 따라서 API 레벨의 오류를 처리하려면 반드시 response.ok 속성(상태 코드가 200-299 범위에 있으면 true)을 확인해야 합니다.

GET: 리소스 조회

GET 요청은 데이터를 조회할 때 사용되며, 필요한 파라미터는 URL 쿼리 문자열로 전달합니다. URLSearchParams 객체를 사용하면 이를 쉽게 구성할 수 있습니다.

async function getUser(userId) {
  const url = `https://api.example.com/users/${userId}`;

  try {
    const response = await fetch(url);

    if (!response.ok) {
      // 4xx, 5xx 같은 오류 응답을 처리
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

getUser(1);

POST: 새 리소스 생성

POST 요청 시에는 options 객체에 method, headers, body를 명시해야 합니다. 특히 Content-Type 헤더를 application/json으로 설정하고, body에는 JSON.stringify()를 사용하여 JavaScript 객체를 JSON 문자열로 변환하여 전달해야 합니다.

async function createUser(userData) {
  const url = 'https://api.example.com/users';

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer YOUR_API_TOKEN' // 인증 토큰이 필요한 경우
      },
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const createdUser = await response.json();
    console.log('User created:', createdUser);
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

const newUser = { name: 'Jane Doe', email: 'jane.doe@example.com' };
createUser(newUser);

PUT & PATCH: 리소스 수정

PUT(전체 교체)과 PATCH(부분 수정) 요청은 POST와 유사하게 method, headers, body를 설정합니다. 요청 본문의 내용이 전체 리소스인지, 변경된 부분만인지에 따라 달라집니다.

// PUT 예제: 사용자 전체 정보 교체
async function updateUserWithPut(userId, fullUserData) {
  const url = `https://api.example.com/users/${userId}`;
  
  const response = await fetch(url, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(fullUserData)
  });
  
  //... 오류 처리 및 응답 처리 로직...
}

// PATCH 예제: 사용자 이메일만 변경
async function updateUserWithPatch(userId, partialUserData) {
  const url = `https://api.example.com/users/${userId}`;
  
  const response = await fetch(url, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(partialUserData)
  });
  
  //... 오류 처리 및 응답 처리 로직...
}

DELETE: 리소스 삭제

DELETE 요청은 보통 요청 본문이 필요 없으며, method만 지정하면 됩니다.

async function deleteUser(userId) {
  const url = `https://api.example.com/users/${userId}`;

  try {
    const response = await fetch(url, {
      method: 'DELETE',
      headers: {
        'Authorization': 'Bearer YOUR_API_TOKEN'
      }
    });

    if (response.status === 204) {
      console.log('User deleted successfully.');
    } else if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

deleteUser(1);

백엔드/데브옵스 엔지니어를 위한 예제: cURL

cURL은 API를 테스트하고 상호작용하는 데 사용되는 강력한 커맨드 라인 도구입니다.

  • GET:Bash curl -X GET "https://api.example.com/users/1"
  • POST: X 플래그로 메서드를, H로 헤더를, d로 데이터를 지정합니다. Bash curl -X POST \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john.doe@example.com"}' \ "https://api.example.com/users"
  • PUT:Bash curl -X PUT \ -H "Content-Type: application/json" \ -d '{"name": "Johnathan Doe", "email": "john.doe.new@example.com"}' \ "https://api.example.com/users/1"
  • PATCH:Bash curl -X PATCH \ -H "Content-Type: application/json" \ -d '{"email": "john.doe.updated@example.com"}' \ "https://api.example.com/users/1"
  • DELETE: Bash curl -X DELETE "https://api.example.com/users/1"

5장: 성공을 위한 아키텍처 - 고급 API 설계 및 보안

기능적으로 동작하는 API를 만드는 것을 넘어, 장기적으로 유지보수 가능하고, 안전하며, 사용하기 좋은 API를 설계하는 것은 또 다른 차원의 과제입니다. 이 장에서는 데이터 형식 선택, 버전 관리 전략, 그리고 필수적인 보안 모범 사례와 같은 고급 주제를 다룹니다.

데이터 교환 형식: JSON이 지배하는 이유

API는 다양한 데이터 형식(XML, Protocol Buffers 등)을 사용할 수 있지만, 현대 웹 API의 세계에서는 JSON(JavaScript Object Notation)이 사실상의 표준으로 자리 잡았습니다.

  • 성능: JSON은 XML에 비해 훨씬 덜 장황합니다. 시작 태그와 종료 태그가 없어 페이로드 크기가 작고, 이는 네트워크 대역폭을 절약하며 파싱 속도를 높여줍니다. 특히 모바일 환경이나 트래픽이 많은 애플리케이션에서 이러한 성능 차이는 사용자 경험에 직접적인 영향을 미칩니다.
  • 개발자 경험: JSON의 가장 큰 장점은 JavaScript 및 대부분의 최신 프로그래밍 언어에서 네이티브 객체로 쉽게 변환된다는 점입니다. 개발자는 복잡한 파싱 라이브러리 없이도 데이터를 직관적으로 다룰 수 있습니다. 반면, XML을 파싱하는 것은 종종 더 많은 코드와 노력을 필요로 합니다.

진화하는 API 관리: 버전 관리 전략

API는 비즈니스 요구사항이 변화함에 따라 함께 진화합니다. 이때 기존 클라이언트 애플리케이션의 작동을 중단시키지 않으면서 호환성이 깨지는 변경(breaking changes)을 도입하려면 API 버전 관리가 필수적입니다.

올바른 버전 관리 전략을 선택하는 것은 장기적인 유지보수성, 캐싱 효율성, 그리고 개발자 경험에 큰 영향을 미칩니다. 다음은 주요 버전 관리 전략들의 장단점을 비교한 표입니다.

전략예시장점단점
URI 경로 (URI Path)api.example.com/v1/products버전이 명시적이고 직관적임. 문서화 및 캐싱이 용이함. 가장 널리 사용됨.URI가 지저분해짐. 버전 변경 시 코드베이스가 크게 변경될 수 있음.
쿼리 파라미터 (Query Parameter)api.example.com/products?version=1구현이 간단하고, 최신 버전을 기본값으로 설정하기 쉬움.라우팅 및 캐싱 구현이 복잡해질 수 있음. URI 경로 방식보다 덜 명시적임.
커스텀 헤더 (Custom Header)Accepts-version: 1.0URI를 깔끔하게 유지할 수 있음. 더 유연한 버전 제어가 가능함.브라우저에서 직접 테스트하기 어려움. 클라이언트 측에서 추가 설정이 필요함.

API 요새화: 보안 모범 사례 입문

API 보안은 선택이 아닌 필수입니다. 안전하지 않은 API는 데이터 유출, 서비스 거부 공격 등 심각한 위협에 노출될 수 있습니다. OWASP API Security Top 10과 같은 자료는 API 보안의 지침서 역할을 합니다.

인증(Authentication)과 인가(Authorization)

  • 인증 (Authentication): "당신은 누구인가?"를 확인하는 과정입니다. 사용자의 신원을 증명하는 단계로, 로그인 과정이 대표적입니다.
  • 인가 (Authorization): "당신은 무엇을 할 수 있는가?"를 결정하는 과정입니다. 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 단계입니다.

다음은 일반적인 API 인증 방식들입니다.

방식작동 원리최적 사용 사례주요 트레이드오프
API 키 (API Key)헤더에 정적인 비밀 토큰을 전송.간단한 서버 간 통신, 내부용 앱.단순함 vs. 낮은 보안 수준.
OAuth 2.0위임된 인가를 위한 프로토콜.제3자 애플리케이션의 사용자 데이터 접근.강력한 보안 vs. 높은 복잡도.
JWT (JSON Web Token)클레임을 포함하는 자체 서명된 토큰.상태 비저장 마이크로서비스, SPA.성능 및 확장성 vs. 내장된 폐기 메커니즘 부재.

전송 계층 보안 (HTTPS)

모든 API 통신은 반드시 HTTPS(TLS)를 통해 암호화되어야 합니다. 이는 중간자 공격(Man-in-the-Middle attack)을 방지하고 전송 중인 데이터를 보호하는 가장 기본적인 조치입니다.

요청 제한 (Rate Limiting)

요청 제한은 특정 클라이언트가 정해진 시간 동안 보낼 수 있는 요청의 수를 제한하는 기술입니다. 이는 서비스 거부(DoS) 공격을 완화하고, 특정 사용자가 시스템 리소스를 독점하는 것을 방지하여 서비스의 안정성을 유지하는 데 매우 중요합니다.

입력값 검증 (Input Validation)

"절대 클라이언트를 신뢰하지 말라"는 보안의 기본 원칙입니다. 클라이언트로부터 들어오는 모든 데이터(URL 파라미터, 요청 본문, 헤더 등)는 SQL 인젝션, XSS(Cross-Site Scripting) 등의 공격을 방지하기 위해 서버 측에서 철저하게 검증하고 정제(sanitize)해야 합니다.


결론

지금까지 우리는 API 개발의 광범위한 지형을 탐험했습니다. REST 아키텍처 스타일의 근본적인 '왜'부터 시작하여, HTTP 메서드와 상태 코드라는 API의 '언어'를 배우고, JavaScript fetchcURL을 통해 이론을 실제 '코드'로 옮기는 방법을 살펴보았습니다. 마지막으로 버전 관리, 데이터 형식 선택, 그리고 보안과 같은 '성공적인 설계'의 원칙들을 다루었습니다.

이 여정을 통해 명확해진 것은, 잘 설계된 API는 단순히 기능을 제공하는 것을 넘어선다는 사실입니다. 훌륭한 API는 신뢰할 수 있고, 안전하며, 무엇보다 개발자에게 훌륭한 사용 경험을 제공합니다. 이는 마치 잘 쓰인 설명서와 같아서, 개발자가 API의 기능을 쉽고 예측 가능하게 사용하여 더 강력하고 혁신적인 애플리케이션을 만들 수 있도록 돕습니다.

이제 이 지식을 실제 프로젝트에 적용해 볼 차례입니다. 작은 개인 프로젝트를 위해 간단한 API를 직접 만들어보거나, Postman이나 cURL 같은 도구를 사용하여 GitHub API와 같은 공개 API를 탐색해 보세요. 우리가 매일 사용하는 웹사이트와 애플리케이션의 네트워크 요청을 개발자 도구로 살펴보는 것만으로도 이 가이드에서 다룬 개념들이 실제로 어떻게 작동하는지 생생하게 경험할 수 있을 것입니다. 결국, 가장 효과적인 학습은 직접 만들어보고 경험하는 것에서 비롯됩니다.

profile
AI/ML Engineer

0개의 댓글