[둥지] 체크리스트 팩토리 아키텍처 결정기: DB 매핑 테이블 vs 코드 파이프라인
property_type만으로는 표현할 수 없는 복잡한 체크리스트 조건(월세, 임대차신고제 등)을 처리하기 위해 DB 매핑 테이블을 버리고 코드 파이프라인 팩토리로 전환한 설계 결정 과정을 정리합니다.
둥지 서비스의 체크리스트는 고정된 항목 목록이 아닙니다. 같은 집이라도 전세냐 월세냐에 따라 깡통전세 항목이 빠지고, 보증금과 지역 조건에 따라 임대차신고 항목이 들어오거나 확정일자·전입신고로 교체됩니다.
기존 구조는 이런 조건을 표현할 수 없었습니다.
기존 구조의 한계
처음 체크리스트는 TypeItemMapping 테이블로 관리했습니다. property_type(다가구, 아파트, 오피스텔 등)에 따라 어떤 항목을 보여줄지 DB 레코드로 매핑해 두는 방식이었습니다.
1
2
TypeItemMapping
property_type → [item_id_1, item_id_2, ...]
주택 유형(property_type)만 같으면 동일한 항목 목록이 나옵니다. 그런데 아래 조건들을 추가로 반영해야 할 상황이 왔습니다.
contract_type == WOLSE→ 깡통전세/보증보험 항목 제외- 임대차신고제 조건 (보증금/월세 금액 + 지역) → 항목 추가 또는 전입신고·확정일자로 교체
- 향후 “신탁 등기”, “대리인 계약” 같은 특수 상황 대응
이 조건들은 DB 조인이나 컬럼 추가로는 표현하기 어렵습니다. 특히 “도 지역의 군은 임대차신고 제외” 같은 지리적 규칙은 테이블 구조로 담는 것 자체가 무리입니다.
“어떤 항목이 들어갈지를 코드가 결정해야 한다.”
이 판단이 리팩토링의 출발점이었습니다.
세 가지 설계 방향
A안. DB 매핑 테이블 유지 + 코드 필터링
기존 TypeItemMapping 테이블을 유지한 채, 항목을 가져온 뒤 코드에서 조건에 맞지 않는 항목을 걸러내는 방식입니다.
property_type기준으로 매핑 테이블에서 항목 목록 조회contract_type == WOLSE이면is_jeonse_only=True항목 제거- 임대차신고 조건 충족 여부에 따라 항목 추가
장점: 기존 구조를 어느 정도 재활용하여 전환 비용이 낮습니다.
단점: 복잡한 지리적 조건(도의 군 제외 등)은 DB 컬럼으로 표현이 사실상 불가능합니다. 비즈니스 로직이 DB 스키마와 코드 사이에 분산되어 추적이 어렵습니다. 테스트를 위해 반드시 테스트 DB를 세팅하고 데이터를 적재해야 합니다.
B안. 순수 코드 기반 팩토리 (Rule Engine)
“어떤 항목이 들어갈지”를 DB 조인이 아닌 파이썬 파이프라인이 결정하는 방식입니다.
1
2
3
4
5
6
NestContext(property_type, contract_type, deposit, rent, address_road)
→ BaseBuilder # 공통 항목 추가
→ TypeFilter # 주택 유형별 전용 항목 추가
→ ContractFilter # 월세이면 깡통전세/보증보험 제외
→ LawFilter # 임대차신고제 대상 여부 → 항목 추가 또는 교체
→ [item_key 목록]
복잡한 임대차신고 지역 조건은 파이썬 함수 is_lease_report_target_area(address_road)로 처리합니다. 도의 군 제외 규칙도 문자열 파싱 한 줄이면 됩니다.
장점: DB 없이 단위 테스트 가능. 새로운 조건이 생겨도 파이프라인에 필터 하나를 끼워 넣으면 됩니다. 모든 비즈니스 로직이 코드에 집중됩니다.
단점: 기존 테이블 삭제와 전체 리팩토링이 필요하여 초기 전환 비용이 높습니다.
2.5안. ChecklistItem 텍스트를 프론트엔드 정적 파일로 이관
B안과 함께 검토한 보완 아이디어입니다. 백엔드는 item_key와 status만 응답하고, 항목의 제목·설명·버튼 같은 텍스트는 프론트엔드 정적 파일(constants/checklist.ts)에서 관리하는 방식입니다.
1
2
3
4
5
// 백엔드 응답 (가볍고 빠름)
[
{ "id": "uuid", "item_key": "REGISTRY_ISSUE", "status": "SUCCESS" },
{ "id": "uuid", "item_key": "REGISTRY_ANALYZE", "status": "LOCKED" }
]
1
2
3
4
5
// 프론트엔드 정적 마스터
const CHECKLIST_MASTER = {
REGISTRY_ISSUE: { title: "등기부등본 발급", stage: "PRE", ... },
REGISTRY_ANALYZE: { title: "등기부등본 분석", stage: "PRE", ... },
}
장점: DB에서 텍스트를 읽는 I/O가 사라져 API 응답이 가볍고 빠릅니다. UI 문구 수정 시 백엔드를 건드릴 필요가 없습니다. 백엔드는 로직(무엇을 해야 하는가)에, 프론트는 표현(어떻게 보여줄 것인가)에 집중하는 역할 분리가 완성됩니다.
단점: 항목 키(item_key) 동기화가 백엔드-프론트 간 암묵적 계약으로 관리됩니다. 프론트 배포 없이 텍스트만 변경하는 것이 불가능합니다.
트레이드오프 비교
| 관점 | A안 (DB 매핑 + 필터링) | B안 + 2.5안 (코드 팩토리 + 정적 파일) |
|---|---|---|
| 로직 위치 | DB 스키마 + 코드에 분산 | 파이썬 파이프라인에 집중 |
| 복잡한 조건 처리 | 어려움 (지리 조건 표현 불가) | 자유로움 (코드 조건문으로 처리) |
| 단위 테스트 | 테스트 DB 필수 | DB 없이 즉시 검증 |
| API 응답 속도 | 텍스트까지 DB에서 읽음 | 상태 코드만 반환, 가벼움 |
| UI 수정 주체 | 백엔드 (DB UPDATE 필요) | 프론트엔드 (정적 파일 수정) |
| 전환 비용 | 낮음 | 높음 (테이블 삭제, 대규모 리팩토링) |
결정: B안 + 2.5안
코드 팩토리 + 프론트엔드 정적 파일 조합으로 결정했습니다.
결정의 핵심 근거는 세 가지입니다.
1. 비즈니스 로직의 복잡도가 DB 매핑의 한계를 이미 넘었습니다. 임대차신고제의 “도 지역의 군 제외” 규칙은 테이블 컬럼으로 표현할 수 없습니다. 앞으로 “신탁 등기”, “대리인 계약” 같은 특수 조건이 추가될수록 A안의 한계는 더 빠르게 드러날 것입니다. 지금 전환하는 것이 나중에 감당할 기술 부채보다 쌉니다.
2. 테스트 가능성이 아키텍처 선택을 결정했습니다. A안은 체크리스트 생성 로직을 검증하려면 테스트 DB를 세팅하고 데이터를 적재해야 합니다. B안은 NestContext 객체 하나를 생성해 파이프라인을 통과시키면 끝입니다. “월세 40만 원, 서울 오피스텔 유저”의 체크리스트가 올바른지 밀리초 단위로 확인할 수 있습니다.
3. 역할 경계가 명확해집니다. 백엔드는 “누가 무엇을 해야 하는가(로직)”만 계산하고, 프론트엔드는 “어떻게 보여줄 것인가(표현)”만 처리합니다. 텍스트 수정을 위해 백엔드 배포가 필요한 구조는 팀 협업 비용을 높입니다.
전환 비용이 크다는 점은 인지하고 있었지만, 복잡도가 이미 A안의 한계를 넘어선 시점에서 전환을 미루는 것은 더 큰 리팩토링을 예약하는 것과 같다고 판단했습니다.
둥지 수정 범위 제한
update_nest()에서 수정 가능한 필드를 이름과 메모로만 제한했습니다.
contract_type, deposit, rent, property_type은 수정 불가입니다. 계약 조건을 바꾸려면 둥지를 삭제하고 재생성해야 합니다.
이유는 연쇄 문제 때문입니다. 계약 조건이 변경되면 체크리스트를 재계산해야 하고, 기존 분석 리포트가 무효화되며, 이미 발급된 등기부등본(유료)을 재발급해야 할 수 있습니다. 이 복잡성을 한 번의 수정 API에 담는 것보다, 명확하게 “계약 조건 변경 = 재생성”으로 정의하는 것이 올바른 설계입니다.
LOCKED / TODO 초기 상태
체크리스트 생성 시점에 PREREQUISITE_MAP(선행 항목 맵)으로 초기 상태를 결정합니다.
1
2
3
4
5
6
7
8
PREREQUISITE_MAP = {
REGISTRY_ANALYZE: REGISTRY_ISSUE, # 등기부등본 분석 ← 발급 선행
INSURANCE_CHECK_DAGAGU: REGISTRY_ISSUE,
BUILDING_ANALYZE: BUILDING_ISSUE,
REGISTRY_REANALIZE: REGISTRY_REISSUE,
CONTRACT_ANALYZE: CONTRACT_UPLOAD, # 계약서 분석 ← 업로드 선행
REGISTRY_FINAL_ANALIZE: REGISTRY_FINAL_ISSUE,
}
선행 항목이 있으면 LOCKED, 없으면 TODO로 생성됩니다. 별도의 플래그 컬럼 없이 맵 하나로 초기 상태 전체를 결정하는 구조입니다.
Cascade Toggle
항목을 토글할 때 의존 항목들의 상태도 자동으로 연동됩니다.
SUCCESS전환 → 이 항목을 선행으로 가진LOCKED항목들 →TODO로 해제TODO전환 → 이 항목을 선행으로 가진TODO / SUCCESS항목들 →LOCKED로 재잠금
PATCH /{statusId} 응답에 cascade로 변경된 항목 전체가 updated[]에 포함됩니다. 프론트엔드는 이 응답 전체를 상태에 적용해 낙관적 업데이트와 서버 동기화를 동시에 처리합니다.
팩토리 파이프라인 설계
NestContext
팩토리의 판단 기준이 되는 컨텍스트 객체입니다.
1
2
3
4
5
6
class NestContext:
property_type: PropertyTypeEnum # 다가구/아파트/오피스텔/연립다세대/단독
contract_type: ContractTypeEnum # 전세/월세
deposit: int # 보증금 (만원 단위)
rent: int # 월세 (만원 단위)
address_road: str # 임대차신고제 지역 판별용 도로명 주소
4단계 파이프라인
1
2
3
4
5
1. BaseBuilder → 모든 주택 공통 항목
2. TypeFilter → property_type별 전용 항목 추가
3. ContractFilter → 월세이면 깡통전세/보증보험 항목 제거
4. LawFilter → 임대차신고제 대상 → LEASE_REPORT 추가
비대상 → MOVE_IN + FIXED_DATE 추가
임대차신고제 판별 로직
대상 조건 (AND):
- 보증금 6,000만 원 초과 OR 월세 30만 원 초과
- 수도권(서울·경기·인천) 전역, 광역시, 세종시, 제주시, 도(道)의 시(市) — 도의 군(郡)은 제외
지역 판별은 법정동 코드 없이 address_road 문자열 파싱으로 처리합니다.
1
2
3
4
5
"서울특별시 마포구 ..." → 대상
"경기도 수원시 장안구 ..." → 대상
"경기도 양평군 ..." → 대상 (수도권 전역 규칙)
"전라남도 여수시 ..." → 대상 (도의 시)
"전라남도 고흥군 ..." → 비대상 (도의 군)
경기·인천의 군 지역(양평군, 가평군, 강화군 등)은 “수도권 전역” 규칙에 의해 신고 대상으로 분류됩니다.
정리
| 결정 사항 | 선택 | 핵심 근거 |
|---|---|---|
| 체크리스트 항목 결정 방식 | 코드 팩토리 파이프라인 | DB 매핑으로 표현 불가한 지리·조건 로직, 테스트 용이성 |
| ChecklistItem 텍스트 위치 | 프론트엔드 정적 파일 | API 응답 경량화, 역할 분리, 프론트 독립 수정 |
| 둥지 수정 범위 | 이름·메모만 허용 | 계약 조건 변경 시 체크리스트·리포트·발급 연쇄 무효화 방지 |
| 선행 항목 관리 | PREREQUISITE_MAP | 별도 플래그 없이 맵 하나로 초기 상태 전체 결정 |
| 토글 연동 | Cascade Toggle | 의존 항목 상태를 응답에 포함, 프론트 단일 동기화 |
TypeItemMapping 테이블의 한계에서 출발해, 비즈니스 로직을 코드로 집중시키고 DB는 상태만 저장하는 구조로 전환했습니다. 전환 비용은 컸지만, 임대차신고제처럼 지리·금액 조건이 얽힌 로직을 테이블로 표현하는 것은 처음부터 한계가 명확했습니다.