사용자 편집의 반영과 충돌 해결
Ontology Manager에서 객체 유형의 Datasources 탭에 있는 Edits를 켜거나 꺼 사용자 편집을 제어합니다. 속성을 직접 고치는 값과 기반 데이터소스에서 들어오는 갱신은 별도로 저장하고, 구성한 전략으로 최종 객체 상태를 결정합니다.
액션에서 저장소까지
액션이 수정 명령을 보내면 Funnel이 오프셋을 추적하는 관리 큐에 저장합니다. OSv2는 객체 유형과 조인 테이블 기반 다대다 링크 유형의 오프셋을 추적하여 실시간 인덱스에 편집을 적용합니다. 성공한 액션 이후의 새 객체 조회에는 해당 편집이 포함됩니다.
인덱스만으로는 편집을 영구 보존하지 않습니다. Funnel이 관리하는 데이터셋에 편집을 주기적으로 반영하고, 입력 데이터와 사용자 편집을 합친 병합 데이터셋(Merged dataset)을 자동 빌드합니다. 이 과정은 편집 큐가 계속 커지는 것도 막습니다. 기본 빌드 조건은 다음과 같습니다.
- 객체 유형의 데이터소스에 새 데이터 트랜잭션이 생겼을 때
- 새 원천 데이터가 없더라도 객체 편집이 감지되었다면 6시간마다
앱이 보는 편집의 반영 시점과 구체화 데이터셋의 갱신 주기는 별도로 구분해야 합니다.
브라우저의 값과 서버의 객체 버전
액션 폼을 열어 값을 본 뒤 제출하기까지 객체가 바뀔 수 있습니다. 프런트엔드는 입력값을 Actions 서버의 /apply에 전달하지만, 화면에서 읽은 객체 버전을 함께 전달하지 않습니다. 서버가 /apply 안에서 다시 읽은 버전은 화면에서 보았던 버전과 다를 수 있습니다.
구형 OSv1에서는 액션 서버가 처음 로드한 객체 버전을 캐시하고, 편집 요청에 포함한 버전을 저장소가 검사했습니다. 중간에 객체가 바뀌면 StaleObject 오류가 발생했습니다. 동기 웹훅, 검증과 편집에 같은 버전을 사용하게 하지만, 관련 없는 속성의 변경도 객체 단위 충돌을 일으킬 수 있었습니다.
OSv2의 액션 서버는 Funnel에 편집을 보내기 전에 편집 생성에 사용한 버전 중 실제 편집 대상의 버전을 검사합니다. 읽기만 한 다른 객체가 실행 도중 바뀌지 않았다는 보장은 없습니다. 객체 A의 값을 B에 복사하는 경우에도 읽은 모든 객체를 잠가 두는 것으로 해석하면 안 됩니다.
동시에 OSv1과 OSv2를 편집하는 구형 교차 백엔드 액션은 OSv1의 모든 읽기 또는 편집 대상과 OSv2의 편집 대상을 검사합니다. OSv1의 지정 지원 종료일은 2026년 6월 30일이며 OSv2 이전을 사용해야 합니다.
액션의 읽기 방식은 실행 모드에 따라서도 다릅니다. 신규 Batched writes의 기본 Snapshot과 Legacy 적용 대기 쓰기의 동작 및 충돌 재시도 조건은 일관성 보장에 따릅니다. 위 버전 검사를 모든 모드의 모든 읽기가 같은 스냅샷이라는 의미로 확대하지 않습니다.
기본 전략: Apply user edits
기본값인 Apply user edits는 사용자가 수정한 속성에 대해 이후 데이터소스 갱신보다 사용자 값을 우선합니다. 수정하지 않은 속성은 데이터소스의 값을 사용합니다.
가상의 다음 사례에서 모든 존재하는 행과 객체의 기본 키는 pk_column=pk1입니다. 표에는 기본 키를 제외한 값을 표시합니다.
| 시점 | 데이터소스 행 | 사용자 작업 | 최종 객체 값 |
|---|---|---|---|
| T0 | col1=val1, col2=val2 | 없음 | col1=val1, col2=val2 |
| T1 | 행 없음 | 없음 | 객체가 보이지 않음 |
| T2 | col1=val1, col2=val2 | 없음 | col1=val1, col2=val2 |
| T3 | col1=val1, col2=val2 | col2=newVal2로 수정 | col1=val1, col2=newVal2 |
| T4 | 행 없음 | 없음 | 객체가 보이지 않음 |
| T5 | col1=val1, col2=val2 | 없음 | col1=val1, col2=newVal2 |
| T6 | col1=newVal1, col2=val2 | 없음 | col1=newVal1, col2=newVal2 |
| T7 | col1=newVal1, col2=val2 | 객체 삭제 | 객체가 보이지 않음 |
| T8 | col1=newVal1, col2=val2, col3=null | 없음 | 객체가 보이지 않음 |
| T9 | col1=newVal1, col2=val2, col3=null | col3=val3으로 객체 생성 | col1=null, col2=null, col3=val3 |
| T10 | col1=newVal1, col2=newVal2, col3=newVal3 | 없음 | col1=null, col2=null, col3=val3 |
| T11 | col1=newVal1, col2=newVal2, col3=newVal3 | col2=newVal22로 수정 | col1=null, col2=newVal22, col3=val3 |
| T12 | 행 없음 | 없음 | col1=null, col2=newVal22, col3=val3 |
| T13 | col1=newVal1, col2=newVal2, col3=newVal3 | 객체 삭제 | 객체가 보이지 않음 |
| T14 | col1=newVal1, col2=newVal2, col3=newVal3 | col2=newVal2, col3=val3으로 수정 시도 | 수정이 실패하며 객체가 보이지 않음 |
T4에서 원천 행이 없어졌을 때 객체는 사라지지만, T5에서 행이 다시 나타나면 이전 속성 편집이 다시 적용됩니다. 반면 사용자 생성 명령은 새 객체 상태의 시작점이므로 T9 이후에는 데이터소스 값이 아니라 생성 및 후속 편집 값으로 상태를 구성합니다. T12에서도 사용자 생성 객체는 유지됩니다.
최신 값 전략: Apply most recent value
OSv2에서는 객체 유형의 Datasources에서 데이터소스마다 충돌 해결 전략을 선택할 수 있습니다. 같은 객체 유형에서도 한 데이터소스는 기본 전략을, 다른 데이터소스는 Apply most recent value를 사용할 수 있습니다.
최신 값 전략에는 기반 데이터소스의 Timestamp 열에 매핑된 속성이 필요합니다. Date 자료형은 사용할 수 없으며 시각은 UTC여야 합니다.
설정을 저장하면 해당 데이터소스에 매핑한 속성 편집에 조건부 적용을 시작합니다. 새 편집은 액션 제출 시각을 사용하지만, 전략을 바꾸기 전에 이미 있던 편집은 전략 변경 시각을 비교 기준으로 사용합니다.
데이터소스의 시각이 편집 시각보다 이전이면 사용자 편집을 적용합니다. 데이터소스 시각이 같거나 더 나중이면 해당 편집을 무시합니다. 한 객체 안에서도 속성별 편집 시각이 달라 일부 값만 적용될 수 있습니다. 여러 데이터소스의 속성을 한 액션으로 바꾸면 각 속성을 제공하는 데이터소스의 전략을 따릅니다.
생성 명령도 이 비교의 영향을 받습니다. 원천 행이 없으면 사용자 생성 상태를 유지합니다. 원천 행이 있고 그 시각이 생성보다 같거나 나중이면 생성 값 대신 원천 값을 사용하며, 후속 속성 편집은 각 편집의 시각으로 판단합니다. 삭제 명령은 이 속성값 우선순위 비교와 별도로 객체를 보이지 않게 합니다.
시각이 있는 행과 없는 행
다음은 가상의 Ticket 원천 데이터입니다. 시각은 UTC입니다.
| Ticket ID | Title | Timestamp | Priority | Type |
|---|---|---|---|---|
| 101 | Ticket One | 2010-01-01 09:00 | P1 | Product Bug |
| 102 | Ticket Two | 값 없음 | P2 | Feature Request |
두 객체에 같은 날 다음 액션을 적용했다고 가정합니다.
- 08:30에
Title을Ticket으로 변경합니다. - 09:30에
Priority를P0으로 변경합니다. - 10:30에
Type을Unknown으로 변경합니다.
| Ticket ID | 최종 Title | Timestamp | 최종 Priority | 최종 Type |
|---|---|---|---|---|
| 101 | Ticket One | 2010-01-01 09:00 | P0 | Unknown |
| 102 | Ticket | 값 없음 | P0 | Unknown |
101의 제목 편집은 원천 시각보다 이전이므로 적용하지 않습니다. 나머지 두 편집은 이후이므로 적용합니다. 102는 원천 시각이 없으므로 세 편집을 모두 적용합니다.
편집 전용 속성과 비교용 시각
편집 전용 속성(Edit-only property)은 원천 데이터의 시각과 무관하게 사용자 편집을 적용합니다. 예를 들어 102번 티켓의 원천 시각이 2050-01-01 09:00 UTC이고 편집 전용 Team이 Sales여도, Change team 액션으로 바꾸면 Recruiting이 됩니다. 한 액션에서 일반 속성도 함께 수정했다면 일반 속성에는 시각 비교를 계속 적용합니다.
비교에는 편집하지 않은 원천의 Timestamp만 사용합니다. 101번 티켓의 표시 시각을 액션으로 2050년으로 바꾸더라도 원천 값이 2010-01-01 09:00 UTC이면 비교 기준은 여전히 2010년입니다. 그 이후의 Priority=P0 편집은 표시된 2050년 시각 때문에 차단되지 않습니다. 원천 시스템이 갱신 시각을 제공하지 않으면 데이터 파이프라인에서 기반 Timestamp 열을 구성해야 합니다.
삭제와 재생성
삭제는 일반적인 속성 편집과 다르게 처리합니다. 삭제를 적용한 객체는 원천 데이터가 남아 있어도 보이지 않습니다. 같은 기본 키로 객체를 다시 생성하면 이전 사용자 편집을 상속하지 않고 새 생성 상태에서 시작합니다. 따라서 재생성은 이전 편집 상태를 그대로 복원하는 작업과 다릅니다.
OSv2의 액션 되돌림은 성공 알림이 남아 있을 때 원래 실행자가 요청할 수 있습니다. 같은 객체에 후속 편집이 없어야 하고 되돌림 허용 조건도 충족해야 합니다. 제출 후 허용 설정을 껐다면 다시 켜도 그 제출 건을 되돌릴 수 없습니다. 이 지원 경로를 사용할 수 없는 경우 추가 액션으로 값을 바꾸거나 객체를 재생성하는 작업에는 새 편집의 의미와 권한이 적용됩니다.
객체 상태가 새로 시작하는 것과 편집 이력의 삭제 여부는 다릅니다. 같은 기본 키의 이전 이력이 남아 있다면 재생성 후에도 조회될 수 있습니다.
유형 전체의 편집 삭제
테스트 편집을 제거하고 원천 상태로 돌아가려면 OSv2의 스키마 이전 명령 Drop all edits를 사용할 수 있습니다. Ontology Manager의 Datasources에서 Edits 구역의 Delete edits를 선택하는 경로입니다. 대상 유형의 모든 객체에 누적된 편집을 제거하므로 특정 사용자나 객체 하나의 편집만 지우는 기능이 아닙니다.
구형 OSv1에는 이 스키마 이전 기능이 없으며, 쓰기용 데이터셋 구성을 제거하면 모든 사용자 편집이 삭제됩니다. 이를 단순한 출력 데이터셋 설정 변경으로 취급하면 안 됩니다.