한 번 등록한 절차를 여러 프로젝트에서 쓰고 싶지만 어느 한쪽을 고쳤다고 다른 쪽까지 바뀌면 곤란하다. 공통 목록에서 처음 가져오는 시점과 프로젝트 안에서 수정하는 시점을 분리한 독립 예제를 만들었다. 파일과 실제 실행을 연결하기 전 이 작은 규칙부터 시험할 수 있도록 메모리 안에서만 동작한다.
공통 등록은 하나, 프로젝트의 상태는 따로 둔다
register는 절차 ID와 설명용 텍스트를 공통 목록에 한 번 등록한다. 같은 ID를 다시 등록하면 거절한다. synchronize는 공통 항목 중 프로젝트에 아직 없는 항목만 가져온다. 같은 프로젝트에서 다시 호출해도 이미 있는 정의를 덮어쓰지 않는다. 단순한 이름별 공유 참조와 다른 지점이다.
각 사본에는 현재 source와 hash, 처음 가져온 base_hash가 있다. inspect가 돌려주는 locally_modified는 현재 공통 버전과의 차이가 아니라 처음 프로젝트에 들어온 내용과의 차이다. 두 비교를 섞으면 공통 목록이 바뀌기만 해도 프로젝트가 스스로 수정된 것처럼 표시할 수 있다. 비교 기준을 자료 안에 명시했다.
공통 업데이트는 기존 사본의 자동 승격이 아니다
update_catalog는 호출자가 알고 있는 이전 hash가 현재 공통 정의와 맞을 때만 새 텍스트를 저장한다. 그 뒤 새 프로젝트를 동기화하면 새 버전을 받지만 기존 프로젝트는 이전 사본을 유지한다. 이 예제에는 전체 프로젝트를 새 버전으로 끌어올리는 버튼도 백그라운드 전파도 없다.
이 선택에는 이전 버전이 오래 남는 비용이 있다. 실제 운영에서는 버전 목록과 변경 이유, 프로젝트별 검토 상태를 더해야 한다. 그렇다고 편의를 위해 자동 덮어쓰기를 넣으면 프로젝트별 수정 보존이라는 목적이 사라진다. 업데이트 배포와 업데이트 적용을 다른 선택으로 남겨 둔 것이다.
수정과 재사용 전에 현재 내용을 다시 맞춘다
edit는 프로젝트·절차 ID와 새 텍스트, 예상 hash를 받는다. 그 사이 다른 수정으로 현재 hash가 바뀌었다면 SNAPSHOT_CHANGED로 거절한다. 오래된 화면을 보고 쓴 변경을 최신 내용 위에 무조건 덮어쓰지 않는 규칙이다. inspect로 돌려준 사전을 바꿔도 내부 자료가 바뀌지 않도록 별도 사본을 반환한다.
require_current도 예상 hash가 현재 사본과 같은지만 검사한다. 이름 때문에 실행 함수로 오해하지 않게 반환 자료의 executed_here를 항상 거짓으로 둔다. 이 함수에 hash를 넣었다는 것이 사람의 실제 검토나 실행 승인을 증명하는 것은 아니다. 검토자가 무엇을 읽고 허용했는지는 별도 연결 계층에서 남겨야 한다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Independent in-memory example of catalog registration and project snapshots."""
from hashlib import sha256
from threading import RLock
import re
class DefinitionError(ValueError):pass
def label(value):
if not isinstance(value,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,79}',value):raise DefinitionError('INVALID_LABEL')
def digest(source):
if not isinstance(source,str) or not source.strip() or len(source)>64000:raise DefinitionError('INVALID_SOURCE')
return sha256(source.encode('utf-8')).hexdigest()
class DefinitionStore:
def __init__(self):
self._lock=RLock()
self._catalog={}
self._projects={}
def register(self,identifier,source):
label(identifier);hash_=digest(source)
with self._lock:
if identifier in self._catalog:raise DefinitionError('ALREADY_REGISTERED')
self._catalog[identifier]={'source':source,'hash':hash_}
return hash_
def update_catalog(self,identifier,source,expected_hash):
label(identifier);hash_=digest(source)
with self._lock:
current=self._catalog.get(identifier)
if not current:raise DefinitionError('CATALOG_NOT_FOUND')
if current['hash']!=expected_hash:raise DefinitionError('CATALOG_CHANGED')
self._catalog[identifier]={'source':source,'hash':hash_}
return hash_
def synchronize(self,project):
label(project)
with self._lock:
local=self._projects.setdefault(project,{})
created=[]
for identifier,entry in self._catalog.items():
if identifier in local:continue
local[identifier]={**entry,'base_hash':entry['hash']}
created.append(identifier)
return created
def _entry(self,project,identifier):
label(project);label(identifier)
entry=self._projects.get(project,{}).get(identifier)
if not entry:raise DefinitionError('SNAPSHOT_NOT_FOUND')
return entry
def inspect(self,project,identifier):
with self._lock:
entry=self._entry(project,identifier)
return {'project':project,'identifier':identifier,**entry,
'locally_modified':entry['hash']!=entry['base_hash'],
'executed_here':False}
def edit(self,project,identifier,source,expected_hash):
hash_=digest(source)
with self._lock:
entry=self._entry(project,identifier)
if entry['hash']!=expected_hash:raise DefinitionError('SNAPSHOT_CHANGED')
entry.update(source=source,hash=hash_)
return self.inspect(project,identifier)
def require_current(self,project,identifier,expected_hash):
with self._lock:
entry=self._entry(project,identifier)
if entry['hash']!=expected_hash:raise DefinitionError('SNAPSHOT_CHANGED')
return self.inspect(project,identifier)
같은 뜻보다 실제 텍스트의 변경을 잡는다
hash는 텍스트를 UTF-8로 인코딩한 SHA-256이다. 공백과 줄바꿈도 그대로 포함하므로 LF와 CRLF가 다르면 다른 값이다. 의미가 비슷한 두 설명을 같다고 추정하지 않는다. 입력은 비어 있지 않은 문자열과 정해 둔 크기로 제한하고 프로젝트와 절차 이름도 짧은 식별자 형식으로 검사한다.
21개 시험은 최초 등록과 동기화, 로컬 수정의 독립성, 공통 변경 뒤 기존 사본 보존과 새 사본 갱신, 오래된 수정의 거절을 다룬다. 여덟 스레드가 처음 동기화를 동시에 요청해도 사본 생성이 한 번인지 확인한다. 잠금은 한 관리자 인스턴스의 메타데이터용이며 여러 프로세스의 저장 경쟁을 해결하지 않는다.
다섯 기록으로 어떤 내용이 남는지 비교한다
초기 사본 두 개, 한쪽만 수정한 결과, 공통 목록 변경 뒤 이전 프로젝트와 새 프로젝트의 차이, 오래된 hash 거절, 일치해도 실행되지 않은 결과를 예제로 남겼다. 아래 뷰어는 실제 로컬 Python이 생성한 합성 기록을 표시한다. 방문자의 프로젝트를 읽거나 수정하는 기능은 없다.
공유 ZIP은 별도 폴더에 추출한 뒤 같은21개 시험을 실행하고 원본 파일과 해시를 맞춘다. 한영 뷰어의 기록 선택은 DOM 대역에서 확인한다. 원래 앱의 프로젝트 시험이나 현재 서비스 화면을 새로 실행한 검수와 구별한다. 기록 안의 hash도 어떤 텍스트가 남았는지 비교하는 값이지 실행 성공 증명은 아니다.
실제 파일 배포에는 별도의 저장 계약이 필요하다
북극성 원본은 파일 기반 프로젝트 정의를 사용하고 manifest를 마지막에 복사하는 완료 표시를 둔다. 이 합성 모듈에는 폴더와 manifest, 파일 원자성, 충돌 복구를 구현하지 않았다. 공통 등록과 수정 보존의 판단만 따로 꺼낸 예제이므로 실제 프로그램 교체기로 소개하지 않는다.
전체 코드·시험·기록 생성기와 README를 수정·재사용할 수 있다. 운영 적용에는 영속 저장과 인증, 정의 검사, 서명 또는 신뢰 판단, 사용 설정과 변경 이력 등이 필요하다. 나는 해시가 맞는다는 이유로 내용이 안전하고 옳다고 말하지 않는다. 서로의 수정본을 보존하는 규칙과 실행할 수 있는 권한은 끝까지 다른 계약이다.