본문으로 건너뛰기

Entity

Entity(엔티티)는 Fascia의 핵심 비즈니스 객체 추상화입니다. 예약(Reservation), 고객(Customer), 주문(Order), 결제(Payment) 등 도메인 개념을 캡슐화하며, 타입이 지정된 필드, 다른 Entity와의 관계, 생명주기 전이를 관리하는 상태 머신(status machine), 비즈니스 규칙을 강제하는 불변식(invariant)을 포함합니다.

Entity는 구조화된 스펙을 통해 정의합니다. Fascia는 해당 스펙에서 데이터베이스 저장 모델(테이블, 컬럼, 인덱스)을 자동으로 도출합니다. SQL 스키마를 직접 작성할 필요가 없습니다.

Entity의 구성 요소

모든 Entity 스펙은 다음을 포함합니다:

  • Fields(필드) -- Entity의 상태를 보유하는 타입 지정 데이터 속성
  • Relationships(관계) -- 다른 Entity와의 연결(외래 키, 조인 테이블)
  • Status Machine(상태 머신) -- 허용되는 상태 및 상태 간 전이
  • Invariants(불변식) -- 항상 참이어야 하는 비즈니스 규칙
  • Row-Level Access(행 수준 접근 제어) -- 멀티테넌트 보안을 위한 소유권 기반 필터링

시스템 필드

모든 Entity에는 다음 시스템 관리 필드가 자동으로 포함됩니다. 스펙에서 수동으로 정의하지 마십시오:

FieldTypePurpose
iduuid고유 식별자
createdAtdatetime레코드 생성 시각
updatedAtdatetime마지막 수정 시각
deletedAtdatetime (nullable)소프트 삭제 시각
versioninteger낙관적 잠금(optimistic locking) 카운터

Fascia는 소프트 삭제(soft delete)만 사용합니다. 레코드가 "삭제"되면 행을 제거하는 대신 deletedAt 필드가 설정됩니다. version 필드는 낙관적 동시성 제어를 가능하게 합니다 -- 동시 수정과 충돌하는 업데이트는 안전하게 거부됩니다.

필드 타입

Entity 필드는 다음 타입을 지원합니다:

TypeDescriptionExample
string텍스트 데이터"John Doe"
number숫자 데이터 (정수 또는 소수)199.99
boolean참 또는 거짓true
date시간 없는 날짜"2025-03-15"
datetime시간대 포함 날짜와 시간"2025-03-15T14:30:00Z"
enum미리 정의된 값 집합 중 하나"confirmed"
uuid범용 고유 식별자"550e8400-e29b-41d4-a716-446655440000"
json임의의 JSON 데이터{"key": "value"}
reference다른 Entity에 대한 외래 키Customer의 id를 참조

각 필드는 required로 표시할 수 있으며 default 값을 지정할 수 있습니다.

관계

Entity는 네 가지 관계 유형을 통해 서로 연결됩니다:

TypeMeaningExample
hasOne이 Entity가 대상을 정확히 하나 소유User hasOne Profile
hasMany이 Entity가 대상을 여러 개 소유Customer hasMany Orders
belongsTo이 Entity가 대상에 의해 소유됨Order belongsTo Customer
manyToMany다대다 관계Product manyToMany Category

관계는 대상 Entity와 외래 키 필드를 지정하여 정의합니다.

상태 머신

모든 Entity는 상태 머신을 가져야 합니다. 단순한 경우(예: active에서 deleted로)라도 마찬가지입니다. 상태 머신은 다음을 정의합니다:

  • States(상태) -- Entity가 가질 수 있는 허용된 상태 목록
  • Initial State(초기 상태) -- Entity가 처음 생성될 때 할당되는 상태
  • Transitions(전이) -- 허용되는 상태 변경
  • Guards(가드) (선택 사항) -- 전이가 발생하기 위해 참이어야 하는 Value DSL 불리언 표현식

전이는 명시적으로 정의해야 합니다. 와일드카드 전이는 없습니다. 목록에 없는 전이는 허용되지 않습니다.

불변식

Invariant(불변식)은 Entity에 대해 항상 참이어야 하는 비즈니스 규칙입니다. Value DSL 불리언 표현식으로 작성되며, Execution Contract의 6단계 -- Flow 그래프 실행 후, 트랜잭션 커밋 전 -- 에서 강제됩니다.

불변식이 하나라도 거짓으로 평가되면 전체 트랜잭션이 롤백됩니다.

행 수준 접근 제어

rowLevelAccess가 활성화되고 ownerField가 지정되면, Fascia는 쿼리 결과를 자동으로 필터링하여 사용자가 자신이 소유한 레코드만 볼 수 있게 합니다. 관리자(admin)와 스태프(staff) 역할은 이 제한을 우회하도록 설정할 수 있습니다.

예시: Reservation Entity

{
"name": "Reservation",
"version": 1,
"description": "A booking made by a customer for a specific time slot and resource",
"fields": {
"customerId": { "type": "reference", "referenceTo": "Customer", "required": true },
"resourceId": { "type": "reference", "referenceTo": "Resource", "required": true },
"startDate": { "type": "datetime", "required": true },
"endDate": { "type": "datetime", "required": true },
"totalPrice": { "type": "number", "required": true },
"depositAmount": { "type": "number", "required": false, "default": 0 },
"notes": { "type": "string", "required": false },
"cancellationReason": { "type": "string", "required": false }
},
"relationships": {
"customer": { "type": "belongsTo", "target": "Customer", "foreignKey": "customerId" },
"resource": { "type": "belongsTo", "target": "Resource", "foreignKey": "resourceId" },
"payments": { "type": "hasMany", "target": "Payment" }
},
"statusMachine": {
"states": ["pending", "confirmed", "checked_in", "completed", "cancelled", "no_show"],
"initialState": "pending",
"transitions": [
{ "from": "pending", "to": "confirmed" },
{ "from": "pending", "to": "cancelled" },
{ "from": "confirmed", "to": "checked_in" },
{ "from": "confirmed", "to": "cancelled", "guard": "now() < reservation.startDate" },
{ "from": "confirmed", "to": "no_show", "guard": "now() > addHours(reservation.startDate, 1)" },
{ "from": "checked_in", "to": "completed" }
]
},
"invariants": [
{ "name": "endDateAfterStart", "expression": "reservation.endDate > reservation.startDate", "message": "End date must be after start date" },
{ "name": "totalPricePositive", "expression": "reservation.totalPrice > 0", "message": "Total price must be positive" },
{ "name": "depositNotExceedTotal", "expression": "reservation.depositAmount <= reservation.totalPrice", "message": "Deposit cannot exceed total price" }
],
"rowLevelAccess": true,
"ownerField": "customerId"
}

이 스펙은 6개의 생명주기 상태, 모든 쓰기 작업에서 강제되는 3개의 불변식, 그리고 고객이 자신의 예약만 볼 수 있도록 하는 행 수준 접근 제어를 갖춘 Reservation을 정의합니다. 전이 가드에 주목하세요 -- 확인된(confirmed) 예약은 시작일 이전에만 취소할 수 있으며, 시작 시각으로부터 1시간이 지나야 노쇼(no_show)로 표시됩니다.