📘 독초 판별 서비스(POISON)

지금까지 여러 프로젝트를 하면서 많이 배웠다고 생각했는데, 막상 사용 기술이나 “왜 그 기술을 썼는지”를 질문받으면 설명이 막힐 때가 있었습니다. 면접 준비를 하며 더 크게 느꼈고, 이번 기회에 프로젝트를 정리하면서 다시 공부해보려고 합니다. 중점은 기술을 왜 선택했는지, 그리고 선택으로 얻은 이점(효과) 입니다. 제가 처음 진행했던 프로젝트 독초 판별 서비스(POISON)를 정리해보겠습니다.

서비스 내용

사용자가 이미지를 업로드하면 AI가 식물 중 가장 비슷한 종을 판별해 결과를 제공하는 서비스입니다. 추가로,학습된 식물들의 도감 리스트/상세 페이지 , 도감 리스트는 전체 로딩이 아닌 무한 스크롤 기반의 점진적 로딩, 도감 검색(오타/유사어 대응) , 판별 결과 기준으로 많이 조회된 식물 랭킹 제공을 구현했습니다.

핵심 기능은 크게 3가지(판별 / 도감 / 검색·랭킹)이며, 기능 자체 설명보다는 기술 선택 이유 중심으로 정리하겠습니다.

백엔드 서버를 두 개로 분리한 이유 (Django + AI 서비스)

본 프로젝트는 백엔드를 다음과 같이 역할 기준으로 분리했습니다.

메인 API 서버: Django

사용자/도감/랭킹/검색 요청 처리

DB 트랜잭션 및 비즈니스 로직 처리

인증/인가 및 API 정책(권한, rate limit 등) 중심

AI 추론 서비스: Flask(또는 경량 Python 서비스)

모델 로딩 및 추론 실행

추론 결과 생성(예: top-k, confidence 등)

웹 기능/DB 로직과 분리된 “연산 중심” 서비스

중요한 점은, 구조적으로 클라이언트(프런트엔드)는 기본적으로 Django API만 호출하고, Django가 내부적으로 AI 작업을 트리거(큐 등록/내부 호출)하는 형태가 더 안전하고 운영이 단순합니다. (클라이언트가 AI 서버를 직접 호출하는 구조는 인증/보안/장애처리 책임이 분산되기 쉬워서 운영 난이도가 올라갑니다.)

백엔드 서버를 두 개로 둔 이유

관심사 분리

메인 서버는 “웹 서비스/데이터”에 집중하고, AI 서비스는 “모델 추론”에 집중하도록 분리했습니다.

결과적으로 코드베이스가 단순해지고, 장애 원인 파악과 운영이 쉬워집니다.

확장성(Scale-out)

AI 추론은 CPU/GPU/메모리를 많이 쓰는 연산 집약 작업이라 트래픽 증가 시 메인 웹 서버와 같은 방식으로 확장하면 비효율이 생깁니다.

웹 서버(Django)와 AI 서비스(Flask)를 분리하면 각자의 부하 특성에 맞게 독립적으로 확장할 수 있습니다.

운영 안정성

추론 작업이 길어질 경우 요청-응답이 붙잡혀서 웹 서버 워커가 고갈되거나 타임아웃이 발생할 수 있습니다.

AI 서비스를 분리하고 비동기 처리와 결합하면, 웹 서버는 빠르게 응답하고 AI는 백그라운드에서 처리하여 전체 시스템 안정성이 좋아집니다.

배포/유지보수성

모델 업데이트/추론 로직 변경은 AI 서비스에 국한되도록 격리할 수 있습니다.

메인 API(회원/도감/랭킹)는 상대적으로 안정적으로 운영하고, AI 서비스만 독립 배포하는 전략이 가능해집니다.

비동기 처리(Celery 등)를 사용한 이유

이 프로젝트에서 “비동기 처리”의 핵심은 단순히 동시 처리라기보다, 장시간 걸릴 수 있는 AI 추론을 요청-응답 사이클에서 분리하는 데 있습니다.

사용자가 이미지를 업로드하면 Django는: 업로드 요청을 검증하고 저장(S3 등) 추론 작업을 작업 큐(Celery)에 등록 사용자에게는 즉시 “진행 중” 상태와 작업 식별자(task id) 를 응답 이후 AI 워커(또는 AI 서비스)가 백그라운드에서 추론을 수행하고 결과를 저장합니다.

이 방식의 이점은 다음과 같습니다.

요청 타임아웃 방지: 추론 시간이 길어져도 웹 요청을 오래 잡고 있지 않음

웹 서버 워커 점유 방지: 한 사용자의 추론 때문에 다른 요청이 밀리는 상황 감소

재시도/모니터링 가능: 큐 기반 처리로 실패 시 재시도, 상태 추적, 워커 분리 운영이 가능

폴링(Polling) 방식을 사용한 이유

비동기 처리로 추론을 백그라운드로 분리하면, 클라이언트는 “작업이 완료됐는지”를 확인할 방법이 필요합니다. 이때 폴링은 클라이언트가 주기적으로 작업 상태를 조회(Pull) 하는 방식으로 구현이 단순하고 안정적입니다.

흐름 예시

업로드 응답으로 task id 수신

클라이언트가 /tasks/{taskId}/status 같은 API를 일정 주기로 호출

완료 상태가 되면 결과를 가져와 UI 갱신

Celery 작업 상태를 조회하려면 일반적으로 Result Backend(예: Redis/DB)가 필요하며, 완료 결과(판별 식물 id, 확률 등)는 DB에 저장해 API로 조회 가능하도록 구성했습니다. 또한 폴링 주기가 너무 짧으면 저장소에 부하가 생길 수 있어 주기/백오프 전략을 고려할 수 있습니다.

추가로, 원문에 있던 “판별이 끝났다고 알람을 보내는 확장”은 폴링만으로는 어렵고, 그것은 WebSocket/SSE/푸시(FCM 등)처럼 서버→클라이언트 Push 채널을 붙였을 때 가능한 확장입니다. 따라서 문장에서는 다음처럼 분리해 설명하는 게 정확합니다.

현재: 폴링으로 완료 감지 및 UI 갱신

확장: WebSocket/SSE/푸시를 통해 완료 알림(푸시) 제공 가능

S3를 사용한 이유

이미지 데이터를 DB에 바이너리로 직접 저장하면 저장 용량 증가, 백업/복구 부담, 성능 저하 등의 문제가 생길 수 있습니다. 그래서 이미지는 객체 스토리지(S3)에 파일(Object)로 저장하고, DB에는 이미지 자체가 아닌 S3 Object Key(경로) 또는 참조 정보를 저장하는 방식으로 설계했습니다.

또한 접근 제어가 필요한 경우에는:

버킷을 비공개로 두고

업로드/다운로드 시 Presigned URL을 발급하는 방식 을 적용하면 보안 및 권한 관리 측면에서 더 안전합니다.

검색엔진(Atlas Search)을 사용한 이유

사용자는 검색어를 항상 정확히 입력하지 않습니다(오타/유사어/부분검색). 단순 LIKE 검색만으로는 UX가 떨어질 수 있어, 오타 허용 및 유사어 검색을 위해 MongoDB Atlas Search를 적용했습니다.

다만 Atlas Search는 MongoDB 컬렉션 기반이므로, 설계 설명에서 아래 중 하나를 명확히 해두는 것이 좋습니다.

도감/검색 대상 데이터 자체를 MongoDB에 저장하고 Atlas Search를 적용했는지

또는 메인 DB가 따로 있고, 검색용으로 MongoDB에 인덱싱/동기화 파이프라인을 구성했는지

면접에서는 “왜 Elasticsearch가 아니라 Atlas Search였는가?”, “동기화는 어떻게 했는가?”가 자주 나오기 때문에, 데이터 위치와 동기화 전략을 한 줄로라도 정리해두는 게 좋습니다.

무한 스크롤(도감 리스트)을 적용한 이유

도감 리스트를 전체 로딩하면 초기 로딩이 느려지고, 네트워크/서버 부담이 커집니다. 무한 스크롤 방식으로 필요한 만큼만 로딩하여 초기 응답성을 높이고, 사용자가 실제로 보는 범위만 효율적으로 제공하도록 했습니다.

추가로 성능/안정성을 위해서는 단순 offset 기반 페이징보다:

lastId, createdAt 등을 기준으로 다음 페이지를 가져오는 cursor 기반 페이지네이션 이 대량 데이터에서 더 유리하다는 점까지 정리해두면 설계 설명이 더 탄탄해집니다.

태그:

카테고리:

업데이트: