뒤바뀐 필드가 만든 버그와 그 해결
백엔드 리팩토링
1. 들어가며 — 필드 이름이 반대로 저장되어 있었습니다
설문 템플릿 에디터를 통해 ItemRule을 등록하면, 프론트엔드에서 실제 규칙이 의도대로 동작하지 않는다는 버그 리포트가 들어왔습니다.
StateRule의 경우 “이 항목이 선택되면 저 항목을 숨겨라”는 규칙을 등록했는데, 실제로는 반대로 동작하는 현상이었습니다. 처음엔 프론트엔드의 조건 평가 로직 문제로 의심했습니다.
코드를 따라가다 보니 문제는 더 근본적인 곳에 있었습니다. 백엔드 엔티티의 필드 이름 자체가 실제 의미와 반대로 붙어 있었습니다.
|
java // 실제 DB에 저장된 데이터의 의미 (수정 전) targetItemKey = "formId:Q3" // 실제로는 트리거(Source) sourceItemKeys = ["formId:Q1"] // 실제로는 효과 대상(Target) |
|---|
트리거(어떤 항목이 변경될 때)가 targetItemKey에, 효과 대상(어떤 항목에 규칙을 적용할지)이 sourceItemKeys에 저장되고 있었습니다. 이름이 반대로 붙어 있으니, 이 데이터를 읽는 프론트엔드가 혼란스러울 수밖에 없었습니다.
2. 얼마나 깊이 퍼진 문제였나
단순히 필드 이름만 바꾸면 되는 문제처럼 보였지만, 실제로는 전 계층에 걸쳐 있었습니다.
|
# |
문제 |
영향 |
|---|---|---|
|
1 |
targetItemKey ↔ sourceItemKeys 의미 반전 |
신규 개발자 혼란, 버그 유발 |
|
2 |
CalculationRule에 sourceItemKeys 필드 없음 |
소스 추적 불가, 의존성 분석 불가 |
|
3 |
소스가 variables, aggregationTargets, conditions 3곳에 분산 |
코드 복잡도 증가 |
|
4 |
AggregationTarget이 Source+Condition 혼합 |
단일 책임 원칙 위반 |
|
5 |
CDO → Entity 변환 시 sourceItemKeys 유실 가능성 |
데이터 정합성 문제 |
단순 네이밍 문제가 아니었습니다. CalculationRule은 계산에 필요한 입력 소스가 하나의 필드로 정리돼 있지 않아, 의존성 추적과 검증이 어려운 구조였습니다.
java
// CalculationRule 수정 전 — 소스를 어디서 찾아야 하는가?
private List<FormulaVariable> variables; // 여기도 소스
private List<AggregationTarget> aggregationTargets; // 여기도 소스
private List<EnableCondition> conditions; // 여기도 소스(조건 내 참조)
소스가 3곳에 분산돼 있고, AggregationTarget이라는 이름은 집계 대상인지 집계 조건인지도 불명확했습니다.
3. 어떻게 바꿨나
3-1. 필드 의미 재정의
가장 먼저 해야 할 일은 sourceItemKeys가 무엇을 의미하는지 다시 정의하는 것이었습니다.
sourceItemKeys = 트리거 (어떤 항목이 변경될 때 규칙이 발동하는가)
targetItemKeys = 효과 대상 (규칙이 적용될 항목들)
StateRule의 예를 들면:
|
“I7번 항목이 변경되면 → I5, I6번 항목을 숨겨라” sourceItemKeys: ["formId:Q3"] → 트리거 targetItemKeys: ["formId:Q1", "formId:Q2"] → 효과 대상 |
|---|
3-2. 영향 범위 전 계층 수정
이 정의를 기준으로 Entity, CDO, JPO, Store, Helper, RDO 전 계층을 수정했습니다.
|
Domain Entity → sourceItemKeys(트리거), targetItemKeys(효과 대상) 재정의 CDO → StateRuleCdo, CalculationRuleCdo 필드 구조 교정 JPO → DB 컬럼 매핑 수정 Store → 저장/조회 로직 정합성 확인 RDO → 프론트엔드 응답 구조 dual-output 적용 |
|---|
3-3. AggregationTarget → CalcSourceGroup 네이밍 정리
AggregationTarget이라는 이름이 Source와 Condition의 혼합 역할을 하고 있었습니다. 각 필드의 실제 역할을 명확히 한 CalcSourceGroup으로 교체했습니다.
java
// 수정 전
class AggregationTarget {
List<String> sourceKeys;
String conditionItemKey; // Source인가 Condition인가?
List<String> contextRelatedKeys;
}
// 수정 후
class CalcSourceGroup {
List<String> sourceKeys; // 계산할 실제 값의 출처 (순수 Source)
@JsonProperty("conditionItemKey") // JSON 키 하위호환 유지
String filterConditionKey; // 이 그룹의 필터 조건 (Condition)
List<String> contextRelatedKeys; // 의존성 맥락 출처
}
@JsonProperty("conditionItemKey")로 JSON 와이어 키는 유지해 기존 데이터 하위호환을 보장했습니다.
4. DB 마이그레이션 설계
코드가 바뀌어도 기존 DB 데이터는 그대로 뒤바뀐 상태로 남아 있었습니다. 배포와 함께 데이터도 교정해야 했습니다.
STATE의 경우 두 스텝의 순서가 중요하다. 먼저 기존 source_item_keys를 새 target_item_keys로 복사한 뒤, target_item_key를 새 source_item_keys로 이관해야 합니다. 순서가 바뀌면 원본 데이터를 덮어쓰게 됩니다.
sql
BEGIN;
-- Step 1: 기존 효과 대상(source_item_keys) → 새 효과 대상(target_item_keys)
UPDATE item_rule
SET target_item_keys = COALESCE(source_item_keys, '[]'::jsonb)
WHERE rule_type = 'STATE';
-- Step 2: 기존 트리거(target_item_key) → 새 트리거(source_item_keys)
UPDATE item_rule
SET source_item_keys = CASE
WHEN target_item_key IS NULL OR target_item_key = '' THEN '[]'::jsonb
ELSE jsonb_build_array(target_item_key)
END
WHERE rule_type = 'STATE';
COMMIT;
CALCULATION의 경우 source_item_keys는 아예 존재하지 않던 필드라, variables와 aggregation_targets에 분산돼 있던 소스 키들을 추출해 역정규화로 채워 넣었습니다.
sql
-- variables.sourceKey + aggregation_targets[].sourceKeys 에서 중복 제거 후 통합
UPDATE item_rule AS c
SET source_item_keys = sub.keys
FROM (
SELECT r.id,
to_jsonb(ARRAY(
SELECT DISTINCT k FROM (
SELECT v->>'sourceKey' AS k
FROM jsonb_array_elements(COALESCE(r.variables, '[]')) v
UNION ALL
SELECT sk#>>'{}' AS k
FROM jsonb_array_elements(COALESCE(r.aggregation_targets, '[]')) agt,
jsonb_array_elements(COALESCE(agt->'sourceKeys', '[]')) sk
) all_keys WHERE k IS NOT NULL AND k <> ''
)) AS keys
FROM item_rule r
WHERE r.rule_type = 'CALCULATION'
AND (r.source_item_keys IS NULL OR r.source_item_keys = '[]')
) sub
WHERE c.id = sub.id;
5. 마치며
이번 리팩토링의 핵심은 이름만 바로잡는 데 있지 않았습니다.
뒤바뀐 필드 의미를 도메인, API, DB, 응답 모델 전반에서 일관된 기준으로 다시 정리하고, 기존 데이터와 프론트엔드 호환성까지 유지한 채 안정적으로 배포하는 것이 더 중요한 과제였습니다.
결과적으로 StateRule은 sourceItemKeys=트리거, targetItemKeys=효과 대상 이라는 의미가 명확해졌고, CalculationRule도 sourceItemKeys와 calcSourceGroups를 중심으로 입력 소스를 더 명확하게 추적할 수 있는 구조로 정리됐습니다.
무엇보다 이번 작업은 필드 이름 하나를 고친 일이 아니라, 오래 누적된 해석의 혼선을 정리하고 이후 규칙 기능을 더 안전하게 확장할 수 있는 기반을 만든 작업이었습니다.
Justin