관리자 도구를 위한 낙관적 잠금: 무음 덮어쓰기 방지
버전 컬럼과 updated_at 검사를 사용해 관리자 도구에 낙관적 잠금을 적용하고, 편집 충돌을 무음 덮어쓰기 없이 처리하는 간단한 UI 패턴을 알아봅니다.

문제: 여러 사용자가 편집할 때 생기는 무음 덮어쓰기
"무음 덮어쓰기"는 두 사람이 같은 레코드를 열어 둘 다 변경을 하고 마지막에 저장한 사람이 승자가 되는 상황입니다. 첫번째 사용자의 수정은 경고 없이 사라지고, 복구하기 어려운 경우가 많습니다.
바쁜 관리자 패널에서는 이런 일이 하루 종일 아무도 모르게 일어납니다. 사람들은 여러 탭을 열어두고 티켓 사이를 오가며 20분 동안 폼을 그대로 두기도 합니다. 그들이 결국 저장할 때는 더 최신 상태의 레코드를 기반으로 하고 있지 않습니다. 덮어써 버리는 거죠.
이 문제는 공용 앱보다 내부 도구에서 더 자주 나타납니다. 작업이 협업적이고 레코드 단위로 이뤄지기 때문입니다. 내부 팀은 같은 고객, 주문, 상품, 요청을 반복해서 편집하는 일이 많습니다. 공용 앱은 보통 "사용자가 자기 것만 편집"하는 경우가 많지만, 관리자 도구는 "여러 사용자가 공유 데이터를 편집"합니다.
즉각적으로 큰 피해가 생기진 않더라도 쌓이면 문제입니다:
- 프로모션 업데이트 직후 가격이 이전 값으로 변경된다.
- 상담사의 내부 메모가 사라져 다음 상담사가 같은 문제를 반복한다.
- 주문 상태가 뒤로 바뀌어 잘못된 후속 조치가 발생한다(예: "Shipped"가 "Packed"로 돌아감).
- 고객의 전화번호나 주소가 오래된 정보로 교체된다.
무음 덮어쓰기는 시스템이 제대로 저장했다고 모두가 생각하기 때문에 더 골칩니다. "문제가 발생했다"는 명확한 순간이 없고, 보고서가 이상하거나 동료가 "이거 누가 바꿨지?"라고 물을 때 혼란이 드러납니다.
이런 충돌은 정상입니다. 도구가 공유되고 유용하다는 신호지, 팀이 잘못하고 있다는 신호가 아닙니다. 목표는 두 사람이 편집하지 못하게 막는 것이 아니라, 누군가 편집하는 동안 레코드가 바뀌었는지 감지하고 그 순간을 안전하게 처리하는 것입니다.
AppMaster 같은 노코드 플랫폼으로 내부 도구를 만든다면 이런 문제를 초기에 대비해 두는 게 좋습니다. 관리자 도구는 빠르게 성장하고 팀이 의존하게 되면 가끔씩 데이터 손실이 반복적인 불신으로 이어집니다.
낙관적 잠금(Optimistic locking)을 쉽게 설명하면
두 사람이 같은 레코드를 열고 둘 다 저장을 누르면 동시성이 발생합니다. 각자는 오래된 스냅샷에서 시작했지만 저장이 일어날 때 "최신"은 하나뿐입니다.
보호 장치가 없으면 마지막 저장이 이깁니다. 그게 무음 덮어쓰기가 발생하는 방식입니다: 두 번째 저장이 조용히 첫 번째 사람의 변경을 대체합니다.
낙관적 잠금은 간단한 규칙입니다: "내가 편집을 시작했을 때 레코드 상태와 같을 때만 변경을 저장하겠다." 만약 그 사이에 레코드가 바뀌었다면 저장을 거부하고 사용자에게 충돌을 보여줍니다.
비관적 잠금은 반대로 "내가 편집하는 동안 누구도 못하게 한다"는 접근입니다. 비관적 잠금은 보통 강제 잠금, 타임아웃, 차단을 의미하며, 돈 이체 같은 드문 경우에는 유용할 수 있지만 많은 소규모 편집이 일어나는 관리자 도구에서는 불편함을 초래합니다.
낙관적 잠금은 일반적으로 기본 선택으로 더 낫습니다. 작업 흐름을 멈추지 않고 여러 사람이 병렬로 편집할 수 있게 하며, 실제 충돌이 발생했을 때만 시스템이 개입합니다.
적합한 경우:
- 충돌이 가능하지만 항상 발생하지는 않을 때
- 편집이 짧고 빠를 때(몇 개 필드, 짧은 폼)
- 다른 사람을 차단하면 팀이 느려질 때
- "누군가 이걸 업데이트했습니다"라는 명확한 메시지를 보여줄 수 있을 때
- API가 모든 업데이트에서 버전(또는 타임스탬프)을 확인할 수 있을 때
낙관적 잠금은 "조용한 덮어쓰기" 문제를 막아줍니다. 데이터가 사라지는 대신 "이 레코드는 당신이 열었을 때와 달라졌습니다"라는 깔끔한 중지 상태를 제공합니다.
하지만 못하는 것도 중요합니다. 같은 오래된 정보를 바탕으로 두 사람이 서로 다른 유효한 결정을 내리는 것을 막지 못하고, 자동으로 변경을 병합해주지도 않습니다. 서버 측에서 검사를 건너뛰면 아무것도 해결되지 않습니다.
일반적인 한계:
- 충돌을 자동으로 해결하지는 않는다(여전히 선택이 필요함).
- 오프라인 상태에서 편집 후 검사 없이 동기화하면 도움이 되지 않는다.
- 권한 설정이 잘못된 경우(권한 없는 사용자가 편집하는 것)는 잡아내지 못한다.
- 클라이언트에서만 검사를 하면 충돌을 잡지 못한다.
실무에서는 낙관적 잠금은 편집과 함께 전달되는 추가 값 하나와 서버 측의 "일치할 때만 업데이트" 규칙일 뿐입니다. AppMaster로 관리자 패널을 만든다면 이 검사는 보통 업데이트가 실행되는 곳의 비즈니스 로직에 위치합니다.
두 가지 일반적인 접근: version 컬럼 vs updated_at
사용자가 편집하는 동안 레코드가 바뀌었는지 감지하려면 보통 두 가지 신호 중 하나를 선택합니다: 버전 번호 또는 updated_at 타임스탬프.
접근법 1: 버전 컬럼(증가하는 정수)
version 필드(보통 정수)를 추가합니다. 편집 폼을 불러올 때 현재 version도 함께 읽습니다. 저장할 때 그 값을 다시 보냅니다.
저장은 저장 시점의 저장된 버전이 사용자가 시작했을 때의 버전과 일치할 때만 성공합니다. 일치하면 레코드를 업데이트하고 version을 1 증가시킵니다. 일치하지 않으면 덮어쓰는 대신 충돌을 반환합니다.
이 방식은 이해하기 쉽습니다: 버전 12는 "이게 12번째 변경"이라는 의미입니다. 시간과 관련된 예외도 피할 수 있습니다.
접근법 2: updated_at(타임스탬프 비교)
대부분의 테이블에는 이미 updated_at 필드가 있습니다. 아이디어는 같습니다: 폼을 열 때 updated_at을 읽고 저장 시 포함합니다. 서버는 updated_at이 변경되지 않았을 때만 업데이트합니다.
잘 작동할 수 있지만 타임스탬프에는 함정이 있습니다. 데이터베이스마다 정밀도가 다르고 초 단위로 반올림되는 경우 빠른 편집을 놓칠 수 있습니다. 여러 시스템이 같은 DB에 쓰면 시계 차이와 시간대 처리로 혼란이 생길 수 있습니다.
간단한 비교 요약:
- 버전 컬럼: 동작이 가장 명확하고 데이터베이스 간 이식성이 좋으며 시계 문제 없음.
updated_at: 이미 존재하는 경우 비용이 적지만 정밀도와 시계 처리 문제에 주의해야 함.
대부분 팀에는 버전 컬럼이 더 좋은 기본 신호입니다. 명시적이고 예측 가능하며 로그나 지원 티켓에서 참조하기 쉽습니다.
AppMaster로 만들 때는 Data Designer에 정수형 version 필드를 추가하고 업데이트 로직이 저장 전에 이를 확인하도록 하세요. updated_at는 감사용으로 남겨두되 편집 안전성 판단은 버전 번호에 맡길 수 있습니다.
각 편집에서 무엇을 저장하고 보내야 하는가
낙관적 잠금은 사용자가 폼을 열었을 때의 "마지막 본" 표시자를 편집과 함께 보내야만 작동합니다. 그 표시자는 version 번호나 updated_at 타임스탬프가 될 수 있습니다. 없으면 서버는 사용자가 편집하는 동안 레코드가 바뀌었는지 알 수 없습니다.
레코드에는 일반 비즈니스 필드와 서버가 제어하는 동시성 필드 하나를 두세요. 최소 구성은 다음과 같습니다:
id(고유 식별자)- 비즈니스 필드(이름, 상태, 가격, 메모 등)
version(성공적인 업데이트마다 증가하는 정수) 또는updated_at(서버가 쓰는 타임스탬프)
편집 화면이 열릴 때 폼은 그 동시성 필드의 마지막 본 값을 저장해야 합니다. 사용자가 편집 중에 이 값을 바꾸면 안 되므로 숨김 필드나 폼 상태에 보관하세요. 예: API가 version: 12를 반환하면 폼은 저장될 때까지 12를 유지합니다.
저장 버튼을 누를 때 클라이언트는 변경사항과 마지막 본 표시자를 함께 보냅니다. 가장 단순한 형태는 id, 변경된 필드, 그리고 expected_version(또는 expected_updated_at)을 업데이트 요청 바디에 포함하는 것입니다. AppMaster에서 UI를 만든다면 이 값을 레코드와 함께 로드해 바인딩하고 업데이트 시 제출하면 됩니다.
서버에서는 업데이트를 조건부로 처리해야 합니다. 절대 "조용히 병합"하지 마세요.
충돌 응답은 명확하고 UI에서 처리하기 쉬워야 합니다. 실용적인 충돌 응답은 다음을 포함합니다:
- HTTP 상태
409 Conflict - "이 레코드는 다른 사람이 업데이트했습니다." 같은 짧은 메시지
- 현재 서버 값(
current_version또는current_updated_at) - 선택적으로 최신 서버 레코드(무엇이 바뀌었는지 UI가 보여줄 수 있게)
예시: Sam이 고객 레코드를 버전 12로 열었습니다. Priya가 변경 사항을 저장하여 버전이 13이 되었습니다. Sam이 expected_version: 12로 저장하면 서버는 409과 함께 버전 13의 현재 레코드를 반환합니다. UI는 Sam에게 Priya의 변경을 덮어쓰기 전에 검토하라고 안내할 수 있습니다.
단계별: 낙관적 잠금 전체 구현 흐름
낙관적 잠금은 본질적으로 한 규칙으로 귀결됩니다: 모든 편집은 레코드의 최신 저장된 버전에 기반했다는 것을 증명해야 합니다.
1) 동시성 필드 추가
모든 쓰기에서 변경되는 하나의 필드를 선택하세요.
전용 정수형 version이 가장 이해하기 쉽습니다. 1로 시작해 업데이트 때마다 1씩 증가시킵니다. 이미 모든 쓰기에서 신뢰할 수 있게 바뀌는 updated_at 타임스탬프가 있다면 그것을 사용할 수 있지만(백그라운드 작업도 포함), 매 쓰기마다 업데이트되는지 확인하세요.
2) 읽기 시 그 값을 클라이언트에 보내기
편집 화면을 열 때 현재 version(또는 updated_at)을 응답에 포함시켜 폼 상태에 보관하세요.
이 값은 "내가 마지막으로 읽은 것"이라는 영수증처럼 생각하면 됩니다.
3) 업데이트 시 그 값을 필수로 요구하기
저장할 때 클라이언트는 편집된 필드와 마지막 본 동시성 값을 함께 보냅니다.
서버에서는 업데이트를 조건부로 만드세요. SQL 관점에서 예시는 다음과 같습니다:
UPDATE tickets
SET status = $1,
version = version + 1
WHERE id = $2
AND version = $3;
업데이트가 1행을 건드렸다면 저장 성공입니다. 0행이면 누군가가 이미 레코드를 변경했습니다.
4) 성공 시 새 값 반환
성공적으로 저장되면 새 version(또는 새 updated_at)이 포함된 업데이트된 레코드를 반환하세요. 클라이언트는 서버가 반환한 값으로 폼 상태를 교체해야 합니다. 그래야 오래된 버전으로 이중 저장하는 일을 막을 수 있습니다.
5) 충돌을 정상적인 결과로 취급하기
조건부 업데이트가 실패하면 명확한 충돌 응답(보통 HTTP 409)을 보내고 다음을 포함하세요:
- 현재 존재하는 최신 서버 레코드
- 클라이언트가 시도한 변경(또는 재구성에 충분한 정보)
- 가능하면 어떤 필드가 다른지 표시
AppMaster에서는 이 흐름이 PostgreSQL 모델 필드, 읽기 엔드포인트에서 버전을 반환하는 단계, 그리고 조건부 업데이트를 수행해 성공 또는 충돌 분기로 나뉘는 Business Process에 깔끔하게 맞습니다.
충돌을 사용자에게 성가시지 않게 처리하는 UI 패턴
낙관적 잠금은 절반의 일일 뿐입니다. 나머지 절반은 사용자가 저장이 거부되었을 때 보게 되는 화면입니다.
좋은 충돌 UI의 목표는 두 가지입니다: 무음 덮어쓰기를 막고 사용자가 작업을 빠르게 마칠 수 있게 돕는 것. 잘하면 방어선 같지 않고 도움이 되는 가드레일처럼 느껴집니다.
패턴 1: 단순 차단 대화상자(가장 빠름)
편집이 작고 사용자가 다시 적용하기 쉬울 때 씁니다.
메시지는 짧고 구체적으로: "편집 중에 이 레코드가 변경되었습니다. 최신 버전을 보려면 다시 불러오세요." 그런 다음 두 가지 명확한 동작을 제공합니다:
- 다시 불러와서 계속하기(주 버튼)
- 내 변경사항 복사(선택적이지만 유용)
"내 변경사항 복사"는 미저장 값을 클립보드에 복사하거나 재로드 후에도 폼에 보관해 재입력 부담을 줄여줍니다.
단일 필드 업데이트, 토글, 상태 변경, 짧은 메모 등에 잘 맞고 구현도 쉽습니다(예: AppMaster 기반 관리자 화면).
패턴 2: "변경사항 검토"(가치가 큰 레코드에 최적)
레코드가 중요하거나 폼이 길 때 사용합니다(가격, 권한, 정산 등). 단순한 에러 화면 대신 충돌 화면으로 보내 사용자에게 비교를 보여줍니다:
- "당신의 편집"(시도한 변경)
- "현재 값"(데이터베이스의 최신 값)
- "열었을 때와 달라진 항목"(충돌한 필드)
핵심만 보여 주세요. 세 필드만 충돌했다면 모든 필드를 보여줄 필요는 없습니다.
충돌한 각 필드에 대해 간단한 선택을 제공하세요:
- 내 것 유지
- 상대의 것 채택
- 병합(태그나 메모처럼 의미가 있을 때만)
병합을 지원하면 리치 텍스트나 긴 메모의 경우 작은 diff 뷰(추가/삭제 표시)를 보여줘 빠르게 판단할 수 있게 합니다. 충돌을 해결한 뒤 최신 버전 값으로 다시 저장하세요.
강제 덮어쓰기를 허용할 때(그리고 누구에게 허용할지)
가끔은 강제 덮어쓰기가 필요하지만 드물고 통제되어야 합니다. 허용한다면 의도적이어야 합니다: 간단한 이유를 요구하고 변경을 기록하며 관리자나 감독자 같은 역할로 제한하세요.
일반 사용자에게는 기본적으로 "변경 검토"를 권합니다. 강제 덮어쓰기는 사용자가 레코드 소유자이거나 위험이 낮거나 시스템이 감독 하에 잘못된 데이터를 고치는 경우에 방어 가능한 선택입니다.
예시 시나리오: 두 동료가 같은 레코드를 편집할 때
두 상담원 Maya와 Jordan이 같은 고객 프로필을 열어 각각 통화 후 상태를 업데이트하고 메모를 남기려 합니다.
타임라인(낙관적 잠금이 version 필드 또는 updated_at 검사로 활성화된 경우):
- 10:02 - Maya가 고객 #4821을 엽니다. 폼에는 Status = "Needs follow-up", Notes = "Called yesterday", Version = 7이 로드됩니다.
- 10:03 - Jordan도 같은 고객을 열어 동일한 값과 Version = 7을 봅니다.
- 10:05 - Maya가 Status를 "Resolved"로 바꾸고 메모를 추가한 뒤 저장합니다.
- 10:05 - 서버는 레코드를 업데이트하고 Version을 8로 증가시키며 감사 로그에 누가 언제 무엇을 바꿨는지 기록합니다.
- 10:09 - Jordan이 다른 메모("Customer asked for a receipt")를 입력하고 저장을 누릅니다.
충돌 검사가 없다면 Jordan의 저장은 Maya의 상태와 메모를 조용히 덮어쓸 수 있습니다. 낙관적 잠금이 있으면 Jordan의 업데이트는 Version = 7을 시도했기 때문에 거부되고 충돌이 발생합니다.
Jordan은 명확한 충돌 메시지를 봅니다. UI는 발생한 일을 보여주고 안전한 다음 단계를 제안합니다:
- 최신 레코드로 다시 불러오기(내 편집 버리기)
- 내 변경을 최신 레코드 위에 적용하기(가능하면 권장)
- 차이 검토("내 것" vs "최신") 후 유지할 것 선택하기
간단한 화면에 다음을 표시할 수 있습니다:
- "이 고객은 10:05에 Maya가 업데이트했습니다"
- 변경된 필드(Status와 Notes)
- Jordan이 입력한 미저장 메모 미리보기
Jordan은 "차이 검토"를 선택해 Maya의 Status = "Resolved"를 유지하고 자신의 메모를 기존 메모에 덧붙입니다. 이번에는 Version = 8로 저장해 성공하고(현재 Version = 9) 업데이트됩니다.
최종 상태: 데이터 손실 없음, 누가 덮어썼는지 추측할 필요 없음, Maya의 상태 변경과 각 메모가 별개의 추적 가능한 편집으로 감사 로그에 남음. AppMaster로 만든 도구라면 이는 업데이트 시점의 한 번의 검사와 간단한 충돌 해결 대화상자로 깔끔하게 구현됩니다.
데이터 손실을 여전히 초래하는 흔한 실수
대부분의 "낙관적 잠금" 버그는 아이디어 자체의 문제가 아닙니다. UI, API, 데이터베이스 사이의 인수인계 과정에서 발생합니다. 어느 한 레이어가 규칙을 잊으면 여전히 무음 덮어쓰기가 발생합니다.
고전적인 실수는 편집 화면을 열 때 버전(또는 타임스탬프)을 수집하지만 저장 시 그것을 보내지 않는 경우입니다. 폼을 재사용하거나 숨김 필드가 빠지거나 API 클라이언트가 변경된 필드만 보내도록 설계된 경우 자주 발생합니다.
또 다른 함정은 브라우저에서만 충돌 검사를 하는 것입니다. 사용자는 경고를 보지만 서버가 업데이트를 받아들이면 다른 클라이언트(또는 재시도)가 데이터를 덮어쓸 수 있습니다. 서버가 최종 관문이어야 합니다.
가장 많은 데이터 손실을 일으키는 패턴:
- 저장 요청에 동시성 토큰(
version,updated_at또는 ETag)이 빠짐 id만으로 업데이트하고id + version같은 원자적 조건을 사용하지 않음- 정밀도가 낮은
updated_at사용(예: 초 단위). 같은 초에 두 번의 편집이 발생하면 동일하게 보일 수 있음 - 큰 필드(노트, 설명)나 배열(태그, 라인 아이템)을 통째로 교체해 무엇이 바뀌었는지 보여주지 않음
- 모든 충돌을 "재시도"로 처리해 오래된 값을 최신 값 위에 다시 적용하는 실수
구체적 예: 두 상담 팀장이 같은 고객 레코드를 열어 한 명은 긴 내부 메모를 추가하고 다른 한 명은 상태를 변경해 저장합니다. 저장 로직이 전체 페이로드를 덮어쓰면 상태 변경이 메모를 지울 수 있습니다.
충돌이 발생했을 때 API 응답이 너무 빈약하면 팀은 여전히 데이터를 잃습니다. 단순히 "409 Conflict"만 반환하지 마세요. 사람이 복구할 수 있게 충분한 정보를 주세요:
- 현재 서버 버전(또는
updated_at) - 관련 필드의 최신 서버 값
- 차이 나는 필드 목록(간단한 필드명이라도)
- 누가 언제 변경했는지(추적하고 있다면)
AppMaster에서 구현한다면 같은 규율을 적용하세요: UI 상태에 버전을 보관하고, 업데이트 시 그것을 전송하며, PostgreSQL에 쓰기 전에 백엔드 로직 안에서 체크를 강제하세요.
배포 전 빠른 점검 목록
출시 전에 "저장은 정상이라 나오는데 누군가의 작업을 조용히 덮어쓰는" 실패 모드를 점검하세요.
데이터 및 API 점검
레코드가 시작부터 끝까지 동시성 토큰을 지니는지 확인하세요. 토큰은 version 정수나 updated_at 타임스탬프일 수 있지만 레코드의 일부로 취급되어야 합니다.
- 읽기 응답에 토큰이 포함되어 있고 UI는 폼 상태에 저장한다.
- 모든 업데이트는 마지막 본 토큰을 다시 보내고 서버는 쓰기 전에 이를 검증한다.
- 성공 시 서버는 새 토큰을 반환해 UI가 동기화된 상태를 유지한다.
- 벌크 편집이나 인라인 편집도 같은 규칙을 따르고 특단의 예외를 두지 않는다.
- 같은 행을 편집하는 백그라운드 작업도 토큰을 확인한다(확인하지 않으면 무작위처럼 보이는 충돌을 만든다).
AppMaster로 만든다면 Data Designer에 필드(version 또는 updated_at)가 있는지, Business Process의 업데이트 흐름이 저장 전에 비교하는지 다시 확인하세요.
UI 점검
서버가 업데이트를 거부했을 때 다음 단계가 명확해야 충돌이 "안전"합니다.
서버가 업데이트를 거부하면 "이 레코드는 당신이 열었을 때와 달라졌습니다" 같은 명확한 메시지를 보여주고 안전한 기본 동작(최신 데이터 재불러오기)을 제시하세요. 가능하면 사용자의 미저장 입력을 보존해 새로고침 후 재적용할 수 있게 하여 작은 수정이 다시 타이핑해야 하는 일이 되지 않게 하세요.
필요하다면 통제된 "강제 저장" 옵션을 추가하세요. 역할로 게이트하고 확인을 요구하며 누가 강제 저장했는지 기록하면 비상 상황은 가능하게 하면서 데이터 손실을 기본값으로 만들지 않을 수 있습니다.
다음 단계: 하나의 워크플로부터 잠금 기능을 추가하고 확장하세요
작게 시작하세요. 사람들이 자주 충돌하는 관리자 화면 하나를 골라 우선 낙관적 잠금을 적용하세요. 충돌이 잦은 영역은 보통 티켓, 주문, 가격, 재고입니다. 한 번 바쁜 화면에서 충돌을 안전하게 처리하면 다른 곳에도 같은 패턴을 반복하기 쉽습니다.
기본 충돌 동작을 미리 결정하세요. 백엔드 로직과 UI를 모두 규정하므로 중요합니다:
- 차단 후 재로드(Block-and-reload): 저장을 중단하고 최신 레코드를 불러와 사용자가 변경을 다시 적용하게 함.
- 검토 및 병합(Review-and-merge): "내 변경"과 "최신"을 보여주고 사용자가 무엇을 유지할지 결정하게 함.
차단 후 재로드는 빌드가 빠르고 상태 변경이나 작은 노트 같은 짧은 편집에 잘 맞습니다. 검토 및 병합은 레코드가 길거나 중요할 때(가격표, 다중 필드 주문 편집) 더 가치가 있습니다.
그다음 한 흐름 전체를 구현하고 테스트한 뒤 확장하세요:
- 하나의 화면을 골라 사용자가 가장 자주 편집하는 필드를 나열합니다.
- 폼 페이로드에 버전(또는
updated_at) 값을 추가하고 저장 시 필수로 하세요. - 데이터베이스 쓰기를 조건부로 만드세요(버전이 일치할 때만 업데이트).
- 충돌 메시지와 다음 행동(재로드, 텍스트 복사, 비교 뷰 열기)을 설계하세요.
- 두 개의 브라우저로 테스트: 탭 A에서 저장한 뒤 탭 B에서 오래된 데이터를 저장해 보세요.
충돌 로그를 가볍게 남기세요. 간단한 "충돌 발생" 이벤트(레코드 유형, 화면 이름, 사용자 역할 포함)만 있어도 핫스팟을 파악하는 데 도움이 됩니다.
AppMaster로 관리자 도구를 만든다면 주요 요소들이 잘 맞습니다: Data Designer에 version 필드를 모델링하고, Business Processes에서 조건부 업데이트를 적용하며, UI 빌더에 작은 충돌 대화상자를 추가하세요. 첫 번째 워크플로가 안정화되면 같은 패턴을 화면마다 반복하고 충돌 UI를 일관되게 유지해 사용자가 한 번 배우면 어디서나 신뢰하게 만드세요.
자주 묻는 질문
두 사람이 서로 다른 탭이나 세션에서 같은 레코드를 편집하고, 마지막 저장이 앞선 변경을 아무 경고 없이 덮어써서 앞선 편집이 사라지는 상황을 말합니다. 문제는 두 사용자 모두 "저장 성공"을 보게 되어 누락된 변경이 나중에야 발견된다는 점입니다.
낙관적 잠금은 사용자가 열었을 때의 상태와 레코드가 동일할 때만 변경을 저장한다는 뜻입니다. 누군가 먼저 저장했다면 저장이 충돌로 거부되고 사용자는 덮어쓰기 대신 최신 데이터를 검토할 수 있습니다.
비관적 잠금은 다른 사람이 편집하지 못하게 차단하는 방식이어서 대기, 타임아웃, "누가 잠궜지?" 같은 문제가 생깁니다. 낙관적 잠금은 여러 사람이 병렬로 작업하게 하되 충돌이 실제로 발생했을 때만 개입하므로 관리자 패널에 더 적합한 경우가 많습니다.
일반적으로는 version 정수 컬럼이 가장 단순하고 예측 가능합니다. 타임스탬프인 updated_at도 쓸 수는 있지만, 정밀도 문제가 있거나 시스템 간 시계 차이로 빠른 편집을 놓칠 수 있습니다.
서버가 제어하는 동시성 토큰이 필요합니다. 보통 version(정수)이나 updated_at(타임스탬프)입니다. 클라이언트는 폼을 열 때 이 값을 읽어 편집 동안 그대로 보관하고, 저장 시 "기대값"으로 함께 보내야 합니다.
클라이언트만으로는 공유 데이터를 보호할 수 없습니다. 서버는 id와 version을 함께 검사하는 조건부 업데이트(예: "id + version"으로 WHERE)를 강제해야 합니다. 그렇지 않으면 다른 클라이언트나 백그라운드 작업이 여전히 덮어쓸 수 있습니다.
기본 동작은 "이 레코드는 열었을 때와 달라졌습니다"라는 차단 메시지를 보여주고 우선 안전한 한 가지 동작(예: 최신 데이터로 다시 불러오기)을 제안하는 것입니다. 가능한 경우 사용자가 입력한 내용을 보존해서 다시 적용할 수 있게 하면 재타이핑을 줄여줍니다.
충돌 응답으로는 보통 409 상태와 함께 현재 서버의 버전 및 최신 값들을 제공하세요. 누가 언제 변경했는지 정보를 포함하면 왜 거부되었는지 이해하는 데 도움이 됩니다.
저장 시 토큰이 빠졌거나 id만으로 업데이트하는 경우, 혹은 초 단위처럼 정밀도가 낮은 타임스탬프를 쓰는 경우에 문제가 자주 발생합니다. 또한 전체 페이로드(긴 노트나 배열)를 통째로 교체하면 다른 사람의 수정이 지워질 가능성이 커집니다.
AppMaster에서는 Data Designer에 version 필드를 추가해 폼 상태로 읽어 들이고, Business Process에서 조건부 업데이트를 강제한 뒤 UI에서 충돌 분기를 처리하면 됩니다. 코드 없이도 같은 패턴을 적용할 수 있습니다.


