Bugs Caused by Mismatched Fields and Their Solutions
Backend Refactoring
1. Introduction — The field names were saved in the opposite order
We received a bug report indicating that the actual rules are not functioning as intended on the frontend when registering ItemRule through the survey template editor.
For StateRuleHide that item if this item is selectedI registered the rule, but it turned out to behave the opposite way. At first, I suspected there was a problem with the front-end's condition evaluation logic.
As I followed the code, I found that the problem was at a more fundamental level. The field names of the backend entities were attached contrary to their actual meanings.
|
java // Meaning of the data stored in the actual DB (before modification) targetItemKey = "formId:Q3" // in reality, it is a trigger (Source) sourceItemKeys = ["formId:Q1"] // In actuality, it is the target of the effect |
|---|
The trigger (when an item changes) was stored in targetItemKey, and the effect target (which item the rule should be applied to) was stored in sourceItemKeys. With the names being reversed, the frontend reading this data couldn't help but be confused.
2. How deep had the problem spread?
It seemed like a problem that only required changing the field name, but in reality, it spanned all layers.
|
# |
Issue |
Impact |
|---|---|---|
|
1 |
targetItemKey ↔ sourceItemKeys meaning inversion |
Confusion for new developers, causing bugs |
|
2 |
No sourceItemKeys field in CalculationRule |
Unable to trace source, unable to analyze dependencies |
|
3 |
Source distributed across variables, aggregationTargets, conditions |
Increased code complexity |
|
4 |
AggregationTarget mixing Source+Condition |
Violation of Single Responsibility Principle |
|
5 |
Potential loss of sourceItemKeys when converting CDO → Entity |
Data integrity issue |
It wasn't just a simple naming problem. The CalculationRule had a structure where the input sources needed for calculations were not consolidated into a single field, making dependency tracing and validation difficult.
java
// CalculationRule 수정 전 — 소스를 어디서 찾아야 하는가?
private List<FormulaVariable> variables; // 여기도 소스
private List<AggregationTarget> aggregationTargets; // 여기도 소스
private List<EnableCondition> conditions; // 여기도 소스(조건 내 참조)
The sources were distributed across three locations, and the name AggregationTarget did not clarify whether it referred to the object of aggregation or the aggregation condition.
3. How did we change it
3-1. Redefining the meaning of fields
The first thing to do was to redefine what sourceItemKeys meant.
sourceItemKeys = 트리거 (어떤 항목이 변경될 때 규칙이 발동하는가)
targetItemKeys = 효과 대상 (규칙이 적용될 항목들)
For example, in the case of StateRule:
|
"If item I7 is changed → hide items I5 and I6" sourceItemKeys: ["formId:Q3"] → trigger targetItemKeys: ["formId:Q1", "formId:Q2"] → effect target |
|---|
3-2. Modifying the entire hierarchy of impact scope
Based on this definition, we modified the entire hierarchy of Entity, CDO, JPO, Store, Helper, and RDO.
|
Domain Entity → Redefining sourceItemKeys (trigger) and targetItemKeys (effect target) CDO → StateRuleCdo, CalculationRuleCdo field structure correction JPO → DB column mapping modification Store → Check consistency of save/retrieve logic RDO → Apply dual-output to frontend response structure |
|---|
3-3. AggregationTarget → Organize naming to CalcSourceGroup
The name AggregationTarget was serving as a mix of Source and Condition. It has been replaced with CalcSourceGroup to clarify the actual role of each field.
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; // 의존성 맥락 출처
}
The JSON wire key is maintained with @JsonProperty("conditionItemKey") to ensure backward compatibility with existing data.
4. DB migration design
Even if the code changed, the existing DB data remained unchanged. The data also needed to be corrected along with the deployment.
In the case of STATEThe order of the two steps is important. First, the existing source_item_keys need to be copied to the new target_item_keys, and then the target_item_key must be transferred to the new source_item_keys. If the order is reversed, it will overwrite the original data.
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;
In the case of CALCULATION, the source_item_keys were fields that did not exist at all, so I extracted the source keys scattered in variables and aggregation_targets and filled them in through denormalization.
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. Conclusion
The key point of this refactoring was not just to correct the names.
The more important task was to reorganize the meanings of the fields that had been swapped using consistent criteria across the domain, API, DB, and response models, while safely deploying them with compatibility with existing data and the frontend.
As a result, the meaning of StateRule became clear as sourceItemKeys=trigger and targetItemKeys=effect target, and CalculationRule was organized into a structure that can more clearly trace input sources centered around sourceItemKeys and calcSourceGroups.
Above all, this work was not just about fixing a field name, but rather about clarifying the long-accumulated confusion of interpretations and establishing a foundation for safely expanding the rules functionality in the future.
Justin