Zero Gravity 백엔드

감정 기록과 AI 분석을 위한 Spring Boot REST API — Time Bucket 샘플링으로 토큰 97% 절감, 클라우드 인프라 직접 구축.

RoleBackend Developer
Timeline2025.10 - Present
TeamSolo
SkillsSpring Boot, Java, MySQL, Docker, Terraform
개요

한마디로

감정 기록과 AI 분석을 제공하는 Zero Gravity의 Spring Boot REST API입니다. 프리티어 클라우드 위에 인프라부터 배포까지 직접 구축하고, 비용 제약 안에서 AI 기능을 설계했습니다.

  • 5개 도메인, 15개 엔드포인트 — 인증, 감정 기록, 통계, AI 분석을 하나의 API로 설계했습니다.
  • OCI + Terraform 6 모듈로 인프라를 직접 구축하고, Build-first 전략으로 Zero-Downtime 배포를 구현했습니다.
  • Time Bucket 샘플링으로 Gemini API 전송량을 97% 절감하여, AI 기능을 비용 한도 내에서 운영했습니다.
전체 구조

팀 프로젝트에서 프로덕션까지.

팀 프로젝트로 시작한 Spring Boot 백엔드를 프로덕션 서비스로 전환했습니다. 기본 CRUD API만 존재했던 프로젝트에 인증, 인프라, 배포 파이프라인을 구축하고 AI 분석 기능을 추가했습니다.

  • 레이어드 아키텍처를 도메인별 구조로 재구성했습니다.
  • NextAuth OAuth → JWT 인증 체계를 구현했습니다.
  • OCI 위에 인프라를 직접 구축하고 Zero-Downtime 배포를 구현했습니다.
  • Gemini API 기반 감정 분석 기능을 추가했습니다.

API Endpoints

DomainEndpointsDescription
AuthPOST /auth/verify, /auth/refreshOAuth 토큰 검증 → JWT 발급, 갱신
UserGET, DELETE /users/me, PUT /users/consent, POST /users/logout프로필 조회, 계정 삭제, 동의 관리, 로그아웃
EmotionPOST, GET, PUT /emotions/records감정 기록 생성 · 조회 · 수정
ChartGET /chart/level, count, reason기간별 감정 통계 (레벨, 빈도, 이유)
AIPOST /ai/emotion-predictions, GET /period-analyses감정 예측, 기간 분석

Infrastructure

OCI Cloud — Terraform 6 Modules

Ampere A1 ARM64 — 4 OCPU · 24GB

Backend (docker-compose)

Frontend (docker-compose)

SSH + Docker

AWS Route53 zerogv.com · api.zerogv.com

GitHub Actions CI/CD · Zero-Downtime

Load Balancer HTTPS · TLS 1.3

Nginx Reverse Proxy · Rate Limit

Next.js 15 :3000

Spring Boot 3.2 :8080

MySQL 8.0 :3306

레이어기술역할
IaCTerraform (6 modules)VCN · Compute · LB · Certificate · Storage · Monitoring
CloudOCI Ampere A1ARM64 인스턴스
ProxyNginx리버스 프록시 · Rate Limit · 도메인별 라우팅
ContainerDocker ComposeFrontend · Backend · MySQL 격리
SSLLet's EncryptACME DNS-01 via Route53 · 자동 갱신
CI/CDGitHub ActionsSSH + Docker Build → Health Check → Auto-Rollback
AI 토큰 최적화

전부 보내면 한 달도 못 버팁니다.

Gemini API로 감정 기록을 분석하는 기능을 구현하려 했습니다. 1년치 기록을 전부 보내면 요청당 Input 토큰이 ~55K에 달했습니다. 프로젝트 예산으로는 감당할 수 없었습니다.

데이터를 줄이면 분석 품질이 떨어지고, 기능 자체를 포기하기엔 핵심 기능이었습니다.

이미 있는 데이터에서 답을 찾았습니다.

전체를 보낼 수 없다면, 각 기간을 대표하는 기록만 골라서 보내면 됐습니다. 문제는 "대표"를 어떤 기준으로 고를 것인가였습니다.

차트 API의 기간별 평균 레벨과 버킷별 최빈 이유를 샘플링 기준으로 사용했습니다. "이 달의 평균 레벨에 가장 가깝고, 최빈 이유를 포함하는" 기록 1건을 각 버킷에서 선별했습니다.

// 차트 API의 평균 레벨을 샘플링 기준으로 재활용
ChartLevelResponse levelChart = chartService.getEmotionLevelChart(
    userId, period, startDate, timezone);
 
// 전체 기록 대신 대표 기록만 Gemini에 전달
List<EmotionRecord> representativeRecords = selectRepresentativeRecords(
    emotionRecords, levelChart, period, startDate, timezone
);

버킷 하나, 대표 기록 하나.

기간을 균등한 시간 단위(버킷)로 나누고, 각 버킷에서 가장 대표적인 기록 1개를 선택했습니다. 상위 12개를 그냥 뽑으면 특정 달에 집중될 수 있기 때문에, 버킷 단위로 균등하게 나눴습니다.

Period버킷 단위샘플 수감소율
Year월 (MONTH)12개365 → 12 (97%)
Month주 (WEEK)4개~30 → 4 (87%)

감정 기록 365건

Time Bucket 분할 월별 12개 버킷

차트 API 기간별 평균 레벨

스코어링 레벨 60% + 이유 40%

버킷별 집계 최빈 이유

버킷당 대표 1건 선별

대표 기록 12건 → Gemini 전송

감정 레벨 60% + 이유 매칭 40%의 가중 스코어로 각 버킷의 대표 기록을 선별했습니다. Daily 기록은 하루 전체를 돌아보며 쓴 기록이므로 1.5배 가중치를 적용했습니다.

private double calculateMatchScore(
    EmotionRecord record, Double targetLevel, String topReason
) {
    // Daily 기록에 1.5배 가중치
    double recordLevel = record.getEmotionId() *
        (record.getEmotionRecordType() == EmotionRecord.Type.DAILY ? 1.5 : 1.0);
    double levelScore = 1.0 - (Math.abs(recordLevel - targetLevel) / 9.0);
 
    double reasonScore = record.getEmotionReasons().contains(topReason) ? 1.0 : 0.0;
 
    return (levelScore * 0.6) + (reasonScore * 0.4);
}

동점이면 일기가 긴 순, 이유가 많은 순, 최신 순으로 타이브레이크했습니다.

예시: Year 분석의 1월 버킷 (평균 감정 레벨: 4.5, 최빈 이유: "Work")

기록감정타입이유계산스코어
ALevel 3DailyWork, Family3×1.5=4.5 (일치) + Work 포함1.0 ✅
BLevel 5MomentHealth5×1.0=5.0 (근접) + Work 없음0.57
CLevel 3MomentWork3×1.0=3.0 (차이) + Work 포함0.90

AI 분석 결과는 24시간 캐싱하고, 감정 기록이 변경되면 해당 기간 캐시를 무효화했습니다.

전송량 97% 절감, 요청당 $0.002

지표Before (전체 전송)After (샘플링)
연간 분석 전송량365건12건 (97% 감소)
요청당 Input 토큰~55K tokens~2.4K tokens
요청당 비용$0.017$0.002

제약이 전략을 만들었습니다. 기능을 포기하는 대신, 이미 존재하는 데이터를 새로운 맥락에서 재활용하는 발상으로 문제를 해결했습니다.

arrow_downward더 자세히 살펴보기arrow_downward