PHP에서 비즈니스 규칙 하드코딩을 중단하고 규칙 엔진을 만든 방법
Source: Dev.to
모든 PHP 개발자는 이런 상황을 한 번쯤은 겪어봤을 것입니다. 고객이 전화를 걸어 “주말에 VIP 고객에게는 주문 금액이 €100 이상일 때 무료 배송을 해 주세요”라고 요청합니다.
코드를 열어 배송 모듈을 찾고, if 문을 추가한 뒤 배포합니다.
다시 코드를 열어봅니다.
이런 루프—고객 요청 → 코드에서 로직 찾기 → 수정 → 배포—는 저에게 많은 시간을 빼앗았습니다. 그리고 배송뿐만 아니라, 맞춤형 전자상거래 솔루션을 만들 때 결제 모듈, 동기화 시스템, 가격 계산기 등 비즈니스 규칙이 곳곳에 존재하고, 계속해서 바뀝니다.
당연히 떠오르는 해결책은 Symfony의 ExpressionLanguage를 사용하는 것이었지만, 저는 원하지 않았습니다. 의존성이 끌려오고, 객체를 탐색하며 메서드를 호출할 수 있어(사용자가 규칙을 직접 작성할 경우 보안 문제가 됩니다) 문제가 될 수 있습니다. 또한 뭔가 잘못됐을 때 왜 그런지 알려주지 않아, 블랙박스가 됩니다.
그래서 php‑ruler를 만들었습니다.
핵심 설계
클래식 파이프라인인 Lexer → AST → Evaluator 로 시작했습니다. 처음부터 엄격한 타입을 적용했으며, 1 = '1' 은 타입 오류이며 true 가 아닙니다. 암묵적인 형 변환이 없습니다.
문제: 실패했을 때 이유를 알 수 없음
→ explain 모드를 만들었습니다. 전체 평가 트리를 반환해 어떤 하위 조건이 통과했는지, 어떤 것이 실패했는지, 어떤 것이 단축 평가됐는지, 그리고 변수가 왜 없었는지를 알려줍니다.
문제: 프로덕션에서는 컨텍스트가 불완전할 때가 있음
→ safe 모드를 만들었습니다. 누락된 변수가 있어도 예외를 발생시키지 않고, 모든 누락 변수를 수집해 나중에 처리할 수 있게 합니다.
문제: customer.group.name 은 사용자에게 친숙하지 않음
→ alias resolver를 만들었습니다. 개발자로서 노출하고 싶은 이름을 지정합니다:
$resolver = (new AliasResolver())
->add('customer.group', 'customer group')
->add('cart.total', 'cart amount');
이제 비개발자도 다음과 같이 쓸 수 있습니다.
customer group = 'VIP' AND cart amount > 100
그리고 어떤 변수가 사용 가능한지는 정확히 제어할 수 있습니다.
실제 예시
$eval = new ExpressionEvaluator();
$context = [
'customer' => ['group' => 'VIP'],
'cart' => ['total' => 150.00],
'day' => 'saturday',
];
$rule = "customer.group = 'VIP' AND cart.total > 100 AND day IN ['saturday', 'sunday']";
$eval->evaluateBoolean($rule, $context); // true -> 무료 배송
이 규칙은 데이터베이스에 저장됩니다. 클라이언트가 규칙을 바꾸고 싶을 때는 직접 수정하면 되니 배포나 코드 변경이 필요 없습니다.
결제 모듈(누가 이 결제 수단을 사용할 수 있나요?), 동기화 시스템(이 가격 이상인 제품에 마진을 적용할까요?) 등, 혹은 어떤 자격 검사에도 같은 패턴을 적용할 수 있습니다.
문제가 발생했을 때 모습
$explainer = new ExpressionExplainer($eval);
$result = $explainer->explain(
"customer.group = 'VIP' AND cart.total > 100",
$context
);
$result->passed; // true | false | null
$result->failures(); // false 를 반환한 항목들
$result->missing(); // 존재하지 않았던 변수들
트리의 모든 노드는 자신의 하위 표현식, 상태, 그리고 해석된 값을 가지고 있습니다. 이제 규칙이 왜 실행되지 않았는지 추측할 필요가 없습니다.
- 의존성 0개
- PHP 8.1+ 지원
composer require ols/php-ruler
또한 로컬 데모 플레이그라운드도 제공합니다(빌드 단계나 Composer 없이).
php -S localhost:8000 -t demo
→ https://github.com/olivier-ls/php-ruler
제가 필요해서 만들었고, 현재 제 전자상거래 고객들의 프로덕션에서도 사용하고 있습니다. 비즈니스 규칙이 자주 바뀌는 시스템을 유지보수한다면, 늦은 밤 배포를 몇 번은 줄일 수 있을 겁니다.
궁금한 점이 있으면 언제든 질문해 주세요.