멱등성(Idempotent)은 수학이나 전산학에서 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다.
이 멱등성을 HTTP Method에 적용하면 다음과 같은 의미를 가진다. 동일한 요청을 한번 보내는 것과 여러번 연속으로 보내는 것이 같은 효과를 가지고, 서버의 상태도 동일하게 남을 때 해당 HTTP Method가 멱등성을 가진다고 한다.
HTTP Method의 멱등성에서 알아두어야 할 점이 있다. 결과가 의미하는 것이 응답 상태코드가 아닌 서버의 상태라는 것이다. 예를 들어, 똑같은 요청을 했을 때 응답하는 상태코드가 바뀌더라도 서버의 상태가 항상 같은 상태라면 멱등성이 있다고 판단한다.
우리가 흔히 사용하는 HTTP Method는 GET, POST, PUT, PATCH, DELETE가 있다. HTTP 스펙에 명시된 것에 의하면, GET, PUT, DELETE는 멱등성을 가지도록, POST와 PATCH는 멱등성을 가지지 않도록 구현해야 한다. 그 이유에 대해서 하나씩 살펴보자.
GET 메서드가 멱등성을 가져야 할 이유는 직관적으로 바로 알 수 있다. GET 메서드는 특정한 리소스를 가져오도록 요청한다. 즉, 데이터를 조회하는 메서드이다. 데이터를 가져오는 요청이 서버의 상태를 변경한다면 그 목적과는 맞지 않는 것이다. 그렇기 때문에 GET 메서드는 멱등성을 가지도록 구현해야 한다.
GET /post/1
1. DB에서 id 값이 1인 게시글을 찾는다.
2. 응답으로 해당 게시글 데이터를 반환한다.
GET 메서드로 호출되는 API에 대한 간략한 예시를 적어보았다. 위의 경우, 같은 요청을 여러번 보내더라도 서버의 상태는 항상 같다. 즉, 멱등성을 가진다고 할 수 있고, HTTP 스펙에 맞게 잘 구현되었다고 할 수 있다.
GET /post/1
1. DB에서 id 값이 1인 게시글을 찾는다.
2. 해당 게시글의 조회수 컬럼의 값을 1 증가시킨다.
3. 응답으로 해당 게시글 데이터를 반환한다.
하지만 만약 위처럼 구현이 되어있다면 어떨까? 게시글을 조회한 횟수를 기록하고 싶어, GET 요청을 할 때 해당 게시글의 조회수를 1 증가시키도록 했다. 이 경우, 같은 요청을 여러번 보낸다면, 서버의 상태는 매번 바뀐다. 즉, 멱등성을 가지지 않는 것이며, HTTP 스펙에 부합하지 않은 구현이라고 볼 수 있다.
만약 이러한 로직이 필요하다면, 조회수 컬럼의 값을 증가시키는 요청을 따로 분리하는 것이 스펙상 올바르다.
Post는 서버로 데이터를 전송하는 메서드이다. POST는 다양한 요청을 처리하는 메서드이지만, CRUD의 관점에서 볼 때 일반적으로 새로운 자원을 생성하는 역할을 한다. 같은 요청을 여러번 보내는 경우 매번 새로운 자원이 생겨날 수 있으며, 이는 서버의 상태가 변경되는 것을 의미한다. 그렇기에 POST 메서드는 멱등성을 가지지 않는다.
PUT은 새로운 리소스를 생성하거나, 대상 리소스를 나타내는 데이터를 덮어쓴다. POST와 PUT의 차이는 멱등성의 유무인데, POST는 매번 새로운 자원을 만드는 반면 PUT은 해당 자원이 이미 있다면 데이터만 덮어쓰기 한다. 즉, 요청을 한번하든 여러번하든 결국 서버의 상태는 같아진다. 그래서 PUT은 멱등성을 가지는 HTTP 메서드이다.
추가로 HTTP 스펙에서 정의된 PUT의 응답 상태코드는 다음과 같다.
해당 정의에서 알 수 있는 점은 동일한 요청을 여러번 보내더라도 상태코드가 변경될 수 있다는 것이다.
만약, PUT으로 요청한 리소스가 없을 경우 새로운 리소스를 만든다. 이 경우 201 상태코드를 응답한다. 이후 동일한 요청을 할 경우, 이미 리소스가 존재하므로 200 혹은 204 상태코드를 응답한다.
이처럼 동일한 요청을 여러번 보낼지라도 상태코드는 변할 수 있다. 다만, 처음에 설명한 것처럼 서버의 상태는 동일하므로 멱등성을 가진다고 할 수 있다.
PATCH는 리소스의 부분적인 수정을 할 때 사용된다. PATCH는 기본적으로 멱등성을 가지지 않는 메서드인데, 그 구현을 PUT과 동일한 방식으로 할 경우 멱등성을 가지게 된다. 이말을 이해하려면, 우선 PUT과 PATCH의 차이에 대해 알아보아야 한다.
기존의 리소스
{
id: 1,
name: "김철수",
age: 15
}
PUT /users/1
{
age: 20
}
변경된 리소스
{
id: 1,
name: NULL,
age: 20
}
PUT은 요청 Body로 덮어쓸 데이터가 위치해야 하며, 기존의 리소스가 해당 데이터로 완전히 덮어씌워진다. 그렇기 때문에, 동일한 요청을 여러번 보내더라도 항상 같은 데이터로 덮어씌워지기에 멱등성을 가진다.
기존의 리소스
{
id: 1,
name: "김철수",
age: 15
}
PATCH /users/1
{
age: {
type: $inc,
value: 1
}
}
변경된 리소스
{
id: 1,
name: "김철수",
age: 16
}
다만 PATCH의 경우 약간 다르다. PATCH는 HTTP 스펙상 구현 방법에 제한이 없다. 그렇기때문에 요청 Body에 꼭 덮어쓸 데이터가 있을 필요가 없다. 위 예시처럼 덮어쓸 데이터가 아닌 동작을 지정해줄 수 있는 것이다.
위 예시의 경우 동일한 요청을 여러번 보내면, 매 요청마다 age가 1씩 증가한다. 즉, 멱등성을 가지지 않는 것이다.
기존의 리소스
{
id: 1,
name: "김철수",
age: 15
}
PATCH /users/1
{
age: 20
}
변경된 리소스
{
id: 1,
name: "김철수",
age: 20
}
다만, 앞에서 얘기했듯이 Patch의 구현을 PUT과 동일한 방식으로 할 경우 멱등성을 가진다. PUT과 같이 요청 Body에 덮어쓸 데이터만 위치하도록 구현을 할 경우, 같은 요청을 여러번 하더라도 같은 결과가 나온다. 즉, 멱등성을 가진다고 할 수 있는 것이다.
DELETE 메서드가 멱등성을 가지는 이유도 GET처럼 직관적으로 알 수 있다. DELETE는 지정한 리소스를 삭제하는 메서드이다. 즉, DELETE 메서드를 처음 요청이 되면, 서버에서 해당 리소스는 삭제가 된다. 이후 DELETE를 여러번 요청하더라도 해당 리소스는 삭제된 상태 그대로 일 것이니 서버의 상태는 변하지 않는다. 즉, 멱등성을 가진다고 할 수 있는 것이다.
추가적으로 이러한 멱등성 때문에, DELETE의 호출에서 정확한 식별자를 통해 리소스를 지정해야 한다.
DELETE /posts/last
예를 들어 위와 같은 API가 있다고 가정하자. 해당 API는 가장 마지막 게시글을 삭제하는 API이다. 이 때, 해당 요청을 여러번 보낼 경우 매번 마지막 게시글을 삭제하기 때문에 매번 서버의 상태가 변한다. 즉, 멱등성을 가지지 않는 것이다.
POST /posts/last
이런 경우에는 오히려 멱등성을 가지지 않는 POST를 쓰는게 더 HTTP 스펙상으로 맞다. 그래도 팀원이랑 상의해서 DELETE 쓰는 것도 나쁘진 않아보인다.
HTTP 메서드의 멱등성이라는 것은 HTTP 스펙의 규약일 뿐이기에 꼭 지켜야하는 것은 아니다. 다만, 이러한 규약들을 제대로 지키지 않는다면 해당 API의 동작을 유추하기 힘들어질 것이다. 이는 원치않는 동작을 야기할 가능성이 크기에, 가능한 규약을 지켜 그에 맞는 동작을 하도록 구현하는 게 좋다고 생각한다.