본문으로 건너뛰기

접근자 객체화 — 인스턴스와 전역 헬퍼

상태: 부분 구현 (2026-08-14) — 접근자 객체화는 완료, context.Tables는 §3.1 결정 대기 · 사용법은 C# · 상위 계획: 검증 사용성과 C# 어셈블리 산출의 6단계

세 가지를 바꿉니다.

  1. 접근자를 인스턴스화합니다 — 한 번의 읽기가 만든 테이블 전체가 하나의 객체가 되고, 여러 인스턴스를 동시에 가질 수 있습니다.
  2. 기존 정적 표면은 전역 헬퍼로 유지합니다 — 「현재 인스턴스」로 위임하며, 싱글턴처럼 쓰는 프로젝트는 지금과 같은 코드를 씁니다.
  3. 검증 규칙은 전역 Tables 대신 context.Tables 로 접근합니다.

단순한 프로젝트는 전역 헬퍼만 쓰고, 필요한 프로젝트는 인스턴스를 직접 만듭니다 — 강제가 아니라 선택권을 주는 설계입니다.


1. 현재 구조 — 내부에 이미 있는 인스턴스

생성 접근자(Tables)의 공개 표면은 전부 정적입니다 — 테이블 속성 (public static ItemTable Item { get; private set; })도, 읽기·키 시임 (ReadAllBytesAsync · EncryptionKey · MacKey · VerifyMac)도 정적입니다.

그런데 내부에는 인스턴스 구조가 이미 있습니다. private sealed class Snapshot이 한 번의 읽기가 만든 테이블 전체를 인스턴스 필드로 갖고, ReadAllAsync는 이렇게 동작합니다 — 스냅샷 생성 → 전체 테이블을 스냅샷 안으로 읽기 → 교차 참조 해석 (SolveCrossReferences(Snapshot)) → 정적 속성으로 공개. 어느 한 파일이라도 실패하면 아무것도 공개하지 않고 기존 데이터가 유지된다는 원자적 교체 의미론이 주석으로 명시되어 있습니다.

따라서 이 설계는 새 구조의 발명이 아니라 Snapshot의 공개 승격입니다. 교차 참조 해석이 이미 스냅샷 단위이므로, 인스턴스 간 참조가 섞이지 않는 성질은 현 구조가 이미 보장합니다.

검증 쪽의 현재 주입 방식도 이 정적 표면에 결속되어 있습니다 — RuleAccessor.Fill (RuleAccessor.cs:224-251)이 생성된 정적 필드에 리플렉션으로 델리게이트를 대입하고 정적 ReadAllAsync를 호출합니다.

2. 설계

구성내용
인스턴스 타입승격된 스냅샷. 테이블 속성이 인스턴스 멤버가 됩니다. 용도: 테스트 격리, 데이터 버전 병행 로드, 이중 버퍼 핫리로드, 한 프로세스에서 여러 데이터 세트를 여는 서버
전역 헬퍼정적 Tables는 남습니다. 정적 ReadAllAsync는 「인스턴스 로드 → 현재 인스턴스로 원자 교체」가 되고, 정적 테이블 속성은 현재 인스턴스로 위임합니다. 기존 게임 코드 호출부는 수정 없이 컴파일됩니다
업데이터핫스왑이 「새 인스턴스 로드 → 교체」로 명시적이 됩니다 — 현재의 원자적 공개 의미론과 같은 성질을 인스턴스 경계로 표현한 것입니다

정적 파사드와 인스턴스 타입은 다른 타입이어야 합니다. C#은 한 타입 안에 같은 이름의 정적 멤버와 인스턴스 멤버를 함께 두지 못하므로, Tables.Item(정적, 기존 호출부)과 instance.Item(인스턴스)을 한 클래스로 만들 수 없습니다. 인스턴스 타입의 이름은 미결입니다 (§5).

시임의 귀속. ReadAllBytesAsync 등 4개 시임의 현재 철학은 「소비 프로젝트가 시작 시 한 번 정하는 것」이라 정적입니다. 인스턴스 모델에서는 「정적 기본값 + 인스턴스(또는 읽기)별 재정의 허용」과 「정적 유지」 중 선택해야 합니다 — 미결(§5).

3. 검증의 접근 경로 — context.Tables

타입 간극이 이 절의 핵심 문제입니다. Context는 선컴파일되는 계약 타입이고 접근자는 실행 시 생성되므로, Context가 접근자 타입의 속성을 직접 가질 수 없습니다.

해법은 접근자와 함께 생성되는 확장 멤버입니다 — 계약 Context는 형 없는 인스턴스 슬롯만 갖고, 생성 코드가 context.Tables를 접근자 인스턴스 타입으로 노출합니다. 규칙 컴파일은 호스트(net10 · 최신 언어 수준)에서 이루어지므로 확장 속성(확장 멤버)을 쓸 수 있고, Unity C# 9 하한과 무관합니다 — 검증 접근자는 호스트 전용 산출물입니다. 확장 속성이 여의치 않으면 확장 메서드 context.Tables()로 같은 효과를 냅니다(구문 차이만).

변경 지점내용
RuleAccessor.Fill리플렉션 정적 대입 → 「인스턴스 생성 → Context 슬롯에 저장」. 접근자의 정적 상태가 검증 경로에서 사라집니다 — 병렬 실행되는 rules/tables/ 스테이지와 장수 프로세스(--serve · 테스트 스위트)에서 상태 격리가 명시적이 됩니다
규칙 파일Tables.Xcontext.Tables.X. 샘플 3세트·픽스처 일괄 전환(외부 사용자 없음). using Tabbit.Rules;는 레코드 타입을 이름으로 적을 때 여전히 필요하므로 두 줄 규약은 유지됩니다
rules/pre/ 제약§3.1로 분리했습니다
문서·안내검증 §6, 상위 계획 1단계의 템플릿 헤더와 2단계의 생성 본문 예시

3.1 스테이지별 컨텍스트 — 실행 시 거부를 컴파일 거부로

context.Tables가 생기는 순간 rules/pre/의 문제가 달라집니다. 지금은 규칙이 Tables를 정적 타입으로 쓰므로 rules/pre/ 규칙이 그 이름을 적으면 using이 없어 컴파일되지 않습니다 — 스테이지 제약이 우연히 컴파일 시점에 걸립니다. 접근자를 context.Tables로 옮기면 그 우연이 사라집니다. Context는 어느 스테이지에서나 같은 타입이므로 rules/pre/에서도 자동 완성에 Tables가 보이고, 실행해야 「아직 시트를 읽지 않았다」는 말을 듣게 됩니다.

같은 성질의 것이 이미 둘 있습니다 — Schemarules/pre/에서 던지고, Db·Redisrules/runtime/ 밖에서 던집니다. 셋 다 「타입은 갖고 있는데 이 자리에서는 못 쓰는 멤버」입니다.

설계: 스테이지가 받는 컨텍스트의 타입을 나눕니다. 상속 사슬로 두면 각 단계가 앞 단계의 것을 그대로 갖습니다.

스테이지받는 타입더해지는 것
rules/pre/기반 컨텍스트보고(메시지) · Option · Files · Json
rules/tables/ · rules/global/ContextTables · Schema · 행/필드를 짚는 보고
rules/runtime/런타임 컨텍스트Db · Redis

얻는 것은 세 가지입니다 — 자동 완성이 그 자리에서 쓸 수 있는 것만 보여주고, 폴더를 잘못 고른 것이 실행이 아니라 컴파일에서 드러나며, 「이 폴더로 옮기라」는 실행 시 메시지가 그만큼 줄어듭니다.

치르는 것도 분명합니다.

  • 엔트리 시그니처가 스테이지마다 달라지므로, 엔트리 탐색이 스테이지별로 기대 타입을 알아야 합니다. 지금은 Context 하나를 찾습니다.
  • rules/runtime/ 규칙 파일이 받는 타입이 바뀝니다. 저장소 안에서는 픽스처와 샘플 몇 개이나, 규칙 작성자에게는 인자 타입이 바뀌는 변경입니다.
  • 타입 이름이 셋으로 늘어 문서와 템플릿 헤더가 스테이지마다 달라집니다.

확정과 구현 — 2026-08-15

상속 사슬로, 넷으로 갈랐습니다. 셋이 아니라 넷인 것은 rules/tables/를 따로 둔 결과이고, 그것이 rules/global/과 갈리는 지점은 context.Table 하나입니다 — 파일 이름이 이미 대상 테이블을 말하므로 규칙이 Schema.Table("Item")으로 이름을 한 번 더 적지 않게 합니다.

이름받는 폴더더해지는 것
IPreContextrules/pre/보고(메시지) · Option · Files · Json
IGlobalContext : IPreContextrules/global/Schema, 행·컬럼을 짚는 보고
ITableContext : IGlobalContextrules/tables/Table
IRuntimeContext : IGlobalContextrules/runtime/Db · Redis

상속으로 둔 이유는 rules/shared/의 헬퍼입니다. 독립 타입 넷이면 컨텍스트를 받는 헬퍼마다 오버로드가 넷 필요합니다. 겹쳐 두면 실제로 쓰는 것 중 가장 좁은 타입 하나로 그 아래 단계 전부에서 불립니다 — 저장소 안의 CharacterRules.cs가 그 예로, 진입점은 ITableContext를 받고 보고만 하는 private 헬퍼 둘은 IGlobalContext를 받습니다.

호스트 객체 하나가 넷을 다 구현합니다. 따라서 캐스팅으로 우회할 수 있고, Db의 실행 시 단계 확인은 그대로 둡니다 — 타입은 실수를 막고, 그 확인이 경계입니다. 픽스처 validation/wrong-stage가 타입 쪽을, validation/misplaced-store가 캐스팅 쪽을 덮습니다.

컴파일 오류가 폴더를 짚습니다. 컴파일러가 낼 수 있는 말은 「ITableContextDb가 없다」 까지이므로, 그 뒤에 어느 폴더가 그것을 갖고 있는지를 붙입니다. rules/pre/에서 Tables를 적었을 때도 마찬가지로 「시트를 읽기 전이라 액세서가 아직 없다」로 바꿔 적습니다.

context.Tables도 같은 주기에 들어갔습니다. 아래가 그 형태입니다.

3.2 context.Tables구현 완료

계약이 형 없는 슬롯 하나를 갖고, 생성된 어셈블리가 그것을 타입 있는 속성으로 덮습니다.

// 계약 — Tabbit.Validation
public interface IGlobalContext : IPreContext
{
/// 생성된 액세서의 스냅숏. 규칙이 직접 만질 자리가 아니라 아래 확장이 건너는 다리입니다.
object TableSnapshot { get; }
}

// 생성 — Tabbit.Rules, 액세서와 같은 어셈블리
public static class RuleContextTables
{
extension(IGlobalContext context)
{
public Tables.Snapshot Tables => (Tables.Snapshot)context.TableSnapshot;
}
}
고른 것버린 것
계약의 object 슬롯 + 생성 쪽 확장 속성제네릭 컨텍스트(IGlobalContext<TTables>) — 규칙 파일마다 시그니처에 스냅숏 타입 이름이 들어갑니다
확장 속성확장 메서드(context.Tables()) — 읽기가 한 겹 어색합니다. 확장 속성이 컴파일되는 것을 확인했으므로 이유가 없습니다
스냅숏을 컨텍스트가 듦생성 쪽 확장이 정적 Tables.Current를 읽음 — 그러면 context.을 거쳐도 실제로는 전역이라 객체화가 규칙에는 도달하지 않습니다

정적 Tables는 그대로 남습니다. 둘 중 하나를 고르라는 것이 아니라 선택지를 주는 것이고, 기존 규칙 파일이 그대로 컴파일되는 것이 그 결과입니다.

치르는 것. 계약 표면에 object 멤버가 하나 생깁니다. 규칙이 그것을 직접 쓸 일은 없고 요약 문서가 그렇게 적지만, 표면에 있다는 사실 자체는 남습니다 — IPreContext에 두지 않는 것이 그 대가를 줄이는 부분입니다(rules/pre/에는 스냅숏이 없으므로 슬롯도 없습니다). 구현에서는 [EditorBrowsable(Never)]를 달아 자동 완성 목록에서도 뺐습니다.

구현에서 정한 것. 확장은 템플릿이 아니라 RuleAccessor가 임시 소스 폴더에 한 파일로 써 넣습니다. 템플릿에 두면 모든 프로젝트의 액세서가 계약 어셈블리 없이는 컴파일되지 않게 되는데, 이 확장이 참인 것은 검증용으로 만든 액세서 하나뿐입니다.

FillReadAllAsync 하나를 부르던 것을 LoadAsyncPublish 둘로 나눴습니다. 앞의 것이 돌려주는 인스턴스가 컨텍스트가 넘겨주는 것이고, 뒤의 것이 정적 Tables를 여전히 쓰는 규칙을 위한 것입니다 — 선택지를 준다는 것이 그 두 줄입니다.

게이트는 pass 픽스처의 ItemCategoryRules.cs입니다. context.Tables로 읽고 정적 Tables와 행 수를 비교하므로, 둘이 갈라지면 통과하지 않습니다.

4. 하위호환

  • 게임 쪽 — 깨지지 않습니다. 정적 표면이 전역 헬퍼로 남으므로 기존 호출부가 그대로 컴파일됩니다.
  • 검증 쪽 — 일괄 전환합니다. 규칙의 접근 경로 변경은 샘플·픽스처·문서와 함께 한 번에 수행합니다.

4.1 구현에서 정한 것

항목정한 것
인스턴스 타입의 이름<액세서>.Snapshot — 중첩 공개 타입입니다. 내부에서 이미 쓰던 이름이고 「한 번의 읽기가 만든 테이블 전체」를 그대로 가리키며, 정본 이름 하나가 진입점으로 남습니다
현재 인스턴스의 노출Current로 공개합니다. 한 번도 읽지 않았으면 null이라, 로드 전에 테이블을 만지면 그 자리에서 드러납니다 — 빈 인스턴스를 미리 두면 그 신호가 없어집니다
시임의 귀속정적 유지입니다. 「소비 프로젝트가 시작 시 한 번 정하는 것」이라는 성질이 그대로이고, 유니티 어댑터가 대입하는 자리도 여기입니다
올리기와 읽기의 분리LoadAsync(읽고 연결, 올리지 않음) + Publish(올리기)로 갈랐습니다. ReadAllAsync는 그 둘을 이어서 하므로 기존 호출부는 수정 없이 컴파일됩니다

context.Tables는 아직입니다. 가장 깔끔한 형태가 §3.1(스테이지별 컨텍스트)과 얽혀 있기 때문입니다 — 생성 코드가 Context를 상속한 타입을 내놓으면 Tables가 확장이 아니라 그냥 프로퍼티가 되고, 그것이 곧 §3.1의 구조입니다. 그 결정 전에 확장 메서드로 먼저 만들면 결정 후에 걷어내야 합니다.

5. 미결

항목선택지
인스턴스 타입의 이름생성 코드의 이름 체계에서 정본으로부터 파생시킵니다. 여기서 정할 것은 접근자 중첩 타입으로 둘지, 최상위 타입으로 둘지입니다
「현재 인스턴스」의 노출전역 헬퍼가 현재 인스턴스 자체를 공개할지, 위임 속성만 둘지
시임의 귀속정적 유지 / 정적 기본값 + 인스턴스별 재정의
확장 멤버의 형태확장 속성 / 확장 메서드 — 호스트 언어 수준에서 확장 속성 가용성 확인 후
타 언어 접근자같은 객체화를 다른 언어 타깃에도 적용할지 — 후속 후보로만 기록
스테이지별 컨텍스트(§3.1)의 구성상속 사슬 / 독립 타입 3개
스테이지별 컨텍스트의 이름Context를 어느 단계에 둘지 포함 — 기존 규칙 파일이 전부 그 이름을 적고 있습니다
스테이지별 컨텍스트의 시점6단계와 같은 주기 / 별도 항목

6. 게이트

  • 인스턴스 2개 병행 로드 · 전역 헬퍼의 현재 인스턴스 위임 · 원자 교체(실패 시 기존 유지)를 검사하는 테스트 신설.
  • 검증 경로는 기존 ValidationPipelineTestscontext.Tables 기준으로 갱신되며 커버합니다.
  • 템플릿 변경이므로 골든 재기록 → 전 언어 비교본 재생성 → 샘플 재생성 → 기록 없이 재검증 순서를 지킵니다.

EOD