포스트

RGB 프로젝트 소개 - AI 스마트 분리수거함의 엣지 추론과 4계층 아키텍처 설계

AI 스마트 분리수거함 프로젝트에서 아두이노-라즈베리파이-Spring-Flutter 4계층 아키텍처를 설계하고, 온디바이스 AI 추론과 운영 관점의 백엔드를 구현한 과정을 정리했습니다.

RGB 프로젝트 소개 - AI 스마트 분리수거함의 엣지 추론과 4계층 아키텍처 설계

1. 프로젝트 개요

  • 프로젝트명: RGB - AI 스마트 분리수거함 (Smart Waste Bin)
  • 한 줄 소개: 카메라에 쓰레기를 비추면 AI가 실시간 분류하고 하드웨어가 자동으로 분리·압축하는 IoT + AI + 풀스택 통합 시스템
  • 핵심 기능: YOLOv4-tiny 온디바이스 분류, 서보모터 자동 분리·압축, 실시간 용량 모니터링, 분류 이력 정답/오답 검수, 시간별 배출 통계, FCM 푸시 알림, 실시간 영상 스트리밍
  • 주요 기술: Spring Boot, MongoDB, Firebase Admin SDK, Python, PyQt6, OpenCV DNN, Flask, Darknet(YOLOv4-tiny), Flutter, Arduino, Raspberry Pi 5
  • 개발 기간: 2025.09.08 ~ 2025.12.13 (2025 인하대학교 캡스톤 디자인)

이 프로젝트는 AI 추론, 하드웨어 제어, 서버, 이 하나의 흐름으로 연결되어야 완성되는 구조였다. 어느 한 계층이라도 멈추면 쓰레기가 분류되지 않기 때문에, 기능 구현뿐 아니라 계층 간 통신 프로토콜 설계실기기 안정성까지 함께 고민해야 했다.

2. 인원

  • 개발 참여 인원 : 3명
  • 나: 백엔드 전체, AI 모델 학습, 라즈베리파이 + 아두이노 소프트웨어 (엣지 시스템·모터/센서 제어 코드)
  • 팀원 1: Flutter 프론트엔드 앱
  • 팀원 2: 하드웨어 — 분리수거함 설계(CAD)와 실제 전선·모터 연결

3. 협업 방식

소프트웨어(백엔드·앱)와 하드웨어가 나뉜 3인 팀이었기 때문에 계약(Contract) 기준을 명확히 하는 것이 협업의 핵심이었다. 백엔드와 앱 사이에서는 API 명세가, 소프트웨어와 하드웨어 사이에서는 시리얼 통신 프로토콜이 팀의 공용 언어가 되어야 했다.

RGB 프로젝트 산출물 - 요구사항 명세서와 기획서

요구사항 명세서, 기획서 등 산출물 문서를 먼저 정리해 팀이 동일한 기준 위에서 개발을 시작하도록 했다.

  • Swagger UI(springdoc-openapi)로 전 컨트롤러의 API 문서를 자동화해 프론트·하드웨어 파트와 계약을 공유했다.
  • 브랜치 전략은 개인 브랜치 → dev → main 플로우로 운영하고, Pull Request 6건을 머지했다.
  • Feat / Add / Fix / Remove 커밋 컨벤션을 수립해 일관 적용하고, Jira(SCRUM)와 연동해 작업을 추적했다.

4. 내 역할

  • 아두이노↔라즈베리파이↔Spring↔Flutter 4계층 아키텍처 설계 및 계층 간 통신 프로토콜 정의
  • Spring Boot 백엔드 전체 설계·구현 (분류 이력, 통계 집계, FCM 알림, 영상 릴레이)
  • YOLOv4-tiny 커스텀 모델 학습부터 엣지 배포까지 AI 파이프라인 전 과정
  • 라즈베리파이 엣지 시스템 전체 구현 (키오스크 UI, 온디바이스 추론, 모터·센서 제어)
  • Docker Compose 기반 개발 환경 구성 및 .env 시크릿 분리

내 핵심 기여는 시스템 전체를 관통하는 축을 설계하고 구현한 것이다. 시리얼 텍스트 포맷, REST API, MJPEG 릴레이까지 계층 간 통신 방식을 모두 직접 정의했고, 데이터셋 구축부터 추론 배포, 피드백 수집 API까지 모델 생애주기 전체를 담당했다.

5. 아키텍처

아래는 내가 직접 설계한 전체 시스템 인프라 구조다.

RGB 전체 시스템 아키텍처

Flutter 앱(Admin)과 라즈베리파이(Robot)의 요청은 모두 HTTPSInternet Gateway를 거쳐 Public subnetALB(+ACM 인증서)로 들어오고, Private subnetSpring Boot(EC2)가 처리한다. 촬영 이미지는 S3에 업로드하고, 분류 결과는 S3 URL과 함께 MongoDB Atlas에 저장한다. 별도의 CUDA 학습 서버(EC2)는 오분류로 판정된 이미지를 S3에서 가져와 재학습하고, 갱신된 추론 모델을 라즈베리파이로 다시 배포한다 — 정답/오답 검수 데이터가 실제 모델 개선으로 이어지는 순환 구조다.

디바이스 계층까지 포함한 데이터 흐름은 다음과 같다.

1
2
3
4
5
[아두이노]──Serial(9600bps)──[라즈베리파이 5]──HTTP──[Spring Boot 서버]──REST──[Flutter 앱]
 · 초음파 센서 x3               · PyQt6 키오스크 UI        · MongoDB 저장
 · 서보모터(분류)               · YOLOv4-tiny 추론         · FCM 푸시 알림
 · 압축 모터                    · Flask 서버(MJPEG/용량)   · 통계 집계 스케줄러
                                                          · MJPEG 프록시 릴레이

사용자가 키오스크 버튼을 누르면 라즈베리파이가 카메라 프레임을 캡처해 OpenCV DNN으로 YOLOv4-tiny 추론을 수행한다. 분류 결과는 1바이트 시리얼 명령(C/P/G)으로 아두이노에 전달되어 서보모터가 해당 칸(캔/플라스틱/일반)을 개방하고, 2초 후 압축 모터가 구동된다. 동시에 분류 결과와 촬영 이미지는 Spring Boot 서버를 거쳐 MongoDB에 축적된다.

핵심 설계 의도는 세 가지였다.

  1. 추론은 온디바이스에서 끝낸다. 클라우드 왕복 없이 라즈베리파이 CPU에서 직접 추론해 분류 지연과 네트워크 의존성을 제거한다.
  2. 앱은 사설망의 라즈베리파이에 직접 접근하지 않는다. 모든 요청은 Spring 서버를 유일한 진입점으로 거치게 해 접근 경로를 단일화한다.
  3. 계층별 장애가 전체를 멈추지 않게 한다. 분류 실패 시 general 폴백, 백엔드 전송의 스레드 분리 등으로 하드웨어 흐름은 항상 진행되도록 설계한다.

하드웨어 설계

하드웨어 형상 설계와 배선은 하드웨어 담당 팀원이, 그 위에서 도는 제어 소프트웨어는 내가 맡았다. 팀원이 분리 칸 배치, 서보모터·압축 모터 위치, 초음파 센서 부착 지점을 CAD 도면으로 설계하고 실제 전선·모터를 연결했으며, 나는 그 하드웨어를 움직이는 아두이노 제어 코드와 시리얼 통신 프로토콜을 구현했다.

RGB 하드웨어 CAD 도면

분리수거함 하드웨어 CAD 도면 — 3칸(일반/캔/플라스틱) 구조와 서보모터·압축 모터 배치.

RGB 실제 외형

완성된 실물 — 상단에 카메라와 키오스크 디스플레이, 내부에 분리·압축 기구부가 들어간다.

6. 핵심 설계 결정

6-1. 왜 YOLOv4-tiny + OpenCV DNN인가

엣지 환경의 제약이 모델과 런타임 선택을 결정했다. 라즈베리파이 5에는 GPU 가속이 없고, 클라우드 추론은 네트워크가 불안정한 시연 환경에서 리스크였다.

  • 모델: YOLOv4-tiny 커스텀 2클래스(can, plastic cup) — CPU 실시간 추론이 가능한 경량 모델
  • 런타임: OpenCV DNN(cv2.dnn.readNet) — PyTorch/TensorFlow 런타임 설치 없이 .weights 파일만으로 배포 가능
  • 학습: Darknet(AlexeyAB) 기반 Google Colab GPU 학습, Roboflow 데이터셋 증강 (입력 416×416, batch 64, max_batches 6000)

추론 파이프라인은 전처리 → forward → confidence 0.5 필터링 → NMS(0.4) 중복 제거 → 최상위 라벨 확정으로 구성했다. 모델은 첫 추론 요청 시 1회만 로드하는 지연 로딩(lazy loading)으로 키오스크 기동 속도를 확보했고, 감지 실패 시에는 general(일반쓰레기)로 폴백해 분류 불가 상황에서도 하드웨어 흐름이 멈추지 않게 했다.

6-2. 실시간 영상 스트리밍 — MJPEG 프록시 릴레이

앱에서 쓰레기통 카메라를 실시간으로 확인하는 기능은 프록시 릴레이 구조로 풀었다. 라즈베리파이는 사설망 안에 있어 앱이 직접 접근할 수 없었기 때문이다.

  • 라즈베리파이 Flask가 카메라 프레임을 JPG(quality 60, 320×240, 15fps)로 인코딩해 multipart/x-mixed-replace MJPEG 스트림으로 송출
  • Spring 서버가 RestTemplate.execute() 콜백에서 라즈베리파이 InputStream을 읽어 클라이언트 OutputStream으로 8KB 버퍼 단위 중계

스트리밍은 자원 점유 문제가 따라오는 기능이라 안정성 장치를 두 가지 넣었다.

  1. 10초 스트리밍 타임리밋 — 커넥션 무한 점유 방지
  2. 클라이언트 연결 종료 감지 시 라즈베리파이 측 스트림도 즉시 close — 앱이 화면을 닫아도 카메라가 계속 열려 있던 자원 누수를 해결했고, Flask 쪽도 generator의 finally에서 camera.release()를 보장했다.

6-3. 용량 알림 — 히스테리시스로 채터링 차단

“80% 넘으면 알림”이라는 단순한 요구사항에도 함정이 있었다. 임계값 하나로 판단하면 용량이 경계값(80%) 근처에서 오르내릴 때 알림이 반복 발송되는 채터링(chattering) 문제가 생긴다.

그래서 히스테리시스 방식의 이중 임계값을 적용했다.

  • 용량 ≥ 80%이고 미발송 상태 → 알림 발송 후 플래그 잠금
  • 용량 < 70%로 떨어져야 플래그 해제 → 다음 알림 허용

아두이노 초음파 센서 값(General:0.85 | Plastic:0.10 | Can:0.00 텍스트 프로토콜)은 라즈베리파이의 백그라운드 데몬 스레드가 상시 파싱해 최신값을 캐싱하므로, 용량 조회 API는 시리얼 대기 없이 즉시 응답한다.

서버 측에서는 Firebase Admin SDK로 등록된 전체 디바이스 토큰에 푸시를 발송하는데, 발송 실패 토큰은 만료로 간주해 자동 삭제하는 자가 정화 로직을 넣어 토큰 저장소가 스스로 깨끗해지도록 했다.

6-4. 정답/오답 검수 — Human-in-the-loop

분류 이력 조회 기능을 단순 로그가 아니라 모델 성능 검증 데이터를 확보하는 통로로 설계했다.

  • 분류 시마다 라벨 + Base64 원본 이미지를 MongoDB에 state = "unseen"으로 저장
  • 사용자가 앱에서 사진을 보고 정답(correct) / 오답(incorrect) 판정 → PUT /api/garbage/{id}/state로 반영

이 피드백 데이터는 모델 정확도 측정과 재학습 데이터셋 확보로 이어지는 Human-in-the-loop 구조다. AI 기능을 “배포하면 끝”이 아니라 “배포 후 검증하고 개선하는 사이클”로 바라본 설계였다.

6-5. 시간별 통계 집계 — 멱등 스케줄러와 읽기 최적화 스키마

통계 기능은 읽기 패턴에 맞춰 스키마를 설계했다. 앱은 항상 “하루치 통계”를 요청하므로, 날짜 1건 = Document 1개, 내부에 24시간 × 3종(일반/플라스틱/캔) 2차원 배열을 담아 단일 조회로 하루 전체를 가져올 수 있게 했다.

집계 로직에는 운영 관점의 장치를 담았다.

  • 멱등(idempotent) 집계: 갱신 전 해당 시간 카운트를 0으로 리셋하고 직전 시간 + 현재 시간을 동시 재계산 → 스케줄러가 몇 번 재실행되어도 결과가 같다.
  • 백필(backfill) API: 스케줄러가 죽어 있던 기간의 과거 데이터를 소급 집계할 수 있는 운영 도구를 제공했다.
  • java.time.Clock Bean 주입: 시간 의존 로직을 고정 Clock으로 단위 테스트할 수 있게 설계했고, 실제로 GarbageStatisticsServiceTest에서 검증했다. 검증용 더미 데이터셋 700여 건도 직접 제작했다.

7. 트러블슈팅

RGB 트러블슈팅

7-1. 스트리밍 자원 누수 — 끊긴 클라이언트가 카메라를 붙잡고 있었다

앱에서 스트리밍 화면을 닫아도 라즈베리파이 쪽 카메라 스트림이 계속 열려 있는 문제가 발생했다. Spring 프록시가 클라이언트 연결 종료를 감지하지 못한 채 라즈베리파이 InputStream을 계속 읽고 있었던 것이다.

클라이언트 OutputStream 쓰기 실패를 감지하면 라즈베리파이 측 스트림도 즉시 끊도록 릴레이 로직을 수정했다. 여기에 10초 타임리밋을 함께 두어, 어떤 경로로든 스트림이 무한히 점유되는 상황을 원천 차단했다.

7-2. UI 프리징 — 네트워크 I/O를 UI 스레드에서 분리

분류 결과와 Base64 이미지를 서버로 전송하는 동안 키오스크 UI가 멈추는 문제가 있었다. 이미지 인코딩 + HTTP POST가 UI 스레드를 블로킹하고 있었다.

백엔드 전송을 별도 데몬 스레드로 분리해 해결했다. 사용자 입장에서 분류→분리→압축 흐름은 네트워크 상태와 무관하게 즉시 진행되고, 기록 저장은 뒤에서 따라온다.

7-3. 실기기의 사소하지만 치명적인 특성들

  • 아두이노는 시리얼 연결 직후 리셋된다connect()2초 안정화 대기를 넣어 초기 명령 유실을 방지했다.
  • 라즈베리파이 5는 카메라 스택이 libcamera로 전환됐다GStreamer(libcamerasrc) 파이프라인을 우선 시도하고 실패 시 V4L2로 폴백하는 이중화로 대응했다.
  • 하드웨어 동작 중 중복 투입is_processing 플래그로 상태 머신의 재진입을 차단했다.

시뮬레이터에서는 절대 만날 수 없는 문제들이었다. 실기기 위에서 도는 시스템은 코드 로직만큼이나 하드웨어의 물리적 특성을 이해하는 것이 중요하다는 걸 배웠다.

8. 회고 — 남은 개선 과제

완성도를 높이는 과정에서 스스로 인지하고 있는 개선 여지도 정리해 둔다.

  • Base64 이미지의 MongoDB 직저장은 Document 크기 부담이 있다 → GridFS 또는 오브젝트 스토리지 전환 고려
  • RestTemplate은 유지보수 모드이므로 WebClient 마이그레이션 필요
  • 통계 스케줄러의 cron 표현식이 주석 의도(“매시간”)와 달리 매분 실행되도록 설정되어 있어 정합성 확인 필요

문제를 해결한 경험만큼, 무엇이 미완인지 아는 것도 성장의 일부라고 생각한다.

마무리

RGB는 AI, 하드웨어, 서버, 앱이라는 이질적인 4개 계층을 하나의 동작하는 시스템으로 통합해 본 프로젝트였다. 나는 이 프로젝트에서 계층 간 통신 프로토콜을 직접 설계했고, GPU 없는 엣지 환경의 제약 안에서 AI 추론을 배포했으며, 멱등 집계·자가 정화 토큰·히스테리시스 알림처럼 “돌아가는 것”을 넘어 “계속 돌아가는 것” 을 고려한 백엔드를 구현했다. 기능 하나하나보다, 시스템 전체가 실기기 위에서 끊기지 않고 동작하도록 만드는 과정이 가장 큰 배움이었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.