항공우주연구원 위성영상 AI 처리 플랫폼 구축
아키텍처 — 6개 모듈
프론트엔드 (Thymeleaf · Spring MVC)
Spring Boot
Spring Boot
Go
Salt-Stack · Python
FastAPI + ONNX Runtime
Aliyun GPUShare — GPU당 10파드 자동 분할
항공우주연구원이 발사한 초소형위성 20기의 군집위성 데이터를 수집·처리·가시화하는 통합 플랫폼입니다. 사용자는 객체탐지·변화탐지·영상분할 등 AI 분석을 신청하고, CesiumJS 지구본 위에서 시계열 비교와 분할화면으로 결과를 직접 확인할 수 있습니다.
어드민 페이지에서는 위성별 수집 현황과 분석 진행 상황을 한눈에 모니터링합니다. API 200여 개·테이블 35개 규모의 시스템을 6개 서비스 모듈로 구성했으며, ETL 파이프라인을 제외한 나머지 모듈 전체를 처음부터 설계·구현했습니다.
핵심 기능
부하 테스트 기반 성능·보안 개선보안 체크리스트 선행 · JUnit 통합 테스트 · k6 50VU 에러율 11.22% → 0%
API 서버
보안 체크리스트 선행 · JUnit 통합 테스트 · k6 50VU 에러율 11.22% → 0%
국가기관 납품 특성상 보안 요구사항이 엄격했습니다. 납품 전 보안 체크리스트를 직접 작성해 개발 단계마다 선제 반영했고, JUnit으로 비즈니스 로직과 API 통합 테스트를 작성했습니다.
부하 테스트는 실제 위성 데이터가 있어야 유효해 운영과 동일한 구성의 격리 클러스터를 별도 구축했습니다. k6 50VU로 테스트하자 에러율 11.22%가 나왔고, 병목 원인은 세 가지였습니다.
- ①위성 메타 목록 — 페이지네이션 없이 전체 행 반환, 파라미터가 없어도 매 요청마다 PostGIS ST_INTERSECTION 실행
- ②수집 현황 집계 — CTE에 명시적 JOIN 조건 없어 카테시안 곱 발생 → 타임아웃
- ③알람 팝업 목록 — base64 PNG BLOB을 목록 쿼리에 포함, HikariCP 커넥션 30개 독점 → 전체 요청 연쇄 타임아웃
조건부 PostGIS 실행, 기본 페이지네이션, BLOB 컬럼 목록·상세 분리, Redis 캐싱으로 수정했습니다. 이 과정에서 32개 MyBatis 매퍼의 ORDER BY 파라미터가 쿼리에 직접 삽입되는SQL injection 취약점도 발견해 화이트리스트 검증으로 교체했습니다.
| 전 | 후 | |
|---|---|---|
| 위성 메타 목록 | 38초 | 159ms (239배) |
| 수집 현황 집계 | 46초 (타임아웃) | 181ms (256배) |
| 알람 팝업 목록 | 25초 | 104ms → 20ms (캐시) |
| 50VU 에러율 | 11.22% | 0% |
| 처리량 | 392 req/s | 1,177 req/s |
파일 기반 양방향 DB 동기화Debezium slot 반복 파손 → Outbox 직접 구현 · 이벤트 유실 0건
API 서버 · 망연계
Debezium slot 반복 파손 → Outbox 직접 구현 · 이벤트 유실 0건
국가기관 납품 환경으로 외부망↔폐쇄망이 물리 분리됐습니다. 위성 메타·추론 결과(외부→폐쇄)와 사용자 요청·처리 상태(폐쇄→외부) 양방향 동기화가 필요했습니다.
앱 코드 수정 없이 DB 변경 로그를 읽는 Debezium CDC를 초기 도입했습니다. 그러나 운영 중 replication slot 반복 파손으로 매번 전체 스냅샷 재수행이 필요했습니다.
CDC 의존을 제거하고 MyBatis Executor 인터셉터 기반 Outbox 라이브러리를 직접 구현했습니다.beforeCommit()으로 비즈니스 트랜잭션과 Outbox 저장을 원자적으로 묶고,ThreadLocal OutboxContext로 폐쇄망 수신 데이터 재발행 시 무한 루프를 방지해 이벤트 유실 없이 안정적으로 운영할 수 있었습니다.
@Intercepts({ @Signature(type = Executor.class, method = "update", ...) })
public class OutboxInterceptor implements Interceptor {
public Object intercept(Invocation inv) throws Throwable {
Object result = inv.proceed();
if (OutboxContext.isReplay()) return result; // 무한루프 방지
captureOutboxEvent(inv);
return result;
}
}
@Override
public void beforeCommit(boolean readOnly) {
outboxRepository.saveAll(OutboxContext.flush()); // 같은 트랜잭션, 원자적 저장
}폐쇄망 분산 ID — Snowflake 알고리즘 직접 구현
외부망↔폐쇄망 물리 분리 환경에서는 ZooKeeper·etcd 같은 외부 코디네이터에 접근할 수 없습니다. UUID v4는 완전 랜덤이라 ID만으로 어느 망·서버에서 생성됐는지 역추적이 불가능했습니다.Snowflake 알고리즘을 직접 구현해 worker ID 비트 영역에 망 정보를 인코딩하고, 외부 코디네이터 없이 단조 증가 · 전역 유일성 · 망 추적을 동시에 확보했습니다.
# 구조: [timestamp 41bit][datacenter 5bit][worker 5bit][sequence 12bit]
# datacenter_id: 망 식별 (0=외부망, 1=내부망, 2=분리망)
def generate(self) -> int:
with self._lock:
ts = int(time.time() * 1000) - EPOCH
if ts == self.last_timestamp:
self.sequence = (self.sequence + 1) & ((1 << SEQUENCE_BITS) - 1)
if self.sequence == 0:
while ts <= self.last_timestamp:
ts = int(time.time() * 1000) - EPOCH
else:
self.sequence = 0
self.last_timestamp = ts
return (ts << (DATACENTER_BITS + WORKER_BITS + SEQUENCE_BITS)
| self.datacenter_id << (WORKER_BITS + SEQUENCE_BITS)
| self.worker_id << SEQUENCE_BITS
| self.sequence)영상 서빙 속도 개선WMTS 타일 캐싱 2.4s → 0.4s · MVT 신규 구현
타일 / 파일 서버
WMTS 타일 캐싱 2.4s → 0.4s · MVT 신규 구현
수십~수백 MB GeoTIFF 원본을 그대로 내려주면 뷰어가 렌더링하지 못합니다. Go로 영상 서빙 서버를 구현하고, 조회 목적에 따라 세 가지 프로토콜을 지원했습니다.
| 프로토콜 | 방식 | 용도 |
|---|---|---|
| WMS | 임의 BBOX 단일 이미지 렌더링 | 고정 단일 영역 조회 — BBOX가 매번 달라 캐시 히트율 0%에 수렴, 인터랙티브 탐색 불적합 |
| WMTS | 256×256 고정 격자 타일 사전 생성 · 디스크 캐싱 | 베이스맵 전체 영역 · 줌 레벨별 점진적 로드 (2.4s → 0.4s) — tile URL 고정으로 캐시 히트율 높음 |
| MVT | ETL 사전 생성 · 줌 레벨별 자동 단순화 | 객체탐지 결과 오버레이 (~5분 → 1초 이내) |
GeoJSON → MVT 전환
기존에는 객체탐지 결과를 GeoJSON으로 요청마다 동적 생성해 내려줬습니다. 영역 기반 조회 특성상 캐시 히트율이 낮아 캐싱 효과도 없었고, GeoJSON은 줌 레벨과 무관하게 항상 풀 디테일로 직렬화되기 때문에 멀리서 볼 때도 수십만 개 좌표를 전부 전송했습니다.
ETL 파이프라인에서 탐지 결과를 MVT(Mapbox Vector Tile)로 사전 생성하도록 바꿨습니다. MVT는 줌 레벨별로 형상을 자동 단순화해 멀리서는 적은 데이터만, 확대할수록 정밀한 형상을 전송합니다. 요청 시 생성 없이 미리 만들어진 타일을 바로 서빙하므로 체감 속도가 완전히 달라졌습니다.
| 구분 | GeoJSON 동적 생성 | MVT 사전 생성 |
|---|---|---|
| 응답 시간 | ~5분 (탐지 결과 규모에 따라) | 1초 이내 |
| 클라이언트 사양 | i5 이상 필요 | 펜티엄급에서도 동작 |
| 줌 대응 | 풀 디테일 고정 | 줌 레벨별 자동 단순화 — 확대해도 보이는 영역만 |
| 캐시 효율 | 영역 기반 — 히트율 낮음 | 타일 단위 — 재사용 가능 |
| 생성 시점 | 요청마다 실시간 | ETL 완료 시 자동 |
MVT는 확대할수록 디테일이 올라가지만 가시 영역 자체가 좁아지기 때문에전송·렌더링해야 할 데이터량은 오히려 일정하게 유지됩니다. GeoJSON처럼 전체 탐지 결과를 한 번에 내려주지 않아 클라이언트 메모리 부담이 크게 줄었고, 사양 제약이 있는 현장 운용 환경에서도 원활하게 동작하게 됐습니다.
관심정보 객체탐지 · 변화탐지 세그멘테이션 성능 향상객체탐지 mAP50 0.644 · 세그멘테이션 mIoU 0.7205
AI 추론
객체탐지 mAP50 0.644 · 세그멘테이션 mIoU 0.7205
객체탐지는 20클래스를 OBB·HBB로 이원화했습니다. 회전 방향이 식별에 중요한 함선·항공기 등 대형 15cls는 OBB(YOLOv11m-obb), 위치만 중요한 차량·트럭 소형 5cls는 HBB(YOLOv11m)로 분리했습니다. 위성은 나디르(직하방) 고정 촬영이라 객체 방향이 이미 정렬되어 있어 45° 회전 augmentation 적용 시 mAP50이 오히려 하락했습니다.
| 모델 | 타입 | augmentation | mAP50 |
|---|---|---|---|
| YOLOv11m | HBB | mosaic + mixup + copy_paste | 0.644 |
| YOLOv11m | HBB | + degrees=45 회전 | 0.577 ↓ |
| YOLOv11m-obb | OBB | mosaic only (ProBIoU OOM 방지) | 0.604 |
세그멘테이션은 땅(나지)과 도로가 색상이 유사해 픽셀 분류 자체가 까다로웠습니다. DINOv2 ViT-B/14를 적용했으나 목표 mIoU 0.72에 미달해 조기 종료했습니다. ImageNet-22k ConvNeXt-Base에 도로 중심선 보조 학습(Skeleton Head)을 추가해 최종 mIoU 0.7205를 달성했습니다.
| backbone | decoder | mIoU | 비고 |
|---|---|---|---|
| ConvNeXt-Base (ImageNet-22k) | UPerNet + Skeleton Head | 0.7205 | ✓ 최종 채택 |
| HRNet-W48 (ImageNet-1k) | UPerNet | 0.6857 | |
| DINOv2 ViT-B/14 | UPerNet | 0.6656 | 조기 종료 |