Published on

Open API를 mock으로 먼저 만들고 OrmDataProvider로 실데이터 전환을 준비한 회고

Authors
  • avatar
    Name
    Hyo814
    Twitter

Open API를 mock으로 먼저 만들고 OrmDataProvider로 실데이터 전환을 준비한 회고

Open API를 만들면서 가장 먼저 한 결정은 "데이터를 어디서 가져오는가"를 코드 한 곳에 가두자 였습니다. 화면과 직렬화 계약(serializer)은 mock으로 빨리 굳히고, 실데이터(ORM) 연결은 백엔드가 안전하게 갈아끼울 수 있도록 분리하고 싶었어요. 그 분리를 StandardDataProvider라는 인터페이스와 OrmDataProvider 골격으로 만든 작업을 정리합니다.


1. 배경 — 화면은 급하고 실데이터는 아직

Open API는 표준데이터(datasets/concepts/schemes/catalogs)를 외부에 노출하는 API입니다. 그런데 화면(명세서·예시·응답 미리보기)은 먼저 보여줘야 했고, 실제 모델 쿼리는 노출 정책·마스킹 같은 민감한 결정이 남아 있어 뒤로 미뤄야 했습니다.

여기서 흔한 함정이 "일단 mock으로 뷰에 박아두고 나중에 ORM으로 바꾸자" 입니다. 그렇게 하면 나중에 뷰 곳곳을 다시 헤집어야 하고, 그 과정에서 응답 형태가 미묘하게 틀어져요. 그래서 데이터 출처를 뷰에서 떼어내 provider로 추상화했습니다.


2. 설계 — 출처를 인터페이스 뒤로 숨기기

핵심은 list/retrieve 두 메서드만 가진 추상 인터페이스입니다. 뷰는 "누가 데이터를 주는지" 모르고, get_data_provider()가 골라준 provider에게 요청만 합니다.

# 디스패처 — 출처(source)에 따라 provider를 고른다 (실제 코드의 logger.warning 등은 생략한 요약)
def get_data_provider() -> StandardDataProvider:
    source = ...  # OPEN_API_DATA_SOURCE (settings)
    if source not in _PROVIDERS:
        if source == "orm":
            # OrmDataProvider 골격은 있지만 미완 → mock 으로 폴백
            _PROVIDERS[source] = MockDataProvider()
        else:
            _PROVIDERS[source] = MockDataProvider()
    return _PROVIDERS[source]

이렇게 두면 mock → orm 전환이 "구현체 한 줄 교체" 가 됩니다. 뷰·serializer·테스트는 손대지 않아요.

가장 중요한 계약은 응답 dict 형태입니다. MockDataProvidermock_data/*.json을 그대로 흘려보내므로, OrmDataProvider도 같은 키·같은 페이지네이션 형태를 돌려줘야 했어요.

항목계약
목록 응답{ count, next, previous, results }
next/previouspage 번호 (절대 URL 변환은 상위 base 계층이 담당)
상세 응답serializer가 정의한 필드 그대로 (= mock json과 동일)

3. 구현 — OrmDataProvider 골격

골격이라고 부른 이유는 지금 켜지 않을 것이기 때문입니다. 하지만 "켜지 않는 코드"라도 페이지네이션과 자명한 필드 매핑은 mock과 1:1로 맞춰뒀어요. 그래야 나중에 백엔드가 TODO만 채우면 됩니다.

class OrmDataProvider(StandardDataProvider):
    @staticmethod
    def _paginate(queryset, serialize, page, page_size) -> dict:
        page = max(1, int(page))
        page_size = max(1, min(int(page_size), MAX_PAGE_SIZE))
        count = queryset.count()
        start = (page - 1) * page_size
        end = start + page_size
        items = list(queryset[start:end])
        return {
            "count": count,
            "next": page + 1 if end < count else None,      # mock 과 동일 계약
            "previous": page - 1 if start > 0 else None,
            "results": [serialize(obj) for obj in items],
        }

리소스별 쿼리셋은 한 곳(_queryset)에 모았습니다. 직렬화는 자명한 필드만 채우고, 도메인 지식이 필요한 부분은 TODO(backend) 주석으로 남겼어요.

@staticmethod
def _serialize_dataset(obj, *, detail: bool) -> dict:
    data = {
        "id": obj.id, "name": obj.name, "title": obj.title,
        "url": obj.url, "type": obj.type, "state": obj.state,
        # TODO(backend): 이메일 마스킹(mock: "ki***@ag.com") — 노출 전 필수
        "author_email": obj.author_email,
        # TODO(backend): 등록자(creator)는 마스킹("관*자")
        "creator": obj.creator.name if obj.creator_id else None,
        "concepts": [c.name for c in obj.concepts.all()],
    }
    ...

4. 함정 — 미완 활성화 = 비공개 데이터 유출

여기서 가장 신경 쓴 게 fail-closed였습니다. provider가 노출하면 안 될 데이터를 실수로 흘리면, 그건 단순 버그가 아니라 심사중·비공개 데이터가 공개 API로 새는 보안 사고예요.

그래서 두 가지 장치를 박았습니다.

  • 노출 필터를 쿼리셋 단계에서 닫아둠. datasets는 state='ACT' & private=False, 나머지는 public=True & invisible=False. 기본이 "닫힘"이라, 매핑을 빼먹어도 노출되는 일은 없게 했어요.
if resource == "datasets":
    qs = Dataset.objects.filter(state="ACT", private=False)  # 보안: private 별도 적용
    ...
if resource == "concepts":
    qs = Concept.objects.filter(public=True, invisible=False)
  • 활성화를 일부러 막아둠. OPEN_API_DATA_SOURCE='orm'이어도 골격이 미완이면 mock으로 폴백하고 logger.warning을 남깁니다. 백엔드가 TODO(마스킹·복합 필터·트리 parent 등)를 끝내고 검증한 뒤에야 아래 한 줄로 켭니다.
#     _PROVIDERS[source] = OrmDataProvider()

"코드는 있지만 일부러 안 켠다" 는 게 어색해 보일 수 있는데, 데이터 노출 영역에서는 이 보수성이 맞다고 봤어요.


5. 교훈

  • 출처를 인터페이스 뒤로 숨기면 전환이 한 줄이 됩니다. 뷰가 provider를 모를수록 mock↔orm 전환 비용이 0에 수렴해요.
  • mock의 응답 형태가 곧 계약입니다. 페이지네이션·키 이름까지 mock과 똑같이 맞춰두면 나중 구현이 "빈칸 채우기"가 됩니다.
  • 노출 계층은 fail-closed로. 필터의 기본값을 "닫힘"으로 두고, 미완 구현은 아예 활성화하지 않는 안전장치를 둡니다. 빠뜨려서 보이는 건 버그지만, 빠뜨려서 보이는 건 사고예요.
  • 켜지 않을 코드라도 자명한 부분은 채워두면 핸드오프가 쉬워집니다. TODO(backend)로 남은 결정을 명시하면, 받는 사람이 무엇을 검증해야 하는지 바로 압니다.

화면을 빨리 보여줘야 하는 압박과 데이터 노출의 신중함을 동시에 만족시키는 방법은, 결국 둘을 한 인터페이스로 분리해 두는 것 이었습니다. 다음에 외부 노출 API를 또 만든다면 첫 커밋부터 provider부터 그릴 것 같아요.