분리배출은 쉽게, 실천은 보상으로 이어지도록 사용자 경험의 흐름을 안정적으로 뒷받침하는 ReDO Backend입니다.
ReDO는 분리배출 정보를 쉽고 빠르게 제공하고, 실천에 대한 보상을 통해 사용자의 행동을 유도하는 서비스입니다.
- 기간 : 2026.06.29 ~ 2026.08.21
- 인원 : Backend 5명
- 목표 : AI 기반 정보 제공과 리워드 시스템을 결합하여 분리배출을 일상의 습관으로 만들어 지속적인 환경 보호 참여를 이끌어내는 것
- 핵심 흐름 : AI 분리배출 정보 → AI 분리배출 인증 → 포인트 적립 → 리워드 구매 → 기여도 시각화
| 천진하 | 안동휘 | 채민수 | 허건우 | 이정우 |
|---|---|---|---|---|
| 회원가입·로그인·프로필 | AI 분리배출 정보 | AI 분리배출 인증 | 인프라·포인트·리워드·기여도 | 커뮤니티 |
- 아이디 중복 확인과 이메일 인증을 통한 일반 회원가입
- 서비스 약관 조회 및 필수 약관 동의 처리
- Google, Kakao, Naver 소셜 회원가입 및 로그인
- JWT 기반 Access Token 발급과 Refresh Token을 이용한 토큰 재발급
- HttpOnly Cookie와 Redis를 활용한 Refresh Token 관리 및 로그아웃
- Redis 원자 연산으로 로그인 실패 횟수를 관리하여 반복적인 로그인 시도 제한
- 사용자 프로필 생성 및 조회
- 닉네임, 프로필 이미지, 캐릭터 수정
- 회원 탈퇴 사유 조회 및 회원 탈퇴 처리
- 품목명을 이용한 분리배출 정보 조회
- 촬영하거나 업로드한 이미지를 AI가 분석하여 품목 식별
- 사용자가 입력한 문제 상황을 분석하여 적합한 분리배출 정보 제공
- Spring AI와 Gemini를 활용한 이미지·텍스트 기반 정보 탐색
- JSON Schema 기반의 구조화된 AI 응답 검증
- AI 분석 결과를 서비스에 등록된 품목 및 분리배출 정보와 연결
- 품목별 배출 방법과 단계별 주의사항 제공
- 자주 확인하는 분리배출 정보 즐겨찾기
- 탐색 결과에서 해당 품목의 배출 인증 기능으로 연결
- 당일 인증 성공 횟수와 남은 인증 가능 횟수 조회
- 일반 인증과 분리배출 정보 탐색 후 인증 지원
- 실시간으로 촬영한 배출 완료 이미지를 Gemini VLM으로 판정
- 인증 성공 여부와 인증 품목, 판정 결과 및 인증 일시 제공
- 인증 방식에 따른 포인트 자동 적립
- 일반 인증: 50P
- 정보 탐색 후 인증: 100P
- 일일 최대 3회 및 동일 품목 중복 인증 제한
- 인증 성공 후 5분간 신규 인증 제한
- AI 판정 실패 사유와 재촬영 방법 제공
- 실패한 인증 이미지의 재촬영 및 AI 재검수
- 인증 전용 비동기 실행기와 타임아웃을 적용하여 장시간 AI 요청 제어
- 인증 상태 변경, AI 판정 저장, 포인트 적립 및 기여도 이벤트를 하나의 트랜잭션으로 처리
- 사용자의 보유 포인트와 이번 달 적립 포인트 조회
- 포인트 적립 및 사용 거래 내역 조회
- 제휴 브랜드 배송 상품과 쿠폰·기프티콘 상품 제공
- 상품 유형별 목록, 상세 정보 및 사용자별 일일 추천 상품 조회
- 주소기반산업지원서비스 API를 이용한 배송지 주소 검색
- 배송지 생성, 조회, 수정 및 삭제
- 포인트를 사용한 리워드 구매
- 리워드 교환 내역과 배송·쿠폰 처리 상태 조회
Idempotency-Key와 DB 유니크 제약을 활용한 동일 구매 요청의 중복 처리 방지- 상품 단위 Redisson 분산 락으로 동일 상품의 동시 구매 요청 제어
- 사용자와 상품 데이터에 DB 비관적 락을 적용하여 포인트와 재고 정합성 보장
- 사용자의 누적 분리배출 인증 횟수 조회
- 인증 횟수에 따른 8단계 환경 기여 마일스톤 제공
- 현재 달성한 결과물과 다음 목표까지 남은 인증 횟수 표시
- 전체 서비스 참여자 수 조회
- 다른 사용자의 최근 분리배출 인증 활동을 피드 형태로 제공
- 커서 기반 페이지네이션을 활용한 기여도 피드 이어보기
- Cache-Aside 방식으로 기여도 이벤트와 참여자 수를 Redis에 캐싱
- 데이터 특성에 따라 서로 다른 TTL을 적용하여 캐시 최신성과 조회 성능 관리
- Redis 장애 시 데이터베이스 조회로 전환하여 기여도 기능 유지
- 정보 공유, 환경 실천, 리워드 후기 카테고리별 게시글 조회
- 게시글 목록 및 상세 정보 조회
- 제목, 내용 및 여러 장의 이미지를 포함한 게시글 등록
- 내가 작성한 게시글 수정 및 삭제
- 게시글별 댓글 조회, 등록, 수정 및 삭제
- 게시글 좋아요 및 좋아요 취소
- 내가 작성한 게시글과 댓글 모아보기
- 게시글 최신순 페이지네이션과 댓글 커서 페이지네이션
- 게시글별 댓글 수와 대표 이미지를 일괄 조회하여 N+1 문제 방지
- 커뮤니티 이미지를 AWS S3에 저장하고 다중 업로드 실패 시 보상 삭제 처리
- 문제
- Gemini API는 네트워크와 모델 상태에 따라 응답이 지연되어 애플리케이션 요청 처리 자원을 장시간 점유할 수 있었습니다.
- AI가 서비스에 존재하지 않는 품목을 생성하거나 약속된 JSON 형식과 다른 응답을 반환할 가능성이 있었습니다.
- 해결
- AI 호출 전용 Executor와 배출 인증 전용 Thread Pool을 분리하여 일반 API 요청과 AI 판정 작업의 실행 자원을 격리했습니다.
- 호출 제한 시간과 인증 전체 제한 시간을 별도로 설정하고, 시간 초과와 Provider 오류를 도메인 예외로 변환했습니다.
- 서비스 DB에 등록된 품목 목록을 프롬프트에 제공하여 해당 목록에서만 결과를 선택하도록 제한했습니다.
- 적합한 품목이 없으면
NOT_FOUND를 반환하도록 하고, JSON Schema를 적용하여 응답 형식을 검증했습니다.
- 효과
- 외부 AI 서비스의 응답 지연이 애플리케이션 전체로 확산되는 범위를 줄였습니다.
- AI 할루시네이션과 비정형 응답을 제어하여 서비스 데이터와 일치하는 결과만 제공할 수 있게 되었습니다.
- 문제
- 동일 상품에 구매 요청이 동시에 들어오면 상품 재고가 음수가 되거나 포인트가 중복 차감될 수 있었습니다.
- 네트워크 지연이나 사용자 재요청으로 동일한 구매 요청이 여러 번 처리될 가능성이 있었습니다.
- 해결
- 상품 ID 단위의 Redisson 분산 락을 적용하여 동일 상품에 대한 구매 요청을 순차적으로 처리했습니다.
- 트랜잭션 내부에서 사용자와 상품에 DB 비관적 락을 적용하고 포인트, 상품 상태 및 재고를 다시 검증했습니다.
- 요청별
Idempotency-Key를 받고 사용자 ID와 멱등성 키 조합에 DB 유니크 제약을 적용했습니다.
- 효과
- 다중 인스턴스 환경에서도 상품 재고와 사용자 포인트의 정합성을 유지했습니다.
- 동일 요청의 중복 처리와 서로 다른 상품의 동시 구매로 발생할 수 있는 포인트 중복 사용을 방지했습니다.
- 문제
- 기여도 피드 조회마다 이벤트와 사용자 프로필 및 전체 참여자 수를 반복 조회하여 DB 부하가 증가할 수 있었습니다.
- Redis 장애가 기여도 기능 전체의 장애로 이어지지 않도록 별도의 실패 경로가 필요했습니다.
- 해결
- Cache-Aside 방식으로 기여도 이벤트를 캐싱하고, 캐시에 없는 이벤트만 DB에서 일괄 조회한 뒤 다시 저장했습니다.
- 기여도 이벤트에는 30분, 변경 가능성이 높은 전체 참여자 수에는 1분의 TTL을 적용했습니다.
- 피드에 필요한 사용자 프로필을 사용자 ID 목록으로 일괄 조회했습니다.
- Redis 조회 또는 저장에 실패하면 DB에서 데이터를 조회하도록 Fail-Open 구조를 적용했습니다.
- 효과
- 기여도 데이터와 사용자 프로필의 반복 조회를 줄여 DB 접근량을 감소시켰습니다.
- Redis 장애가 발생하더라도 DB 조회를 통해 기여도 기능을 계속 제공할 수 있게 되었습니다.
- 문제
- 게시글별 댓글 수와 대표 이미지를 조회하기 위해 게시글마다 추가 쿼리가 실행되었습니다.
- 게시글이 N개일 때
1 + 2N개의 쿼리가 발생하여 10개 게시글 조회 시 총 21개의 쿼리가 필요했습니다.
- 해결
- 먼저 페이지에 표시할 게시글 목록을 확정한 뒤 해당 게시글의 댓글 수와 이미지를 각각 일괄 조회했습니다.
- 댓글 수는
GROUP BY community_id집계 결과를 Projection으로 조회하여 게시글별 Map으로 구성했습니다. - 이미지는
displayOrder순으로 조회하고 게시글별 가장 앞선 이미지를 대표 이미지로 선택했습니다.
- 효과
- 페이지 크기와 관계없이 커뮤니티 목록 조회 쿼리를 3개로 고정했습니다.
- 게시글 수가 증가하더라도 추가 쿼리가 비례하여 증가하지 않도록 개선했습니다.
- 문제
- S3와 DB는 하나의 트랜잭션으로 묶을 수 없어 다중 이미지 업로드의 일부만 성공하면 고아 객체가 남을 수 있었습니다.
- 프로필 이미지를 변경할 때 기존 이미지를 먼저 삭제하면 이후 DB 저장 실패 시 사용자 이미지가 유실될 수 있었습니다.
- 해결
- 업로드에 성공한 S3 객체 키를 추적하고, 이후 처리에서 오류가 발생하면 먼저 업로드된 객체를 삭제하는 보상 로직을 적용했습니다.
- 프로필 이미지는
신규 이미지 업로드 → DB 반영 및 URL 생성 → 기존 이미지 삭제순서로 처리했습니다. - DB 반영에 실패하면 새로 업로드한 이미지를 정리하고, 보상 처리 실패 원인을 로그로 남기도록 구성했습니다.
- 효과
- 다중 이미지 업로드 과정에서 발생할 수 있는 고아 객체를 줄였습니다.
- 프로필 이미지 변경 실패 시 기존 이미지를 유지하여 사용자 데이터 유실 가능성을 낮췄습니다.
- 문제
- 서버에 직접 접속하기 전까지 애플리케이션 장애와 EC2·Redis 자원 부족을 빠르게 파악하기 어려웠습니다.
- 컨테이너 로그 수집을 위한 Docker Socket 권한 노출과 EC2 간 로그 전송 구간의 보안을 고려해야 했습니다.
- 일시적인 지표 누락으로 동일한 Grafana 장애 알림이 반복되는 문제도 있었습니다.
- 해결
- Prometheus로 Spring Boot 애플리케이션, EC2 및 Redis 메트릭을 통합 수집했습니다.
- Grafana Alloy, Loki, Grafana를 이용해 컨테이너 로그 수집과 운영 대시보드를 구축했습니다.
- Docker Socket Proxy를 적용하여 Alloy에 필요한 조회 권한만 제한적으로 제공했습니다.
- Service EC2와 Monitoring EC2 사이의 로그 전송에 TLS와 Basic Auth를 적용했습니다.
- Grafana의 No Data 처리, 평가 대기시간 및 알림 정책을 조정하고 장애·복구 알림을 Discord로 전송했습니다.
- 효과
- 애플리케이션 로그와 인프라 메트릭을 하나의 대시보드에서 확인할 수 있게 되었습니다.
- Docker Socket과 로그 전송 경로의 보안을 강화하고 장애 알림의 정확도와 신뢰도를 높였습니다.
| 도메인 | Base Path | 주요 기능 |
|---|---|---|
| 인증 | /api/auth |
회원가입, 로그인, 소셜 로그인, 토큰 재발급, 로그아웃 |
| 사용자 | /api/users |
프로필 관리, 회원 탈퇴 |
| 약관 | /api/terms |
서비스 약관 조회 |
| 분리배출 정보 | /api/guides |
가이드 조회, AI 검색, 즐겨찾기 |
| 분리배출 인증 | /api/certification |
인증 정책 조회, 신규 인증, 재촬영 |
| 포인트 | /api/rewards/points |
보유 포인트 및 거래 내역 조회 |
| 리워드 상품 | /api/rewards/products |
상품 목록, 상세, 미리보기 |
| 리워드 교환 | /api/rewards/redemptions |
상품 교환 및 교환 내역 조회 |
| 배송지 | /api/shipping-addresses |
주소 검색 및 배송지 관리 |
| 기여도 | /api/contributions |
나의 기여도, 전체 기여도 피드 |
| 커뮤니티 | /api/community |
게시글, 댓글, 좋아요 |
전체 API 명세와 요청·응답 예시는 Swagger에서 확인할 수 있습니다.
- Swagger UI: https://redo-backend.site/swagger-ui/index.html#/
- 로컬 Swagger UI:
http://localhost:8080/swagger-ui/index.html
Back/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ ├── workflows/
│ │ └── ci-cd.yml
│ ├── CONTRIBUTING.md
│ └── PULL_REQUEST_TEMPLATE.md
├── gradle/
├── k6/
│ ├── config/ # 실행 프로필 및 성능 기준
│ ├── lib/ # 인증, HTTP 요청, 안전장치
│ └── scenarios/ # 도메인별 부하 테스트
├── monitoring/
│ ├── alloy/ # 컨테이너 로그 수집
│ ├── grafana/ # 대시보드 및 알림 프로비저닝
│ ├── loki/ # 로그 저장소
│ ├── loki-proxy/ # Loki TLS·인증 프록시
│ └── prometheus/ # 메트릭 수집 설정
├── src/
│ ├── main/
│ │ ├── java/com/redo/
│ │ │ ├── domain/
│ │ │ │ ├── certification/
│ │ │ │ ├── community/
│ │ │ │ ├── contribution/
│ │ │ │ ├── point/
│ │ │ │ ├── recycleGuide/
│ │ │ │ ├── reward/
│ │ │ │ ├── term/
│ │ │ │ └── user/
│ │ │ └── global/
│ │ │ ├── ai/
│ │ │ ├── apiPayload/
│ │ │ ├── config/
│ │ │ ├── redis/
│ │ │ ├── s3/
│ │ │ ├── security/
│ │ │ └── util/
│ │ └── resources/
│ │ ├── application.yaml
│ │ └── application-certification.yaml
│ └── test/
├── .env.example
├── build.gradle
├── compose.yaml
├── Dockerfile
├── gradlew
└── settings.gradle
- Java 17
- MySQL
- Redis
- AWS S3 Bucket
- Gmail SMTP 계정 및 App Password
- Gemini API Key (AI 기능 사용 시)
- 도로명주소 API Key (배송지 주소 검색 기능 사용 시)
AI 기능을 사용하지 않는 경우 GEMINI_ENABLED=false로 실행할 수 있습니다. 이 경우 이미지·텍스트 기반 분리배출 정보 탐색과 AI 배출 인증 기능은 사용할 수 없습니다.
git clone https://github.com/REDO-Team/Back.git
cd Backcp .env.example .env주요 환경변수는 다음과 같습니다.
| 환경변수 | 설명 |
|---|---|
DB_URL |
MySQL JDBC URL |
DB_USERNAME |
MySQL 사용자명 |
DB_PASSWORD |
MySQL 비밀번호 |
JWT_SECRET |
JWT 서명 Secret |
MAIL_USERNAME |
이메일 인증 발송 계정 |
MAIL_PASSWORD |
Gmail App Password |
S3_BUCKET |
이미지 저장 S3 Bucket |
AWS_REGION |
AWS Region |
JUSO_API_KEY |
도로명주소 API 승인 키 |
APP_BASE_URL |
CORS와 Swagger에 사용할 서비스 URL |
GEMINI_ENABLED |
Gemini 기능 활성화 여부 |
GEMINI_API_KEY |
Gemini API Key |
GEMINI_MODEL |
사용할 Gemini 모델 |
GEMINI_TIMEOUT |
단일 Gemini 호출 제한 시간 |
AWS 인증 정보는 로컬 AWS Profile 또는 EC2 IAM Role 등 AWS SDK 기본 인증 체인을 통해 제공합니다.
set -a
source .env
set +a
./gradlew bootRun애플리케이션은 기본적으로 다음 포트를 사용합니다.
| 포트 | 용도 |
|---|---|
8080 |
Backend API |
9101 |
Actuator Health 및 Prometheus Metrics |
6379 |
Redis |
일반 테스트는 외부 Gemini API를 호출하지 않습니다.
./gradlew test실제 Gemini API 연동 테스트는 별도 Gradle Task로 분리되어 있습니다.
./gradlew geminiIntegrationTestGemini 통합 테스트를 실행하려면 유효한 API Key와 관련 환경변수가 필요합니다.
k6 테스트는 Docker 기반으로 실행하여 팀원이 동일한 버전과 성능 기준을 사용할 수 있도록 구성했습니다.
cp k6/.env.example k6/.env
./k6/run.sh scenarios/examples/authenticated-read.js제공하는 실행 프로필은 다음과 같습니다.
| 프로필 | 용도 |
|---|---|
smoke |
API와 시나리오 정상 동작 확인 |
load |
예상되는 일반 부하에서 성능 측정 |
stress |
시스템 한계와 병목 확인 |
공통 성능 기준은 HTTP 오류율 1% 미만, p95 500ms 미만, p99 1,000ms 미만입니다.
자세한 구성과 운영 방법은 k6 부하 테스트 운영 가이드를 참고합니다.
develop 브랜치에 코드가 반영되면 GitHub Actions를 통해 자동 배포가 진행됩니다.
- GitHub Actions가 Docker 이미지를 빌드
- 이미지를 GitHub Container Registry에 Push
- Self-hosted Runner가 배포 서버의 소스를 갱신
- Docker Compose가 새 이미지를 Pull
- 애플리케이션과 Redis 등 운영 컨테이너 재기동
- Swagger UI 응답을 통한 배포 상태 확인
- 일정 기간이 지난 미사용 이미지 정리
Spring Boot Actuator와 Micrometer를 통해 애플리케이션 메트릭을 수집합니다.
- Prometheus: 애플리케이션, EC2, Redis 메트릭 수집
- Grafana: 운영 현황 및 부하 테스트 대시보드
- Loki: 애플리케이션과 컨테이너 로그 저장
- Grafana Alloy: Docker 컨테이너 로그 수집 및 전송
- Node Exporter: EC2 CPU, Memory, Disk 메트릭
- Redis Exporter: Redis 상태와 메모리 메트릭
- Discord: 장애 발생 및 복구 알림
운영 대시보드는 다음 항목을 제공합니다.
- 애플리케이션 요청 처리량과 응답 시간
- HTTP 오류율
- JVM Heap과 Thread 상태
- EC2 CPU, Memory, Disk 사용량
- Redis 메모리와 연결 상태
- k6 테스트별 p95·p99 응답 시간
- 애플리케이션 및 인프라 장애 알림
자세한 구성과 운영 방법은 모니터링 운영 가이드를 참고합니다.
- 기본 통합 브랜치:
develop - 이슈 단위 브랜치 및 Pull Request 생성
- CodeRabbit을 활용한 코드 리뷰
- 공통 API 응답과 예외 처리 규칙 적용
자세한 협업 규칙은 .github/CONTRIBUTING.md를 참고합니다.
올바른 분리배출이 일상의 습관이 되도록, ReDO ♻️
