메인 콘텐츠로 건너뛰기

정책 규칙이란?

**정책 규칙(Policy Rule)**은 조건 → 액션의 쌍입니다. 입력 facts가 조건을 만족하면 규칙의 액션이 실행됩니다. 규칙은 우선순위 순서로 평가되며 1이 가장 높습니다. 한 버전 안에서 우선순위는 항상 1…N 연속입니다. 새 규칙은 끝에 붙고, 순서를 바꾸면 전체가 다시 매겨집니다.
각 규칙은 특정 정책 버전에 속하며, 이름 / 우선순위 / 조건(트리 구조 로직) / 하나 이상의 액션을 가집니다.

조건 문법

조건은 SINGLE과 GROUP 두 노드 타입의 트리 구조입니다.
LexQ는 type: "SINGLE" / type: "GROUP", field, operator, value, valueType 필드를 사용하는 자체 condition DTO 포맷을 사용합니다. Console 내부 폼 표현(다른 필드명 사용 가능)과 혼동하지 마세요. API 또는 CLI 호출 시에는 항상 엔진 포맷을 사용해야 합니다.

SINGLE 조건

단일 fact를 값과 비교하는 leaf 노드:

GROUP 조건

자식 조건을 AND 또는 OR로 결합하는 branch 노드:

연산자

IN / NOT_IN의 경우 호환 타입 열은 검사 대상 fact의 타입(STRING 또는 NUMBER)을 가리킵니다. 제공하는 value는 리스트 자체이며, 그 valueType은 LIST_STRING 또는 LIST_NUMBER입니다.
목록 타입 fact에 CONTAINS를 쓰지 마세요. CONTAINS는 STRING 부분 일치입니다. LIST_STRING / LIST_NUMBER fact에는 HAS_ANY / HAS_ALL / HAS_NONE을 사용하고, 값은 쉼표로 이은 문자열이 아니라 배열(["electronics"])로 보내야 합니다.IN / NOT_IN은 반대 방향입니다. 스칼라 fact를 목록 값과 대조합니다. HAS_*는 양쪽이 모두 목록입니다.

값 타입

중첩 조건 예시

(customerTier = "VIP" AND paymentAmount >= 100000) OR region IN ["KR", "JP"]
위 예시에서 customerTier, region, paymentAmount는 사용자 정의 fact로 Fact Definitions에 등록하는 것이 좋습니다. 규칙이 읽기만 하는 키는 등록하지 않은 채로 참조해도 됩니다. LexQ는 이런 미등록 키를 거부하지 않고 비차단 경고로 알립니다. 다만 등록해야 타입 검증과 값 제약이 동작합니다. 액션 대상은 먼저 등록해야 합니다(액션 타입 참고). 모든 테넌트에는 미리 등록된 fact가 있습니다(사전 등록 fact 참고).

액션 타입

LexQ 엔진의 액션은 도메인에 종속되지 않는 원시 연산입니다. 엔진은 숫자와 구조만 볼 뿐 커머스, 핀테크, 보험 같은 특정 업종을 가정하지 않습니다. 업무 의미는 여러분이 정한 fact 이름에 담깁니다. 엔진은 외부 시스템을 호출하지 않습니다. fact를 바꾸고 결정을 기록할 뿐이며, 응답을 읽고 무엇을 할지는 호출한 쪽이 정합니다. 엔진 상태와 외부 상태를 분리해야 감사 추적이 성립하기 때문입니다. SET_FACT와 MUTATE_FACT의 targetVar는 등록된 fact여야 합니다. 등록되지 않은 키에 값을 쓰는 규칙은 저장과 발행이 ACT-032로 거절되므로, fact 정의에서 먼저 등록하세요. isBlocked와 blockReason에는 BLOCK만 값을 씁니다. 두 키를 대상으로 쓰는 SET_FACT나 MUTATE_FACT가 있으면, 두 키가 등록된 fact인데도 규칙 저장과 발행이 ACT-033으로 거절됩니다.

MUTATE_FACT 파라미터

operator × method 매트릭스 DIV + AMOUNT 조합에서는 operand가 0 이면 안 됩니다.
targetVar는 요청 facts에 숫자로 들어 있어야 합니다. 없으면 오류가 발생하며, 엔진이 0으로 대신 채우지 않습니다. 값을 누적하는 정책이라면 시작값을 함께 보내야 합니다 (pointsEarned: 0). 엔진은 상태를 갖지 않으므로 여러분의 데이터베이스를 읽지 않습니다.요청에 없던 키에 값을 쓰는 일은 SET_FACT가 합니다.

targetVar와 refVar가 다를 때

refVar는 PERCENTAGE × 세 칸에서만 의미를 갖습니다. 다른 칸에서 지정하면 조용히 무시되지 않고 오류가 납니다. AMOUNT에는 기준이라는 개념이 없고, MUL × PERCENTAGE는 배수 표기라서 기준을 읽지 않기 때문입니다. 계산 기준이 변경 대상과 다를 때 사용합니다.
loyaltyPoint += orderTotal × 5% 입니다. 적립 대상과 계산 기준이 서로 다른 fact 입니다. refVar를 생략하면 loyaltyPoint += loyaltyPoint × 5%가 되어 결과가 완전히 달라집니다.
값의 범위는 제한하지 않습니다. 음수 operand와 100을 넘는 비율 모두 유효합니다. 환급(-5), 가산금(150), 위험 점수, 게임 포인트처럼 범위를 벗어나는 값이 필요한 도메인이 실제로 있습니다. 범위 검증은 엔진이 아니라 fact 정의에서 하세요. fact 자체에 min과 max로 값 제약을 선언합니다.그것이 묶는 것은 호출자가 보내는 입력이지 액션이 계산해 낸 값이 아닙니다. MUTATE_FACT는 여전히 paymentAmount를 선언한 범위 밖으로 보낼 수 있고, 그 결과는 Dry Run이 볼 자리입니다.

MUTATE_FACT — 정률(Percentage)

paymentAmount를 10% 줄입니다. 통화 fact라면 아래 반올림 절을 함께 보세요.

MUTATE_FACT — 정액(Fixed Amount)

MUTATE_FACT — 반올림(Rounding)

rounding을 지정하지 않으면 결과가 무손실 정밀도로 유지됩니다. 통화 단위로 맞춰야 할 때 지정하세요.
mode 에는 UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN을 넣을 수 있습니다. 기본값은 HALF_UP 입니다.

SET_FACT — 값 대입

타입에 관계없이 값을 대입합니다. 요청에 없던 키에 값을 쓸 수 있는 유일한 액션입니다. 그래도 targetVar는 등록된 fact여야 합니다.
SET_FACT는 덧붙이지 않고 통째로 대입합니다. LIST_STRING fact에 값을 누적하려면 현재 목록을 요청에 담아 보내고 새 목록 전체를 대입하세요. 산술 변경이 아니라 대입이므로 SET_FACT는 __delta를 만들지 않습니다. 같은 fact를 MUTATE_FACT도 바꾸면, 그 __delta에는 SET_FACT가 바꾼 몫도 들어갑니다(요청이 실행되는 순서 참고).

BLOCK — 거부 결정 기록

BLOCK은 실행을 중단하지 않습니다. 같은 규칙의 뒤따르는 액션도, 이후 선택된 다른 규칙도 그대로 실행됩니다. isBlocked(true)와 blockReason(reason 값, 없으면 Policy Violation) 두 fact를 기록할 뿐이며, 실제로 요청을 막는 일은 호출한 쪽의 몫입니다. 같은 요청의 조건은 이 값을 보지 못합니다.

요청이 실행되는 순서

요청 하나는 두 단계로 실행됩니다.
  1. 모든 규칙의 조건을 요청에 담긴 fact로 판정합니다. 어떤 액션보다도 먼저 일어나므로, 같은 요청에서 액션이 쓴 값을 조건은 보지 못합니다.
  2. 조건이 맞은 규칙의 액션을 우선순위 순서로 실행합니다. 각 액션은 앞 액션이 남긴 값 위에서 계산합니다. 앞 규칙의 액션이 남긴 값도 마찬가지입니다.
요청이 customerTier: "STAFF"와 paymentAmount: 100을 보내고 규칙 두 개가 맞으면 이렇게 됩니다. 규칙 2의 조건은 20이 아니라 요청 값 100을 보므로 여전히 맞습니다. 그 액션은 규칙 1이 남긴 20에 3을 곱합니다. 응답은 mutatedFacts.paymentAmount = 60, generatedVariables.paymentAmount__delta = -40입니다. __delta는 최종 값을 요청 값과 비교하므로 SET_FACT가 바꾼 몫도 들어갑니다. 두 규칙이 같은 EXCLUSIVE 상호 배타 그룹에 있으면 규칙 1만 발동하고 결과는 20입니다. 뒤 판단이 앞 결과에 따라 달라져야 하면, 그 결과를 담아 API를 한 번 더 호출하거나 두 액션을 한 규칙에 넣으세요.

생성 변수 (Generated Variables)

변경된 facts와 별도로, 엔진은 액션별 변화 정보를 generatedVariables로 노출합니다: 여러 액션이 같은 fact를 바꾸면, 한 규칙 안이든 여러 규칙에 걸쳐서든 __delta는 요청 값과 최종 값의 차이를 보고합니다. 액션 하나하나의 스냅샷은 executionTraces에 남아, 감사 시 원인을 끝까지 되짚을 수 있습니다.

상호 배타 그룹(Mutex) — 규칙 간 충돌 해결

한 버전 안에서 규칙은 **상호 배타 그룹(mutex group)**에 속할 수 있습니다. 상호 배타 그룹은 조건이 맞는 규칙 중 몇 개를 발동시킬지 제어합니다. mutexMode가 EXCLUSIVE이면 경쟁에서 이긴 규칙 하나만 발동합니다. MAX_N이면 우선순위 순서로 최대 mutexLimit개의 규칙이 발동합니다. 발동하지 못한 나머지 규칙은 Decision Trace에 status BLOCKED, reasonCode MUTEX_PRIORITY_LOST 또는 MUTEX_LIMIT_REACHED로 기록됩니다 (Decision Trace 참조).

Decision Trace

규칙 평가 결과는 모두 decision trace 엔트리로 기록되어 해당 규칙이 발동했는지, 왜 그렇게 결정됐는지를 설명합니다. LexQ가 모든 결정을 검증 가능하다고 말하는 근거가 여기에 있습니다. 어떤 결과든 status와 reasonCode 조합으로 되짚을 수 있습니다. decision trace 엔트리는 두 분류 필드를 가집니다:
  • status: 결과의 상위 분류
  • reasonCode: 그 분류 안에서의 구체적 사유

DecisionStatus

DecisionReasonCode

Status × ReasonCode 매핑

“왜 내 규칙이 발동하지 않았지?”를 디버깅할 때 decision trace는 두 단계로 답을 줍니다. status는 어떤 분류의 필터링이 규칙을 제거했는지 알려주고, reasonCode는 정확히 어떤 검증이 규칙을 거부했는지 알려줍니다.
CONDITION_MISMATCH에는 두 가지 사실이 섞여 있습니다. 조건이 거짓이었던 경우와, 조건을 평가할 수 없었던 경우(타입 불일치, 해소되지 않은 fact)입니다. 구체적인 사유는 reasonDetail에 담깁니다. 예를 들어 LIST_STRING fact에 평문 문자열이 들어오면 Evaluation error: FACT_TYPE_MISMATCH가 기록됩니다. 평가에 실패한 규칙이 있어도 실행 전체가 멈추지는 않으며 나머지 규칙은 정상 실행됩니다.

완전한 규칙 예시

이 규칙을 저장하기 전에 두 액션 대상 paymentAmount와 discountApplied를 fact로 등록하세요.

다음 단계

Fact Definitions

규칙이 기대하는 입력 변수를 정의하세요.

Dry Run

배포 전에 규칙을 테스트하세요.