Skip to main content
Summary

요청은 어떻게 서버까지 도달하는가

웹 서비스는 결국 클라이언트와 서버가 데이터를 주고받는 구조다. 클라이언트는 브라우저, 모바일 앱, 다른 백엔드 서버일 수 있다. 서버는 요청을 받아 처리하고 응답을 돌려준다. 네트워크를 이해한다는 것은 이 요청과 응답이 어디를 지나며, 어디서 실패할 수 있는지 아는 것이다.

사용자가 브라우저에 https://example.com/orders를 입력했다고 하자. 먼저 브라우저는 example.com이라는 도메인을 실제 서버 주소로 바꿔야 한다. 이 역할을 하는 것이 DNS다. DNS는 사람이 읽기 쉬운 이름을 컴퓨터가 찾을 수 있는 IP 주소로 변환한다.

IP 주소는 네트워크에서 장비를 찾기 위한 주소다. 하지만 한 서버 안에서도 여러 프로그램이 동시에 실행될 수 있다. 그래서 포트가 필요하다. 포트는 서버 안에서 어떤 프로그램과 통신할지 구분하는 번호다. 웹 서버는 보통 HTTP는 80번, HTTPS는 443번 포트를 사용한다.

IP와 포트가 정해지면 클라이언트는 서버와 연결을 맺는다. HTTP/1.1과 HTTP/2에서 주로 쓰는 연결 방식은 TCP다. TCP는 데이터가 순서대로, 빠짐없이 도착하도록 관리한다. 이 과정 덕분에 개발자는 대부분의 경우 패킷이 쪼개지고 다시 조립되는 세부 과정을 직접 다루지 않아도 된다. 다만 최근에는 HTTP/3처럼 TCP 대신 QUIC이라는 방식을 사용하는 경우도 있다는 점만 알아두면 좋다. 기본 원리를 익히는 단계에서는 먼저 TCP 기반 흐름을 이해해도 충분하다.

HTTPS라면 여기에 TLS가 추가된다. TLS는 통신 내용을 암호화하고, 접속한 서버가 믿을 수 있는 서버인지 인증서로 확인한다. 인증서가 만료되었거나 도메인과 맞지 않으면 브라우저나 API 클라이언트는 연결을 거부할 수 있다.

이 흐름을 간단히 정리하면 다음과 같다.

  1. 도메인을 DNS로 IP 주소로 바꾼다.

  2. IP 주소와 포트를 정한다.

  3. TCP 연결을 맺는다.

  4. HTTPS라면 TLS 협상을 통해 암호화 통신을 준비한다.

  5. HTTP 요청을 보내고 응답을 받는다.

"내 컴퓨터에서는 되는데 서버에서는 안 된다"는 문제는 이 단계 중 하나에서 자주 발생한다. DNS 설정이 다르거나, 서버 방화벽이 포트를 막고 있거나, TLS 인증서가 잘못되었거나, 사내망에서만 접근 가능한 주소를 배포 환경에서 호출했을 수 있다. 이럴 때는 막연히 코드를 의심하기보다 curl -v, dig, nslookup, ping 같은 도구로 어느 단계까지 성공하는지 확인해야 한다. 단, ping이 성공한다고 해서 HTTP 요청까지 반드시 성공한다는 뜻은 아니다. 서버가 ICMP 응답은 허용해도 웹 포트는 막혀 있을 수 있고, 반대로 보안 정책상 ping만 막혀 있을 수도 있다.

curl -v https://api.example.com/orders

curl -v는 접속하려는 IP, TLS 연결 과정, 요청 헤더, 응답 상태 코드 등을 함께 보여준다. API 문제가 네트워크 문제인지, 서버 애플리케이션 문제인지 나누는 데 유용하다.

네트워크와 HTTP, API 이해하기AI 생성 이미지

HTTP는 웹의 공통 약속이다

HTTP는 클라이언트와 서버가 요청과 응답을 주고받는 약속이다. HTTP를 잘 이해하면 API를 더 명확하게 설계하고, 장애 상황도 빠르게 파악할 수 있다.

먼저 HTTP 메서드는 요청의 의도를 표현한다.

  • GET: 데이터를 조회한다.

  • POST: 새 데이터를 만들거나 특정 처리를 요청한다.

  • PUT: 리소스 전체를 교체한다.

  • PATCH: 리소스 일부를 수정한다.

  • DELETE: 리소스를 삭제한다.

예를 들어 주문 목록 조회는 GET /orders, 새 주문 생성은 POST /orders, 특정 주문 취소는 POST /orders/{id}/cancel 또는 정책에 따라 PATCH /orders/{id}로 표현할 수 있다. 중요한 것은 팀과 서비스 안에서 같은 기준을 유지하는 것이다.

상태 코드는 요청 결과를 숫자로 표현한다. 서버가 응답 본문에 "성공"이라고 적어도 상태 코드가 부정확하면 클라이언트, 프록시, 모니터링 도구가 결과를 제대로 해석하기 어렵다.

  • 200 OK: 요청이 성공했다.

  • 201 Created: 새 리소스가 생성되었다.

  • 204 No Content: 성공했지만 응답 본문이 없다.

  • 400 Bad Request: 요청 형식이나 값이 잘못되었다.

  • 401 Unauthorized: 인증이 필요하거나 인증 정보가 잘못되었다.

  • 403 Forbidden: 인증은 되었지만 권한이 없다.

  • 404 Not Found: 요청한 리소스가 없다.

  • 409 Conflict: 현재 상태와 충돌한다. 예를 들어 이미 취소된 주문을 다시 취소하려는 경우다.

  • 429 Too Many Requests: 요청이 너무 많아 제한되었다.

  • 500 Internal Server Error: 서버 내부 오류다.

  • 503 Service Unavailable: 서버가 일시적으로 요청을 처리할 수 없다.

헤더는 요청과 응답에 붙는 부가 정보다. Content-Type은 본문 형식을 알려주고, Authorization은 인증 정보를 담는다. Accept-Language는 선호 언어를 전달할 수 있고, Cache-Control은 캐시 정책을 알려준다. 본문이 실제 데이터라면, 헤더는 그 데이터를 어떻게 해석하고 다룰지 알려주는 설명서에 가깝다.

쿠키는 서버가 클라이언트에 저장해 두는 작은 데이터다. 주로 세션 식별자, 사용자 설정, 추적 정보 등에 쓰인다. 브라우저는 조건이 맞으면 다음 요청에 쿠키를 자동으로 포함한다. 편리하지만 보안 설정이 중요하다. HttpOnly는 JavaScript에서 쿠키를 읽지 못하게 하고, Secure는 HTTPS에서만 전송되도록 하며, SameSite는 다른 사이트에서 시작된 요청에 쿠키를 보낼지 제어한다.

캐시는 같은 데이터를 반복해서 가져오지 않도록 저장해 두는 방식이다. 이미지, 정적 파일, 자주 바뀌지 않는 API 응답에 유용하다. 하지만 캐시 정책이 잘못되면 사용자는 오래된 데이터를 보게 된다. 사용자별 데이터와 공개 데이터를 구분하고, 변경 가능성이 높은 데이터에는 신중한 캐시 정책을 적용해야 한다.

REST API는 리소스를 일관되게 다루는 방식이다

REST API를 설계할 때 핵심은 "무엇을 대상으로 어떤 동작을 하는가"를 명확히 표현하는 것이다. 여기서 대상은 보통 리소스라고 부른다. 회원, 주문, 상품, 댓글이 모두 리소스가 될 수 있다.

리소스 이름은 동사가 아니라 명사로 표현하는 것이 좋다.

GET /users/15
GET /users/15/orders
POST /orders
PATCH /orders/1001
DELETE /comments/77

반대로 다음처럼 동사를 URL에 반복해서 넣으면 기준이 흐려지기 쉽다.

/getUser
/createOrder
/deleteComment

물론 모든 동작이 CRUD로 깔끔하게 떨어지지는 않는다. 주문 취소, 비밀번호 재설정, 이메일 인증처럼 업무상 의미가 강한 동작도 있다. 이때도 억지로 REST 형식에 끼워 맞추기보다 팀에서 이해하기 쉬운 리소스나 하위 동작으로 표현하는 편이 낫다.

POST /orders/1001/cancel
POST /password-reset-requests
POST /email-verifications

목록 조회 API에는 페이지네이션이 필요하다. 데이터가 많아질수록 전체 목록을 한 번에 내려주는 방식은 느려지고 불안정해진다.

GET /orders?page=1&size=20
GET /orders?cursor=eyJpZCI6MTAwMX0&limit=20

page 방식은 이해하기 쉽지만 데이터가 자주 추가되는 목록에서는 중복이나 누락이 생길 수 있다. cursor 방식은 구현이 조금 더 복잡하지만 무한 스크롤이나 대규모 데이터 조회에 더 안정적인 경우가 많다.

필터링과 정렬도 일관된 규칙이 있어야 한다.

GET /orders?status=paid&from=2026-01-01&sort=-createdAt

API를 사용하는 개발자는 URL만 보고도 어떤 조건으로 어떤 데이터가 반환될지 예측할 수 있어야 한다.

에러 응답 형식도 중요하다. 실패했을 때마다 응답 구조가 다르면 클라이언트는 예외 처리를 복잡하게 작성해야 한다. 다음처럼 공통 형식을 정해두면 좋다.

{
  "code": "ORDER_NOT_FOUND",
  "message": "주문을 찾을 수 없습니다.",
  "details": {
    "orderId": "1001"
  }
}

message는 사용자나 개발자가 이해할 수 있는 설명이고, code는 클라이언트가 분기 처리할 수 있는 안정적인 값이다. 메시지는 문구가 바뀔 수 있지만 코드는 쉽게 바꾸지 않는 것이 좋다.

인증과 인가는 다르다

인증은 "당신이 누구인가"를 확인하는 과정이다. 로그인, 비밀번호 확인, 소셜 로그인, 인증서 확인이 여기에 해당한다. 인가는 "당신이 이 작업을 해도 되는가"를 판단하는 과정이다. 로그인한 사용자라도 다른 사용자의 주문 내역을 볼 수 없다면 인가에서 거절되어야 한다.

세션 방식에서는 사용자가 로그인하면 서버가 세션을 만들고, 클라이언트는 세션 ID를 쿠키로 보관한다. 이후 요청마다 쿠키가 전송되고, 서버는 세션 저장소에서 사용자를 확인한다. 서버가 세션을 직접 관리하므로 강제 로그아웃이나 세션 만료 처리가 비교적 쉽다. 대신 서버가 여러 대일 때 세션 저장소를 공유하거나 고정 세션 같은 전략이 필요하다.

토큰 방식에서는 로그인 후 서버가 토큰을 발급하고, 클라이언트는 요청마다 Authorization 헤더에 토큰을 담아 보낸다.

Authorization: Bearer <token>

토큰은 모바일 앱이나 서버 간 통신에서 자주 사용된다. 다만 토큰이 탈취되면 만료 전까지 악용될 수 있으므로 저장 위치, 만료 시간, 재발급 방식, 폐기 전략을 함께 설계해야 한다.

OAuth는 사용자가 비밀번호를 직접 넘기지 않고 제3자 서비스의 권한을 위임하는 흐름이다. 예를 들어 어떤 애플리케이션이 사용자의 Google 계정에 있는 일정이나 연락처에 접근해야 할 때, 사용자는 Google 비밀번호를 애플리케이션에 넘기지 않고 필요한 범위의 접근만 허용할 수 있다. "Google 계정으로 로그인"처럼 로그인에 사용하는 흐름은 보통 OAuth 위에 사용자 신원을 확인하는 OpenID Connect 같은 규약이 함께 쓰인다. 세부 규약을 모두 외울 필요는 없지만, OAuth의 핵심이 비밀번호 공유가 아니라 권한 위임이라는 점은 기억해 두는 것이 좋다.

여기서 중요한 개념은 권한 범위다. 프로필 조회만 필요한 서비스가 이메일 발송 권한까지 요구하면 위험하고 불필요하다. 필요한 권한만 요청하고, 사용자가 어떤 권한을 허용했는지 서버에서 명확히 확인해야 한다.

인증과 인가는 API 설계 전체에 영향을 준다. 단순히 로그인 여부만 확인하면 안 된다. "이 사용자가 이 리소스에 접근할 수 있는가"를 매 요청마다 판단해야 한다.

분산 환경의 API는 실패를 기본값으로 생각한다

현대의 서비스는 하나의 서버만으로 동작하지 않는다. API 서버가 데이터베이스를 호출하고, 결제 서버를 호출하고, 알림 서버와 외부 인증 서버를 호출한다. 이런 환경에서는 네트워크 지연, 일시적 장애, 중복 요청이 언제든 발생할 수 있다.

먼저 타임아웃을 반드시 설정해야 한다. 외부 API가 응답하지 않는데 무한히 기다리면 요청 스레드나 커넥션이 고갈되어 전체 서비스가 느려질 수 있다. 타임아웃은 "얼마나 기다릴 것인가"에 대한 명시적인 결정이다.

재시도는 일시적 실패를 줄이는 데 도움이 된다. 하지만 모든 요청을 재시도하면 더 큰 문제가 생길 수 있다. 결제 요청을 무작정 재시도하면 중복 결제가 발생할 수 있다. 재시도는 네트워크 끊김, 일시적 503, 429 같은 상황에 제한적으로 적용하고, 재시도 간격을 점점 늘리는 방식이 안전하다. 서버가 Retry-After 같은 헤더로 다시 시도할 시점을 알려준다면 그 값을 존중하는 것도 중요하다.

여기서 중요한 개념이 멱등성이다. 멱등성이란 같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 같다는 뜻이다. GET 조회 요청은 일반적으로 멱등적이다. DELETE /users/1도 이미 삭제된 상태를 유지한다는 관점에서는 멱등적으로 설계할 수 있다. 반면 POST /payments는 중복 실행되면 결제가 여러 번 발생할 수 있다.

중복 요청 가능성이 있는 API에는 멱등성 키를 사용할 수 있다.

Idempotency-Key: 20260721-user15-order1001

서버는 같은 키로 들어온 요청을 한 번만 처리하고, 이후 같은 요청에는 이전 결과를 반환한다. 결제, 주문 생성, 쿠폰 사용처럼 중복 실행이 치명적인 기능에 특히 중요하다.

Rate Limit은 일정 시간 동안 허용할 요청 수를 제한하는 정책이다. 특정 사용자나 클라이언트가 너무 많은 요청을 보내면 서버 자원이 고갈될 수 있다. 이때 429 Too Many Requests로 응답하고, 가능하다면 언제 다시 요청할 수 있는지 알려준다.

API는 정상 흐름보다 실패 흐름에서 품질 차이가 크게 드러난다. 타임아웃이 없는 API, 재시도 기준이 없는 클라이언트, 중복 요청을 고려하지 않은 결제 API, 형식이 제각각인 에러 응답은 작은 트래픽에서는 드러나지 않다가 실제 운영 환경에서 문제를 만든다.

네트워크와 HTTP를 안다는 것은 단순히 상태 코드 이름을 외우는 일이 아니다. 요청이 어디를 지나고, 어떤 약속으로 해석되며, 실패했을 때 어떻게 안전하게 복구할지 판단하는 능력이다. 이 기초가 있어야 API를 더 안정적으로 설계하고, 운영 중 발생하는 문제도 빠르게 좁혀갈 수 있다.

책 목록

개발자에게 필요한 필수 지식 정리

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기