Idempotency(멱등성)과 REST API. Method의 idempotency는 가변적이다.

728x90

서론

Spring을 이제 막 배우던 때에, @RequestMapping 기능을 막 익혔을 때의 이야기다. 

다음과 같이 컨트롤러에서 @GetMapping과 @PostMapping의 URL을 "/login"과 "/login/confirm"으로 정했지만, 마음  한 켠 찝찝한 기분이 느껴졌다. 

> 'URL을 이렇게 정해도 괜찮은 건가?'



GET은 로그인 사이트에 사용자가 접속했을 때 보는 정보가 담겨져있고, POST에는 사용자가 로그인을 위해 입력한 ID와 패스워드 정보가 담겨있다. 어쨌든 이 둘의 기능은 이렇게 구분되므로 이렇게 이렇게 "/confirm"이라는 URL로 구분지었지만, 이것이 알맞은 방법인지는 조금 의문이 들었다.

관련된 정보를 알기 위해 'RESTful API' 키워드로 검색했지만, 

- 동사가 아닌 명사를 사용한다.
- 대문자보다는 소문자를 사용한다.
- 마지막에 슬래시를 포함하지 않는다.
- 밑줄은 사용하지 않고 하이픈으로 대체한다.
- 파일확장자를 포함하지 않는다

따위의 정보만 나오고 내가 원하는 정보는 나오지 않았다. 

그래서 의문사항을 멘토에게 질문하니 피드백을 다음과 같이 받을 수 있었다.

> (1) 우선 login에서 GetMapping을 사용할 이유는 없다. (어짜피 HTML에서 알아서 하는데 왜 굳이?)
(2) 만일 GetMapping과 PostMapping을 함께 사용하려면, 같은 URL로 지정해도 된다. 어짜피 method로 구분이 되기 때문이다.
(3)  URL을 구분하려한다면 PathVariable을 넣어주는 것이 바람직하다. 
(4) 물론 QueryParameter도 URL을 구분시킬 수 있으나, 이는 은닉화시키지 않아도 되는 정보에 사용한다. 주로 일정 시간동안 동일한 요청에 동일한 응답이 돌아와야되는 경우, idempotent 한 결과를 위해서 쿼리를 사용하게 된다. idempotency(멱등성)에 대해서 알아보도록 해라.

이렇게 해서 Idempotency에 대해 알아보는 계기가 되었다. 서론이 조금 길었지만, 어쨌든 Idempotency 개념이 REST API 설계에 필요한 개념이라는 점은 충분히 이해가 되었을 것이다.  

REST API and HTTP Method

REST API란

Idempotency에 대해 이야기하기 전에 REST API에 대해서도 짚고 넘어가보자. (서로 연관이 깊은 주제이다.)

REST API는 'REST 규칙을 지키는 API'를 의미한다.

REST(REpresentational State Transfer): resource를 representation으로 구분하여 자원의 state를 주고 받는 형식을 말한다. 

API(Application Programming Interface): 어플리케이션이나 디바이스가 서로 통신하기 위해 따라야하는 인터페이스(혹은 규칙)을 의미한다. 

즉, REST API는 '웹 상에서 클라이언트와 서버가 정보를 주고 받기 위한 API 중에서 REST 규칙을 따르는 것'을 의미한다. 클라이언트와 서버가 통신하기 위해서는 특정 웹페이지를 가리기키 위해 URI이 필요하다. 그래서 REST API도 URI에 대한 서술이 포함되어있다.

REST API 구성요소


URL을 어떻게 적을까 고민하는 것으로 글을 시작했지만, REST API는 URL만을 지칭하지는 않는다. REST API를 구성하는 요소는 다음과 같이 3가지가 존재한다.

 1. 자원(Resource)
 자원(Resource)은 **URI로 표시**한다. 이번 글의 주된 관심사다.

 2. 행위(Verb)
 행위는 **HTTP의 Method**를 사용하여 표시한다.
 
 3. 표현(Representation)
 클라이언트와 서버가 주고받는 데이터 형태를 말한다. Text, JSON, XML 등이 있다.


HTTP Method


같은 URL을 사용해도 Method가 다르면 다른 API로 인식된다. HTTT Method는 다양하게 있으나 주로 다음 4가지를 사용한다.

1. GET: 정보 읽기 요청 (DB의 READ와 유사함)

2. POST: 정보 쓰기 요청 (DB의 CREATE과 유사함)

3. PUT: 정보 업데이트 요청 (DB의 UPDATE와 유사함)

4. DELETE: 정보 삭제 요청 (DB의 DELETE)

서론에 서술했듯이 똑같이 "/login" URL을 사용하여 컨트롤러을 매핑해도 annotation에 기록된 method가 다르면 다른 API로 인식되어 동작한다. 그러므로 @GetMapping과 @PostMapping은 아래처럼 동일한 URL을 가질수 있다. 

 


Idempotency(멱등성)


REST API를 설계할 때면 해당 기능이 Idempotency 성질을 가지는지 아닌지 고민을 해봐야한다. 특정 기능이 idempotent하냐 아니냐에 따라 사용자의 경험하는 서비스의 질은 매우 달라질 것이다.


Idempotency란


Idempotency를 한마디로 표현하면 '여러 번 수행해도 같은 결과가 나오는 것'을 의미한다. 수학에서는 '연산을 여러번 해도 결과가 달라지지 않는 성질'이라고 말하기도 한다.

지금 당장 naver.com 을 주소창에 입력하고 엔터를 눌러보면, 네이버 홈 화면으로 이동한다. 이것을 계속해서 반복하면 어떻게 될까? 그래도 똑같이 네이버 홈 화면으로 이동한다. (배너의 광고 내용은 바뀔 수 있겠지만, 그것까지 고려하진 말자;;) 주소창에서 엔터로 연타를 "따다닥" 해도 어쨌든 홈 화면으로 이동하는 것은 동일하다. 이것을 Idempotent하다고 표현하는 것이다.


HTTP Method의 Idempotency

각 HTTP Method에 대해 검색을 해보면 대략 다음과 같은 내용이 나온다.

1. GET: idempotent하다 여러 번 요청해도 같은 값. 
2. POST: idempotent 하다. 같은 POST를 계속 보내도 같은 결과물이 나오는 것을 보장하지는 않음. 서버에는 데이터가 계속 추가되기 때문에 서버에서의 데이터 상태가 달라짐.
3. PUT: idempotent하다. 여러 번 호출해도 최종으로 반환되는 결과값 동일하다. 수정 데이터 상태 그대로 결과가 유지됨.
4. DELETE: idempotent하다. 여러 번 호출했을 때, 404 응답이 나올 수는 있으나, 이것이 서버의 상태에 영향을 주는 건 아님. 서버에서 해당 데이터가 지워지는 상태는 일관되게 유지가 된다.

소위 이렇게만 정리가 되어있으면 GET, PUT, DELETE는  idempotent하고, POST는 idempotent 하지 않다고 생각하고 그러한 형태로 사용할 수도 있다. 하지만 이것은 이 내용을 대단히 오해한 것이다. idempotency는 각 method 별로 정해져 있는 속성이 아니라, 설계에 따라 변할 수 있는 가변적인 속성인 것이다.


POST도 idempotent 할 수 있다.


흔히 GET, PUT, DELETE는 Idempotent하다고 알려져있고, POST는 non-idempotent하다고 알려져있으나 이것은 각 Method의 idempotency의 default 성질에 불과하고, 설계하는 사람이 원하는 방향에 따라 각 method의 Idempotency 성질은 언제든지 바뀔 수 있다. POST 요청도 idempotent할 수 있다는 것이다. 예시를 들기 위해 다음과 같은 상황을 가정해보겠다. 

 


어느 한 사람이 배가 고파서 배달앱으로 치킨을 주문했다. 양념치킨을 주문했는데, 주문이 실패하고 앱에서 다시 주문하라는 안내문구가 나온다. 그 사람은 당황했지만, 다시 주문 메뉴로 돌아가서 양념치킨을 고르고 주문버튼을 누른다. 이번에는 성공적으로 주문이 완료된다. 하지만 이상한 점이 있다. 주문현황이 뜨는데 이게 웬 걸, 양념치킨이 두 번 주문되었다. 당연히 결제 계좌에서도 이중으로 돈이 빠져나갔다. '나는 한 마리 밖에 못 먹는데?' 당혹스럽고 화가나지만 주문을 받은 가게가 주문을 취소해줄지는 미지수다. 설령 그 쪽에서 주문, 결제를 취소하지 않아도 음식점 잘못은 없으니까...



POST 요청 에러가 발생했음에도 불구하고 에러 처리를 제대로 못하거나 요청을 복구하지 못한 것 때문에 소비자는 원하지도 않은 주문을 덤터기 씌워야한다. 이런 경우는 POST의 요청이 중복적으로 발생해도 하나의 주문 결과만 발생하도록 설계를 할 필요가 있다. 이를 해결하기 위한 방법은 여러가지가 존재할 수 있겠으나, 간략하게 다음과 같은 방법을 사용할 수도 있다.

1. 클라이언트 쪽에서 요청을 보낼 때 x-idempotence-key를 함께 보낸다. x-idempotence-key는 uuid 등을 이용하여 생성한 임의의 숫자 값을 가지고 있으며, 이 key는 서버 쪽에서 식별 용도로 활용된다.
2. 서버 측에는 키-값 쌍을 저장할 수 있는 Cache(Map 형태로 데이터를 저장할 수 있는 저장소)가 존재한다. 서버로 요청이 날라오면, 요청 헤더에 포함된 x-idempotence-key를 빼내와서 이 Cache의 key로 저장이 된다. 
3. 그럼 Cache의 값은 무엇으로 저장될까? 바로 각 요청의 응답을 저장하면 된다. 키-값으로 x-idempotence-key와 response가 짝지어 Cache에 저장되는 것이다.
4. 요청이 들어오면 x-idempotence-key를 가지고 Cache에 대응되는 값이 있는지 검사한다. 값이 있다면, 응답으로 상태코드 304를 날려서 이미 응답이 되었음을 클라이언트에게 알린다. 이런 방식을 사용하면 중복된 요청이 들어와도 하나의 응답으로만 처리해줄 것이다.

 



본 사이트에서 JS로 구현한 코드를 살펴볼 수 있다.
https://medium.com/dsc-hit/creating-an-idempotent-api-using-node-js-bdfd7e52a947

이처럼 POST 요청만 하더라도 idempotent하게 만들 수 있는 것이다. 전적으로 REST API에서 각 기능을 idempotent하게 할 것인지 말 것인지는 해당 기능을 설계하는 사람에게 달려있다. 


결론

Idempotency는 단순한 개념이지만, REST API를 설계할 때 신경써야할 중요한 고려사항이기도 하다. 각 기능이 Idempotent 해야 하는지 아닌지에 따라 API 설계의 방향이 다르게 결정되기 때문이다.

특정 HTTP method가 기본적으로 idempotency 성질을 가지느냐 아니냐가 중요한 것이 아니라, REST API를 설계할 때 각 method가 idempotent 해야하는지 아닌지 스스로 생각해보는 것이 중요하다.