NIPA 로고
NIPA 로고
한컴인스페이스

NIPA 위성 변화탐지 AI 플랫폼 — MSA 설계

2025.07 ~ 진행중
백엔드 엔지니어
RabbitMQNext.jsTypeScriptCesiumJSFastAPIGoONNX RuntimeKubernetes

아키텍처 — MSA

단독 설계·구현일부 기여·고도화
웹 뷰어
사용자 · 어드민 (Next.js 15 + FSD)
Envoy Gateway
OIDC 인증 · 경로 라우팅 · TLS (Keycloak)

FastAPI

API 서버
CRUD · 인증 · 작업 관리 · Snowflake ID

Go

영상 서빙 서버
WMS · WMTS · 타일 캐싱

Python

수집
위성 영상 수집
카탈로깅
전처리 · DB 등록

Python + ONNX

변화탐지 AI
ECT · MambaCD · MINIMA — 모델 선정 진행 중
RabbitMQ · PostgreSQL · Kubernetes

NIPA(정보통신산업진흥원) 지원으로 구축한, 두 시점의 위성 영상을 비교해 지표 변화를 AI로 탐지하는 변화탐지 플랫폼입니다.

기능 하나를 수정해도 전체를 재배포해야 했던 모놀리식 구조를 MSA로 분리했습니다. 백엔드는 서비스별 독립 배포가 가능해졌지만, 프론트엔드는 굳이 서비스를 나눌 필요가 없었습니다. 대신 동일한 이미지를 환경변수(MAP_TYPE: EARTH | MOON)만 바꿔 지구 변화탐지 / 달지도 모드로 분기하는 구조를 택했습니다. RabbitMQ 비동기 파이프라인과 Next.js 15 FSD도 처음 도입해 지역별 변화 통계·달지도(아폴로 탐사 경로·크레이터) 등 도메인 특화 뷰어 기능을 구현했습니다.

핵심 기능

Envoy Gateway + Keycloak OIDC — 게이트웨이 레벨 인증

각 서비스 인증 코드 제거 · SecurityPolicy로 HTTPRoute 단위 적용

백엔드 공통

초기에는 각 FastAPI 서비스가 JWT 검증 로직을 직접 처리했습니다. 서비스가 늘어날수록 인증 코드가 중복되고, 인증 정책 변경 시 전 서비스를 동시에 수정해야 했습니다.

Keycloak을 OIDC Provider로 도입하고Envoy Gateway SecurityPolicy를 HTTPRoute 단위로 적용해 인증을 게이트웨이 레벨로 끌어올렸습니다. 인증된 요청에만 forwardAccessToken: true로 Access Token을 헤더에 실어 백엔드로 전달하고, 백엔드 서비스는 토큰 검증 코드 없이 헤더의 사용자 정보만 사용합니다.

구분이전 (서비스 내 JWT)이후 (게이트웨이 OIDC)
인증 위치각 FastAPI 서비스Envoy Gateway SecurityPolicy
정책 변경전 서비스 동시 수정SecurityPolicy 1개만 수정
신규 서비스JWT 미들웨어 직접 추가HTTPRoute에 Policy 연결만
SSO직접 구현 불가Keycloak 세션 공유 기본 제공

Keycloak 클라이언트를 서비스별로 분리해 권한 범위를 세밀하게 제어하고, SealedSecret으로 클라이언트 시크릿을 암호화해 Git에 커밋할 수 있게 했습니다.

RabbitMQ ack/nack · DLQ 비동기 파이프라인

Salt 폴링 작업 고착 → 작업 유실 0건

백엔드 공통

이전 프로젝트에서 Salt 스케줄러로 작업을 디스패치했을 때 노드가 재시작되면 완료 콜백이 호출되지 않아 작업이 RUNNING 상태로 고착됐습니다. 타임아웃 복구 전까지 후속 작업이 쌓이고 수동 DB 수정이 반복됐습니다.

변화탐지 작업은 수 분~수십 분이 소요됩니다. 처음부터 ack/nack 기반 큐로 설계해 이 문제를 원천 차단했습니다. worker는 처리를 완전히 마친 뒤에만 ack를 보내고, 처리 중 노드가 죽으면 RabbitMQ가 자동으로 메시지를 재투입합니다. 3회 초과 실패 시 DLQ로 격리해 운영자가 원인을 파악한 뒤 수동 republish합니다.

# DLX 설정 — 재시도 3회 초과 시 DLQ 격리
channel.exchange_declare(exchange='gprocessor.dlx', exchange_type='direct', durable=True)
channel.queue_declare(queue='gprocessor.dlq', durable=True)
channel.queue_declare(
    queue='gprocessor', durable=True,
    arguments={'x-dead-letter-exchange': 'gprocessor.dlx'}
)

def callback(ch, method, properties, body):
    death_count = len((properties.headers or {}).get('x-death', []))
    try:
        process(body)
        ch.basic_ack(delivery_tag=method.delivery_tag)       # 성공: 큐에서 제거
    except Exception:
        if death_count >= 3:
            ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)  # DLQ 격리
        else:
            ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)   # 재시도
모놀리식 → MSA · FastAPI 전환

재배포 월 10건 → 1건 · 배포 속도 4분 → 30초

전체 시스템

기존 구조는 기능 하나를 배포하려면 전체 서비스를 재시작해야 했습니다. 영상 처리 로직 수정이 인증 서비스 다운타임으로 이어졌습니다. 전 서비스를 독립 서비스로 분리하고Envoy Gateway를 게이트웨이로 두어 경로별 라우팅과 OIDC 인증을 처리했습니다.

ETL·AI 추론 워커들은 Python 스크립트·셸로 운영되던 것을 전부 FastAPI로 전환했습니다. 표준화된 인터페이스 덕분에 Envoy Gateway 라우팅에 그대로 편입할 수 있었고, 헬스체크·메트릭 엔드포인트도 일관되게 붙일 수 있었습니다.

역할기술담당API
API 서버FastAPICRUD · 인증 · 작업 관리 · Snowflake ID28개
영상 서빙Go위성 영상 서빙 · WMS/WMTS · 타일 캐싱8개
수집Python위성 영상 수집 · 스케줄링4개
카탈로깅Python전처리 · DB 카탈로깅3개
AI 추론Python + ONNX변화탐지 AI 추론 (RabbitMQ consumer)3개
후처리Python변화탐지 후처리 · 결과 저장3개
메시지 큐RabbitMQ비동기 메시지 큐 StatefulSet
데이터베이스PostgreSQL + PostGIS공간 데이터 저장소
게이트웨이Envoy Gateway · KeycloakOIDC 인증 · 경로 라우팅 · TLS · 업로드 제한
웹 뷰어Next.js 15CesiumJS 웹 UI · 달지도 · 지역 통계

서비스 분리로 배포 속도 4분 → 30초, 월 재배포 횟수 10건 → 1건으로 줄었습니다.

Next.js 15 FSD · 멀티 배포 뷰어 · 달지도 · 지역 통계

Thymeleaf → FSD 마이그레이션 · 동일 이미지를 K8s env로 지구/달 모드 분리 배포

웹 뷰어

기존 Thymeleaf 기반 SSR은 페이지·컴포넌트 경계가 불명확해 수정 범위 예측이 어렵고 타입 안전성이 없었습니다. Next.js 15를 처음 도입하면서 FSD(Feature-Sliced Design)를 함께 적용해 지구 변화탐지·지역통계·달지도를 독립 feature slice로 분리했습니다.

피처를 분리해두면 동일한 Docker 이미지를 K8s pod의 환경변수만 바꿔 여러 배포판으로 나눌 수 있습니다.MAP_TYPE: EARTH|MOON 하나로 뷰어 모드가 결정되고, 백엔드 URL은 BACKEND_API_URL로만 주입돼 클라이언트에 노출되지 않습니다.dynamic import로 달지도 청크는 MOON 모드 진입 시점에만 로드됩니다.

# viewer-nipa.pod.yaml (지구 변화탐지)   /   viewer-moon.pod.yaml (달지도)
# 동일 이미지, env만 다름
env:
  - name: MAP_TYPE
    value: "EARTH"          # ← MOON으로 바꾸면 달지도 모드
  - name: VIEWER_TITLE
    value: "국토 변화 정보 서비스"
  - name: BACKEND_API_URL   # 서버사이드 프록시 — 클라이언트에 백엔드 URL 미노출
    value: "http://[내부IP]:16103"
  - name: UI_MAP_INDEX_VISIBLE
    value: "true"           # MOON 배포에서는 false — 변화 인덱스 위젯 숨김
// Client — MAP_TYPE env에 따라 피처 청크를 필요 시점에 동적 로드
const EarthViewer = dynamic(() => import("@/features/earth-viewer"));
const MoonViewer  = dynamic(() => import("@/features/moon-viewer"));

export function ViewerWidget({ mapType }: { mapType: string }) {
  return mapType === "MOON"
    ? <MoonViewer />    // 달지도 청크 — MOON 배포에서만 로드
    : <EarthViewer />;  // 지구 변화탐지 청크
}

지구 모드 — PostGIS 지역별 변화 통계

변화 폴리곤과 행정구역을 공간 조인해 시·도 / 시·군·구 / 사용자 AOI 단위 변화 면적·변화율을 집계합니다. 지역 클릭 시 시계열 차트, 임계값 초과 지역은 히트맵으로 강조됩니다.

달 모드 — 아폴로 경로 · 크레이터

레이어데이터 출처표현 방식
달 기본 지형JAXA SELENE / NASA LRO 타일CesiumJS ImageryLayer
아폴로 탐사 경로NASA 아폴로 11 ~ 17호 EVA 좌표Polyline (임무별 색상)
크레이터IAU 크레이터 카탈로그 GeoJSONPoint · 직경 비례 크기 스케일
착륙 지점아폴로 착륙 좌표Billboard (임무 아이콘)

CesiumJS는 레이어 재정렬을 지원하지 않아 내부 imageryLayers를 직접 조작했고, 토글 시 삭제 대신 layer.show = false로 WebGL 텍스처를 보존했습니다. MVT·MBTiles·ImageLayer·달지도 등 이종 레이어를 단일 인터페이스로 추상화해 신규 레이어 타입 추가 시 기존 코드 수정 없이 확장 가능했습니다.