Unknownpgr

멱등성 (idempotent)에 대한 설명

2021-07-19 05:36:15 | Korean

이번에 프로젝트를 진행하며 API관련 공부를 하다 보니 멱등성(idempotent)에 대해 명확하게 정리해둘 필요를 느꼈습니다.

멱등성(idempotnet)란?

간단하게, 컴퓨터과학에서는 어떤 동일한 작업을 두 번 하더라도 똑같은 결과가 나올 경우 이 작업을 Idempotent하다고 말합니다.

on-off

위 이미지는 기차의 목적지 표지판(그게 뭔지는 잘 모르겠습니다만)을 끄거나 켜는 장치입니다. 전원 버튼이 켜는 버튼과 끄는 버튼으로 분리되어있으므로 버튼을 여러 번 누르더라도 한 번 누른 것과 동일하게 동작합니다. 그러므로 이 전원 버튼을 누르는 작업은 idempotent하다고 말할 수 있습니다. 반대로 TV나 컴퓨터 등의 장치를 생각해보면 전원 버튼이 하나밖에 없습니다. 그러므로 처음 전원 버튼을 누를 때에는 전원이 켜지고, 두 번째 누르면 전원이 꺼집니다. 그러므로 이런 전자기기의 전원 버튼을 누르는 작업은 idempotent하지 않습니다.

왜 멱등성을 가지게 설계해야 하는가?

API를 비롯한 여러 연산들을 멱등성을 가지게 설계해야 하는 이유는 무엇일까요? 물론 여러가지 장점이 있겠습니다만, 제가 생각하는 장점 두 가지는 functional programming을 고려한 설계가 저절로 이뤄진다는 것과 양방향 통신을 할 필요가 없어 신뢰도가 높고 실행 속도가 빠르다는 것이라고 봅니다.

먼저 functional programming의 측면에서 멱등성을 살펴보겠습니다. 연산이 멱등하려면 오직 그 결과가 입력에만 의존해야 합니다. 왜냐하면 연산이 입력에만 의존하지 않을 경우, 같은 연산을 두 번 해도 다른 결과를 얻을 수 있으므로 멱등하지 않기 때문입니다. 예를 들어 어떤 변수 xx의 값을 설정하는 연산을 생각해봅시다. 만약 이 값을 예컨대 랜덤한 값이나 현재 시간, 혹은 x+1x+1 등으로 설정한다면 연산을 여러 번 하게 되면 xx값이 매번 달라집니다. 그러므로 어떤 연산을 멱등하게 설계하면 그 연산의 결과는 자연스럽게 입력에만 의존할 수밖에 없고, 따라서 저절로 functional 한 로직을 작성할 수 있습니다.

다음으로 양방향 통신의 측면에서 멱등성을 살펴보겠습니다. 이는 특히 웹이나 DB측면에서 멱등성을 바라본 것입니다. 먼저 신뢰도 측면에셔, 멱등한 연산은 여러 번 적용하더라도 결과가 같기 때문에 실수로, 혹은 오류로 인해 요청이 두 번 들어가는 경우라도 결과가 바뀌는 경우가 없어 신뢰도가 높습니다.

그런 상황이 얼마나 발생하겠느냐고 할 수도 있겠지만, Kafka와 같은 파이프라인 구조를 사용할 경우 경우에 따라 delivery-once가 지켜지지 않을 수도 있습니다. 좀 low-level이기는 하지만, TCP 통신의 경우에도 패킷이 전달되었지만 ACK패킷이 유실되어 같은 패킷이 두 번 전달되는 경우가 발생할 수 있습니다. 물론 커널에서 중복 패킷을 무시하므로 일반적인 케이스에서는 이를 신경쓸 필요가 없습니다.

그리고 속도 측면에서 보통 연산이 멱등하지 않은 경우 DB참조가 필요하기 때문인 경우가 많은데, 클라이언트가 서버에서 값을 받아오는 것이 매우 오래 걸릴 뿐만 아니라 서버 내부에서도 보통 DB가 병목 지점인 경우가 많으므로 DB참조가 많으면 좋지 않습니다. 이때 연산을 멱등하게 설계하면 단방향, 즉 DB를 참조하는 연산 없이 DB에 쓰는 연산만으로 구현할 수 있으므로 실행 속도를 향상시킬 수 있습니다.

멱등성에 대한 실용적인 예시

예를 들어 웹 사이트 게시글에서 '좋아요'를 누르는 연산을 가정해보겠습니다. '좋아요'를 두 번 누르면 취소됩니다. 이 연산을 다음과 같이 구현한다고 가정해봅시다.

  1. 모든 유저의 '좋아요' 상태는 기본적으로 False이다.
  2. 유저가 사이트를 방문하면 기존에 '좋아요'를 눌렀는지 여부를 브라우저로 전송한다.
  3. 브라우저에서 유저가 버튼을 누른다.
  4. 서버로 clickLike 요청이 전송된다.
  5. 서버는 clickLike 요청을 받으면 DB에서 '좋아요' 상태를 반전한다.
  6. 브라우저에서 버튼의 상태를 반전한다.

이 로직은 간단하게 생각하면 별로 문제될 것이 없어보입니다. 그러나 다음과 같은 시나리오를 생각해볼 수 있습니다.

  1. 유저가 데스크톱으로 게시글을 읽고 있습니다.
  2. 그러다가 집 밖에 나갈 필요가 생겨서 스마트폰으로 읽던 글을 이어서 읽습니다. (YouTube, Facebook 등 대부분의 매체가 모바일과 데스크톱을 동시 지원하므로 별로 특별한 케이스가 아닙니다.)
  3. 글을 다 읽은 유저는 글이 마음에 들어서 좋아요를 누릅니다. (서버에서 '좋아요' 상태가 True로 바뀝니다.)
  4. 이후에 다시 데스크톱으로 돌아온 유저는 '좋아요'가 눌려있지 않은 것을 발견합니다. (유저가 새로고침을 아직 하지 않았다고 가정해봅시다.)
  5. 유저는 좋아요를 한 번 더 누릅니다. (서버에서 '좋아요' 상태가 False로 바뀝니다.)
  6. 그런데 브라우저에서 버튼의 상태는 좋아요가 눌린 것으로 표시됩니다.

위 예시에서 든 '좋아요' 기능은 별로 중요하지 않은 기능이지만, 유저의 개인정보나 환경설정과 같은 중요한 기능으로 쉽게 확장해볼 수 있습니다.

이 문제를 기존 로직을 그대로 가져가면서 해결하려면 Websocket등을 사용해서 실시간 동기화를 구현하거나 서버에서 요청을 받은 후 새로운 상태를 브라우저로 반환하도록 구현해야 합니다.

그러나 실시간 동기화를 모든 유저에 대해 구현하면 서버에 엄청난 부담이 될 것이고, 반전한 상태를 브라우저로 반환하면 서버에서 응답이 전송될 때까지 유저가 기다려야 합니다.

이때 API를 멱등하게 설계하여 서버로 clickLike 요청을 전송하는 대신 like상태를 True로 설정하는 like/true 요청과 False로 설정하는 like/false 요청을 전송한다고 가정해봅시다. 그러면 위와 같은 시나리오가 발생했을 경우 유저는 서버에 요청을 보내기만 하고 아무런 응답을 받지 않아도 유저가 의도한 대로 기능이 작동하는 것을 알 수 있습니다.


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -