애플리케이션 코드 수정 없이 비즈니스 규칙을 바꾸는 방법
비즈니스 로직이 배포 파이프라인의 모든 무게를 함께 짊어질 이유는 없습니다. 규칙을 코드에서 분리하는 세 가지 방법과 각각이 무너지는 지점을 짚어봅니다.
다섯 글자를 고치는 데 2주가 걸립니다
오전 10시. PM이 메시지를 보냅니다.
"VIP 할인을 10%에서 15%로 바꿔주세요. 마케팅팀이 금요일까지 반영되길 원해요."
고쳐야 할 자리는 이미 압니다. 지난 몇 달 동안 여러 번 열어본 파일이고, 그 안의 상수 하나입니다.
private static final double VIP_DISCOUNT_RATE = 0.10;
0.10을 0.15로. 편집 자체는 4초입니다. 그 다섯 글자가 고객에게 닿기까지는 2주가 걸립니다.
내역은 이렇습니다. 브랜치를 따고 PR을 올립니다. 리뷰어 두 명을 지정하지만 한 명은 회의에 들어가 있고, 한 명은 점심을 먹으러 갔고, 둘 다 자기 몫의 "진짜 일"을 하고 있습니다. 퇴근 무렵에 approve 하나가 붙습니다.
다음 날 아침에는 QA가 테스트 실패를 발견합니다. 이번 변경과 아무 상관 없는 테스트인데, 내 브랜치에서 터졌으니 내 몫이 됩니다. 원인을 찾고, 고치고, 푸시하고, 파이프라인을 다시 돌리는 동안 수요일이 지나갑니다.
목요일에 main으로 머지되고, 스테이징에는 야간 배포로 올라갑니다. QA 리드는 프로덕션에 나가기 전에 스테이징에서 한 번 더 확인하겠다고 합니다. 금요일 아침에 승인이 떨어지고, 오후 2시에 라이브가 됩니다.
마감은 지켰습니다. 대신 PM 쪽에는 다른 게 남습니다. 이 팀에 "간단한 변경"이라는 범주가 존재하지 않는다는 인식입니다.
여기서 짚을 게 있습니다. 이 2주 어디에도 실제로 위험한 작업은 없었습니다. 데이터 구조를 건드리지 않았고, 스키마를 바꾸지 않았고, 외부에 노출된 API 시그니처도 그대로입니다. 배포된 것은 소수점 한 자리입니다.
문제는 팀이 느린 게 아닙니다. 비즈니스 로직이 애초에 배포 파이프라인 안에 놓여 있다는 배치가 문제입니다.
다섯 글자가 2주를 먹는 이유
먼저 인정하고 갈 것이 있습니다. 애플리케이션 코드는 그 무거운 파이프라인을 받을 자격이 있습니다. 코드 리뷰, 자동화 테스트, 카나리, 롤백 절차. 이 의식(ceremony)들은 관료주의라서 생긴 게 아니라 깨진 코드를 프로덕션에 올렸을 때 치르는 값이 크기 때문에 생겼습니다. 잘못된 DB 마이그레이션 하나가 데이터를 망가뜨립니다. 잘못된 API 변경 하나가 외부 연동을 끊습니다. 이런 일에 2주는 아깝지 않습니다.
비즈니스 규칙은 종류가 다릅니다. 판별 기준은 셋입니다. 데이터 구조를 바꾸지 않고, DB 스키마를 건드리지 않고, 외부와 맺은 계약도 그대로 둡니다. 규칙이 실제로 하는 말은 이 정도입니다.
장바구니 금액이 10만원을 넘고 고객 등급이 VIP면 15% 할인.
그런데 이 문장이 자바 상수로 서비스 안에 박히는 순간, 규칙은 코드의 속성을 전부 물려받습니다. 테스트를 붙여야 하고, 리뷰 프로세스를 통과해야 하고, 배포 사이클을 기다려야 하고, 온콜 로테이션의 관심사가 되고, 롤백 도구의 대상이 됩니다. 규칙이 그 대접을 필요로 해서가 아닙니다. 규칙이 놓인 자리가 그렇게 만듭니다.
비용의 진짜 원인은 여기 있습니다. 배포 인프라는 "DB 커넥션 풀 리팩토링"과 "할인율 0.10을 0.15로"를 구분하지 못합니다. 둘 다 코드 변경이고, 파이프라인은 코드 변경 하나에 똑같은 무게를 매깁니다.
그래서 배포를 더 빠르게 만드는 쪽으로는 답이 나오지 않습니다. 애플리케이션 코드와 비즈니스 규칙이 다른 종류의 일이라는 것을 인정하고, 처음부터 다르게 다루는 것이 답입니다.
남는 질문은 시점입니다. 상수를 언제 코드 밖으로 꺼낼지는 팀마다 다릅니다. 판단이 서지 않는다면 하드코딩한 조건문이 한계에 닿았다는 신호를 먼저 보세요.
비즈니스 규칙을 코드에서 분리하는 세 가지 방법
선택지는 셋입니다. 다만 셋 중 하나를 고르는 문제로 보면 판단이 어긋납니다. 이 셋은 서로를 배제하는 후보가 아니라 하나의 스펙트럼 위에 놓인 위치입니다. 오른쪽으로 갈수록 유연성이 올라가고, 같은 방향으로 복잡도도 함께 올라갑니다.
Option 1: Config 파일
가장 먼저 떠오르는 방법입니다. 값을 코드 밖으로 빼서 config 파일이나 환경 변수에 둡니다.
# pricing-config.yaml
vip_discount_rate: 0.15
min_order_for_discount: 100000
서비스는 시작할 때, 또는 주기적으로 이 파일을 읽습니다. 값을 바꾸려면 파일을 고치고 재시작하면 되고, 핫 리로드가 붙어 있으면 재시작도 필요 없습니다. CI/CD를 태우는 애플리케이션 재배포는 어느 쪽이든 없습니다.
여기까지가 config 파일이 해결하는 범위입니다. 리뷰 사이클을 거치지 않고 단순한 값 하나를 바꿀 수 있습니다.
해결하지 못하는 것은 넷입니다.
첫째, 진짜 로직이 들어오는 순간 무너집니다. "유럽 VIP는 15%, 나머지는 10%" 한 줄이면 YAML은 담을 수 없는 것을 담기 시작합니다. 둘째, 변경 이력이 없습니다. 값은 남지만 누가 언제 왜 바꿨는지는 어디에도 없습니다. 셋째, 타입 검증이 없습니다. float 자리에 문자열이 들어가면 프로덕션에서 터집니다. 넷째, 미리 보기가 없습니다. "20%로 하면 어떻게 되죠?"에 답하려면 실제로 20%로 설정해서 트래픽에 태우는 것 말고는 방법이 없습니다.
적정선은 분명합니다. 튜닝 가능한 파라미터 몇 개까지입니다. 분기 조건이 들어간 로직이 등장하면 역부족입니다.
Option 2: DB 테이블 + 어드민 UI
자연스러운 다음 단계입니다. 규칙을 테이블 행으로 만들고, 그 위에 작은 어드민 UI를 얹어 기획자와 운영 담당자가 직접 편집하게 합니다.
CREATE TABLE discount_rules (
id SERIAL PRIMARY KEY,
customer_tier VARCHAR(20),
region VARCHAR(20),
min_cart_total DECIMAL(10,2),
discount_rate DECIMAL(4,3),
priority INT,
active BOOLEAN
);
런타임에 서비스는 이 테이블을 쿼리하고, 매칭되는 규칙을 골라 할인을 적용합니다. 어드민 UI에서는 할인율을 바꾸고, 새 등급을 추가하고, 규칙을 켜고 끕니다.
세 가지가 해결됩니다. 코드 리뷰 사이클 없이 값을 직접 바꿀 수 있고, 감사 컬럼을 몇 개 붙이면 DB가 이력을 보관해주고, 스키마가 표현할 수 있는 범위 안에서는 분기 로직도 담깁니다.
문제는 시간이 지나면서 터집니다.
- 스키마가 곧 규칙 언어입니다. "시간대" 조건 하나를 추가하려면 스키마 마이그레이션을 하고, 이 테이블을 읽는 모든 서비스의 코드를 고쳐야 합니다. 배포 문제에서 빠져나왔는데 규칙 구조 레벨에서 같은 문제를 다시 만든 꼴입니다.
- 우선순위와 충돌 해소를 직접 떠안습니다. 두 규칙이 동시에 매칭되면 어느 쪽을 쓸지 누군가 정해야 합니다. priority 컬럼을 넣고, 동점을 처리하고, 예외를 붙이다 보면 결국 엔진을 직접 짜고 있습니다.
- 시뮬레이션이 없습니다. active 플래그를 토글하는 순간 라이브 트래픽에 그대로 반영됩니다. 잘못됐을 때 그 사실을 알려주는 채널은 화가 난 고객입니다.
- 소유 범위가 통째로 넘어옵니다. 어드민 UI, 감사 로그, 권한 시스템, API, 테스트 하네스까지 팀이 짓고 유지합니다. 세 번째 프로덕트 팀이 찾아와 "작은 기능 하나만"이라고 말하는 지점에서 무너집니다.
저는 예전 회사에서 이 경로를 두 번 지었습니다. 두 번 다 아무도 유지보수를 맡고 싶어 하지 않는 시스템으로 자랐습니다. 팀이 못해서가 아닙니다. 그걸 짓는 게 본업이 아니었기 때문입니다. 본업은 제품을 내보내는 일이었습니다.
Option 3: 외부 의사결정 운영 플랫폼
세 번째는 이 문제 하나만을 위해 만들어진 전용 인프라를 쓰는 것입니다. 규칙은 애플리케이션 밖, 규칙을 저장하고 버전을 매기고 시뮬레이션하고 실행하기 위해 설계된 시스템 안에 있습니다.
못박아 둘 것이 있습니다. 여기서 말하는 건 특정 제품이 아니라 하나의 범주입니다. 규칙이 코드 밖으로 나간 뒤에 조직이 어떻게 굴러가는지는 의사결정을 운영 대상으로 다루는 방식에 따로 정리해 두었습니다.
호출은 이렇게 생겼습니다. 서비스가 POST /api/v1/execution/groups/{groupId}를 x-api-key 헤더와 함께 부르고, 바디에 facts와 context를 담습니다.
# 서비스가 플랫폼을 호출합니다
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": 200.00
},
"context": {}
}'
플랫폼은 전달받은 facts에 규칙을 실행하고 결과를 돌려줍니다.
{
"result": "SUCCESS",
"data": {
"traceId": "4943c160-...",
"inputFacts": {
"loyaltyTier": "VIP",
"purchaseSubtotalUsd": 200.00
},
"mutatedFacts": {
"purchaseSubtotalUsd": 170.00
},
"generatedVariables": {
"purchaseSubtotalUsd__delta": -30.00
},
"executionTraces": [
{
"tenantId": "acme-corp",
"policyGroupId": "01f2b274-...",
"policyVersionId": "a6062090-...",
"ruleId": "3b16ced1-...",
"ruleName": "VIP 15% 할인",
"executedAt": "2026-04-21T09:36:48Z",
"matched": true,
"matchExpression": "(loyaltyTier == 'VIP') && (purchaseSubtotalUsd >= 100)",
"inputFacts": {
"loyaltyTier": "VIP",
"purchaseSubtotalUsd": 200.00
},
"generatedActions": [
{
"type": "MUTATE_FACT",
"parameters": {
"operand": 15,
"method": "PERCENTAGE",
"targetVar": "purchaseSubtotalUsd",
"operator": "SUB",
"rounding": { "mode": "HALF_UP", "scale": 2 }
}
}
]
}
],
"decisionTraces": [
{
"ruleId": "3b16ced1-...",
"ruleName": "VIP 15% 할인",
"policyGroupId": "01f2b274-...",
"policyVersionId": "a6062090-...",
"status": "SELECTED",
"reasonCode": "FINAL_WINNER",
"reasonDetail": null
}
]
}
}
애플리케이션은 질문을 하고, 플랫폼은 결정과 그 결정의 설명을 함께 돌려줍니다.
data 안에는 traceId 하나와 다섯 개의 블록이 있습니다.
inputFacts: 들어온 값입니다. 고객 등급과 결제 금액.mutatedFacts: 규칙이 바꾼 값입니다. 15% 깎인 결제 금액.generatedVariables: 시스템이 자동으로 만드는 값입니다.mutatedFacts의 변경 하나마다{factKey}__delta키가 붙고 부호를 포함한 차이값이 담깁니다. 여기서는purchaseSubtotalUsd__delta가-30.00입니다.executionTraces: 평가된 모든 규칙과, 정확히 어떤 표현식으로 매칭됐는지, 그 결과 어떤 액션이 만들어졌는지.decisionTraces: 최종으로 선택된 규칙과 선택된 이유.
금요일 오후의 변경이 싸지는 이유가 여기 있습니다. PM이 "10%에서 15%로"라고 말한 것이 라이브 트래픽에 실제로 적용됐는지를 mutatedFacts.purchaseSubtotalUsd가 바로 확인해 줍니다. 애플리케이션 코드는 한 줄도 바뀌지 않았습니다.
값은 시간이 지난 뒤에 더 드러납니다. 이 고객이 왜 15% 할인을 받았는지 묻는 질문이 6개월 뒤에 들어오면 IDE를 열지 않습니다. 트레이스를 읽습니다. 이 fact가 들어왔고, 이 규칙이 이 값을 이렇게 바꿨고, 이 규칙이 최종 승자였습니다. 그 질문을 하는 쪽은 보통 재무팀이거나 컴플라이언스팀이거나 고객의 변호사입니다. 그때 손에 쥘 것이 있는지가 갈립니다.
정리하면 이 방식이 해결하는 것은 다섯입니다.
- 규칙 구조가 바뀌어도 코드 변경도 DB 마이그레이션도 필요 없습니다.
- 모든 변경에 버전이 붙고, 감사가 되고, 되돌릴 수 있습니다.
- 프로덕션에 닿기 전에 시뮬레이션이 먼저 돕니다. 새 규칙을 과거 데이터에 돌려서 결과를 봅니다.
- 기획자와 운영 담당자가 초안을 만들고, 엔지니어가 리뷰한 뒤 퍼블리시하는 워크플로우가 성립합니다.
- 실행 트레이스가 감사 때 바로 꺼내 쓰는 산출물이 됩니다.
물론 값을 치릅니다. 핫패스에 네트워크 홉이 하나 늘어납니다. 밀리초 단위지만 0은 아닙니다. 이해하고 신뢰하고 모니터링해야 할 시스템도 하나 늘어납니다. 그리고 외부 서비스에 의존하거나, self-host라면 그 운영 비용을 집니다.
나란히 놓고 비교
| 항목 | Config 파일 | DB 테이블 + UI | 의사결정 운영 플랫폼 |
|---|---|---|---|
| 애플리케이션 재배포 없이 변경 | ✅ | ✅ | ✅ |
| 분기 로직 표현 | ❌ | 제한적 | ✅ |
| 내장된 감사 추적 | ❌ | 직접 구현 | ✅ |
| 롤아웃 전 시뮬레이션 | ❌ | ❌ | ✅ |
| 초 단위 롤백 | ❌ | 직접 구현 | ✅ |
| 스키마 없이 규칙 구조 확장 | ❌ | ❌ | ✅ |
| 구축 비용 | 낮음 | 높음 | 0 (관리형) 또는 중간 (self-host) |
| 런타임 비용 | 0 | 0 | 네트워크 홉 +1 |
| 실행별 의사결정 trace | ❌ | ❌ | ✅ |
세 방식이 동률인 축은 하나뿐입니다. 애플리케이션 재배포 없이 값을 바꿀 수 있다는 점입니다. 나머지 여덟 개 축에서는 전부 갈립니다.
고르는 기준은 둘입니다. 규칙이 얼마나 자주 바뀌는지, 그리고 규칙 안에 로직이 실제로 얼마나 들어 있는지.
튜닝할 상수가 몇 개뿐이라면 config 파일입니다. 할인율 필드 다섯 개짜리 어드민 패널로 충분하다면 DB 테이블입니다. 세 팀이 주 단위로 편집하고, 분기 조건이 실제로 존재하고, 감사 요구사항이 있고, 프로모션이 잘못 나갔을 때 되돌려야 하는 가격 규칙이라면 의사결정 운영 플랫폼입니다.
가격만의 얘기도 아닙니다. 요금제별 기능 한도와 접근 권한이 같은 모양입니다. 등급이 하나 늘 때마다 조건문이 함께 늘어나는 영역을 보면 구조가 그대로 겹칩니다.
이 패턴이 필요 없는 경우
모든 팀이 규칙을 코드베이스에서 빼야 하는 건 아닙니다. 아래 세 경우라면 이 글의 조언은 무시해도 됩니다.
- 규칙이 한 분기(quarter)에 한 번도 바뀌지 않는 경우입니다. 2주짜리 배포 비용이 몇 달에 걸쳐 상각되면, 별도 시스템을 도입하는 오버헤드가 수지에 맞지 않습니다.
- 모든 규칙 변경이 정당하게 엔지니어 리뷰를 요구하는 경우입니다. 일부 규제 도메인, 예를 들어 특정 헬스케어 워크플로우나 특정 금융 규제 준수 로직은 변경 하나하나에 코드 리뷰와 테스트 커버리지와 완전한 배포 규율이 필요합니다. 건너뛰면 감사 요건 위반입니다. 이때 마찰은 없애야 할 마찰이 아니라 통제 그 자체입니다.
- 규칙이 딱 하나일 때입니다. 세율 하나, 피처 플래그 하나. 코드 안 상수로 충분하고, 플랫폼으로 옮겨도 제자리걸음입니다.
결국 글 전체는 질문 하나로 줄어듭니다.
모든 비즈니스 변경이 배포 파이프라인의 무게를 전부 짊어져야 하는 것인가요?
"아니오"라고 답할 일이 충분히 자주 생긴다면 분리하세요. "예"라면 지금 구조를 그대로 두세요. 후자도 그 자체로 합리적인 선택입니다.
LexQ가 놓이는 자리
LexQ는 그런 의사결정 운영 플랫폼 중 하나입니다. 유일한 해법이라고 말할 생각은 없습니다. 앞에서 설명한 범주의 한 사례이고, 비즈니스 결정을 배포 파이프라인 밖으로 꺼내고 싶은 팀을 위해 만들었습니다.
설계에서 하나만 짚자면 시뮬레이션을 부가 기능이 아니라 코어에 넣었습니다. 배포 게이트를 걷어내면 그 자리가 비는데, 배포 게이트를 걷어낸 자리를 예행연습이 대신해야 한다는 판단이 그대로 제품 구조가 됐습니다.
모든 팀에 맞지는 않습니다. 가격 로직을 DB 마이그레이션과 같은 파이프라인에 실어 나르는 데 지친 엔지니어링 팀에 맞습니다.