1. لائبريري
  2. HTTP와 웹
  3. HTTP 메서드

اپڊيٽ ٿيل -1 m

요청을 보냈습니다. 네트워크 타임아웃이 발생했습니다. 서버가 처리했을까요?

정말로 알 수 없습니다. 요청이 도착해서 성공했지만 응답만 유실되었을 수도 있습니다. 아니면 요청이 전송 중에 사라졌을 수도 있습니다. 네트워크는 요청이 성공했는지 알려주지 않습니다—응답을 받았는지 알려줄 뿐입니다. 이 둘은 같은 것이 아닙니다.

멱등성은 이 불확실성을 다루는 방법입니다.

멱등성의 의미

HTTP 메서드는 동일한 요청을 여러 번 보내도 한 번 보낸 것과 서버 측 결과가 동일할 때 멱등하다고 합니다.

DELETE는 멱등합니다. 리소스를 한 번 삭제하면 사라집니다. 열 번 더 삭제해도—여전히 사라져 있습니다. 응답은 다를 수 있지만 (처음에는 204 No Content, 이후에는 404 Not Found를 반환), 결과는 동일합니다: 리소스가 존재하지 않습니다.

POST는 일반적으로 멱등하지 않습니다. 폼을 세 번 제출하면 세 개의 리소스가 생성될 수 있습니다. 신용카드를 세 번 청구하면 고객에게 세 번 청구됩니다. 각 요청이 서버 상태를 새로운 방식으로 변경합니다.

멱등한 메서드

GET

리소스를 조회하는 것은 리소스를 변경하지 않습니다. 동일한 게시글을 백 번 요청해도—서버 상태는 변하지 않습니다.

GET /articles/123 HTTP/1.1

→ 게시글 내용 (첫 번째)
→ 동일한 게시글 내용 (백 번째)

주의: 잘못 설계된 API는 GET에 부작용을 끼워 넣습니다—조회수 카운터 증가, 특정 액션 트리거, 로깅 등. 이는 HTTP 명세를 위반하며, 프록시, 브라우저, 로드 밸런서가 안전하다고 가정하고 GET 요청을 재시도할 때 실제 문제를 야기합니다.

PUT

리소스를 특정 상태로 업데이트하는 것은 멱등합니다. 게시글 제목을 "Updated Title"로 한 번 설정하면 그 제목이 됩니다. 다시 설정해도—여전히 그 제목입니다.

PUT /articles/123 HTTP/1.1
{"title": "Updated Title", "content": "Updated content"}

→ 게시글 업데이트됨 (첫 번째)
→ 게시글이 이미 그 상태임 (두 번째)

DELETE

리소스를 제거하는 것은 멱등합니다. 첫 번째 요청 후 리소스는 사라집니다. 열 번째 요청 후에도 여전히 사라져 있습니다.

DELETE /articles/123 HTTP/1.1

→ 204 No Content (첫 번째)
→ 404 Not Found (열 번째—하지만 리소스는 동일하게 사라져 있음)

HEAD와 OPTIONS

HEAD는 아무것도 수정하지 않고 메타데이터를 조회합니다. OPTIONS는 서버가 지원하는 기능을 확인합니다. 둘 다 서버 상태를 변경하지 않습니다.

멱등하지 않은 메서드

POST

POST를 보낼 때마다 새로운 상태가 만들어질 수 있습니다:

POST /articles HTTP/1.1
{"title": "New Article"}

→ 게시글 #123 생성됨 (첫 번째 요청)
→ 게시글 #124 생성됨 (두 번째 요청)
→ 게시글 #125 생성됨 (세 번째 요청)

이게 바로 중복 제출 문제입니다. 네트워크 타임아웃, 참을성 없는 사용자가 "제출" 버튼을 여러 번 클릭, 자동 재시도—이 모든 것이 의도치 않은 중복을 만들어냅니다.

PATCH

PATCH는 패치 형식에 따라 멱등할 수도 있고 그렇지 않을 수도 있습니다.

멱등함 (절대값 설정):

PATCH /users/123 HTTP/1.1
{"email": "new@example.com"}

→ 이메일이 이제 new@example.com임 (항상)

멱등하지 않음 (상대적 변경):

PATCH /users/123 HTTP/1.1
{"balance": {"increment": 10}}

→ 잔액이 10 증가함 (항상)

JSON Merge Patch (RFC 7396)는 멱등합니다. JSON Patch (RFC 6902)는 사용하는 작업에 따라 멱등하지 않을 수 있습니다.

멱등성이 중요한 이유

안전한 재시도

멱등한 메서드에서는 자유롭게 재시도할 수 있습니다:

async function fetchArticle(id) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await fetch(`/articles/${id}`);
    } catch (error) {
      if (attempt === 2) throw error;
      await sleep(1000 * (attempt + 1));
    }
  }
}

GET은 멱등합니다. 재시도해도 아무 문제가 없습니다.

POST에서는 재시도가 위험합니다:

// 재시도할 때마다 게시글이 하나씩 더 만들어질 수 있음
async function createArticle(data) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await fetch('/articles', {
        method: 'POST',
        body: JSON.stringify(data)
      });
    } catch (error) {
      // 타임아웃—요청이 성공했을까요? 재시도하면 중복이 생길 수 있음
    }
  }
}

로드 밸런서의 자동 재시도

백엔드 서버가 타임아웃될 때, 로드 밸런서는 멱등한 요청을 다른 서버에 재시도합니다:

클라이언트 → 로드 밸런서 → 서버 1 (타임아웃)
                         → 서버 2 (재시도—GET/PUT/DELETE에 안전)

특별한 설정 없이는 POST를 재시도하지 않습니다. 위험하다는 걸 알기 때문입니다.

브라우저가 사용자를 보호하는 방법

폼을 제출한 후 페이지를 새로고침하면 이런 메시지를 보게 됩니다:

"이 양식을 다시 제출하시겠습니까?"

브라우저는 POST가 멱등하지 않다는 것을 알고 있습니다. 반복하기 전에 경고를 보냅니다.

캐싱의 동작 원리

멱등한 메서드만 안전하게 캐시할 수 있습니다. 요청이 서버 상태를 변경한다면, 캐시된 응답은 거짓말이 됩니다.

GET 응답은 기본적으로 캐시됩니다. POST 응답은 그렇지 않습니다.

POST를 멱등하게 만들기

POST는 재시도가 필요한 경우가 많기 때문에, 멱등성을 추가하는 몇 가지 패턴이 있습니다:

멱등성 키

클라이언트는 각 논리적 작업에 고유한 키를 생성합니다:

const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Idempotency-Key': idempotencyKey,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(orderData)
});

서버는 키를 추적합니다. 같은 키가 두 번 오면? 원래 결과를 그대로 반환합니다:

app.post('/api/orders', async (req, res) => {
  const key = req.get('Idempotency-Key');
  const cached = await cache.get(key);
  
  if (cached) return res.json(cached);
  
  const order = await createOrder(req.body);
  await cache.set(key, order, { ttl: 3600 });
  res.status(201).json(order);
});

이제 재시도가 안전해집니다.

클라이언트 지정 ID

클라이언트가 직접 ID를 제공하도록 합니다:

const orderId = crypto.randomUUID();

fetch(`/api/orders/${orderId}`, {
  method: 'PUT',
  body: JSON.stringify({ id: orderId, items: [...] })
});

특정 URL에 대한 PUT은 멱등합니다. 주문이 이미 존재하면 업데이트하고, 없으면 새로 만듭니다. 재시도해도 동일한 주문 하나만 남습니다.

일회용 토큰

폼에 토큰을 숨겨둡니다:

<form method="POST" action="/submit">
  <input type="hidden" name="token" value="unique-token-123">
</form>

서버가 검증하고 즉시 무효화합니다:

app.post('/submit', (req, res) => {
  if (!validateAndInvalidateToken(req.body.token)) {
    return res.status(400).send('Invalid or expired token');
  }
  processForm(req.body);
  res.redirect('/success');
});

중복 제출은 실패합니다.

RESTful 설계

메서드를 의미에 맞게 사용하세요:

멱등한 작업 → PUT 또는 DELETE

  • 리소스를 특정 상태로 설정: PUT
  • 리소스 제거: DELETE

멱등하지 않은 작업 → POST

  • 서버가 ID를 생성하며 만들기: POST
  • 반복되어서는 안 되는 작업: POST
PUT /users/123/status HTTP/1.1
{"status": "active"}

멱등합니다. 상태를 "active"로 열 번 설정해도 결과는 active 상태 하나입니다.

POST /users/123/notifications HTTP/1.1
{"message": "Welcome back!"}

멱등하지 않습니다. 두 번 보내면 메시지가 두 개 전송됩니다.

멱등성 테스트

멱등한 메서드가 실제로 멱등한지 검증하세요:

test('PUT /articles/:id는 멱등합니다', async () => {
  const data = { title: 'Test', content: 'Content' };
  
  await request(app).put('/articles/123').send(data);
  await request(app).put('/articles/123').send(data);
  await request(app).put('/articles/123').send(data);
  
  const count = await countArticles({ title: 'Test' });
  expect(count).toBe(1);
});

흔한 실수

GET에 부작용 추가: GET 핸들러에서 상태를 변경하지 마세요. 프록시와 브라우저는 묻지도 않고 GET 요청을 재시도합니다.

POST 무분별한 재시도: 멱등성 키나 토큰 없이 재시도하면 중복이 발생합니다.

PATCH가 항상 멱등하다고 가정: 패치 형식에 따라 다릅니다. 증분 연산은 멱등하지 않습니다.

HTTP 메서드 멱등성에 관한 자주 묻는 질문

DELETE가 멱등하다면 왜 이후 요청에서 404를 반환하나요?

멱등성은 응답 코드가 아닌 서버 측 효과를 말합니다. DELETE 이후 리소스는 존재하지 않습니다—한 번 삭제했든 열 번 삭제했든 결과는 같습니다. 404는 단순히 "여기 없습니다"를 뜻하며, 이것이 바로 원하던 상태입니다. 효과는 동일하고, 응답 코드만 다를 뿐입니다.

타임아웃된 모든 요청을 안전하게 재시도할 수 있나요?

멱등한 요청(GET, PUT, DELETE, HEAD, OPTIONS)만 가능합니다. POST의 경우 멱등성 키나 유사한 보호 수단이 필요합니다. 없으면 타임아웃된 POST를 재시도했을 때 중복이 발생할 수 있습니다—원래 요청이 성공했는지 알 방법이 없기 때문입니다.

멱등한 메서드와 안전한 메서드의 차이는 무엇인가요?

안전한 메서드(GET, HEAD, OPTIONS)는 서버 상태를 전혀 수정하지 않습니다. 멱등한 메서드는 상태를 수정할 수 있지만, 반복해도 결과가 달라지지 않습니다. DELETE는 멱등하지만 안전하지는 않습니다—상태를 수정하지만, 두 번 실행해도 한 번 실행한 것과 효과가 같습니다.

멱등성 키를 얼마나 오래 저장해야 하나요?

재시도 윈도우와 네트워크 지연을 충분히 커버할 만큼—사용 사례에 따라 보통 1~24시간입니다. Stripe는 24시간을 사용합니다. 대부분의 API에서는 몇 시간이면 충분합니다. 너무 짧으면 재시도가 빠져나갈 수 있고, 너무 길면 저장 공간을 낭비하게 됩니다.

ڇا هي صفحو مددگار هو؟

😔
🤨
😃