스마트농장 IoT 카메라 관리 플랫폼 구축기
농장 현장에는 수십 대의 CCTV 카메라가 설치되어 작물 생육 모니터링, 보안 감시, 이상 상황 감지 등 다양한 역할을 수행합니다. 그런데 카메라가 많아질수록 개별 기기 관리가 점점 어려워집니다.
김동은

프로젝트 배경
농장 현장에는 수십 대의 CCTV 카메라가 설치되어 작물 생육 모니터링, 보안 감시, 이상 상황 감지 등 다양한 역할을 수행합니다. 그런데 카메라가 많아질수록 개별 기기 관리가 점점 어려워집니다.
카메라마다 IP를 기억하고 웹 인터페이스에 접속하는 건 비현실적이고,
녹화 스케줄, 화질 설정, 관심 영역(ROI) 같은 설정을 일일이 변경하기엔 시간이 너무 많이 들며,
영상 파일을 서버로 전송할 때 실패하면 어떤 파일이 빠졌는지 추적하기도 어렵습니다.
OpenIoT Farm 프로젝트는 이러한 문제를 해결하기 위해 시작되었습니다. 농장 내 모든 IoT 카메라를 하나의 관리자 페이지에서 등록·모니터링·설정·영상 전송까지 통합 관리할 수 있는 플랫폼을 만드는 것이 목표였습니다.
시스템 아키텍처
전체 시스템은 4개의 독립 서비스로 구성되며, Docker Compose로 한 번에 배포됩니다.
서비스 | 기술 스택 | 역할 |
|---|---|---|
프론트엔드 | Next.js 15, React 19, Tailwind CSS, Zustand | 관리자 대시보드 UI |
백엔드 API | Spring Boot 3.5, Java 21, PostgreSQL, JPA | 기기·그룹·알림·템플릿 CRUD |
큐 서버 | Python, Flask, SQLite, paho-mqtt | 영상 전송 큐, MQTT 브릿지 |
카메라 펌웨어 | AWS IoT SDK, MQTT | 디바이스 측 에이전트 |
만든 기능들
1. 계층적 그룹 관리 — "농장 → 동 → 라인"
농장에는 여러 동(건물)이 있고, 각 동에는 여러 라인(줄)이 있습니다. 카메라를 물리적 위치 기반으로 트리 형태 그룹에 배치하면 찾기가 훨씬 수월해집니다.
백엔드에서는 DeviceGroup 엔티티가 자기 참조(self-referencing) 관계로 트리를 표현합니다. parentId, depth, path, displayPath 필드를 두어 그룹 생성·이동·삭제 시 경로를 자동으로 재계산합니다.
// DeviceGroup.java — 경로 자동 업데이트
public void updatePath() {
if (isRoot()) {
this.path = this.groupId.toString();
this.displayPath = this.groupName;
} else if (parent != null) {
this.path = parent.getPath() + "/" + this.groupId.toString();
this.displayPath = parent.getDisplayPath() + "/" + this.groupName;
}
}
프론트엔드에서는 GroupTable 컴포넌트로 3단계 계층 UI를 제공하며, 그룹별 기기 필터링·검색을 지원합니다. 그룹 삭제 시에는 하위 그룹과 기기 존재 여부를 재귀적으로 검사하여 데이터 손실을 방지합니다.
2. 디바이스 대시보드 — 실시간 상태 한눈에
메인 대시보드는 등록된 모든 카메라를 카드 형태로 보여줍니다. 각 카드에는 다음 정보가 표시됩니다.
실시간 스크린샷 (캐시 무효화를 위해 타임스탬프 쿼리스트링 추가)
기기 상태 — 연결/전송/녹화/에러 상태를 색상 뱃지로 구분
하드웨어 모니터링 — CPU 온도, CPU 사용률, RAM 사용률
상태별 필터 버튼과 정렬 토글로 원하는 기기를 빠르게 찾을 수 있고, 카드를 클릭하면 상세 설정 페이지로 이동합니다.
3. 기기 설정 — ROI와 녹화 스케줄
기기 상세 페이지에서는 카메라의 세부 설정을 변경할 수 있습니다.
ROI(관심 영역) 설정: react-konva 라이브러리를 활용하여 캔버스 위에서 사각형 영역을 직접 그려 관심 영역을 지정합니다. 좌표는 퍼센트 기반으로 변환되어 해상도에 무관하게 동작합니다.
// KonvaPolygonSelector — 캔버스 위에서 ROI 사각형 그리기
<Stage width={size.width} height={size.height}>
<Layer>
<Image image={image} />
<Line points={flatPoints} stroke="red" strokeWidth={2} closed={isClosed} />
{points.map((p, i) => (
<Circle key={i} x={p.x} y={p.y} radius={5} fill="red" />
))}
</Layer>
</Stage>
녹화 스케줄: TimeTable 컴포넌트는 요일 × 시간 2차원 격자를 제공합니다. 셀을 클릭하거나 드래그하여 녹화 시간대를 설정하며, 행/열 단위 일괄 선택도 지원합니다.
설정 템플릿: 감마, 화이트밸런스, 해상도, FPS 등 영상 설정을 템플릿으로 저장해두면, 여러 카메라에 동일한 설정을 빠르게 적용할 수 있습니다.
4. 영상 전송 큐 시스템 — MQTT 기반 순차 처리
카메라가 촬영한 영상 파일을 서버로 전송할 때, 여러 카메라가 동시에 전송하면 네트워크 병목이 발생합니다. 이를 해결하기 위해 MQTT 기반 메시지 큐 시스템을 구축했습니다.
메시지 라이프사이클
카메라 또는 관리자가
queue/req토픽으로 전송 요청큐 서버가 SQLite DB에
pending상태로 저장워커 스레드가 FIFO 순서로 하나씩 꺼내어
locked후 디바이스에 MQTT로 전달디바이스가
queue/res로 완료 응답 →processed처리 및 기록타임아웃(기본 10분) 초과 시 자동으로
failed_messages테이블로 이동
# 순차 처리 워커 — 한 번에 하나씩만 처리
def _worker_loop():
while True:
message = queue_manager.get_next_message()
if not message:
time.sleep(1)
continue
# MQTT로 디바이스에 전송
payload = {"deviceId": device_id, "filepath": filepath, "origin": "server"}
published = publish_raw("queue/req", payload)
if not published:
queue_manager.mark_message_failed(msg_id, "mqtt publish failed")
프론트엔드에서는 대기열 목록과 실패 목록을 각각 관리할 수 있으며, 실패한 전송을 재시도하거나 특정 메시지를 우선 처리(순서 무시)할 수도 있습니다.
5. AWS IoT Fleet Provisioning — 자동 인증서 발급
IoT 디바이스를 AWS IoT Core에 연결하려면 각 디바이스마다 고유한 X.509 인증서가 필요합니다. 수십 대의 카메라에 수동으로 인증서를 설치하는 것은 비현실적이므로, Fleet Provisioning을 활용하여 자동화했습니다.
def start_connection():
if has_new_certificates():
# 이미 발급받은 인증서가 있으면 바로 연결
connect_to_aws_with_new_cert()
else:
# 최초 실행 — Claim 인증서로 프로비저닝 진행
connect_to_aws_with_claim_cert()
한 번 발급받은 인증서는 파일로 저장되므로, 재시작 시에도 프로비저닝 과정 없이 바로 연결됩니다.
6. MQTT HTTP 브릿지 — REST API로 디바이스와 대화
프론트엔드에서 직접 MQTT 프로토콜을 사용하기 어렵기 때문에, 큐 서버가 HTTP ↔ MQTT 브릿지 역할을 합니다.
내부적으로 request_and_wait_response() 함수가 요청 토픽으로 발행 후, 응답 토픽을 구독하고 threading.Event로 최대 5초간 동기 대기합니다.
def request_and_wait_response(topic_req, topic_res, payload={}, timeout=5.0):
event = threading.Event()
_pending_responses[topic_res] = (event, None)
publish_raw(topic_req, payload)
event.wait(timeout)
_, resp = _pending_responses.pop(topic_res)
return resp
7. 알림 시스템
디바이스 이벤트(연결 끊김, 에러 발생 등)가 감지되면 알림이 생성됩니다. 프론트엔드 헤더에 읽지 않은 알림 카운트가 표시되며, 일괄 읽음 처리를 지원합니다.
기술적 포인트
🔧 Docker Compose로 원클릭 배포
4개 서비스(PostgreSQL, Spring Boot, Flask, Next.js)를 하나의 docker-compose.yml로 관리합니다.
services:
postgres: # PostgreSQL 15 — 디바이스/그룹/알림 데이터
app: # Spring Boot — RESTful API 서버
queue-server: # Flask + MQTT — 영상 전송 큐
frontend: # Next.js — 관리자 대시보드
각 서비스는 openiot-net 브릿지 네트워크로 연결되고, PostgreSQL 데이터는 Named Volume으로 영속화됩니다. unless-stopped 재시작 정책으로 서비스 안정성을 확보했습니다.
🔒 스레드 안전한 SQLite 큐
Python의 SQLite는 기본적으로 싱글 스레드에서만 안전합니다. QueueManager는 threading.Lock으로 모든 DB 접근을 보호하고, locked 상태 검사와 업데이트를 하나의 트랜잭션에서 처리하여 **경쟁 조건(race condition)**을 방지합니다.
class QueueManager:
def __init__(self, db_path):
self.lock = threading.Lock()
def get_next_message(self):
with self.lock:
with sqlite3.connect(self.db_path) as conn:
# 타임아웃된 locked 메시지 → failed로 이동
# locked가 존재하면 None 반환 (순차 처리 보장)
# pending 중 가장 오래된 것을 locked로 전이
📊 대기 시간·처리 시간 통계
큐에 진입한 시점(wait_started_at)과 전송이 시작된 시점(transmission_started_at), 처리 완료 시점(processed_at)을 각각 기록하여 대기 시간 / 처리 시간 통계를 제공합니다. 이를 통해 병목 지점을 파악하고 시스템 튜닝에 활용할 수 있습니다.
🎨 Zustand 기반 상태 관리
프론트엔드에서는 React Query 대신 Zustand를 채택하여 전역 상태를 관리합니다. 도메인별로 스토어를 분리하여 관심사를 분리했습니다.
스토어 | 역할 |
|---|---|
| 기기 CRUD, 페어링, 검색, 설정 |
| 그룹 계층 관리, 선택 상태 |
| 설정 템플릿 CRUD |
| 영상 대기열/실패 목록 |
| 알림 목록, 읽음 처리 |
| 앱 정보 (이름, 아이콘) |
| ROI 좌표 상태 |
| 사이드바/메뉴 UI 상태 |
🔁 MQTT 토픽 설계 — 요청/응답 패턴
MQTT는 기본적으로 pub/sub 모델이라 요청-응답 패턴이 내장되어 있지 않습니다. 이를 해결하기 위해 토픽 네이밍 컨벤션으로 req/res 패턴을 구현했습니다.
origin: "server" 필드를 페이로드에 포함시켜 서버가 포워딩한 메시지의 재큐잉을 방지합니다.
프로젝트 구조 한눈에 보기
openiot-201-adminpage-frontend/ # 프론트엔드 (Next.js 15)
├── src/app/ # 페이지 라우팅
│ ├── devices/ # 기기 목록·등록·상세·그룹
│ ├── templates/ # 설정 템플릿
│ ├── video/ # 대기열·실패 관리
│ ├── notification/ # 알림
│ ├── setting/ # 앱 설정
│ └── cctv/ # CCTV 뷰
├── src/components/ # 공통 컴포넌트
├── src/stores/ # Zustand 스토어
└── src/utils/ # API 함수, 유틸리티
openiot-201-adminpage-server/ # 백엔드 (Spring Boot 3.5)
├── controller/ # REST API 엔드포인트
├── service/ # 비즈니스 로직
├── entity/ # JPA 엔티티
├── repository/ # 데이터 접근 계층
├── dto/ # 요청/응답 DTO
└── exception/ # 전역 예외 처리
openiot-201-queue-server/ # 큐 서버 (Python/Flask)
├── main.py # 진입점 (MQTT + Flask + Worker)
├── server.py # Flask REST API
└── libs/
├── mqtt.py # AWS IoT MQTT 연결/프로비저닝
└── queue_manager.py # SQLite 큐 관리자
openiot-201-firmware-aws-camera/ # 카메라 펌웨어
└── (디바이스 측 에이전트)
배운 점과 회고
MQTT와 HTTP의 간극
IoT 디바이스는 MQTT로 통신하지만, 웹 프론트엔드는 HTTP/REST가 자연스럽습니다. 두 프로토콜을 이어주는 브릿지 계층이 생각보다 많은 복잡도를 추가했습니다. 타임아웃 처리, 응답 매칭, 연결 끊김 복구 등 고려할 사항이 많았고, 이를 threading.Event 기반의 동기 대기 패턴으로 해결한 것이 핵심이었습니다.
큐 시스템의 중요성
처음에는 "카메라가 알아서 보내면 되지 않나?"라고 생각했지만, 실제 현장에서는 네트워크 불안정, 동시 전송 병목, 전송 실패 추적 등의 문제가 빈번했습니다. SQLite 기반의 가벼운 큐 시스템이 이런 문제들을 효과적으로 해결해 주었습니다.
Fleet Provisioning의 편리함
AWS IoT Fleet Provisioning을 도입하기 전에는 카메라를 추가할 때마다 AWS 콘솔에서 인증서를 발급하고 SD카드에 복사하는 작업이 필요했습니다. Claim 인증서 하나만 공장 출하 시 심어두면 나머지는 자동으로 처리되기 때문에, 현장 배포가 크게 단순화되었습니다.
Docker Compose의 힘
개발 환경에서 4개 서비스를 각각 실행하는 것은 상당히 번거롭습니다. Docker Compose 하나로 전체 스택을 docker compose up -d 한 줄에 띄울 수 있게 만든 것이 개발 생산성과 배포 안정성 양쪽에서 큰 도움이 되었습니다.
마치며
OpenIoT Farm은 작은 규모의 스마트팜에서 시작했지만, 계층적 그룹 관리, 메시지 큐, MQTT 브릿지 같은 패턴은 공장 자동화, 물류 센터 모니터링 등 다양한 IoT 도메인에 그대로 적용할 수 있습니다.
특히 **"디바이스와 관리자 사이의 통신을 어떻게 안정적으로 설계할 것인가"**라는 질문에 대해, MQTT 큐 + HTTP 브릿지 + Fleet Provisioning이라는 조합이 효과적인 해답이 될 수 있다는 것을 이 프로젝트를 통해 확인할 수 있었습니다.
코드는 모두 오픈소스로 공개되어 있으니, 비슷한 IoT 관리 시스템을 구축하려는 분들에게 참고가 되길 바랍니다.


