공유 지출 수정이 분할을 다시 명시해야 하는 이유
수정의 기본값은 돈에 관한 버그이므로, Dimesum은 모든 지출 PATCH에서 participants와 split_type을 필수로 요구하고 오래된 수정을 거부하는 base_version을 함께 둡니다.
오타를 고친다고 해서 누가 얼마를 갚아야 하는지가 바뀌어서는 안 됩니다. Dimesum에서는 공유 지출을 수정하면 분할 전체를 다시 명시합니다. 모든 PATCH에는 participants와 split_type이 필요하며, 둘 중 하나라도 빠진 수정은 기본값을 채우는 대신 400으로 돌아옵니다. 생성 시 기본값은 현재 구성원 전체와 균등 분할인데, 수정에서는 이 기본값이 돈에 관한 버그가 됩니다.
두 가지 실패가 이 점을 분명하게 보여 줍니다. 8월에 합류한 룸메이트가 7월 저녁 식사에 끌려 들어옵니다. "현재 구성원 전체"가 지출이 기록된 시점이 아니라 수정이 도착한 시점에 평가되기 때문입니다. 의도적으로 정한 70/30 임대료 분할이 50/50으로 뭉개집니다. split_type이 없으면 EQUAL을 뜻하기 때문입니다. 두 실패 모두 오류를 일으키지 않으며, 둘 다 실제 돈을 움직입니다.
수정의 기본값은 돈에 관한 버그입니다. 생성 시점의 기본값은 작성자가 지금 보고 있는 그룹을 추측합니다. 수정은 나중에, 이미 달라진 그룹을 상대로 도착하며, 같은 추측이 사람들이 갚아야 할 금액을 조용히 다시 씁니다.
생성 기본값은 새 지출을 설명하지 오래된 지출을 설명하지 않습니다
생성 시점에는 기본값이 정직합니다. 작성자는 현재 상태의 그룹을 보고 있고, 모두에게 균등하게 나누는 것이 흔한 경우이므로 Dimesum이 둘 다 채웁니다. 지출은 결과만이 아니라 입력값을 저장합니다. splits.percent_bp, splits.weight, splits.exact_minor가 사용자가 입력한 내용을 보관하므로 나중에 수정할 때 다시 열 수 있습니다.
수정은 다른 행위입니다. 같은 지출은 몇 주 뒤에, 새 룸메이트가 합류했거나 유령 구성원이 실제 사람으로 확인된 뒤에 바로잡힐 수 있습니다. 구성원 명단은 계속 바뀌지만 분할은 그렇지 않습니다. 생성 기본값을 다시 쓰는 것은 오래된 지출이 이미 답한 질문을 오늘의 그룹에게 다시 묻는 셈입니다.
추측을 거부하는 것은 여기서 새로운 일이 아닙니다. 결제자가 여러 명인 지출은 결제자도 다시 명시해야 합니다. soleStoredPayer는 지출에 결제자가 정확히 한 명일 때만 저장된 결제자를 다시 사용하므로, 결제자가 두 명인데 이에 대해 아무 말도 하지 않는 수정은 재지정되지 않고 거부됩니다. 결제자에 대해 아무 말도 하지 않는 수정은 지출 자체의 결제자를 유지하며, 수정하는 사람을 결제자로 삼지 않습니다.
API는 분할을 다시 명시하지 않는 수정을 거부합니다
게이트웨이 검사는 어떤 금액이 계산되기 전에 실행됩니다. participants가 비어 있거나 split_type이 공란이면, 요청은 코드 invalid_expense와 메시지 "an edit must restate the split: participants and split_type are required"와 함께 400으로 돌아옵니다. 지출 서비스는 validateAmend에서 이 규칙을 다시 적용하므로, 다른 경로로 서비스에 도달한 호출자도 같은 거부를 만납니다.
| 필드 | 생성 시 | 수정 시 | 기본값이 치를 대가 |
|---|---|---|---|
participants | 선택. 기본값은 현재 구성원 전체 | 필수 | 이후에 합류한 구성원이 오래된 지출에 들어옵니다 |
split_type | 선택. 기본값은 EQUAL | 필수 | 70/30 분할이 50/50으로 뭉개집니다 |
payers | 선택. 기본값은 작성자 | 생략하면 지출의 단독 결제자가 유지되고, 결제자가 두 명이면 다시 명시해야 합니다 | 수정자가 결제자가 되어 누가 누구에게 갚는지가 뒤집힙니다 |
base_version | 보내지 않음 | 필수이며, 현재 버전과 같아야 합니다 | 오래된 수정이 작성자가 읽은 적 없는 변경을 덮어씁니다 |
revision_id | 클라이언트 UUIDv7, 멱등성 키 | 동일, 버전당 하나 | 재시도된 수정이 그룹에 두 번 청구합니다 |
currency | 지출별로 명시 | 일치해야 하며, 변경은 거부됩니다 | 잔액 행은 구성원당 하나의 통화를 담습니다 |
통화도 같은 종류의 거부에 속합니다. 잔액 행은 구성원당 하나의 통화를 담기 때문에, 수정은 지출의 통화를 다시 매길 수 없습니다. 쓰기 측이 변경을 거부하며, 추가 전용 원장은 그런 수정이 혹시 도달하더라도 한쪽에 세워 둡니다. 입구에서 거부하면 두 절반이 서로 어긋나지 않습니다.
오래된 수정은 작성자에게 되돌려 거부될 뿐, 병합되지 않습니다
base_version은 계약의 나머지 절반입니다. 모든 PATCH는 작성자가 읽은 버전을 담고 있으며, checkTransition이 이를 트랜잭션이 방금 잠근 행과 비교합니다. 같으면 수정이 버전 더하기 1로 적용됩니다. 다르면 호출자는 코드 stale_version과 함께 HTTP 409를 받습니다.
병합은 솔깃한 대안이지만 틀렸습니다. 하나의 지출에 대한 두 번의 수정은 청구서의 의미에 대한 두 개의 완전한 진술입니다. 이를 병합하면 아무도 작성하지 않은 세 번째 진술이 만들어지며, 어느 작성자도 알아보지 못할 몫이 나옵니다. 거부는 갈등을 해결할 수 있는 단 한 사람에게 되돌려 줍니다.
멱등성과 동시성은 의도적으로 분리되어 있습니다. 수정본 삽입은 버전 검사보다 먼저 실행됩니다. 재시도된 수정은 원래 읽었던, 이제는 오래된 기준 버전을 담고 있기 때문입니다. 재생은 갈등이 아니라 재생으로 읽혀야 하므로, revision_id가 먼저 응답하고 저장된 결과를 반환합니다.
응답은 다음 수정에 필요한 것을 이미 담고 있습니다
PATCH에 더 많은 필드를 요구하는 것은 클라이언트가 그것을 저렴하게 얻을 수 있을 때만 공정합니다. 모든 지출 응답은 version을 담고 있으므로, 방금 지출을 기록한 클라이언트는 두 번째 읽기 없이 수정할 수 있습니다. 생성 응답, 수정 응답, 그리고 모든 목록 행이 같은 필드를 담고 있습니다.
방금 지출을 기록하지 않은 클라이언트의 경우, GET /v1/groups/{id}/expenses/{id}가 버전, 계산된 몫, 그리고 그 뒤의 입력값을 반환합니다. 입력값은 PATCH가 받아들이는 것과 같은 이름으로 돌아옵니다. participants, split_type, percents, weights, shares, items, pools입니다. 클라이언트는 하나의 형태를 읽고 수정과 함께 되돌려 보내며, 하나의 청구서를 두 어휘 사이에서 옮기지 않습니다.
이 대칭은 재구성할 수 없는 분할에서 가장 중요합니다. PERCENT 분할이나 항목별 청구서는 이미 반올림이 적용되었고 베이시스 포인트와 항목이 사라졌기 때문에, 확정된 몫만으로는 다시 명시할 수 없습니다. 수정본 스냅숏은 입력값도 담고 있으므로, 이력 시트는 과거 어느 버전에서 무엇이 항목화되었는지 보여 줄 수 있습니다.
지금 필수로 두는 이유, 요구 사항은 나중에 추가할 수 없기 때문입니다
첫날에 필드를 요구하는 것은 오늘이 아니라 미래에 관한 결정입니다. 나중에 필수 필드를 완화하는 것은 하위 호환됩니다. 클라이언트는 이미 그것을 보내고 있고, 서버는 그것 없이도 요청을 받아들이기 시작합니다. 나중에 요구 사항을 추가하면 예전 기본값에 의존하던 모든 클라이언트가 망가집니다.
그래서 방향은 이르게 한 번 정해집니다. Dimesum은 클라이언트 수가 아직 바꿀 수 있을 만큼 적을 때 수정에서 participants, split_type, base_version을 요구합니다. 수정에 안전한 기본값이 언젠가 발견되면, 그 필드들은 선택이 되고 이미 배포된 것은 어느 것도 멈추지 않습니다.
수정이 그 의미를 다시 명시하게 하십시오
기본값은 작성자가 동의하는 그룹을 볼 수 있는 생성에 속합니다. 수정에서는 같은 기본값이 그 사이 달라진 그룹에 대한 추측입니다. 다음 단계는 이렇습니다. 자신의 읽기 엔드포인트를 열어, 쓰기 엔드포인트가 받아들이는 것과 정확히 같은 필드 이름으로 분할 입력값을 돌려주는지 확인하십시오. 둘 사이를 변환해야 하는 클라이언트는 결국 그중 하나를 잘못 변환하게 됩니다.
자주 묻는 질문
공유 지출 수정할 때 participants와 split_type이 왜 필수인가요?
Dimesum은 생성 기본값이 수정에는 맞지 않기 때문에 두 필드를 모두 요구합니다. 생성 시 Dimesum은 현재 구성원 전체에 균등 분할을 기본값으로 두며, 이는 작성자가 보고 있는 그룹과 일치합니다. 수정은 누군가 합류한 뒤 몇 주가 지나 도착할 수 있습니다. 그 기본값을 다시 쓰면 새 룸메이트를 오래된 저녁 식사에 끌어들이고, 의도적으로 정한 70/30 임대료 분할을 아무 오류 표시 없이 50/50으로 되돌려 뭉갭니다.
두 사람이 같은 지출을 동시에 수정하면 어떻게 되나요?
두 번째 수정은 HTTP 409와 오류 코드 stale_version으로 거부됩니다. 모든 PATCH는 작성자가 읽은 버전인 base_version을 담고 있으며, 수정 경로가 이를 잠긴 행과 비교합니다. 불일치는 지출이 바뀌었다는 뜻이므로, 수정은 작성자가 이제 볼 수 있는 버전에 맞춰 다시 적용하도록 되돌아옵니다. 병합되는 것은 없습니다.
지출을 수정하면 원장 행이 그 자리에서 갱신되나요?
아니요, Dimesum에서 지출 수정은 원장 행을 갱신하지 않습니다. 수정은 하나의 트랜잭션에 두 개의 분개를 기록합니다. 예전 버전을 한 항목씩 상쇄하는 EXPENSE_REVERSAL, 그리고 새 버전을 위한 EXPENSE 분개입니다. UPDATE와 DELETE는 원장 자체의 데이터베이스 역할에서 회수되어 있어, 버그가 있는 코드조차 변경이 불가능합니다. 사용자에게는 수정됨 배지와 이력 시트가 보입니다.
공유 지출 수정 전에 API를 한 번 더 호출해야 하나요?
아니요, 방금 지출을 기록한 클라이언트는 수정에 필요한 버전을 이미 가지고 있습니다. 모든 지출 응답은 version을 담고 있으며, 이것이 다음 PATCH가 base_version으로 보내는 값입니다. 지출을 기록하지 않은 클라이언트는 GET /v1/groups/{id}/expenses/{id}를 호출하며, 이는 PATCH가 받아들이는 것과 같은 필드 이름으로 버전과 분할 입력값을 반환합니다.
participants와 split_type을 나중에 추가하지 않고 지금 요구하는 이유는 무엇인가요?
첫날에 필드를 요구하는 것은 되돌릴 수 있지만, 나중에 추가하는 것은 그렇지 않습니다. 나중에 필수 필드를 완화하는 것은 하위 호환됩니다. 클라이언트는 이미 그것을 보내고 있고, 서버는 그것 없이도 요청을 받아들이기 시작합니다. 나중에 요구 사항을 추가하면 예전 기본값에 의존하던 모든 클라이언트가 망가지며, 돈을 다루는 API에서는 누군가의 잔액이 틀리기 전까지 그 고장이 조용합니다.