Idempotent와 Pure Function은 어떻게 다를까?

개발을 하다 보면 idempotent라는 단어를 자주 만나게 된다. 한국어로는 보통 멱등성이라고 번역한다.

처음 들으면 수학 용어처럼 느껴지지만, 실제 개발에서는 API 설계, 초기화 스크립트, 배포 자동화, 재시도 로직, 함수형 프로그래밍 등 다양한 곳에서 매우 실용적으로 쓰이는 개념이다.

그런데 idempotent를 설명하다 보면 자주 헷갈리는 개념이 있다. 바로 pure function, 즉 순수 함수다.

둘 다 “여러 번 호출해도 결과가 같다”는 느낌이 있어서 비슷해 보이지만, 사실 관심사가 다르다.

간단히 말하면:

pure function은 함수 자체의 입력과 출력, 그리고 side effect 여부에 대한 성질이고,
idempotent는 같은 연산을 반복했을 때 최종 결과가 같은지에 대한 성질이다.


1. Pure Function이란?

먼저 pure function부터 보자.

순수 함수는 보통 다음 두 조건을 만족하는 함수다.

  1. 같은 입력이면 항상 같은 출력을 반환한다.

  2. 함수 밖의 상태를 변경하지 않는다. 즉, side effect가 없다.

예를 들어 아래 함수는 순수 함수다.

function addOne(x) {
  return x + 1;
}

addOne(3)은 언제 호출해도 항상 4를 반환한다.

addOne(3); // 4
addOne(3); // 4
addOne(3); // 4

이 함수는 외부 상태를 읽거나 변경하지 않는다. DB를 수정하지도 않고, 파일을 쓰지도 않고, 전역 변수를 바꾸지도 않는다.

반대로 아래 함수는 순수 함수가 아니다.

let count = 0;
 
function increment() {
  count += 1;
  return count;
}

호출할 때마다 결과가 달라진다.

increment(); // 1
increment(); // 2
increment(); // 3

이 함수는 외부 변수 count를 변경한다. 따라서 side effect가 있고, pure function이 아니다.


2. Idempotent란?

idempotent는 “같은 작업을 여러 번 실행해도 최종 결과가 한 번 실행한 것과 같은 성질”을 말한다.

예를 들어 어떤 사용자의 이름을 "Kevin"으로 설정하는 작업을 생각해보자.

function setName(user, name) {
  user.name = name;
}

아래처럼 같은 작업을 여러 번 실행해도:

setName(user, "Kevin");
setName(user, "Kevin");
setName(user, "Kevin");

최종 상태는 계속 같다.

user.name === "Kevin";

이 작업은 user.name이라는 외부 상태를 변경하므로 pure function은 아니다. 하지만 여러 번 실행해도 최종 이름은 계속 "Kevin"이므로 idempotent하다고 볼 수 있다.

즉, idempotent는 side effect가 있어도 될 수 있다.

이 점이 pure function과의 가장 큰 차이다.


3. Pure Function과 Idempotent의 핵심 차이

두 개념을 비교하면 다음과 같다.

구분Pure FunctionIdempotent
관심 대상함수의 입력/출력과 side effect반복 실행 후 최종 상태
같은 입력이면 같은 출력?반드시 그래야 함꼭 핵심 조건은 아님
side effect 허용?허용하지 않음허용 가능
외부 상태 변경?하면 안 됨해도 됨
대표 예x => x + 1setName("Kevin"), deleteUser(id)

정리하면:

pure function = 같은 입력 → 같은 출력 + side effect 없음
idempotent = 여러 번 실행해도 최종 결과가 같음

4. Pure Function이지만 Idempotent하지 않은 경우

헷갈리기 쉬운 예가 있다.

function double(x) {
  return x * 2;
}

이 함수는 pure function이다.

같은 입력이면 항상 같은 출력을 반환하고, 외부 상태도 바꾸지 않는다.

double(2); // 4
double(2); // 4

하지만 수학적으로 함수가 idempotent하다고 말할 때는 보통 다음 조건을 본다.

f(f(x)) = f(x)

double에 적용해보면:

double(2);          // 4
double(double(2));  // double(4) = 8

double(double(2))는 8이고, double(2)는 4다. 서로 다르다.

따라서 double은 pure function이지만, 수학적 의미에서는 idempotent한 함수가 아니다.

반대로 아래 함수는 pure function이면서 idempotent하다.

function abs(x) {
  return Math.abs(x);
}
abs(-3);      // 3
abs(abs(-3)); // abs(3) = 3

abs(abs(x)) = abs(x)가 성립하므로 abs는 idempotent한 함수라고 볼 수 있다.


5. Idempotent하지만 Pure Function은 아닌 경우

이번에는 반대 예를 보자.

function deleteUser(id) {
  db.users.delete(id);
}

이 함수는 DB 상태를 변경한다. 따라서 pure function이 아니다.

하지만 API나 시스템 관점에서는 이 작업을 idempotent하게 설계할 수 있다.

예를 들어 id = 10인 사용자를 삭제한다고 하자.

1번째 호출: 사용자가 삭제됨
2번째 호출: 이미 삭제되어 있음
3번째 호출: 여전히 삭제되어 있음

최종 상태는 계속 같다.

user 10 does not exist

따라서 DELETE /users/10 같은 API는 보통 idempotent하게 설계된다.

물론 구현에 따라 다를 수 있다. 삭제 요청을 받을 때마다 delete_count를 증가시키거나, 매번 알림을 보내거나, 매번 audit log를 추가한다면 전체 시스템 상태는 계속 달라진다.

이 경우 strict하게 보면 idempotent하지 않다고 볼 수 있다.


6. 이름 변경 API는 Idempotent할까?

예를 들어 아래 API가 있다고 하자.

PUT /users/me/name
{
  "name": "Kevin"
}

이 요청을 여러 번 보내면 최종 이름은 계속 "Kevin"이다.

1번째 요청 후: name = Kevin
2번째 요청 후: name = Kevin
3번째 요청 후: name = Kevin

사용자의 이름이라는 핵심 비즈니스 상태만 보면 이 API는 idempotent하다.

하지만 실제 시스템에서는 이름 변경과 함께 여러 부수 효과가 발생할 수 있다.

updated_at 갱신
audit log 추가
notification 발송
name_change_count 증가

이런 side effect까지 전체 결과에 포함하면 같은 요청을 여러 번 보낼 때마다 시스템 상태가 달라질 수 있다.

1번째 요청 후: audit log 1개
2번째 요청 후: audit log 2개
3번째 요청 후: audit log 3개

그렇다면 strict하게는 idempotent하지 않다.

그래서 실무에서는 “idempotent하다”는 말을 사용할 때 어떤 상태를 기준으로 멱등성을 판단하는지를 명확히 해야 한다.

보통 API 설계에서 idempotent라고 할 때는 “같은 요청이 재시도되어도 핵심 비즈니스 상태가 중복으로 변경되지 않는다”는 의미로 쓰는 경우가 많다.


7. 같은 인자로 여러 번 호출하는 것과 f(f(x))는 다르다

여기서 중요한 구분이 하나 있다.

API나 시스템 작업에서 idempotent라고 할 때는 보통 “같은 요청을 여러 번 반복한다”는 의미다.

same request repeated → same final state

예를 들어:

PUT /users/me/name
{
  "name": "Kevin"
}

같은 요청을 여러 번 보내도 최종 이름은 같다.

하지만 수학이나 함수 자체의 성질로 idempotent를 말할 때는 보통 다음을 본다.

f(f(x)) = f(x)

이때 두 번째 호출의 인자는 원래의 x가 아니라 f(x)다.

예를 들어:

double(2);          // 4
double(double(2));  // 8

두 번째 호출의 인자는 2가 아니라 4다.

따라서 double은 같은 인자에 대해 항상 같은 결과를 반환하는 pure function이지만, f(f(x)) = f(x)를 만족하지 않으므로 idempotent한 함수는 아니다.

이 차이를 이해하면 pure function과 idempotent의 혼동이 많이 줄어든다.


8. 실무에서 왜 Idempotent가 중요할까?

실무에서 idempotent가 중요한 이유는 대부분 재시도 retry 때문이다.

네트워크 요청은 실패할 수 있다. 서버가 요청을 처리했지만 클라이언트가 응답을 받지 못했을 수도 있다. 이때 클라이언트가 같은 요청을 다시 보내면 어떻게 될까?

만약 요청이 idempotent하지 않다면 문제가 생길 수 있다.

예를 들어 주문 생성 API가 있다고 하자.

POST /orders
{
  "item": "book"
}

같은 요청을 두 번 보내면 주문이 두 개 생길 수 있다.

1번째 요청: order #100 생성
2번째 요청: order #101 생성

이 요청은 기본적으로 idempotent하지 않다.

반대로 특정 주문 ID를 기준으로 상태를 설정하는 API는 idempotent하게 만들 수 있다.

PUT /orders/100/status
{
  "status": "PAID"
}

같은 요청을 여러 번 보내도 최종 상태는 계속 같다.

order #100 status = PAID

그래서 결제, 주문, 배포, 파일 처리처럼 재시도가 필요한 시스템에서는 idempotency를 매우 신경 써야 한다.


9. 정리

pure function과 idempotent는 비슷해 보이지만 서로 다른 개념이다.

Pure function은 함수형 프로그래밍에서 중요한 개념이다.

같은 입력이면 항상 같은 출력
side effect 없음

반면 idempotent는 반복 실행 안정성에 대한 개념이다.

같은 작업을 여러 번 실행해도 최종 결과가 같음

따라서 다음과 같은 조합이 모두 가능하다.

경우예시
pure function이지만 idempotent하지 않음double(x)
pure function이면서 idempotent함abs(x)
idempotent하지만 pure function은 아님setName("Kevin"), deleteUser(id)
둘 다 아님increment(), createOrder()

가장 중요한 차이는 이것이다.

pure function은 side effect가 없어야 하지만, idempotent operation은 side effect가 있어도 된다.
단, 같은 작업을 반복했을 때 관찰하고자 하는 최종 상태가 같아야 한다.

개발에서 idempotent라는 말을 들으면 단순히 “여러 번 호출해도 같은 값이 나오나?”가 아니라, 다음 질문을 같이 해야 한다.

무엇을 결과로 볼 것인가?
핵심 비즈니스 상태만 볼 것인가?
로그, 카운터, 알림 같은 side effect까지 포함할 것인가?
같은 요청을 retry해도 안전한가?

이 질문에 답할 수 있어야 진짜 의미에서 idempotent한 시스템을 설계할 수 있다.