정책 룰이란?
정책 룰(Policy Rule) 은 조건 → 액션의 쌍입니다. 입력 facts가 조건을 만족하면 룰의 액션이 실행됩니다. 룰은 우선순위 순서 (0이 가장 높은 우선순위)로 평가됩니다.
IF 조건이 매칭되면 → THEN 액션을 실행
각 룰은 특정 정책 버전에 속하며, 이름 / 우선순위 / 조건(트리 구조 로직) / 하나 이상의 액션을 가집니다.
조건 문법
조건은 SINGLE과 GROUP 두 노드 타입의 트리 구조입니다.
LexQ는 type: "SINGLE" / type: "GROUP", field, operator, value, valueType 필드를 사용하는 자체 condition DTO 포맷을 사용합니다. Console 내부 폼 표현(다른 필드명 사용 가능)과 혼동하지 마세요. API 또는 CLI 호출 시에는 항상 엔진 포맷을 사용해야 합니다.
SINGLE 조건
단일 fact를 값과 비교하는 leaf 노드:
{
"type" : "SINGLE" ,
"field" : "payment_amount" ,
"operator" : "GREATER_THAN_OR_EQUAL" ,
"value" : 100000 ,
"valueType" : "NUMBER"
}
GROUP 조건
자식 조건을 AND 또는 OR로 결합하는 branch 노드:
{
"type" : "GROUP" ,
"operator" : "AND" ,
"children" : [
{ "type" : "SINGLE" , "field" : "customer_tier" , "operator" : "EQUALS" , "value" : "VIP" , "valueType" : "STRING" },
{ "type" : "SINGLE" , "field" : "payment_amount" , "operator" : "GREATER_THAN" , "value" : 50000 , "valueType" : "NUMBER" }
]
}
연산자
연산자 호환 타입 설명 EQUALSAll 정확히 일치 NOT_EQUALSAll 부정 GREATER_THANNUMBER >GREATER_THAN_OR_EQUALNUMBER >=LESS_THANNUMBER <LESS_THAN_OR_EQUALNUMBER <=CONTAINSSTRING 부분 문자열 일치 INSTRING, NUMBER 값이 제공된 리스트에 포함 NOT_INSTRING, NUMBER 값이 제공된 리스트에 미포함
IN / NOT_IN의 경우 호환 타입 열은 검사 대상 fact 의 타입(STRING 또는 NUMBER)을 가리킵니다. 제공하는 value는 리스트 자체이며, 그 valueType은 LIST_STRING 또는 LIST_NUMBER입니다.
값 타입
타입 JSON 값 예시 STRING"string""VIP"NUMBERnumber100000BOOLEANtrue / falsetrueLIST_STRING["a", "b"]["KR", "US"]LIST_NUMBER[1, 2][10000, 20000]
중첩 조건 예시
(customer_tier = "VIP" AND payment_amount >= 100000) OR region IN ["KR", "JP"]
{
"type" : "GROUP" ,
"operator" : "OR" ,
"children" : [
{
"type" : "GROUP" ,
"operator" : "AND" ,
"children" : [
{ "type" : "SINGLE" , "field" : "customer_tier" , "operator" : "EQUALS" , "value" : "VIP" , "valueType" : "STRING" },
{ "type" : "SINGLE" , "field" : "payment_amount" , "operator" : "GREATER_THAN_OR_EQUAL" , "value" : 100000 , "valueType" : "NUMBER" }
]
},
{ "type" : "SINGLE" , "field" : "region" , "operator" : "IN" , "value" : [ "KR" , "JP" ], "valueType" : "LIST_STRING" }
]
}
위 예시에서 customer_tier, region, payment_amount는 사용자 정의 fact로, 사용 전에 Fact Definitions 에 등록되어야 합니다. 시스템 fact는 user_id, user_tags 두 개뿐입니다.
액션 타입
LexQ 엔진의 액션은 도메인 중립 원시(primitive) 입니다. 엔진은 숫자와 구조만 봅니다 — 커머스 / 핀테크 / 보험 / 특정 비즈니스 모델을 가정하지 않습니다. 도메인 의미는 fact 이름과 integration 페이로드에 담깁니다.
타입 설명 주요 파라미터 MUTATE_FACT산술 연산자로 숫자 fact를 in-place 변경 refVar, operator, method, rate (PERCENTAGE 일 때) 또는 value, rounding (선택)INCREMENT_FACT계산된 양만큼 숫자 fact를 누적 증가 targetVar, method, refVar, rate (PERCENTAGE 일 때) 또는 value, rounding (선택)EMIT_EVENT외부 integration에 이벤트 발행 integrationId, eventPayloadEMIT_NOTIFICATIONintegration을 통해 알림 발송 integrationId, targetVar, notificationPayloadEMIT_WEBHOOK외부 URL 또는 webhook integration 호출 url, method, payloadTemplate (선택)BLOCK요청 차단 reasonSET_FACTfact를 고정 값으로 설정 key, valueADD_TAG리스트 fact에 태그 추가 tag, targetVar
MUTATE_FACT — Percentage
payment_amount를 10% 감소:
{
"type" : "MUTATE_FACT" ,
"parameters" : {
"refVar" : "payment_amount" ,
"operator" : "SUB" ,
"method" : "PERCENTAGE" ,
"rate" : 10
}
}
payment_amount가 200,000이면 → 180,000으로 감소. 변화량(-20,000)은 generatedVariables의 payment_amount__delta로 노출됩니다.
연산자(Operator)
연산자 AMOUNT PERCENTAGE ASSIGNrefVar = valuerefVar = refVar × rate / 100ADDrefVar += valuerefVar += refVar × rate / 100SUBrefVar -= valuerefVar -= refVar × rate / 100MULrefVar *= valuerefVar *= (rate / 100 + 1)DIVrefVar /= valueinvalid — 역수 rate를 사용한 MUL로 대체
DIV + PERCENTAGE 조합은 DRAFT 저장 시점에 거부됩니다. DIV + value=0도 마찬가지입니다.
MUTATE_FACT — Fixed Amount
{
"type" : "MUTATE_FACT" ,
"parameters" : {
"refVar" : "payment_amount" ,
"operator" : "SUB" ,
"method" : "AMOUNT" ,
"value" : 5000
}
}
MUTATE_FACT — 반올림(Rounding)
기본적으로 calculator 출력은 무손실(lossless) 정밀도로 보존됩니다. 고정 자릿수가 필요할 때만 선택적 rounding 필드를 사용하세요.
{
"type" : "MUTATE_FACT" ,
"parameters" : {
"refVar" : "payment_amount" ,
"operator" : "SUB" ,
"method" : "PERCENTAGE" ,
"rate" : 13.7 ,
"rounding" : { "scale" : 0 , "mode" : "FLOOR" }
}
}
rounding.scale은 [0, 16] 범위의 정수. rounding.mode는 HALF_UP(default), HALF_DOWN, HALF_EVEN, FLOOR, CEILING, DOWN, UP 중 하나. rounding을 생략하면 무손실 정밀도가 유지됩니다 — 가능하면 엔진이 아니라 다운스트림(예: 통화 표시 시점)에서 반올림하세요.
INCREMENT_FACT — Percentage
payment_amount의 1%를 total_point에 적립:
{
"type" : "INCREMENT_FACT" ,
"parameters" : {
"targetVar" : "total_point" ,
"refVar" : "payment_amount" ,
"method" : "PERCENTAGE" ,
"rate" : 1
}
}
payment_amount가 100,000이면 → total_point가 1,000 증가. 증분량은 generatedVariables의 total_point__delta로 노출됩니다.
INCREMENT_FACT는 엔진 내부 fact 변경만 수행합니다. 외부 시스템(예: 포인트 서비스) 동기화가 필요하면 동일 룰 안에서 [INCREMENT_FACT, EMIT_EVENT] 체인으로 구성하세요. 엔진은 INCREMENT_FACT 대신 외부 시스템을 호출하지 않습니다 — 이는 엔진 상태와 외부 상태 사이의 audit-grade 분리를 보존하기 위함입니다.
INCREMENT_FACT — Fixed Amount
{
"type" : "INCREMENT_FACT" ,
"parameters" : {
"targetVar" : "total_point" ,
"method" : "AMOUNT" ,
"value" : 500
}
}
EMIT_EVENT
외부 integration에 generic 이벤트 페이로드를 발행합니다. 엔진은 integrationId와 비어있지 않은 eventPayload 맵의 존재만 검증합니다 — 페이로드 구조는 integration provider의 책임입니다.
{
"type" : "EMIT_EVENT" ,
"parameters" : {
"integrationId" : "<your-integration-id>" ,
"eventPayload" : {
"couponId" : "WELCOME_VIP_2026"
}
}
}
도메인 전용 키(couponId, ticketId, claimId 등)는 integration provider가 라우팅합니다. 이로써 EMIT_EVENT는 진정한 generic primitive가 됩니다 — 쿠폰 발행, 티켓 생성, 보험 청구 제출 등 어떤 이벤트성 외부 호출에도 사용할 수 있습니다.
EMIT_NOTIFICATION
외부 integration을 통해 알림을 발송합니다.
{
"type" : "EMIT_NOTIFICATION" ,
"parameters" : {
"integrationId" : "<your-notification-integration-id>" ,
"targetVar" : "phone_number" ,
"notificationPayload" : {
"channel" : "SMS" ,
"templateId" : "ORDER_CONFIRM_001" ,
"variables" : { "order_id" : "order_id" }
}
}
}
targetVar는 수신자 fact를 지정합니다 (예: phone_number, email, device_token). notificationPayload는 채널, 템플릿, 변수 매핑을 integration provider에 전달 — 엔진은 이 키들을 해석하지 않습니다.
BLOCK — 요청 차단
{
"type" : "BLOCK" ,
"parameters" : { "reason" : "Suspected fraud" }
}
SET_FACT — Output Variable
SET_FACT는 리터럴 값 setter 이지 expression evaluator가 아닙니다. 출력의 키에 고정 값을 할당합니다. 산술 표현식이나 다른 fact 참조는 지원하지 않습니다.
{
"type" : "SET_FACT" ,
"parameters" : { "key" : "risk_level" , "value" : "HIGH" }
}
ADD_TAG
{
"type" : "ADD_TAG" ,
"parameters" : {
"tag" : "VIP_VERIFIED" ,
"targetVar" : "user_tags"
}
}
리스트 fact에 태그를 추가합니다. 중복은 자동으로 방지됩니다. targetVar를 생략하면 user_tags가 기본값입니다.
EMIT_WEBHOOK — Basic
{
"type" : "EMIT_WEBHOOK" ,
"parameters" : {
"url" : "https://api.example.com/webhooks/orders" ,
"method" : "POST"
}
}
payloadTemplate 없이 사용하면 엔진은 모든 input/output facts를 요청 본문으로 전송합니다 (시스템 변수 제외).
EMIT_WEBHOOK — With Payload Template
payloadTemplate로 요청 본문을 커스터마이징하세요. 템플릿 변수는 실행 시점에 치환됩니다.
{
"type" : "EMIT_WEBHOOK" ,
"parameters" : {
"url" : "<integration-id-or-direct-url>" ,
"method" : "POST" ,
"payloadTemplate" : {
"text" : "🔔 Rule {{ruleName}} fired \n Customer: {{fact.customer_tier}} \n Amount: {{output.payment_amount}}"
}
}
}
사용 가능한 템플릿 변수
변수 설명 예시 값 {{fact.xxx}}입력 fact 값 {{fact.payment_amount}} → 150000{{output.xxx}}출력 변수 (액션 후) {{output.payment_amount__delta}} → -20000{{timestamp}}실행 시각 (ISO-8601) 2026-04-14T05:30:00Z{{ruleName}}매칭된 룰 이름 VIP 20% Discount{{groupName}}정책 그룹 이름 Payment Policy{{versionNo}}실행된 버전 번호 5{{xxx}}직접 fact 조회 (단축형) {{customer_tier}} → VIP
Slack 은 {"text": "..."}, Discord 는 {"content": "..."} 형식이 필요합니다. 각 플랫폼이 기대하는 형식에 맞춰 payloadTemplate을 사용하세요.
{{fact.xxx}}와 {{output.xxx}}는 동일한 데이터 맵을 참조합니다. 이전 액션(예: MUTATE_FACT)이 payment_amount를 변경하면, {{fact.payment_amount}}와 {{output.payment_amount}} 모두 변경된 값을 반영합니다.
생성 변수 (Generated Variables)
변경된 facts와 별도로, 엔진은 액션별 변화 정보를 generatedVariables로 노출합니다:
패턴 의미 생성 액션 {refVar}__deltarefVar의 순변화량 (mutated - original, 부호 포함)MUTATE_FACT{targetVar}__deltatargetVar의 순증분량 (항상 0 이상)INCREMENT_FACT
한 룰에서 여러 액션이 같은 fact를 변경하면 __delta는 누적 변화량을 보고합니다. 액션별 스냅샷은 audit drill-down용으로 executionTraces에 보관됩니다.
Mutex — 룰 단위 충돌 해소
한 버전 내에서 룰들은 mutex 그룹 에 속할 수 있으며, 이를 통해 매칭되는 룰 중 몇 개를 발화시킬지 제어합니다.
필드 설명 mutexGroup관련 룰을 그룹핑하는 문자열 키 (예: "best-discount") mutexModeNONE(default), EXCLUSIVE, 또는 MAX_NmutexStrategyFIRST_MATCH, HIGHEST_PRIORITY, MAX_BENEFITmutexLimit이 그룹에서 발화 가능한 최대 룰 수 (MAX_N 모드용)
mutexMode가 EXCLUSIVE이면 winning 룰 하나만 발화합니다. MAX_N이면 우선순위 순서로 최대 mutexLimit개의 룰이 발화합니다. 발화하지 못한 다른 매칭 룰은 Decision Trace 에 status BLOCKED + reason MUTEX_PRIORITY_LOST 또는 MUTEX_LIMIT_REACHED로 기록됩니다 (Decision Trace 참조).
Decision Trace
룰 평가 결과는 모두 decision trace 엔트리 로 기록되어 해당 룰이 발화했는지 / 왜 그렇게 결정됐는지 를 설명합니다. 이는 LexQ의 audit-grade 추론 영역의 핵심입니다 — 모든 결과는 특정 status와 reason code로 추적할 수 있습니다.
decision trace 엔트리는 두 분류 필드를 가집니다:
status — 결과의 상위 분류
reasonCode — 그 분류 안에서의 구체적 사유
DecisionStatus
Status 의미 SELECTED룰이 발화하고 액션이 실행됨 NO_MATCH룰의 condition 이 false 로 평가됨 NOT_SELECTED경쟁 이전 사전 필터링 (예: effective date — 향후 지원 예정) BLOCKEDmutex 또는 activation 그룹 경쟁에서 탈락 ERROR평가 도중 액션 또는 엔진 오류 발생
DecisionReasonCode
ReasonCode 의미 FINAL_WINNER이 평가에서 선택된 룰 EFFECTIVE_DATE_INVALID룰의 유효 기간 [from, to] 범위 밖 (향후 지원 예정) CONDITION_MISMATCH룰의 condition 트리가 입력 facts와 불일치 MUTEX_PRIORITY_LOSTEXCLUSIVE mutex (limit=1) — 같은 그룹의 다른 룰이 더 높은 우선순위 또는 점수 MUTEX_LIMIT_REACHEDMAX_N mutex (limit>1) — mutexLimit 이 더 높은 우선순위 룰들로 이미 채워짐 GROUP_PRIORITY_LOSTEXCLUSIVE activation 그룹 (limit=1) — 다른 룰이 priority 로 승리 GROUP_LIMIT_REACHEDMAX_N activation 그룹 (limit>1) — executionLimit 이 이미 채워짐 ACTION_ERROR이 룰의 액션 executor 가 실행 중 throw ENGINE_ERROR엔진 자체가 룰 처리 전/도중에 실패
Status × ReasonCode 매핑
Status 가능한 ReasonCode SELECTEDFINAL_WINNERNO_MATCHCONDITION_MISMATCHNOT_SELECTEDEFFECTIVE_DATE_INVALID (향후 지원 예정)BLOCKEDMUTEX_PRIORITY_LOST, MUTEX_LIMIT_REACHED, GROUP_PRIORITY_LOST, GROUP_LIMIT_REACHEDERRORACTION_ERROR, ENGINE_ERROR
“왜 내 룰이 발화하지 않았지?”를 디버깅할 때 decision trace는 두 단계로 답을 줍니다 — status는 어떤 분류의 필터링이 룰을 제거했는지 알려주고, reasonCode는 정확히 어떤 검증이 룰을 거부했는지 알려줍니다.
완전한 룰 예시
{
"name" : "VIP 20% Discount" ,
"priority" : 0 ,
"condition" : {
"type" : "GROUP" ,
"operator" : "AND" ,
"children" : [
{ "type" : "SINGLE" , "field" : "customer_tier" , "operator" : "EQUALS" , "value" : "VIP" , "valueType" : "STRING" },
{ "type" : "SINGLE" , "field" : "payment_amount" , "operator" : "GREATER_THAN_OR_EQUAL" , "value" : 100000 , "valueType" : "NUMBER" }
]
},
"actions" : [
{ "type" : "MUTATE_FACT" , "parameters" : { "refVar" : "payment_amount" , "operator" : "SUB" , "method" : "PERCENTAGE" , "rate" : 20 } },
{ "type" : "SET_FACT" , "parameters" : { "key" : "discount_applied" , "value" : true } }
],
"mutexGroup" : "best-discount" ,
"mutexMode" : "EXCLUSIVE" ,
"mutexStrategy" : "HIGHEST_PRIORITY" ,
"isEnabled" : true
}
다음 단계
Fact Definitions 룰이 기대하는 입력 변수를 정의하세요.