Files
joungmin 80b553ec19 docs: 현행화 17개 설계서 Approved + 후속 이슈 백로그 등록
Reviewer 결과 17 PASS / 1 REJECT (#267 admin 권한 critical).
- 17개 설계서를 Draft → Approved로 갱신
- #267(backend-user)은 critical 결함으로 06-Reviewer 유지
- 후속 17개 개선 이슈(#289~#305) 자동 등록 — 결함 124건 백로그 반영
  (critical 3 / major 46 / minor 75)
- docs/README.md에 18개 설계서 인덱스 추가
- CHANGELOG.md 2026-06-15 섹션 추가

Refs: #266 #268-#283 (현행화 완료) #267 (대기) #289-#305 (백로그)
2026-06-15 11:08:18 +09:00

8.0 KiB

설계서: 백엔드 - 통계/대시보드 (#274)

상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #274 · 관련 ADR: 없음 · 구현 파일: backend-java/src/main/java/com/tasteby/service/StatsService.java, backend-java/src/main/java/com/tasteby/controller/StatsController.java · 테스트: TBD (현재 없음)

1. 목적 (Why)

사이트 방문 트래픽을 일별/누적으로 집계하여, 운영자가 서비스 사용량을 즉시 확인 가능한 가벼운 대시보드 데이터를 제공한다. 풋터/관리자 페이지의 "오늘/총 방문수" 표시에 사용된다.

2. 범위 (Scope)

  • 포함:
    • 사이트 방문 1건 기록 (POST /api/stats/visit)
    • 오늘 방문수 + 누적 방문수 조회 (GET /api/stats/visits)
  • 제외 (out of scope):
    • 사용자별/세션별 추적, 유니크 방문자(UU) 집계
    • 페이지별 PV, 체류 시간, 이탈률
    • 트래픽 소스/UTM, 외부 분석 도구 연동(GA, Mixpanel 등)
    • 채널/식당/리뷰 등 도메인 통계 (#273, #272 영역)
    • 시계열 차트, 기간 필터 조회

3. 인수조건 (Acceptance Criteria)

  • POST /api/stats/visit 호출 시 DB의 오늘자 카운터가 1 증가하고 {ok:true} 반환
  • GET /api/stats/visits{today, total} (모두 int) 반환
  • 인증 없이 호출 가능 (퍼블릭 엔드포인트)
  • 데이터 없으면 today=0, total=0 반환 (DB null 안전)
  • 동일 사용자 다중 호출 시 호출 횟수만큼 카운터 증가 (PV 카운트 모델)

4. 컨텍스트 & 제약

  • DB: Oracle 23ai. 테이블 site_visit_stats (가정: 일자 PK + count 컬럼 또는 카운터 테이블).
  • MyBatis: StatsMapper (recordVisit, getTodayVisits, getTotalVisits).
  • 권한: 공개 엔드포인트. 인증 불필요. 어뷰즈 방어 없음 (현재).
  • 성능: 매 페이지 로드마다 POST /api/stats/visit 호출 → DB write QPS 비례 증가.
  • 트랜잭션: recordVisit는 단일 INSERT/UPDATE, 자동 커밋 또는 Spring 기본 트랜잭션.
  • 가정:
    • 오늘 일자 기준은 DB 서버 시간(SYSDATE / CURRENT_DATE).
    • 카운터는 일별 행 upsert 또는 단일 incremental 행.
    • 누적은 합계 집계 또는 별도 단일 행.

5. 아키텍처 개요

  • 모듈/파일 구조:
    • controller/StatsController.java (2개 엔드포인트)
    • service/StatsService.java (얇은 위임)
    • mapper/StatsMapper.java + XML
    • domain/SiteVisitStats.java
  • I/O ↔ 순수 로직 경계: 비즈니스 로직 거의 없음. Service는 단순 Mapper 위임 + 빌더로 응답 객체 조립.
[Browser/Client]
   │ POST /api/stats/visit (페이지 로드 시)
   ▼
[StatsController]
   │ statsService.recordVisit()
   ▼
[StatsService] ── statsService.getVisits() (대시보드 GET)
   │
   ▼
[StatsMapper] (MyBatis XML)
   │ INSERT/UPDATE | SELECT today/total
   ▼
[Oracle 23ai: site_visit_stats]

6. 데이터 모델

  • SiteVisitStats (domain/SiteVisitStats.java):
    • today: int — 오늘 방문수
    • total: int — 누적 방문수
  • 응답 JSON: {"today": 123, "total": 456789}
  • DB 테이블 (추정): site_visit_stats(visit_date DATE PK, count NUMBER) 또는 단일 row 카운터.
  • 경계 검증:
    • 음수 카운트 불가 (DB CHECK 권장).
    • 오버플로: int 범위(2^31-1) → 누적이 21억 도달 시 long으로 확장 필요.

7. 함수 명세 (Function Specs)

함수 책임(1줄) 시그니처(잠정) 입력 출력 에러/실패 복잡?
StatsService.recordVisit 방문 1건 기록 void() 없음 없음 DB 오류 → 전파 단순
StatsService.getVisits 오늘/누적 집계 조회 SiteVisitStats() 없음 SiteVisitStats(today,total) DB 오류 → 전파 단순
StatsController.recordVisit REST: 방문 기록 Map<String,Object>() 없음 {ok:true} 500 가능 단순
StatsController.getVisits REST: 방문 조회 SiteVisitStats() 없음 SiteVisitStats 500 가능 단순

모두 단순. 별도 fn 설계서 불필요.

8. 흐름 / 알고리즘

  1. 방문 기록:
    • Client(브라우저/Next.js) → 페이지 마운트 시 POST /api/stats/visit 1회 호출.
    • Controller → Service → Mapper.recordVisit() → DB.
    • SQL (가정): MERGE INTO site_visit_stats USING dual ON (visit_date = TRUNC(SYSDATE)) WHEN MATCHED THEN UPDATE SET count = count + 1 WHEN NOT MATCHED THEN INSERT (visit_date, count) VALUES (TRUNC(SYSDATE), 1)
    • 응답 {ok:true}.
  2. 집계 조회:
    • Controller → Service.getVisits() →
    • Mapper.getTodayVisits() (오늘 일자 행 SELECT) +
    • Mapper.getTotalVisits() (SUM 또는 누적 행 SELECT) →
    • Lombok Builder로 SiteVisitStats 조립 → 응답.

9. 엣지케이스 & 에러 처리

  • 자정 경계 race: Mapper가 TRUNC(SYSDATE)를 사용한다고 가정 → DB 시간대 일관. 서버 시간대(Asia/Seoul) 설정 의존.
  • 첫 호출/빈 DB: 오늘 행이 없으면 getTodayVisits 0 또는 null 반환 → int 변환 시 NPE 위험. → Mapper SQL에서 NVL(SUM(count), 0) 권장.
  • 봇/크롤러 트래픽: 인플레이션 가능. UA/IP 필터 부재.
  • 사용자 빠른 새로고침 어뷰즈: 동일 IP 다중 호출 모두 +1. 레이트 리밋 없음.
  • DB 다운: 방문 기록/조회 모두 500 전파. 사이트 페이지 로드는 fire-and-forget이면 무영향, 동기 대기면 UX 저하.
  • MERGE 동시 INSERT 경합: 자정 직후 두 트랜잭션 동시 INSERT → 유니크 충돌 시 한쪽 500. 재시도 권장.
  • 카운터 오버플로: int 한계 → 향후 long 마이그레이션 + DB NUMBER 확장.

10. 테스트 계획

  • 단위
    • StatsService.getVisits: Mapper 모킹, today=10, total=100 → 빌드된 객체 검증.
    • StatsService.recordVisit: Mapper 호출 1회 검증.
  • 통합
    • 신규 DB 상태에서 POST 3회 → GET 시 today=3, total=3
    • 다음 날(시뮬레이션) POST 1회 → today=1, total=4
    • 빈 DB 상태에서 GET → {0, 0} (null 안전)
  • 부하: 초당 100회 POST 지속 → 응답 시간 100ms 이하 (성능 SLA).
  • 모킹: H2 또는 Testcontainers Oracle. 자정 경계는 JVM 시간 모킹 또는 DB 시퀀스 가짜 주입.
  • 현재 테스트 디렉토리 없음 → TBD.

11. 리스크 & 대안 검토

  • 선택: 모든 페이지 로드마다 동기 POST 1건 + DB INCR.
    • 대안 A: Redis INCR + 주기 flush → DB write 폭주 방지, 정확도 약간 손실.
    • 대안 B: 로그 기반 집계 (Nginx access log → 배치) → 실시간성 손실.
    • 대안 C: 외부 분석(GA) 연동 → 운영 부담 감소, 데이터 주권 손실.
    • 트레이드오프: 현 트래픽 규모에서 DB 직접 카운트가 단순/충분. 1000 QPS 도달 시 Redis 캐시 ADR 검토.
  • 유니크 방문자(UU) 미지원: 현 PV 모델 한계. 쿠키/세션 ID 도입 시 schema 확장 필요.
  • 봇 필터링 부재: User-Agent 블랙리스트 또는 robots.txt 준수 봇 제외 로직 후속 작업.
  • 시간대 의존: DB 서버 TZ가 KST가 아니면 "오늘" 정의가 사용자 인식과 불일치. 서버 TZ 명시 필요.

12. 미해결 질문 (Open Questions)

  • 봇/크롤러 트래픽 필터링 정책 (UA 화이트리스트? Cloudflare 통계 활용?).
  • 유니크 방문자(UU) 메트릭이 필요한지? 필요하면 쿠키 기반 vs IP+UA 해시.
  • 통계 데이터 보존 기간/롤업 정책 (예: 90일 이전은 월 단위 압축).
  • 인증된 사용자만의 활성 사용자(DAU/MAU) 지표 도입 시점.
  • 어뷰즈 방어(레이트 리밋) 추가 필요 — Bucket4j 또는 Nginx 단에서 처리?
  • 관리자 대시보드 UI에서 필요한 차원(채널별 트래픽, 디바이스 등) 범위 확정.