LexQLexQ
블로그로 돌아가기
아키텍처비즈니스 로직룰 엔진

Config 파일, Feature Flag, 아니면 다른 무엇? 규모에 맞는 비즈니스 로직 관리

비즈니스 로직이 커지면 어디에 둬야 할까요? config 파일, feature flag, DB 룰 테이블, 관리형 플랫폼. 각 단계가 언제 한계에 부딪히는지 짚어봅니다.

Sanghyun Park·2026년 6월 7일11분 읽기

모든 백엔드 코드베이스에는 작게 시작한 config 파일이 하나씩 있습니다. 여기 timeout 하나, 저기 기능 한도 하나, 임계값 몇 개. 그러다 누군가 할인율을 고객 등급에 따라 다르게 줘야 한다고 합니다. 그다음엔 그 등급 로직에 시즌 프로모션 예외가 붙습니다. 그리고 어느 날 재무팀이 "지난 3월에 이 주문이 왜 이 할인을 받았는지" 묻습니다. 아무도 대답하지 못합니다.

바로 이 순간이 비즈니스 로직이 처음 놓아둔 자리를 넘어서는 지점입니다. 대부분의 팀이 이 지점을 만납니다. 그런데 미리 내다보는 팀은 드뭅니다. 변화가 점진적이기 때문입니다. 변경 하나하나는 작고, 그중 어느 것도 "파일을 무너뜨린 그 변경"처럼 느껴지지 않습니다.

비즈니스 로직은 보통 네 군데 중 하나에 자리잡습니다. config 파일, feature flag, 자체 DB의 룰 테이블, 그리고 관리형 플랫폼입니다. 팀은 대개 이 순서대로, 고통스러운 마이그레이션을 한 번씩 거치며 옮겨갑니다. 이 글은 각 단계가 무엇에 적합한지, 그리고 더 중요하게는 각 단계가 언제 더 이상 맞지 않게 되는지를 다룹니다. 다 읽고 나면 자신의 로직을 보고 "지금 어느 단계에 속하는가"를 짚을 수 있을 겁니다.

1단계: Config 파일

config 파일은 설정값을 위한 것입니다. 거의 바뀌지 않고 조건을 담지 않는 값들이죠. 요청 timeout, 페이지 크기 한도, 서비스 URL, 기능의 하드 한도. 어떤 값이 단일 숫자나 문자열이고 같은 로직이 그 값을 항상 같은 방식으로 읽는다면, config 파일이 맞는 자리입니다. 가능한 한 오래 머무르세요. 앞으로 만날 선택지 중 가장 저렴하니까요.

한계는 그 값이 무언가에 의존해야 하는 순간 드러납니다.

config 항목은 문장이 아니라 명사입니다. vip_discount_rate = 0.20은 담을 수 있습니다. 하지만 "VIP 고객은 $100 초과 주문에 20% 할인, 단 시즌 프로모션 중에는 예외, 그리고 할인은 $50까지"는 담지 못합니다. 이 문장에는 조건이 있고, 조건은 config가 아니라 그 값을 읽는 코드에 들어갑니다.

// config는 숫자만 갖습니다. 결정은 여전히 코드 안에 있습니다.
if (customer.tier() == Tier.VIP && order.subtotal() >= 100) {
    discount = order.subtotal() * config.vipDiscountRate();
}

여기서 두 문제가 겹칩니다. 첫째, 숫자가 아니라 로직을 바꾸려면 코드를 수정하고 다시 배포해야 합니다. 둘째, config는 과거의 결정이 왜 일어났는지를 전혀 기록해주지 않습니다. 지금 값이 무엇인지만 알려줄 뿐입니다.

config 파일을 넘어섰다는 신호는 분명합니다. 분기 로직이 코드 안으로 들어가 있고, config는 그저 상수만 던져주고 있는 상태입니다.

2단계: Feature flag

그래서 feature flag를 꺼내듭니다. 그리고 많은 팀이 여기서 길을 잘못 듭니다. flag가 마치 이 문제를 풀어주는 것처럼 보이기 때문입니다.

feature flag는 배포(deploy)와 출시(release)를 분리하기 위해 존재합니다. 코드를 꺼진 채로 배포하고, boolean을 toggle하고, 문제가 생기면 즉시 roll back합니다. 가지고 있으면 훌륭한 도구이고, LaunchDarkly나 Unleash 같은 도구가 이 일을 잘합니다. 기능을 출시하고 다시 내리는 데에는 flag가 맞는 답입니다.

하지만 비즈니스 로직을 담아 둘 곳은 아닙니다. 세 가지 이유 때문입니다.

flag는 boolean입니다. 비즈니스 로직은 여러 변수에 걸친 조건의 트리입니다. "VIP 할인: on/off"는 flag로 표현할 수 있습니다. 하지만 "VIP는 $100 초과에 20%, Gold는 10%, 둘 다 시즌 프로모션과 중첩 불가, 전체 $50 상한"은 flag로 표현할 수 없습니다. 실제 로직을, flag가 막아야 할 바로 그 코드 안으로 도로 밀어 넣지 않고서는요. 결국 flag는 여전히 애플리케이션 안에 남아 있는 결정 앞에 달린 on/off 스위치가 됩니다.

flag는 본래 일시적입니다. 기능을 출시하려고 하나 만들고, 끝나면 지웁니다. 반면 비즈니스 룰은 영구적이고 계속 바뀝니다. flag 시스템을 룰 엔진처럼 쓰면 flag를 더 이상 지우지 않게 됩니다. 그러면 flag 대시보드는 아무도 끝내 정리하지 않을 문서화되지 않은 결정 트리로 서서히 썩어갑니다.

일부 flag 플랫폼이 제공하는 targeting("세그먼트 X 사용자에게 variant B를 노출")은 실제로 쓸모 있는 기능입니다. 하지만 그것이 답하는 질문은 누가 그 기능을 보는가입니다. 비즈니스 로직이 답하는 질문은 어떤 사실이 주어졌을 때 어떤 값이 나와야 하는가, 그리고 왜인가입니다. flag의 출력은 variant입니다. 정작 필요한 출력은 근거가 함께 남는 계산 결과입니다.

feature flag는 "이 사용자에게 이게 켜져 있는가?"에 답합니다. 비즈니스 로직은 "이 사실들이 주어졌을 때 무슨 일이 일어나야 하는가?"에 답합니다. 서로 다른 질문이고, 코드베이스를 갉아먹고 있는 건 바로 두 번째 질문입니다.

3단계: DB의 룰 테이블

다음 수순은 올바른 직관입니다. 룰을 코드에서 빼내 데이터로 옮기는 거죠. rules 테이블을 만듭니다. 조건 컬럼, 액션 컬럼, 우선순위 컬럼. 그리고 행을 편집할 admin 화면을 붙입니다. 이제 룰은 배포 없이 바뀌고, 로직도 더 이상 애플리케이션 코드에 묻혀 있지 않습니다.

분명히 더 나은 방향입니다. 룰 데이터를 그것을 실행하는 엔진에서 분리해냈으니까요. 한 사람이 관리하는 작고 안정적인 룰 집합이라면, 여기서 멈추는 게 맞는 경우가 많습니다.

하지만 지금 직접 만들고 무기한 유지보수하기로 떠맡은 게 무엇인지 알아야 합니다.

  • 조건 평가기(evaluator). 행에 조건을 무엇으로 저장하나요? JSON? 작은 expression 문법의 문자열? 어느 쪽이든 그걸 안전하게 parse하고 평가할 무언가가 필요합니다. 이제 expression 엔진을 직접 작성하는 셈입니다. 이미 있는 것을 더 느리고 더 버그 많게 다시 만드는 팀을, 저는 한 번 이상 봤습니다.
  • 충돌 해소. 두 룰이 같은 입력에 매칭됩니다. 어느 쪽이 이기나요? 절대 둘 다 적용되면 안 되는 룰은요? 우선순위는 컬럼 하나지만, 그 우선순위를 지키는 로직은 결국 직접 짜야 하는 코드입니다.
  • 버전 관리. 누군가 라이브 룰을 편집했는데 그게 틀렸습니다. 이전 버전이 무엇이었고, 누가 바꿨고, 한 번에 roll back할 수 있나요? 맨 테이블은 그 위에 audit 레이어를 직접 쌓지 않는 한 이 중 아무것도 주지 않습니다.
  • production 전 테스트. 새 룰을 라이브로 올리기 전에, 그게 실제 트래픽에서 오작동하지 않으리란 걸 어떻게 확인하나요? 대부분의 자체 제작 테이블에는 여기에 대한 답이 없습니다. 행을 바꾸고 대시보드를 지켜볼 뿐이죠. 즉, 실제 고객을 상대로 production에서 테스트하는 셈입니다.
  • 결정별 추적 기록. 6주 전 거래가 결과를 받은 이유를 compliance가 물을 때, 그걸 재구성할 수 있나요? 변경 가능한 테이블에 실행별 기록이 없다면, 솔직한 답은 대개 "아니오"입니다.

룰 테이블은 데이터베이스 schema입니다. 룰 엔진은 거기에 더해 평가기, 충돌 해소, 버전 관리, 테스트, 감사 추적까지 갖춘 것입니다. schema는 한나절이면 끝낼 수 있습니다. 끝나지 않는 건 나머지이고, 바로 새벽 2시에 붙들고 있게 될 부분입니다.

4단계: 관리형 플랫폼

이것이 제목의 "다른 무엇"입니다. 그리고 앞의 작업이 더 이상 감당할 만한 부업이 아니게 됐을 때의 답이기도 합니다.

관리형 플랫폼은 테이블처럼 룰을 애플리케이션 코드 밖에 두되, 어려운 부분은 플랫폼이 맡습니다. 평가, 버전 관리, 실제 데이터 기반 테스트, 모든 결정의 추적 기록까지요. 룰은 작성하되, 엔진을 직접 만들고 돌보지는 않습니다.

제 입장을 분명히 밝히겠습니다. 저는 "룰 테이블이 조용히 자체 제작 엔진이 되어가는" 경로를 너무 여러 번 봐서 LexQ를 만들었습니다. 제가 6년간 Spring 백엔드를 하면서 본 것도 포함해서요. LexQ는 의사결정 운영 플랫폼입니다. 룰은 버전 관리되고, 라이브 전에 실제 production 트래픽으로 테스트되며, 모든 실행이 전체 추적 기록을 남깁니다. LexQ가 하는 세 가지는 자체 제작 테이블에서 비어 있던 바로 그 부분을 메웁니다.

  • 라이브 전에 실제 production 트래픽에서 룰 변경을 테스트(Test)합니다. 이것이 변경 영향 시뮬레이션입니다. 후보 룰셋을 과거 실행 기록에 돌려보고, 현재 룰셋과 결과를 비교합니다.
  • 전체 추적으로 모든 결정을 이해(Understand)합니다.
  • 자신 있게 배포(Deploy)합니다.

룰은 코드가 아니라 데이터입니다. 사실(facts)을 보내면 결과와 그 근거를 돌려받습니다.

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": {
      "loyalty_tier":"VIP",
      "purchase_subtotal_usd":150.00
    },
    "context": {}
  }'

응답에는 입력, 변경된 값, 그리고 자체 제작 테이블이 거의 갖지 못하는 부분인 룰별 실행 추적과 최종 결정 추적이 담깁니다.

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

decisionTraces 블록이 "왜 이런 일이 일어났는가"에 대한 답입니다. 6주 뒤에 대시보드를 보며 재구성하는 게 아니라, 결정이 내려진 바로 그 순간에 기록된 것입니다.

이것이 대부분의 팀에게 빠져 있는 컬럼입니다. LexQ는 그 "managed(관리형)" 컬럼이 실제로 어떤 모습인지 보여줍니다.

단계를 규모에 맞추기

이건 맨 위 칸이 항상 최선인 사다리가 아닙니다. 도구마다 알맞은 상황이 따로 있습니다. 네 선택지를 나란히 놓으면 이렇습니다.

config 파일feature flag룰 테이블 (직접 구현)관리형 플랫폼
용도정적 설정값출시 토글동적 룰규모가 커진 비즈니스 룰
조건을 담는가?못 담음거의 못 담음 (boolean)담음 (평가기를 직접 구현)담음 (내장)
배포 없이 변경불가가능가능가능
라이브 전 실제 트래픽 테스트해당 없음제한적직접 구현내장 (변경 영향 시뮬레이션)
결정별 추적 기록없음 (git 이력뿐)flag 로그직접 구현내장 (decision trace)
엔진을 누가 유지보수하나본인본인 / 벤더본인LexQ

컬럼을 따라 내려가면 경계가 구체적으로 보입니다.

  • config 파일: 거의 안 바뀌고 조건이 없는 설정값. 분기 로직이 코드로 옮겨가기 전까지는 건드리지 마세요.
  • feature flag: 기능을 출시하고 roll back하는 일. 딱 그 용도로만 쓰세요. 비즈니스 조건을 flag에 넣고 싶어지는 순간이, flag를 하나 더 추가하라는 신호가 아니라 멈추라는 신호입니다.
  • DB의 룰 테이블: 룰 몇 개, 그것을 관리하는 엔지니어 한 명, 드문 변경, 그리고 아무도 감사하지 않는 결과. 이 구간이라면 자체 제작 테이블이 분명히 맞는 선택입니다.
  • 관리형 플랫폼: 룰이 불어나고, 둘 이상이 안전하게 룰을 바꿔야 하고, 변경이 돈이나 사용자에 닿기 전에 테스트해야 하고, 언젠가 누군가 "왜 이 결정이 났는지" 묻게 될 때.

LexQ가 필요 없는 경우

솔직히 두 가지 경우뿐입니다.

config 값 하나로 로직이 다 커버된다면 이 중 아무것도 필요 없습니다. 임계값 하나 때문에 관리형 플랫폼은커녕 룰 테이블도 세우지 마세요. config 파일의 상수가 정답이고, 그 위에 무언가를 덧씌우는 건 비용일 뿐입니다.

엔지니어 한 명이 로직을 혼자 맡고, 변경이 드물고, 결과를 감사하는 사람이 없다면, 자체 DB의 룰 테이블이면 충분합니다. 관리형 플랫폼은 아직 정당화하기 어려운 overhead죠. 옮길 신호는 "할 수 있어서"가 아닙니다. "이게 계속 깨지고, 둘 이상이 건드리고, 왜 결정이 났는지 답할 수 없어서"입니다. 이 중 최소 둘이 사실이 되기 전까지는 지금 자리에 머무르세요.

출시에는 feature flag. 비즈니스 로직에는 의사결정 운영 플랫폼.

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


관련 글

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

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

무료로 시작하기