본문으로 이동

대표 프로젝트

운영 중

포우브릿지

보호동물 조회와 입양 준비를 돕는 서비스입니다. 처음부터 배포와 운영을 맡아 사진 검색을 추가하고, 검색 성능과 데이터 처리 구조를 개선해 왔습니다.

2025.11부터 개발운영 서비스 열기 ↗

개발과 개선 한눈에 보기

저장 구조 단순화

두 저장소를 하나로

업무 정보와 검색 데이터를 함께 관리

검색 응답 개선

792 → 382ms

요청 95%의 응답 완료 기준 시간

수집과 갱신 안정화

미완료 구간 재수집

저장 성공과 수집 완료 여부를 구분

실종동물 찾기

사진으로 후보 검색

보유 컴퓨터에서 이미지 AI 실행

서비스 간 이벤트 전달

Outbox와 중복 처리 방지

업무 변경과 전달할 이벤트를 함께 기록

이미지와 배포 운영

저장 용량과 비용 고려

작은 사진 파일 저장과 배포 설정 관리

검색 수치는 사진 검색을 제외하고 최대 100명의 가상 사용자가 요청한 시험 결과입니다. PostgreSQL에서 반복 검사를 개선한 전후를 비교했으며, 미리 저장된 조회 결과와 다른 운영 작업량은 완전히 같지 않았습니다.

개발 범위
아키텍처와 기술 선정, 동물·상점·결제 백엔드, React와 TypeScript 프론트엔드 전체 개발
운영 책임
처음부터 서비스 전반의 배포와 운영을 담당했습니다. 동료가 초기 구현한 회원과 커뮤니티 서비스도 이후 유지보수했습니다.

운영하며 개선하고 확장한 경험

운영 구조

MySQL과 Elasticsearch를 PostgreSQL로 통합했습니다

해결하려던 문제
두 저장소를 함께 운영하면서 메모리를 관리하고, 양쪽의 변경 내용을 맞추고 검색용 데이터를 다시 만드는 작업도 필요했습니다.

변경과 결과
업무 정보, 글 검색과 사진 비교에 필요한 데이터를 PostgreSQL로 모아 관리할 저장소를 줄였습니다.

선택 이유와 검증 과정

기존 선택

초기에는 한국어를 분석해 관련성이 높은 결과를 찾으려고 업무 정보는 MySQL에, 검색용 데이터는 Elasticsearch에 저장했습니다. 대신 양쪽의 정보를 맞추고 검색용 데이터를 다시 만드는 작업까지 관리해야 했습니다.

전환 이유

Elasticsearch의 모든 기능을 대체하는 것이 아니라 서비스에 필요한 검색을 유지하면서 운영 대상을 줄이는 선택이었습니다. Redis와 Kafka는 유지했습니다.

검색 구조

전환하면서 동물의 품종과 특징에서 검색할 단어를 추출해 PostgreSQL 안의 별도 테이블에 저장했습니다. 동물 정보와 키워드 검색용 데이터는 구분하되, 같은 DB에서 함께 관리하도록 바꿨습니다. 사진 비교용 벡터는 확장 기능인 pgvector로 저장했습니다.

확인한 변화

전환 전후 운영 구간에서 클러스터 내 DB 평균 메모리는 약 2.76GiB에서 1.29GiB로 관측됐습니다. 요청량이 달랐으므로 동일 부하에서의 절감률이나 실제 요금 절감으로 표현하지 않습니다.

고려한 점

통합 후에도 동물 정보의 변경이 키워드 검색용 데이터에 반영됐는지 확인해야 합니다. 여러 작업이 한 DB의 자원을 나눠 쓰므로 장애 영향도 집중됩니다. 이어지는 검색 응답 개선은 DB 교체 자체가 아니라 전환 후 반복 검사를 줄인 결과입니다.

공공데이터 수집

저장 성공과 데이터를 빠짐없이 받았는지를 구분했습니다

해결하려던 문제
저장을 마쳐도 외부 제공처가 알려 준 건수와 수집 결과가 다르면 작업 전체가 실패해, 처리 오류와 수집 미완료를 구분하기 어려웠습니다.

변경과 결과
저장과 검색 반영을 마친 작업은 완료하고, 건수가 맞지 않는 조회 구간은 다음 실행에서 다시 수집하도록 남겼습니다.

선택 이유와 검증 과정

문제

외부 제공처가 알려 준 건수와 중복을 제외하고 확보한 동물 수가 달랐습니다. 제공된 데이터의 중복이나 누락 때문인지, 조회 도중 정보가 바뀌었기 때문인지는 확정하지 못했습니다.

실패 기준

저장과 검색 반영은 성공하고 건수 차이만 남았다면 작업은 완료하되 수집은 미완료로 기록했습니다. 통신 실패, 요청 한도 초과, DB와 검색 엔진 오류는 계속 실패로 처리합니다.

검증

9월 14일 배포 후 첫 예약 실행을 확인하고 DB와 검색용 데이터의 전체 건수 60,199건을 대조했습니다. 외부 데이터의 불일치 자체를 해결한 것은 아니며, 다음 실행에서 다시 확인하도록 남겼습니다.

사진 검색 기능 확장

등록 동물 추천에서 사용자 사진 검색으로 확장했습니다

해결하려던 문제
기존 추천은 서비스에 등록된 동물이 기준이어서, 보호자가 가진 사진으로 보호동물 후보를 찾는 흐름을 지원하지 못했습니다.

변경과 결과
사진을 올리면 보호동물 후보를 찾고, 최신 보호 상태와 공고 및 보호소 연락처까지 확인할 수 있게 했습니다.

선택 이유와 검증 과정

기존 구조

DINOv2를 사용하는 유사 동물 추천과 Gemini 입양 상담은 Python과 FastAPI로 만든 별도 서비스에 뒀습니다. Java로 처리하는 업무 기능과 사진 분석 및 외부 AI 호출을 나눠 다루는 구조입니다.

선택 이유

보유 컴퓨터의 남는 그래픽 처리 자원(GPU)을 활용하고, 검색할 때마다 외부 AI를 호출하는 비용을 고려했습니다. 사진 특징을 뽑는 DINOv3와 사진에서 동물 영역을 분리하는 SAM3를 직접 실행했습니다. 발견 시기와 지역도 후보 순위에 활용하며, 대화형 언어 모델(LLM)은 이 사진 검색에 사용하지 않습니다.

사용자 흐름

사진을 올려 후보를 찾은 뒤 최신 보호 상태를 확인하고 공고와 보호소 연락처를 살펴봅니다. 사진이 비슷하다는 점수는 같은 동물일 확률이 아니라, 추가 확인할 후보를 좁히는 기준입니다.

품질 확인

9월 26일 동물 20마리의 사진으로 별도 비교한 결과, 배경을 지워도 얼굴 사진으로 전신 사진을 찾는 결과가 일관되게 좋아지지는 않았습니다. 기능이 작동하는 것과 검색 품질이 좋아지는 것을 구분해, 가정만으로 운영 설정을 바꾸지 않았습니다.

이미지 저장

파일이 더 작아질 때만 사진 형식을 바꿨습니다

해결하려던 문제
사진을 계속 수집하는 서비스에서 원본 저장 용량과 동일 이미지의 반복 업로드를 줄일 필요가 있었습니다.

변경과 결과
사진의 가로·세로 크기를 유지한 채 WebP 형식으로 바꾸고, 원본보다 작을 때만 저장했습니다. 같은 사진을 반복 업로드하는 것도 줄였습니다.

선택 이유와 검증 과정

선택 이유

저장 용량을 줄이기 위한 변환이 오히려 파일을 키우거나 반복 압축을 만들지 않도록 조건을 뒀습니다. 이미 WebP인 이미지는 재압축하지 않습니다.

구현

파일 내용으로 계산한 식별값(해시)으로 저장 경로를 정했습니다. 이미지 저장소인 Cloudflare R2에 같은 파일이 이미 있는지 확인해 중복 업로드를 줄였습니다. 저장 용량을 줄이는 목적과 실제 요금 감소는 구분하며, 확인하지 않은 절감액은 제시하지 않습니다.

서비스를 지탱하는 설계와 운영 경험

서비스 간 이벤트 전달

Outbox로 업무 변경과 이벤트를 함께 저장했습니다

해결하려던 문제
DB 저장 후 변경 알림을 보내지 못하면 다른 서비스에 반영되지 않고, 다시 보내면 같은 작업을 중복 처리할 위험이 있었습니다.

변경과 결과
업무 변경과 이벤트(다른 서비스에 전달할 변경 알림)를 함께 저장하는 Outbox를 적용했습니다. 처리 이력을 확인해 같은 이벤트를 중복 처리하지 않도록 했습니다.

선택 이유와 검증 과정

문제와 선택

DB 저장과 이벤트 발행을 따로 하면 그 사이에 실패할 수 있습니다. 업무 변경과 전달할 이벤트를 같은 트랜잭션에 기록해 둘 다 저장되거나 함께 취소되도록 했습니다. DB 변경을 읽는 CDC 방식의 Debezium과 이벤트 전달을 맡는 Kafka를 연결했습니다.

실패 대응

같은 대상을 나타내는 번호로 알림을 묶고 받는 서비스에서 처리 이력을 확인했습니다. 찜 추가가 실패하면 등록을 취소하도록 별도 알림을 보내는 처리도 뒀습니다. 이 구성만으로 모든 알림이 반드시 한 번만 처리된다고 보장하지는 않습니다.

동시 요청

캐시 재생성과 주문 재고 변경을 조율했습니다

해결하려던 문제
빠른 조회를 위해 임시 저장한 결과가 만료되면 요청이 DB에 몰릴 수 있습니다. 여러 주문이 같은 재고를 동시에 바꾸는 상황에도 대비해야 했습니다.

변경과 결과
임시 저장한 조회 결과를 다시 만드는 작업은 Redis 분산 락으로 조율했습니다. 재고 변경에는 같은 DB 행을 먼저 잠그는 비관적 락을 적용했습니다.

선택 이유와 검증 과정

조회 결과 재사용

캐시 만료 때 DB 요청이 몰리는 것을 완화하려고, 조회 결과를 다시 만드는 작업을 Redis 분산 락으로 조율했습니다. 작업 차례를 얻은 뒤에도 다른 요청이 이미 결과를 만들었는지 다시 확인하고, 저장된 결과의 만료 시간을 서로 다르게 뒀습니다.

재고 변경

같은 재고를 읽고 바꾸는 주문이 서로 겹치지 않도록 해당 DB 행을 먼저 잠그는 비관적 락을 적용했습니다. 다른 주문이 기다릴 수 있으므로 처리량을 높이기보다 재고가 잘못 계산되는 것을 막기 위한 선택입니다.

배포와 운영 점검

처음부터 배포와 운영을 맡고 검증을 이어왔습니다

해결하려던 문제
서비스를 계속 운영하려면 클라우드 자원 비용을 고려하면서 여러 서비스의 배포 설정과 운영 상태를 관리해야 했습니다.

변경과 결과
클라우드 서버에서 직접 관리하는 가상 서버로 옮겼습니다. 서비스별 배포 설정을 관리하고, Prometheus와 Grafana로 자원 사용량을 확인했습니다. Slack 경보를 구성해 알림 수신도 확인했습니다.

선택 이유와 검증 과정

운영 선택

클라우드 자원 비용을 고려해 EC2에서 직접 관리하는 가상 서버의 Kubernetes 환경으로 옮겼습니다. Helm과 Argo CD로 배포 설정을 관리하고, Prometheus와 Grafana로 자원 사용량과 요청 상태를 살펴봤습니다. 직접 서버를 관리해야 하므로 클라우드와 같은 운영 조건이라고 설명하지 않습니다.

과거 부하 검증

MySQL과 Elasticsearch를 사용하던 시기의 공개 기록에는 최대 100명 가상 사용자, 5분 혼합 시나리오에서 17,356건 처리, 전체 p95 69.69ms, 보고 오류율 0%가 남아 있습니다. 당시 원시 결과와 오류 집계 기준은 현재 확보한 자료만으로 재확인하지 못해, 모든 HTTP 응답이 성공했다는 뜻으로 확대하지 않습니다.

현재 검증과 구분

과거에는 유사 동물 추천까지 섞어 시험했습니다. 현재의 일반 조회 시험과는 접속 경로, 데이터량, 미리 저장된 조회 결과와 서버 자원이 다릅니다. 두 시험의 수치로 개선율을 계산하지 않고 각 시점의 검증 기록으로 남겼습니다.

목록 처리 구조 보완

목록 전달과 처리를 제한된 묶음으로 나눴습니다

해결하려던 문제
초기 설계에서 전체 목록의 메모리 비용을 충분히 고려하지 못해, 응답을 만드는 도중 검색 데이터 갱신이 중단됐습니다.

변경과 결과
보내는 쪽과 받는 쪽 모두 처리량을 나눠, 2026년 9월 16일 운영 환경에서 54,530건의 갱신을 완료했습니다.

선택 이유와 검증 과정

선택 이유

메모리 한도를 늘려도 전체 목록을 한꺼번에 담는 구조는 남습니다. 양쪽을 묶음 단위로 처리하고, 새 검색 데이터가 완성되기 전에는 기존 데이터를 유지했습니다.

검증

갱신 완료와 검색 후보 반환을 각각 확인했습니다. 해당 데이터량에서의 확인이며, 더 큰 데이터량에서도 항상 안전하다는 뜻은 아닙니다.

서비스 기능도 계속 확장했습니다

보호소 정보 조회와 보호소 담당자 신청·승인, 기간과 지역별 보호동물 현황, 반려동물 동반 여행 정보를 추가했습니다. 현황은 접수일 기준 집계와 현재 보호 상태를 구분해 안내합니다.

기술 구성

2026년 9월 개선 기록 기준
백엔드
Java, Spring Boot
프론트엔드
React, TypeScript
데이터
PostgreSQL, pgvector, Redis, Kafka, Debezium
AI와 이미지
Python, FastAPI, DINOv3, SAM3, Gemini, Cloudflare R2
배포와 관측
Docker, Kubernetes, Helm, Argo CD, Prometheus, Grafana