PHP에서 비즈니스 규칙 하드코딩을 중단하고 규칙 엔진을 만든 방법

발행: (2026년 6월 9일 AM 06:36 GMT+9)
6 분 소요
원문: Dev.to

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

제가 필요해서 만들었고, 현재 제 전자상거래 고객들의 프로덕션에서도 사용하고 있습니다. 비즈니스 규칙이 자주 바뀌는 시스템을 유지보수한다면, 늦은 밤 배포를 몇 번은 줄일 수 있을 겁니다.

궁금한 점이 있으면 언제든 질문해 주세요.

0 조회
Back to Blog

관련 글

더 보기 »