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

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

2025.07 ~ 진행중
백엔드·인프라 엔지니어
RabbitMQ 3.13Next.js 15TypeScriptCesiumJSFastAPI 0.115Go 1.23ONNX Runtime 1.15Kubernetes 1.30Cilium 1.19ArgoCD 3.4Envoy Gateway 1.8Keycloak 26.7OpenTelemetry

아키텍처 - MSA

단독 설계·구현일부 기여·고도화
웹 뷰어
사용자 · 어드민 (Next.js 15 + FSD)
kube-vip
고정 VIP 진입점 · 3노드 control-plane HA
Envoy Gateway
L7 라우팅 · TLS 종료
Keycloak
OIDC 인증 · SSO
Cilium
Pod 네트워킹 · eBPF 보안 정책 (kube-proxy 대체)
OpenBao + ESO
시크릿 저장 · 자동 주입

FastAPI

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

Go

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

Python

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

Python + ONNX

변화탐지 AI
ECT · MambaCD · MINIMA - 모델 선정 진행 중
PostgreSQL + PostGIS
위성 메타 · 변화탐지 결과 저장

변화탐지 AI 호출 흐름

  1. 1웹 뷰어전·후 영상 선택 · 분석 신청
  2. 2API 서버작업 생성 · 큐 발행
  3. 3RabbitMQack/nack · DLQ
  4. 4변화탐지 AIONNX 추론 (consumer)
  5. 5후처리결과 저장 · 타일화
  6. 6웹 뷰어지도 위 변화 가시화

CI/CD 파이프라인

  1. 1Bitbucket코드 커밋 & 푸시
  2. 2Argo Eventswebhook → NATS → Sensor 트리거
  3. 3Argo Workflows빌드 · 테스트 · 이미지 생성 · OPA 게이트
  4. 4Pulp이미지 · 패키지 저장소
  5. 5ArgoCDGit 선언 상태 ↔ 클러스터 동기화
  6. 6클러스터 배포새 버전 롤아웃
OTel 계측 → Prometheus · Grafana · Tempo · OpenSearch (전 구간 관측)

NIPA(정보통신산업진흥원) 지원 사업으로, 서로 다른 두 시점의 위성 영상을 비교해 지표 변화를 자동으로 탐지하는 AI 플랫폼입니다. 9개 서비스로 구성된 MSA 구조의 설계·구현을 주도했고, 클러스터 네트워킹·시크릿·배포 등 인프라 고도화는 팀원과 설계 방향을 협의하며 함께 확장하고 있습니다.

이전 시스템은 기능 하나만 고쳐도 전체를 재배포해야 하는 모놀리식 구조였습니다. 서비스를 FastAPI 기반 독립 배포 단위로 쪼개고, 수집 → 전처리 → 추론 → 후처리로 이어지는 처리 파이프라인은 RabbitMQ ack/nack 비동기 큐로 처음 도입해 병목 단계만 골라 확장할 수 있게 만들었습니다.

반면 프론트엔드는 굳이 서비스를 나눌 이유가 없어, 하나의 Next.js 15 코드베이스를 K8s 환경변수(MAP_TYPE: EARTH | MOON)만 바꿔 지구 변화탐지·달지도 두 배포판으로 나누는 방식을 택했습니다. 이 구조 위에서 지역별 변화 통계, 달지도 (아폴로 탐사 경로·크레이터) 같은 도메인 특화 뷰어 기능을 구현했습니다.

인프라

CI/CD - Jenkins/Nexus → Argo Events·Workflows·ArgoCD·Pulp 전환

웹훅 트리거부터 클러스터 동기화까지 GitOps로 일원화

CI/CD

기존에는 Jenkins가 빌드·배포를 함께 수행하고 아티팩트는 Nexus에 저장했습니다. 파이프라인 정의가 Jenkins UI·스크립트에 흩어져 있어 변경 이력 추적이 어렵고, 배포 상태가 클러스터 실제 상태와 어긋나도 감지할 방법이 없었습니다.

Bitbucket 푸시를 Argo Events(webhook → NATS → Sensor)가 감지해Argo Workflows를 트리거하고, 빌드·테스트·이미지 생성을 수행합니다. 생성된 이미지·패키지는 Pulp에 저장하고,ArgoCD가 Git에 선언된 상태와 클러스터 실제 상태를 지속적으로 동기화합니다. 배포 전에는 Workflows에 OPA 정책 게이트를 통합해 정책 위반 이미지가 클러스터에 반영되기 전에 차단합니다.

구분이전 (Jenkins·Nexus)이후 (Argo·Pulp)
빌드·배포 트리거Jenkins 스케줄/수동 트리거Bitbucket 웹훅 → Argo Events
빌드 정의Jenkinsfile·UI 설정 혼재Argo Workflows 선언적 정의
아티팩트 저장소NexusPulp
배포 상태클러스터 상태와 어긋나도 미감지ArgoCD가 Git↔클러스터 상태 지속 동기화·자동 교정
정책 검증배포 후 수동 확인Workflows 단계에 OPA 게이트 통합

Argo Workflows는 빌드까지만 책임지고, ArgoCD는 배포를 직접 실행하는 대신 Git 상태를 클러스터가 계속 따라가게 만드는 역할이라 - 이 전환 과정과 OPA 게이트 도입 배경은 별도 글로 자세히 정리했습니다.

클러스터 네트워킹·보안 - Cilium · kube-vip · OpenBao

kube-proxy 대체, control-plane HA, 시크릿 중앙 관리

클러스터 인프라

Cilium(eBPF)을 CNI로 도입해 kube-proxy를 대체하고 L3~L7 네트워크 정책을 하나의 계층으로 일원화했습니다. Hubble 기반 egress 관측으로 비인가 통신을 탐지할 수 있게 됐습니다. control-plane은 3노드 HA로 구성하고kube-vip로 고정 VIP를 두어, 특정 노드가 죽어도 API 서버 진입점이 바뀌지 않도록 했습니다.

시크릿은 OpenBao(Vault 계열)에 중앙 저장하고External Secrets Operator가 이를 각 네임스페이스의 Kubernetes Secret으로 동기화·주입합니다. 서비스 코드가 시크릿 저장소를 직접 호출하지 않아도 되고, 시크릿 로테이션 시 배포 재시작만으로 반영됩니다.

네트워킹·HA 영역은 서비스 증가에 따른 처리 오버헤드와 단일 장애점이라는 문제 자체는 같이 겪었지만, 구체적인 설계 방향(Cilium 도입 범위, control-plane HA 구성)은 팀원과 협의하며 정했습니다.

관측 스택 - OTel · Prometheus · Grafana · Tempo · OpenSearch

CI/CD·게이트웨이·앱·DB 전 구간 계측 - 장애 원인분석 1~2시간 → 30분 이내

observability

OTel(OpenTelemetry)로 계측 표준을 통일해 메트릭·트레이스·로그를 CI/CD 파이프라인부터 게이트웨이, 앱 Pod, DB까지 전 구간에서 수집합니다. 수집된 데이터는 Prometheus(메트릭)·Tempo(분산 트레이싱)·OpenSearch(로그)로 각각 적재되고, Grafana에서 하나의 대시보드로 조회합니다.

서비스가 9개로 분리되며 장애 시 어느 단계에서 문제가 발생했는지 파악하는 데 걸리는 시간이 길어질 위험이 있었는데, 요청 하나를 트레이스 ID로 전 구간(Envoy Gateway → 앱 Pod → DB)에서 추적할 수 있게 되면서 장애 원인분석 시간을 1~2시간에서 30분 이내로 단축했습니다. 요청이 죽은 구간이나 지연 구간을 로그를 수동으로 뒤지지 않고 대시보드에서 바로 특정할 수 있게 됐습니다.

단계별 큐 분리와 워커 수평 확장

단일 큐로는 병목 단계만 골라 확장 불가 → 처리 워커 1개 → 15개 수평 확장

처리 파이프라인

큐를 하나만 두고 워커를 늘리는 방법도 있었지만, 파이프라인 단계마다 부하 특성이 달랐습니다. AI 추론은 무겁고 후처리는 가벼운데, 단일 큐로는 병목 구간만 골라 확장할 수 없었습니다.

그래서 수집 → 전처리 → 추론 → 후처리 단계별로 큐를 분리했습니다. 각 단계 워커는 자기 큐에서 메시지를 consume하고, 끝나면 결과를 다음 큐에 publish합니다. 단계 간 직접 통신 없이 큐로만 데이터를 넘기는 구조라, 한 단계가 일시적으로 느려져도 큐가 버퍼 역할을 해서 앞뒤 단계가 영향을 받지 않습니다.

단일 큐단계별 큐 분리
확장 단위전체 워커 일괄 확장병목 단계 워커만 선택 확장
결합도한 단계 지연이 전체 파이프라인에 전파큐가 버퍼 역할 - 앞뒤 단계 영향 차단
DB 의존작업 상태 조회를 위해 DB 폴링 필요작업 상태가 큐에 있어 폴링 불필요

추론 단계가 병목이면 추론 워커만 늘리고, 후처리가 가벼우면 워커를 줄이는 식으로 단계마다 자원을 따로 조절할 수 있게 되면서, 처리 워커 컨테이너를1개에서 15개로 수평 확장했습니다. 요청을 큐로 받아 워커에 분산하는 이 패턴은 더 큰 트래픽의 AI 추론 인프라에도 그대로 적용됩니다.

백엔드

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건

백엔드 공통

메시지 큐 도입을 앞두고 KafkaRabbitMQ 중 하나를 선택해야 했습니다. Kafka는 처리량이 높지만 운영 오버헤드가 큽니다. 위성 처리 작업은 단위가 명확하고 ack/nack 기반의 확실한 전달 보장이 더 중요해 RabbitMQ를 선택했습니다.

이전 프로젝트에서 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 · 인증 · 작업 관리28개
영상 서빙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건으로 줄었습니다.

db-api 계층화 · 위성영상 메타 조회 성능 개선

models·crud·router 단일 파일 → 3계층 분리 · 컬럼 프로젝션·페이지네이션 도입

db-api

초기 db-api는 FastAPI 튜토리얼 구조를 그대로 사용해 테이블이 늘어날 때마다 models.py·crud.py·router.py 각 1개 파일에 로직이 계속 쌓이는 구조였습니다. 리소스별 관심사가 한 파일에 뒤섞여 있어 변경 영향 범위를 파악하기 어려웠습니다.

테이블(엔티티) 단위로 models를, CRUD 로직을 services의 서비스 클래스로, HTTP 라우팅을 api의 라우터 모듈로 분리했습니다. 이후 즐겨찾기· 페이지네이션 같은 신규 리소스는 이 3계층 패턴을 그대로 따라 확장됐습니다.

위성영상 메타 조회 성능 개선

메타데이터 조회 API는 ORM 풀 엔티티를 하이드레이션한 뒤 재직렬화하는 구조였고, 이미 4326 SRID로 저장된 컬럼에 불필요한 ST_SetSRID를 매번 재적용하고 있었습니다. 페이지네이션도 없어 대량 결과를 한 번에 반환했습니다.

항목이전이후
조회 방식ORM 풀 엔티티 하이드레이션·재직렬화필요한 컬럼만 select하는 컬럼 프로젝션
SRID 변환이미 4326인 컬럼에 매번 재적용불필요한 ST_SetSRID 제거
필터필드 검증 없음모델에 실제 존재하는 컬럼만 허용하는 화이트리스트
응답 크기페이지네이션 없이 전체 반환page/limit 페이지네이션, 기본 1000건

반복 파싱되던 datetime 값도 행마다 재계산하지 않도록 루프 밖에서 한 번만 계산하도록 바꾸고, fetch·serialize 단계를 분리 계측할 수 있는 타이밍 로그를 추가했습니다.

AI 탐지 품질 개선

조명·색상 차이와 그림자가 변화로 오탐 → 전처리 색상 정규화 · 후처리 필터링

변화탐지 AI

완성된 AI 모듈을 그대로 연동하던 기존 방식에서는, 전후 영상의 조명·색상 차이와 그림자가 변화로 오탐되는 문제가 누적됐습니다. 모델 출력을 후처리 없이 그대로 사용해 노이즈가 결과에 그대로 반영됐습니다.

모델 출력을 직접 분석해 전처리에 그림자 제거와 색상 정규화를 추가하고, 후처리에 면적 기반 필터링과 모폴로지 연산을 적용해 픽셀 단위 노이즈를 제거했습니다.

단계적용 기법목적
전처리그림자 제거 · 색상 정규화조명·색상 차이로 인한 오탐 방지
후처리면적 기반 필터링작은 노이즈 폴리곤 제거
후처리모폴로지 연산변화 폴리곤 경계 정제

프론트엔드

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·달지도 등 이종 레이어를 단일 인터페이스로 추상화해 신규 레이어 타입 추가 시 기존 코드 수정 없이 확장 가능했습니다.