LexQLexQ
블로그로 돌아가기

하드코딩 vs 룰 엔진: 전환 시점은 언제인가

if-else로 잘 돌아가던 코드가 어느 순간 짐이 되는 순간이 있습니다. 하드코딩한 비즈니스 규칙이 부채가 되는 시점과 마이그레이션 방법을 정리했습니다.

Sanghyun Park·2026년 4월 13일11분 읽기

아무도 건드리고 싶지 않은 코드

어느 코드베이스에나 아무도 열고 싶어 하지 않는 파일이 하나씩 있습니다. 이름은 대개 PricingService, DiscountCalculator, EligibilityChecker 같은 것들입니다. 처음 만들 때는 20줄이었습니다. 지금은 800줄이고, 중첩 조건문이 몇 겹인지 세어본 사람도 없습니다. 이 파일 전체를 이해하는 사람은 조직에 남아 있지 않습니다.

if (customer.getTier().equals("VIP") && cart.getTotal() > 100_000) {
    if (!promotion.isBlackoutPeriod() && !cart.hasExcludedCategory("ELECTRONICS")) {
        if (customer.getLoyaltyYears() >= 3) {
            discount = 0.20;
        } else {
            discount = 0.15;
        }
    } else if (cart.getTotal() > 200_000) {
        discount = 0.10; // 블랙아웃 기간 대형 주문 예외
    }
} else if (customer.getTier().equals("GOLD")) {
    // ... 40줄 더
}

if가 세 겹으로 겹쳐 있습니다. VIP 티어인지 보고, 장바구니 합계가 10만원을 넘는지 보고, 블랙아웃 기간과 제외 카테고리 ELECTRONICS를 걸러낸 다음, 충성도 3년을 기준으로 20%와 15%를 가릅니다. 여기에 블랙아웃 기간이라도 20만원이 넘는 대형 주문이면 10%를 주는 예외가 하나 더 붙습니다. GOLD 티어부터는 주석 한 줄로 넘어갑니다. // ... 40줄 더.

먼저 짚고 갈 게 있습니다. 이 코드는 고장나지 않았습니다. 2년째 프로덕션에서 돌고 있고 수백만 건을 처리했습니다. 이 파일로 올라온 결함 보고서는 없습니다.

문제는 바꾸는 데 드는 비용입니다. 임계값 하나를 조정하거나 조건 하나를 추가할 때마다 같은 순서가 그대로 반복됩니다. Jira 티켓이 생기고, 개발자가 마지못해 파일을 엽니다. 딱 그 시나리오 하나만 커버하는 테스트를 짜고, // TODO: 리팩토링 주석을 달고 배포합니다.

그 리팩토링은 끝내 일어나지 않습니다. 정리해서 얻는 이득보다 잘못 건드렸을 때 치를 비용이 크기 때문입니다. 이 판단은 대체로 옳습니다. 그래서 파일은 계속 자랍니다.

하드코딩의 한계가 드러나는 5가지 신호

선부터 긋겠습니다. 모든 애플리케이션에 룰 엔진이 필요하지는 않습니다. 할인 로직이 price * 0.9 한 줄이고 1년에 한 번 바뀐다면 하드코딩이 정답입니다. 그 위에 계층을 얹는 쪽이 손해입니다.

아래 다섯 가지는 하드코딩이 한계에 닿았는지 판정하는 신호입니다.

1. 비즈니스 변경에 코드 배포가 필요합니다

기획자가 무료 배송 기준을 10만원에서 7만5천원으로 낮춰 달라고 합니다. 기획자 머릿속에서 이 작업은 5분짜리입니다. 실제로는 3일이 걸립니다.

3일을 채우는 건 로직이 아니라 절차입니다. PR을 올리고, 코드 리뷰를 받고, QA를 거치고, 스테이징에 올리고, 프로덕션에 배포하고, 배포 후 지표를 모니터링합니다. 어느 하나 건너뛸 수 있는 게 없습니다. 다만 이 절차는 숫자 하나를 바꾸려고 만들어진 게 아닙니다.

규칙이 바뀌는 속도를 정하는 건 규칙의 내용이 아니라 소프트웨어 개발 사이클입니다.

2. 전체 규칙을 아는 사람이 없습니다

팀에 한 번 물어보세요. "지금 프로덕션에 적용 중인 할인 규칙 전부 말해줄 수 있어?"

답이 바로 나오지 않고 여러 파일의 소스 코드를 열어봐야 나온다면, 그건 코드 문제가 아니라 지식 관리가 무너진 겁니다. 비즈니스 로직의 원천 정보(source of truth)를 확인하는 데 IDE가 필요해서는 안 됩니다.

3. 규칙이 충돌하는데 아무도 모릅니다

VIP 할인 20%, 충성도 할인 15%, 여기에 프로모션 쿠폰까지 한 고객에게 동시에 걸립니다. 최종 할인율 45%. 의도한 건가요? 아무도 모릅니다.

하드코딩된 규칙이 여러 서비스에 흩어져 있으면 충돌을 찾아내는 방법은 사람이 코드를 하나씩 읽어 보는 것뿐입니다. 서비스가 늘어나면 그마저도 안 됩니다.

4. 테스트로 못 잡는 케이스가 프로덕션에서 터집니다

규칙이 3개일 때는 테스트로 충분합니다. 20개가 되면 서로 겹치는 조합이 수백 가지로 불어납니다. 전수 테스트는 현실적으로 불가능합니다. 그래서 예상하지 못한 조합이 프로덕션에서 터집니다.

장애 보고서에는 매번 같은 문장이 들어갑니다. "테스트를 더 촘촘하게 짜야 합니다." 그 처방은 다음 사고를 막지 못합니다. 더 촘촘하게 짜도 다음번에 또 빠진 케이스가 나오기 때문입니다.

5. 엔지니어링 대기열이 쌓입니다

기획자와 운영팀, 비즈니스 분석가는 어떤 규칙을 원하는지 정확히 알고 있습니다. 이들에게 없는 건 지식이 아니라 그걸 구현할 수단입니다.

그래서 모든 규칙 변경이 스프린트 백로그에서 기능 개발과 자리를 놓고 경쟁합니다. "이 임계값 좀 바꿔주세요" Jira 티켓은 우선순위에서 계속 밀리고, 그렇게 대기열이 쌓입니다.

다섯 개 중 세 개 이상 해당한다면, 규칙을 외부화할지가 아니라 언제 할지가 남은 질문입니다.

여기 적은 다섯 가지는 코드에서 드러나는 신호입니다. 코드를 열기 전에 먼저 눈에 띄는 것들도 있는데, 그쪽은 팀이 일하는 방식에서 먼저 튀어나오는 신호로 따로 정리해 뒀습니다.

룰 엔진이 바꾸는 것

룰 엔진은 비즈니스 규칙을 바꾸기 어려운 곳에서 바꾸기 편한 곳으로 옮깁니다. 어려운 곳은 애플리케이션 코드이고, 편한 곳은 규칙을 다루려고 만든 전용 시스템입니다.

자리를 옮기는 일입니다. 규칙 자체는 하나도 줄지 않습니다. 800줄이 내리던 판단은 그대로 남고, 그 판단이 놓이는 자리만 달라집니다.

아키텍처 비교

하드코딩 방식에서 요청 하나가 반영되는 경로입니다.

비즈니스 요청 → Jira 티켓 → 개발자 PR → 코드 리뷰
→ QA → 스테이징 → 프로덕션 배포 → 모니터링
소요: 2~5일

룰 엔진 방식에서는 이렇게 됩니다.

비즈니스 요청 → 콘솔/API → 시뮬레이션 → 배포
소요: 5~30분

단계가 8개에서 4개로 줄고 소요 시간이 25일에서 530분으로 줄었습니다. 그런데 진짜 차이는 시간이 아니라 관심사의 분리입니다.

애플리케이션 코드가 맡는 건 어떻게입니다. 결제를 처리하고, 주문을 이행하고, API를 오케스트레이션합니다. 규칙이 맡는 건 무엇을입니다. 누구에게 할인을 줄지, 어떤 주문에 승인이 필요한지, 언제 거래를 차단할지.

이 둘이 한 파일에 섞여 있으면 한쪽을 고치다가 다른 쪽을 깹니다. 할인율을 바꾸려고 연 파일에서 결제 호출 순서를 건드리게 됩니다. 분리해 두면 각각 따로 진화합니다.

코드에 남는 것

룰 엔진이 걷어가는 건 애플리케이션 안의 의사결정 로직입니다. 그래서 코드가 하는 일은 세 가지로 줄어듭니다. 관련 fact를 담아 룰 엔진 API를 호출하고, 어떤 규칙이 매칭됐고 어떤 액션을 취해야 하는지 응답을 받고, 그 액션을 실행합니다. 할인을 적용하고, 알림을 보내고, 거래를 차단하는 일이 그 실행에 해당합니다. 앞에서 본 800줄 중첩 조건문이 하던 일이 이 세 가지가 됩니다.

호출은 이렇게 생겼습니다.

# 애플리케이션이 룰 엔진을 호출합니다
curl -X POST https://api.lexq.io/api/v1/execution/groups/{groupId} \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "facts": {
      "loyaltyTier": "VIP",
      "purchaseSubtotalUsd": 150.00
    },
    "context": {}
  }'

loyaltyTierVIP이고 purchaseSubtotalUsd가 150.00인 요청 하나입니다. 응답은 이렇습니다.

{
  "result": "SUCCESS",
  "data": {
    "traceId": "4943c160-...",
    "inputFacts": {
      "loyaltyTier": "VIP",
      "purchaseSubtotalUsd": 150.00
    },
    "mutatedFacts": {
      "purchaseSubtotalUsd": 120.00
    },
    "generatedVariables": {
      "purchaseSubtotalUsd__delta": -30.00
    },
    "executionTraces": [
      {
        "tenantId": "acme-corp",
        "policyGroupId": "01f2b274-...",
        "policyVersionId": "a6062090-...",
        "ruleId": "3b16ced1-...",
        "ruleName": "VIP 20% 할인",
        "executedAt": "2026-04-27T09:44:10Z",
        "matched": true,
        "matchExpression": "(loyaltyTier == 'VIP') && (purchaseSubtotalUsd >= 100)",
        "inputFacts": {
          "loyaltyTier": "VIP",
          "purchaseSubtotalUsd": 150.00
        },
        "generatedActions": [
          {
            "type": "MUTATE_FACT",
            "parameters": {
              "operand": 20,
              "method": "PERCENTAGE",
              "targetVar": "purchaseSubtotalUsd",
              "operator": "SUB",
              "rounding": { "mode": "HALF_UP", "scale": 2 }
            }
          }
        ]
      }
    ],
    "decisionTraces": [
      {
        "ruleId": "3b16ced1-...",
        "ruleName": "VIP 20% 할인",
        "policyGroupId": "01f2b274-...",
        "policyVersionId": "a6062090-...",
        "status": "SELECTED",
        "reasonCode": "FINAL_WINNER",
        "reasonDetail": null
      }
    ]
  }
}

이 응답을 받은 애플리케이션이 하는 일은 20% 할인을 적용하는 것 하나입니다.

data 블록은 상태를 정확히 세 층위로 갈라서 보여줍니다. inputFacts는 들어온 값, mutatedFacts는 규칙이 바꾼 값, generatedVariables는 규칙이 새로 만든 값입니다. 어떤 값이 어디서 왔는지 섞이지 않습니다.

executionTraces에는 평가된 모든 규칙과 정확한 매칭 표현식, 그리고 생성된 액션이 남습니다. decisionTraces에는 최종 승자와 그 사유가 남습니다. 둘을 합치면 엔지니어가 옆에서 해석해 주지 않아도 읽히는 완전한 기록이 되고, 이 기록은 추가 전용(append-only)으로 보존됩니다. 6개월 뒤에 고객이 왜 20% 할인을 받았느냐고 물어도 그날 실행의 traceId 하나면 답을 꺼낼 수 있습니다.

내일 이 비율을 25%로 올려야 한다면, 애플리케이션 코드는 한 줄도 건드리지 않습니다. 규칙만 고칩니다. 새 비율이 실제로 걸렸는지는 다음 실행 응답의 mutatedFacts에서 확인합니다.

그 변경이 프로덕션에 닿기까지의 실제 경로는 배포 파이프라인을 타지 않고 값을 바꾸는 절차에서 단계별로 정리했습니다.

마이그레이션 방법

한 번에 전부 옮기려는 시도는 실패합니다. 800줄을 통째로 들어내는 계획은 중간에 멈추고, 멈춘 자리에서 두 시스템을 동시에 떠안게 됩니다.

아래 다섯 단계로 하나씩 옮기는 편이 낫습니다. 그 전에 결정할 게 하나 있습니다. 어느 엔진으로 옮길지입니다. 실무에서 이 선택은 대개 Drools를 직접 운영할지 관리형 엔진을 빌릴지의 양자택일로 좁혀집니다.

1단계: 규칙 하나를 고릅니다

가장 자주 바뀌고 가장 많은 마찰을 일으키는 규칙을 하나 고릅니다. 가격과 할인, 자격 검증, 승인 워크플로가 전형적인 후보입니다.

가장 복잡한 규칙을 고르지는 마세요. 기획팀이나 운영팀이 "이건 바꾸는 데 너무 오래 걸린다"고 가장 크게 불만을 내는 규칙을 고릅니다.

2단계: 현재 로직을 정리합니다

기존 코드에 있는 모든 조건과 결과를 문서로 적습니다. 다섯 단계 가운데 가장 고통스럽고 가장 오래 걸리는 대목이 여기입니다.

적다 보면 반드시 세 종류가 나옵니다. 아무도 기억하지 못하는 규칙, 다른 규칙과 모순되는 엣지 케이스, 그리고 조건상 절대 도달할 수 없는 분기입니다.

이 단계는 룰 엔진을 끝내 도입하지 않더라도 남습니다. 여러 파일에 흩어져 있던 판단이 문서 하나로 모인 것만으로도 값어치가 있습니다.

3단계: 룰 엔진에 같은 규칙을 만듭니다

기존 코드의 규칙을 룰 엔진 콘솔에서 똑같이 정의합니다. 그다음 섀도 실행을 겁니다. 실제 요청이 들어올 때마다 기존 코드와 룰 엔진 양쪽에 같은 입력을 보냅니다.

할인을 실제로 적용하는 건 기존 코드입니다. 룰 엔진은 "나라면 이 결과를 줬을 것이다"를 기록만 합니다. 두 결과가 갈리면 마이그레이션에서 빠뜨린 조건이 있다는 뜻입니다.

해 보면 2단계 문서화에서 놓친 엣지 케이스가 거의 반드시 나옵니다.

4단계: 실제 데이터로 시뮬레이션을 돌립니다

전환 직전 마지막 확인입니다. 최근 10,000건의 실제 주문 데이터를 넣고, 기존 코드가 내린 결정과 룰 엔진이 내리는 결정을 나란히 비교합니다.

결과가 같으면 전환 준비가 끝난 겁니다. 다르면 아직 옮기지 못한 규칙이 남아 있다는 뜻입니다. 프로덕션에 영향이 가기 전에 발견했으니 오히려 다행인 쪽입니다.

5단계: 전환합니다

애플리케이션 코드의 if-else 블록을 룰 엔진 API 호출로 바꿉니다.

기존 코드는 바로 지우지 마세요. 주석 처리한 상태로 30일 둡니다. 문제가 생기면 주석만 풀면 되돌아갑니다. 30일 동안 아무 일도 없으면 그때 지웁니다. 다시 찾을 일은 없습니다.

전환하지 말아야 할 때

룰 엔진은 공짜가 아닙니다. 네트워크 홉이 하나 늘고, 외부 의존성이 하나 늘고, 운영 복잡성이 늘어납니다. 이 교환이 언제나 남는 장사는 아닙니다.

하드코딩을 유지해야 할 때:

  • 규칙이 분기에 한 번 바뀔까 말까 할 때
  • 전체 규칙이 5개 이하일 때
  • 순수하게 기술적인 규칙일 때 (재시도 정책, 서킷 브레이커 임계값)
  • 레이턴시 요구사항이 서브밀리초일 때 (고빈도 거래)

룰 엔진을 고려해야 할 때:

  • 규칙이 주 단위 이상으로 자주 바뀔 때
  • 여러 이해관계자가 규칙을 직접 수정해야 할 때
  • 규칙 충돌이 프로덕션 장애의 반복 원인일 때
  • 모든 규칙 변경과 실행에 대한 감사 추적이 필요할 때
  • 배포 전에 실제 데이터로 규칙 변경을 테스트하고 싶을 때

작게 시작하세요

마이그레이션에 한 방을 걸 필요는 없습니다. 규칙 하나를 골라 코드 바깥으로 꺼내고, 팀의 속도가 실제로 개선되는지 봅니다. 개선되면 다음 규칙으로 넓히면 됩니다.

안 되더라도 잃는 건 하루치 셋업 비용입니다. 분기 하나는 그대로 남아 있습니다.

규칙이 하나에서 끝나지 않고 여러 개가 코드 바깥으로 나오기 시작하면 문제의 성격이 바뀝니다. 그때부터 다뤄야 하는 건 엔진을 어떻게 호출하느냐가 아니라 버전과 감사 기록을 갖춘 체계로 의사결정을 운영하는 일입니다.

LexQ는 엔지니어링 팀을 위한 의사결정 운영 플랫폼입니다. 정확히 이 문제를 풀려고 만들었습니다. 무료 플랜에 월 1,000건 실행이 포함되어 있어서 프로덕션 수준의 PoC를 돌려 보기에 충분합니다.

lexq.io에서 무료로 시작하기

결정을 배포 파이프라인 밖으로 꺼낼 준비가 되셨나요?

신용카드 없이 무료로 시작하세요. fact를 보내면 결과와 근거를 돌려받습니다.

무료로 시작하기