바이너리 에셋 동시 수정 방지: Lore의 파일 잠금 서버 메커니즘과 충돌 해결 가이드
언리얼 엔진 및 유니티의 바이너리 에셋 충돌을 원천 차단하는 Lore 파일 잠금 서버의 아키텍처, Redis TTL 기반 임대 메커니즘, 동시성 제어 구현 및 충돌 해결 절차를 상세히 정리합니다.

핵심 요약
게임 개발에서 .uasset, .prefab, .fbx 같은 바이너리 파일은 Git의 3-Way Merge가 불가능하여 동시 수정 시 데이터 유실이 발생합니다. Lore 파일 잠금(Locking) 서버는 중앙 집중식 임대(Lease) 모델과 Redis 원자적 연산, 엔진 플러그인 하트비트(Heartbeat)를 결합하여 에셋 수정 권한을 선점 제어하고 잠금 충돌을 사전에 방지합니다.
왜 게임 개발에서 바이너리 에셋 충돌은 치명적인가?
소프트웨어 엔지니어링에서는 소스 코드(텍스트 파일)의 동시 수정을 분기(Branch)와 3-Way Merge를 통해 해결합니다. 텍스트 파일은 줄 단위 비교(Diff)가 가능하여 여러 개발자가 동일한 파일을 수정하더라도 충돌 영역을 수동 병합할 수 있습니다.
반면 게임 클라이언트와 레벨 디자인에 사용되는 에셋은 대부분 바이너리 포맷입니다.
- 언리얼 엔진:
.uasset,.umap - 유니티: 바이너리 세팅의
.prefab,.unity,.asset - DCC 툴:
.max,.maya,.blend,.psd
flowchart LR
A["User A의 수정본<br/>(바이너리 스트림 A)"] --> C{"병합 불가<br/>(Merge Conflict)"}
B["User B의 수정본<br/>(바이너리 스트림 B)"] --> C
C --> D["❌ 한쪽 작업 완전 유실"]
바이너리 데이터는 내부 직렬화 포맷이 복잡하게 얽혀 있어 비트 레벨에서 자동 병합이 불가능합니다. 덮어쓰기(Last-Write-Wins)가 발생하면 누군가의 몇 시간 혹은 며칠 분량의 작업이 완전히 파괴됩니다. 따라서 바이너리 에셋 파이프라인에서는 사후 병합이 아닌 사전 비관적 잠금(Pessimistic Locking) 모델이 필수적입니다.

Lore 파일 잠금 서버의 아키텍처와 동작 원리
Lore 파일 잠금 서버는 버전 관리 시스템(VCS) 및 게임 엔진 클라이언트와 연동되어 에셋 경로에 대한 배타적 소유권을 관리하는 고성능 백엔드 서비스입니다.
1. 상태 저장소 이원화 (Redis + PostgreSQL)
Lore 서버는 초당 수천 건의 잠금 쿼리를 지연 없이 처리하기 위해 인메모리 캐시와 영속 데이터베이스를 분리하여 운영합니다.
| 계층 | 사용 기술 | 담당 역할 | 정합성 보장 전략 |
|---|---|---|---|
| 임대 캐시 (Cache) | Redis Cluster | 실시간 Lock 선점, TTL 만료 관리, 원자적 잠금 처리 | Lua Script를 통한 원자적 CAS(Compare-And-Swap) |
| 영속 계층 (Persistence) | PostgreSQL | 전체 파일 잠금 히스토리, 강제 해제 감사 로그(Audit Log) | WAL 기반 트랜잭션, 쓰기 큐 비동기 반영 |
2. 임대(Lease)와 하트비트(Heartbeat) 메커니즘
클라이언트가 파일을 체크아웃할 때 서버는 영구 잠금이 아닌 유효 기간(TTL)이 부여된 임대 티켓을 발급합니다. 작업자의 엔진 에디터는 백그라운드 데몬을 통해 주기적으로 하트비트 핑을 전송하여 TTL을 갱신합니다. 작업자가 비정상 종료되거나 네트워크가 끊어지면 하트비트가 중단되어 설정된 만료 시간(기본 300초) 후 잠금이 자동 해제됩니다.
sequenceDiagram
autonumber
actor Client as 에디터 클라이언트
participant Server as Lore Locking Server
participant Redis as Redis (State/TTL)
participant DB as PostgreSQL (Audit Log)
Client->>Server: POST /v1/locks/acquire (filePath, userId)
Server->>Redis: EVAL Lock Script (SETNX with TTL)
alt 선점 성공
Redis-->>Server: Lock Token 발급
Server->>DB: INSERT lock_history (Active)
Server-->>Client: 200 OK (Lock Token, ExpiresIn: 300s)
loop 매 60초마다 Heartbeat
Client->>Server: POST /v1/locks/heartbeat (filePath, Lock Token)
Server->>Redis: EXPIRE key 300
Server-->>Client: 200 OK (Renewed)
end
else 이미 다른 사용자가 선점
Redis-->>Server: 선점 실패 (현재 소유자 정보 반환)
Server-->>Client: 409 Conflict (Owner: UserB, CreatedAt)
end
파일 잠금 서버를 어떻게 구현할까? 핵심 백엔드 설계
Lore 파일 잠금 서버의 코어 로직은 동시성 요청이 몰려도 단 하나의 클라이언트만 잠금을 획득하도록 보장해야 합니다.
1. Redis 기반 원자적 잠금 획득 (Go 예제)
경쟁 상태(Race Condition)를 방지하기 위해 단일 네트워크 왕복 내에서 잠금 확인과 TTL 설정을 원자적으로 수행하는 Lua 스크립트를 사용합니다.
package locker
import (
"context"
"errors"
"time"
"github.com/redis/go-redis/v9"
)
const acquireLockScript = `
if redis.call("EXISTS", KEYS[1]) == 0 then
redis.call("HMSET", KEYS[1], "owner", ARGV[1], "token", ARGV[2], "locked_at", ARGV[3])
redis.call("EXPIRE", KEYS[1], ARGV[4])
return 1
else
local currentToken = redis.call("HGET", KEYS[1], "token")
if currentToken == ARGV[2] then
redis.call("EXPIRE", KEYS[1], ARGV[4])
return 1
end
return 0
end
`
type LockManager struct {
rdb *redis.Client
}
func (m *LockManager) Acquire(ctx context.Context, filePath, userID, token string, ttlSeconds int) (bool, error) {
key := "lock:asset:" + filePath
now := time.Now().UTC().Format(time.RFC3339)
result, err := m.rdb.Eval(ctx, acquireLockScript, []string{key}, userID, token, now, ttlSeconds).Result()
if err != nil {
return false, err
}
success, ok := result.(int64)
if !ok || success != 1 {
return false, errors.New("file is already locked by another user")
}
return true, nil
}
2. REST API 엔드포인트 규격
Lore 서버는 엔진 플러그인과 CLI 도구가 직관적으로 통신할 수 있는 인터페이스를 제공합니다.
POST /api/v1/locks/acquire: 특정 에셋 경로에 대한 잠금 획득 요청POST /api/v1/locks/release: 정상 작업 완료 후 잠금 반환POST /api/v1/locks/heartbeat: 에디터 구동 중 TTL 연장GET /api/v1/locks/query?path={path}: 파일의 현재 잠금 소유자 및 생성 시간 조회POST /api/v1/locks/force-unlock: 관리자 권한으로 고착된 잠금 강제 해제

잠금 충돌 및 예외 상황은 어떻게 해결하는가?
팀 규모가 커지면 잠금 선점 경쟁, 자리 비움, 네트워크 단절로 인한 잠금 고착(Stale Lock) 현상이 발생합니다. Lore 시스템은 다음 3단계 절차로 충돌을 해결합니다.
1단계: 실시간 소유자 알림 및 선점 협의
다른 작업자가 이미 잠근 에셋을 열려고 시도하면 클라이언트 에디터 팝업에 현재 잠금 소유자(User ID, 팀명, 잠금 시점)가 노출됩니다. 또한 Lore 서버는 사내 메신저(Slack/Discord) Webhook API를 호출하여 선점자에게 작업 공유 알림을 전달합니다.
2단계: 유휴 세션(Stale Lock)의 자동 만료와 세션 복구
- 작업자가 PC를 끄고 퇴근하거나 비정상 종료된 경우, 추가 하트비트가 발생하지 않으므로 설정된 TTL(예: 300초) 이후 잠금이 메모리에서 자동 제거됩니다.
- 로컬 클라이언트는 네트워크 순단이 복구되면 즉시
re-acquire핸드셰이크를 시도하여 이전 토큰의 유효성을 검증합니다.
3단계: 관리자 강제 해제(Force Unlock)와 감사 추적
선점자가 긴급 휴가 중이거나 장시간 잠금을 독점하여 배포 파이프라인이 차단된 경우, 권한을 가진 테크 리드가 강제 해제를 실행합니다.
- 관리자가 관리 도구에서
Force Release를 호출합니다. - Lore 서버는 Redis의 키를 삭제하고 PostgreSQL의
audit_log테이블에 실행자 ID, 사유, 강제 해제 시각을 기록합니다. - 이전 잠금 소유자의 클라이언트 에디터에는 알림 이벤트가 브로드캐스트되어 로컬 수정본이 덮어쓰여지지 않도록 읽기 전용 상태로 전환합니다.
자주 묻는 질문 (FAQ)
Q1. Git LFS의 기본 Lock 기능(git lfs lock)과 Lore 파일 잠금 서버의 차이는 무엇인가요?
Git LFS Lock은 원격 Git 호스팅 서버(GitHub/GitLab)의 API를 호출하며 수동 락 획득/해제 방식입니다. 하트비트 메커니즘이 없어 잠금을 건 작업자가 수동으로 해제하지 않으면 무한히 유지되는 취약점이 있습니다. 반면 Lore 서버는 엔진 에디터에 임베딩되어 파일 오픈 시 자동 잠금, 백그라운드 하트비트 기반 TTL 자동 회수, 실시간 웹소켓 알림을 제공하여 인적 오류를 최소화합니다.
Q2. 작업 중 인터넷 연결이 끊기면 로컬 에셋 수정본이 손실되나요?
손실되지 않습니다. 하트비트 실패 시 에디터는 즉시 경고 배너를 띄우고 파일 저장을 임시 차단하거나 로컬 스크래치 폴더(.lore/backup/)로 사본을 분기 저장합니다. 네트워크가 복구되었을 때 다른 사용자가 잠금을 가로채지 않았다면 자동으로 잠금을 재획득하고 작업을 이어갈 수 있습니다.
Q3. 다중 리전(Multi-Region) 분산 환경에서 잠금 정합성은 어떻게 유지하나요?
다중 리전 환경에서는 분할 뇌(Split-Brain) 현상을 방지하기 위해 잠금 상태를 관장하는 단일 글로벌 마스터 Redis 클러스터를 두고 리전별 엣지 프록시 서버가 gRPC를 통해 마스터 서버로 잠금 획득 요청을 라우팅하는 방식을 채택합니다. 읽기 쿼리는 리전별 복제본에서 처리하여 지연 시간을 줄입니다.
요약 및 결론
바이너리 에셋의 동시 수정 충돌은 프로젝트 일정 지연과 데이터 파괴를 초래하는 심각한 리스크입니다. Lore 파일 잠금 서버는 Redis 기반의 원자적 임대 처리, 하트비트 세션 갱신, 관리자 감사 로그를 결합하여 견고한 동시성 방어 체계를 구축합니다. 견고한 파일 잠금 파이프라인을 도입하여 아티스트와 레벨 디자이너의 협업 안정성을 극대화해 보세요.



