하나의 데이터, 7개의 소비자 - 크로스 언어 데이터 파이프라인
배경
신용평가에 사용되는 신용 프로파일 데이터는 외부 신용정보 기관의 API를 통해 조회한다. 772개의 코드를 요청하면 각 코드에 대한 응답값이 돌아오고, 이 데이터가 시스템 내 7개의 서로 다른 소비자에게 전달된다.
문제는 이 파이프라인이 Python(Django)과 Java 두 언어에 걸쳐 있고, 소비자마다 필요한 데이터 형태가 다르다는 것이다.
전체 데이터 흐름
flowchart TD
A["코드 마스터 테이블<br>(772개, Java 관리)"] -->|"Java: 코드 목록 조회"| B["Java 서버"]
B -->|"외부 API 요청<br>(772개 코드 전달)"| C["외부 신용정보 API"]
C -->|"응답"| D["원본 응답 테이블<br>(managed=False)"]
D -->|"Django: 전체 저장"| E["Dict 저장소<br>(HStoreField)"]
D -->|"Django: 필터링 저장"| F["컬럼 저장소<br>(개별 컬럼 ~486개)"]
E --> G["ML 모델 A<br>(ML 추론 서비스, 전체 772개)"]
F --> H["ML 모델 B<br>(~130개 코드)"]
F --> I["외부 기관 전문<br>(1개 코드)"]
F --> J["대출 자격 판단<br>(1개 코드)"]
F --> K["투자자 필터링<br>(5개 코드)"]
F --> L["주택담보 심사<br>(전체 → dict)"]
F --> M["주식담보 심사<br>(전체 → dict)"]
핵심 설계 결정: 왜 두 가지 저장 방식인가
같은 데이터를 두 가지 다른 형태로 저장한다:
저장소 1: HStoreField (Dict 형태)
1
2
class CpsDataDict(models.Model):
data = HStoreField() # {"CODE_001": "1234", "CODE_002": "5678", ...}
| 장점 | 단점 |
|---|---|
| 772개 코드를 한 번에 저장/조회 | 개별 코드로 필터링 어려움 |
| 스키마 변경 없이 코드 추가 가능 | 타입 안전성 없음 (모든 값이 문자열) |
| ML 모델에 통째로 전달하기 좋음 | SQL WHERE 조건 사용 불편 |
저장소 2: 개별 컬럼
1
2
3
4
class CpsDataColumns(models.Model):
d10187d00 = models.CharField(...) # 연체이력
l2m000112 = models.CharField(...) # DSR 분류
# ... ~486개 컬럼
| 장점 | 단점 |
|---|---|
| 개별 코드로 WHERE 가능 | 코드 추가 시 마이그레이션 필요 |
| 타입 체크 가능 | 컬럼 수가 매우 많음 |
| Django ORM과 자연스럽게 사용 | 전체를 dict로 변환하려면 추가 로직 필요 |
flowchart LR
subgraph dict_store["Dict 저장소 (HStoreField)"]
A["전체 772개 코드<br>스키마 변경 불필요<br>ML 모델 입력용"]
end
subgraph col_store["컬럼 저장소 (개별 컬럼)"]
B["~486개 코드<br>SQL 필터링 가능<br>비즈니스 로직용"]
end
C["외부 API 응답"] --> dict_store
C --> col_store
왜 둘 다 필요한가? 소비자마다 데이터 접근 패턴이 다르기 때문이다:
- ML 모델: “772개 코드를 통째로 줘” → Dict 저장소
- 비즈니스 로직: “연체이력 코드가 특정 값인 건만 찾아줘” → 컬럼 저장소
크로스 언어 경계 관리
sequenceDiagram
participant Django
participant Java
participant ExternalAPI as 외부 API
Django->>Java: POST /cb-inquiry/<br>{name, ssn, deal_application_id}
Java->>Java: 코드 마스터 테이블에서<br>772개 코드 조회
Java->>ExternalAPI: API 요청 (772개 코드)
ExternalAPI-->>Java: 응답 (CODE/VALUE 쌍)
Java->>Java: 원본 응답 테이블에 INSERT
Java-->>Django: 성공 응답
Django->>Django: Dict 저장소에 전체 저장
Django->>Django: 컬럼 저장소에 필터링 저장
중간 테이블: managed=False
Java가 원본 응답을 저장하는 테이블은 Django에서 managed=False로 선언한다.
1
2
3
4
class ExternalCreditRaw(models.Model):
class Meta:
managed = False # Django가 이 테이블의 스키마를 관리하지 않음
db_table = 'external_credit_raw'
이렇게 하면:
- Java: 테이블 구조를 관리하고, 데이터를 INSERT
- Django: 테이블 구조는 건드리지 않고, SELECT만 수행
두 언어가 같은 테이블을 공유하되, 소유권은 명확히 나뉜다.
7개 소비자의 데이터 사용 패턴
flowchart TD
subgraph consumers["7개 소비자"]
G["ML 모델 A: 전체 772개"]
H["ML 모델 B: ~130개"]
I["외부 기관 전문: 1개<br>(L2M000112 - DSR 분류)"]
J["대출 자격 판단: 1개<br>(D10187D00 - 연체이력)"]
K["투자자 필터링: 5개"]
L["주택담보 심사: to_dict()"]
M["주식담보 심사: to_dict()"]
end
| 소비자 | 사용 코드 수 | 접근 방식 | 저장소 |
|---|---|---|---|
| ML 모델 A (ML 추론 서비스) | 전체 772개 | 통째로 전달 | Dict |
| ML 모델 B | ~130개 | feature_names 리스트로 필터 | 컬럼 |
| 외부 기관 전문 | 1개 | 특정 코드 직접 접근 | 컬럼 |
| 대출 자격 판단 | 1개 | 특정 코드 직접 접근 | 컬럼 |
| 투자자 필터링 | 5개 | 특정 코드들 접근 | 컬럼 |
| 주택담보 심사 | 전체 | to_dict() 변환 | 컬럼 |
| 주식담보 심사 | 전체 | to_dict() 변환 | 컬럼 |
새 코드 추가 시 체크리스트
시스템이 복잡해지면 “이걸 추가하려면 어디를 고쳐야 하지?”가 가장 큰 고통이다. 그래서 코드 추가 절차를 체크리스트로 문서화했다.
flowchart TD
A["1. 코드 마스터 테이블에<br>새 코드 추가 (Java/DB)"] --> B["2. 컬럼 저장소 모델에<br>컬럼 추가 + 마이그레이션"]
B --> C["3. 상수 파일에<br>코드-컬럼 매핑 추가"]
C --> D{"어디서 사용?"}
D -->|"ML 모델 A"| E["Dict 저장소에<br>자동 포함 (추가 작업 없음)"]
D -->|"ML 모델 B"| F["feature_names<br>리스트에 추가"]
D -->|"비즈니스 로직"| G["해당 서비스 코드에서<br>직접 접근"]
느낀 점
같은 데이터의 “두 가지 뷰”가 필요할 때
하나의 원본 데이터를 소비자 패턴에 맞게 변환하여 저장하는 것은 정규화 원칙에 어긋나 보이지만, 실용적인 선택이었다. ML 모델에게는 dict를, 비즈니스 로직에게는 컬럼을 제공함으로써 각 소비자가 자연스러운 방식으로 데이터에 접근할 수 있다.
managed=False는 크로스 언어 경계의 좋은 도구
Django와 Java가 같은 DB를 공유할 때, managed=False로 소유권을 명확히 하면 마이그레이션 충돌 없이 협업할 수 있다. “이 테이블은 Java 것이고, Django는 읽기만 한다”는 계약.
체크리스트는 복잡한 시스템의 생존 도구
코드 추가 하나에 4개 파일을 수정해야 하는 시스템에서, 체크리스트 없이는 반드시 하나를 빠뜨린다. 문서화의 ROI가 가장 높은 곳은 이런 “여러 곳을 동시에 바꿔야 하는” 절차다.