[V1]SLI SLO 설계 - 100-hours-a-week/KTB4-15th-CLOUD GitHub Wiki

1. 서비스 범위와 전제

1.0. 근거 등급 정의

이 문서의 모든 수치와 주장에는 아래 등급 중 하나를 붙인다.

등급 뜻 예시
실측 우리 시스템에서 직접 측정·확인한 값 2026-09-26 인프라 점검 결과
공식 명세(외부) 공급자의 공식 문서 AWS SLA 문서
공식 명세(내부) 팀 위키 등에 문서로 기록된 확정 사항 개발 일정, compose 설정
벤치마크 타사·연구의 공개 자료. 조건이 달라 그대로 쓸 수 없음 NN/g 응답시간 연구, 무신사 사례
가정 근거 문서 없이 팀이 전제로 둔 것 외부 사용자 유입 없음
미정 아직 결정하거나 확인하지 못한 것

여러 값을 조합해 계산한 결과는 입력값 중 가장 낮은 등급을 따른다. 예를 들어 공식 명세 두 개를 곱했더라도 "두 장애가 서로 독립"이라는 가정이 들어가면 결과는 "가정" 등급으로 명시한다.

1.1. 핵심 사용자

항목 내용 근거 등급
서비스 대상 옷을 고르는 데 많은 시간을 쓰기 어려운 바쁜 20·30대 남성 공식 명세(내부)
접속 방식 모바일 웹 브라우저를 통해 https://lookddak.com 접속 공식 명세(내부)
초기 외부 사용자 모집 부트캠프 내 다른 수강생과 디스콰이엇 게시물을 통해 초기 사용자를 모집 운영 계획
초기 사용자 성격 LookDDak 서비스의 사용 욕구가 높은 메인 타깃층 운영 계획
활용 목적 핵심 사용자 여정 검증, 장애 및 사용성 피드백 수집, 초기 SLI 측정 데이터 확보 운영 계획

1.2. 1 운영 기간 및 트래픽 전제

항목 내용 등급
1차 배포 주간 09/28~10/02 내부 일정
V2 개발 기간 10/05~10/16 내부 일정
2차 배포 주간 10/19~10/23 내부 일정
V1 기준 버전 운영 기간 약 17~25일 내부 일정
운영 시간 외부 사용자 모집 기간에는 24시간 운영을 원칙으로 하며, 별도의 정기 중지 시간을 두지 않는다. 팀 내부 논의
배포 방식 FE·BE·AI 저장소의 main 브랜치가 갱신되면 대상 서비스 컨테이너를 교체하는 중단 배포 방식을 사용한다. 현재 배포 설계
배포 중 서비스 영향 배포 대상 컨테이너를 재생성하는 동안 해당 서비스 요청이 일시적으로 실패할 수 있다. 현재 배포 설계
배포 중단 시간의 SLO 처리 배포 중 발생한 사용자 요청 실패와 지연을 사용자 관점의 SLI에 포함한다. SLO 정책(초안)
V1·V2 예상 트래픽 기존 인프라 설계 문서에서 정의한 예상 사용자 수와 요청량을 용량 설계 및 부하테스트 조건으로 사용한다. 인프라 설계 가정
V1 실제 요청량 외부 사용자 모집 전이므로 알 수 없다. 운영 시작 후 실제 요청을 집계한다. 미정(실측 전)
사용 시간대 피크 점심시간과 퇴근 이후에 요청이 증가할 것으로 가정한다. 가정
상품 데이터 전제 초기 상품 약 50,000건의 수집이 완료된 상태에서 V1을 운영한다. 확인된 운영 상태

외부 사용자 모집 기간에는 24시간 운영을 원칙으로 한다. 기존에는 비용 절감을 위해 EC2를 수동으로 중지할 가능성이 있었지만, 외부 사용자를 모집한 뒤 EC2를 중지하면 사용자가 예고 없이 서비스에 접속하지 못한다. 따라서 V1 사용자 모집 기간에는 별도의 정기 중지 시간을 두지 않는다. 불가피하게 전체 서비스를 중지해야 한다면 사전에 공지된 유지보수 시간으로 기록한다.

V1은 중단 배포 방식을 사용한다. FE·BE·AI 저장소의 main 브랜치가 갱신되면 GitHub Actions가 이미지를 ECR에 업로드하고, SSM을 통해 EC2의 배포 스크립트를 실행한다. 배포 스크립트는 전체 Compose 환경을 다시 띄우는 것이 아니라 해당 서비스 컨테이너만 교체한다.

Nginx와 PostgreSQL은 일반적인 애플리케이션 배포에서 계속 실행되지만, 대상 컨테이너를 교체하는 동안 해당 경로에서 짧은 오류 또는 지연이 발생할 수 있다. 현재는 Blue/Green이나 Rolling 배포가 아니므로 무중단 배포를 전제로 하지 않는다.

배포 중단도 사용자 관점의 SLI에 포함한다. 사용자는 요청 실패의 원인이 배포인지 장애인지 구분하지 못한다. 따라서 배포 중 발생한 5xx, 타임아웃, 연결 실패도 사용자 SLI에는 포함한다. 다만 CloudWatch 로그와 배포 이력에 배포 시각을 남겨 원인 분석 시 배포 영향임을 구분한다.

V1은 별도의 개발 서버 없이 하나의 운영 환경에서 배포, 부하테스트 및 실제 사용자 운영을 수행한다. 기존 인프라 설계에서 정의한 V1·V2 예상 트래픽은 운영 EC2에서 수행하는 부하테스트의 요청량과 동시 사용자 수를 결정하는 데 사용한다. 부하테스트 요청은 전용 헤더, User-Agent 또는 테스트 계정으로 식별하여 실제 사용자 요청과 분리한다. 부하테스트 요청 자체는 사용자 SLI 계산에서 제외하지만, 부하테스트가 진행되는 동안 발생한 실제 사용자 요청의 성공·실패와 지연은 사용자 SLI에 포함한다.

*요청 종류 구분 방식

요청 종류 사용자 SLO 포함 여부 이유
실제 사용자 요청 포함한다. 사용자가 실제로 경험한 결과이기 때문이다.
부하테스트 요청 제외한다. 시스템 용량을 검증하기 위해 인위적으로 발생시킨 요청이기 때문이다.
헬스체크 요청 제외한다. 컨테이너와 배포 상태를 확인하기 위한 내부 요청이기 때문이다.

운영 SLO는 실제로 발생한 사용자 요청을 기준으로 계산한다. 예상 트래픽은 아직 발생하지 않은 요청이므로 운영 SLO의 직접적인 분모로 사용하지 않는다. 운영 시작 후에는 측정 기간에 실제로 발생한 유효 요청 수를 분모로 사용한다. 따라서 예상 트래픽과 실제 트래픽은 다음과 같이 서로 다른 목적으로 사용한다.

1.3. V1 포함 기능과 제외 기능

기능 V1 SLO 대상 근거 등급
회원가입·로그인·기본 정보 등록 포함 FE·BE PL 미팅 기록 및 현재 구현 범위 공식 명세(내부)
대화형 쇼핑 에이전트 포함 릴리즈 플래닝 PB1 및 현재 서비스 범위 공식 명세(내부)
AI 가상 피팅 포함 팀 확정 범위 및 현재 FE·BE·AI 구현 공식 명세(내부)
초기 상품 데이터 조회·추천 포함 기능을 지원하는 데이터로 사용한다. 약 50,000건의 상품 데이터 수집 완료(1,000건 적재) 확인된 운영 상태
주기적 상품 크롤러 제외 V1에서는 주기적으로 실행하거나 배포하지 않는다. 운영 계획(팀 결정)
상품 가격·재고 최신성 제외 주기적 크롤러를 운영하지 않으므로 최신성을 보장하지 않는다. 운영 제약
애플리케이션 헬스체크 사용자 SLO에서는 제외하고 배포 성공 판정에 사용한다. 헬스체크 기반 CD 설계 적용 예정 배포 설계
CI/CD 및 배포 결과 사용자 SLO에서는 제외하고 별도의 배포 운영 지표로 관리한다. GitHub Actions·ECR·SSM 배포 구조 운영 지표

상품 데이터 최신성을 보장하지 않는다. 주기적 크롤러를 실행하지 않으므로 V1에서는 상품 가격, 재고, 판매 상태가 외부 쇼핑몰의 최신 상태와 항상 일치한다고 약속할 수 없다. 사용자에게 최신성이 중요한 기능을 제공한다면 별도의 안내 또는 조회 시점 검증이 필요하다.

애플리케이션 헬스체크를 배포 성공 판정에 사용한다. 새 컨테이너가 지정된 헬스체크를 통과해 healthy 상태가 되면 배포 성공으로 판정한다. 제한 시간 안에 healthy 상태가 되지 않으면 배포 실패로 처리하고 배포 스크립트의 정책에 따라 이전 버전으로 롤백한다.

헬스체크 요청은 사용자 SLI 계산에서 제외한다. 헬스체크는 실제 사용자 기능보다 단순하고 일정한 주기로 반복된다. 이를 사용자 요청과 함께 계산하면 헬스체크 성공 건수가 실제 기능 실패를 가려 가용성이 과도하게 높게 보일 수 있다. 따라서 헬스체크는 컨테이너 상태 확인과 배포 성공 판정에만 사용한다.

CI/CD 결과는 별도의 배포 운영 지표로 관리한다. 배포 성공 여부는 애플리케이션 헬스체크 결과를 기준으로 판정한다. 배포 횟수, 배포 실패율, 롤백 여부, 실패 후 복구시간을 기록하여 배포 과정의 안정성을 확인한다. 다만 배포 중 실제 사용자 요청이 실패하거나 지연되면 해당 요청은 사용자 SLI에 포함한다.

애플리케이션 헬스체크
→ 새 컨테이너의 배포 성공 여부를 판정한다.

배포 운영 지표
→ 배포 성공·실패, 롤백, 복구시간을 측정한다.

사용자 SLI
→ 실제 기능 요청의 성공률과 응답시간을 측정한다.

V1 SLI는 핵심 사용자 경험을 중심으로 최대 네 개로 제한한다. 초기 후보는 일반 API 가용성, 일반 API 응답시간, 채팅 첫 응답시간, 가상 피팅 완료 성공률로 구성한다. 헬스체크와 배포 성공률은 사용자 경험을 직접 나타내는 지표가 아니므로 이 다섯 개에 포함하지 않고 별도의 운영 지표로 관리한다.

1.4. 요청 흐름 및 측정 지점 후보

사용자 브라우저
  │
  ├─ DNS 조회 ──▶ Route 53
  │
  └─ HTTPS 443 ──▶ EC2 EIP
                      │
                      ▼
                    Nginx
                    TLS 종료 및 경로 라우팅
                    ◀ 측정 지점 후보 ①: lookddak.com 외부 웹·API 요청
                      │
                      ├─ / ─────▶ Next.js :3000
                      │              │
                      │              └─ SSR 내부 호출 ──▶ Spring :8080
                      │                   ◀ Nginx를 거치지 않는다.
                      │
                      └─ /api/ ─▶ Spring :8080
                                      │
                                      ├─▶ RDS MySQL :3306
                                      │    회원·대화·찜 데이터
                                      │
                                      ├─▶ FastAPI :8000
                                      │      ├─▶ PostgreSQL + pgvector :5432
                                      │      └─▶ 외부 AI API
                                      │           LLM·VTON
                                      │
                                      └─▶ S3
                                           이미지 객체 관리 및
                                           presigned URL 발급
흐름 근거
Route 53 → EC2 EIP Route 53의 lookddak.com A 레코드가 EC2 EIP를 가리킨다.
Nginx 경로 라우팅 Nginx default.conf에서 /와 /api/를 각각 Next.js와 Spring으로 전달한다.
Next.js → Spring 내부 호출 운영 .env에서 API_BASE_URL=http://spring:8080을 사용한다.
Spring → RDS 보안 그룹에서 EC2 애플리케이션이 RDS의 3306 포트에 접근하도록 허용한다.
Spring → FastAPI Docker app-network에서 서비스 이름으로 내부 통신한다.
FastAPI → PostgreSQL Docker db-network에서 서비스 이름으로 내부 통신한다.
Spring·브라우저 → S3 Spring이 IAM 역할을 사용하고 사용자에게 presigned URL을 제공한다.
컨테이너 네트워크 분리 Nginx·Next.js·Spring·FastAPI는 app-network, FastAPI·PostgreSQL은 db-network를 사용한다.

Nginx가 웹과 API 요청의 공통 측정 지점이 된다. lookddak.com을 통해 들어오는 일반 웹·API 요청은 Nginx를 통과하므로 HTTP 상태 코드와 전체 요청시간을 측정하기 적합하다.

Nginx가 모든 사용자 동작을 관측하지는 못한다. Next.js가 SSR 과정에서 Spring을 직접 호출하는 요청과 presigned URL을 사용한 브라우저의 S3 요청은 Nginx를 통과하지 않는다. 따라서 Nginx 로그만으로 전체 사용자 여정의 성공 여부를 판단하지 않는다.

채팅 SSE의 사용자 체감 응답 지연은 일반 API 응답시간과 분리해 측정한다. Nginx의 전체 요청시간은 요청 시작부터 스트리밍 연결 종료까지의 시간으로, 답변 길이와 연결 유지시간의 영향을 받는다. 따라서 요청 접수 시점부터 첫 번째 사용자 표시용 콘텐츠 이벤트가 전송될 때까지의 시간을 Spring 또는 FastAPI에서 별도 SLI로 기록한다. 실제 사용자 수신 지연은 프론트엔드 지표로 보완한다.

측정 지점 측정 대상 한계
① Nginx access log 외부 HTTP 요청의 상태 코드와 전체 시간 SSR 내부 실패와 SSE 첫 응답시간을 정확히 알기 어렵다.
② Next.js·Spring 로그 SSR 내부 호출과 일반 API의 비즈니스 처리 결과 브라우저 렌더링 실패를 알기 어렵다.
③ Spring·FastAPI 이벤트 로그 채팅 첫 응답과 가상 피팅 완료시간 현재 이벤트 로그가 없어 V1 측정 불가
④ S3 요청 결과 이미지 업로드·다운로드 성공 여부 Nginx 로그에 남지 않는다.

1.5. 외부 의존성과 가용성 설계 참고치

이 절에서는 핵심 사용자 요청이 성공하기 위해 필요한 인프라와 외부 서비스를 정리한다.

구성요소 현재 구성 및 역할 공급자의 공식 SLA 또는 상태 등급
Route 53 lookddak.com Public Hosted Zone Hosted Zone 월 100% 서비스 약속 공식 명세(외부)
EC2 t4g.medium 1대, 단일 AZ 단일 EC2 인스턴스 월 99.5% 공식 명세(외부)
RDS MySQL db.t4g.micro, Single-AZ Single DB Instance 월 99.5% 공식 명세(외부)
PostgreSQL EC2 내부 Docker 컨테이너 독립적인 SLA가 없으며 EC2·EBS 및 컨테이너 상태에 종속 공식 명세(내부)
S3 Private 이미지 버킷 S3 Standard 요청 기준 월 99.9% 공식 명세(외부)
Runware VTON 이미지 생성용 외부 API 공개 상태 페이지는 있으나 일반에 공개된 공식 가용성 SLA 수치는 확인 필요 미정
OpenAI API 채팅 답변 생성 및 SSE 스트리밍 일반 API 사용자에게 공통 적용되는 월 가용성 SLA 수치는 공식 공개 문서에서 확인하기 어려우며, 계약·서비스 티어에 따라 달라질 수 있음 계약 확인 필요

현재 인프라는 단일 EC2가 주요 장애 지점이 된다. Nginx, Next.js, Spring, FastAPI, PostgreSQL 컨테이너가 하나의 EC2에서 실행된다. 컨테이너가 다섯 개이더라도 EC2 장애 관점에서는 하나의 장애 영역을 공유한다. EC2가 중단되면 다섯 컨테이너와 EC2 내부 PostgreSQL이 함께 중단된다.

EC2는 단일 인스턴스 SLA를 적용한다. AWS는 단일 EC2 인스턴스에 대해 월 99.5%의 인스턴스 수준 가동률을 서비스 약속으로 제시한다. 여러 AZ에 EC2를 동시에 배치하는 리전 수준 99.99% 조건은 현재 구성에 적용되지 않는다. Amazon EC2 SLA

RDS는 Single-DB Instance SLA를 적용한다. AWS는 Single-DB Instance에 대해 월 99.5%를 서비스 약속으로 제시한다.

S3와 Route 53도 사용자 요청의 의존성이다. Route 53 장애가 발생하면 사용자가 도메인을 해석하지 못할 수 있다. S3 장애가 발생하면 이미지 업로드·조회 또는 가상 피팅 결과 조회가 실패할 수 있다. Route 53 Hosted Zone에는 월 100%, S3 Standard 요청에는 월 99.9%의 서비스 약속이 존재한다. Route 53 SLA, Amazon S3 SLA

1.6. 현재 한계(2026.09.27 기준)

한계 내용
헬스체크가 포트만 확인 Docker의 healthy는 포트가 열렸는지만 본 결과. HTTP 헬스체크는 Next.js /health 404, Spring /actuator/health 401, FastAPI /health 404
테스트 미수행 E2E는 다음 주 마일스톤, 부하테스트는 이번 주 시나리오 작성 후 다음 주 수행
세부 QA 미완료 정상 입력 외의 잘못된 입력, 권한 오류, 중복 요청, 데이터 없음, 새로고침, 재접속 등 세부 시나리오는 아직 전체 검증하지 않았다.
외부 AI 의존성 검증 필요 LLM·VTON API의 지연, 오류 응답, 타임아웃, 호출 제한 상황에서 서비스가 사용자에게 적절한 오류를 반환하는지 확인해야 한다.
부하테스트 미수행 예상 V1·V2 트래픽에서 오류율, p95 응답시간, CPU·메모리와 데이터베이스 병목을 아직 측정하지 않았다.

1.7. 범위수준의 예외 원칙

세부 규칙은 4장에 쓰고, 여기서는 "무엇을 예외 후보로 볼지"만 정한다.

예외 후보 방침 이유
부하테스트·E2E·probe 트래픽 사용자 SLI에서 분리해 별도로 집계 실제 사용자 경험이 아님
배포 중 발생한 실패 예외로 빼지 않고 포함 아래 설명
외부 AI 공급자 장애 사용자 SLI에 포함하되 원인을 태그로 구분 사용자는 원인과 무관하게 실패를 겪음

1.8. 결론

  1. LookDDak V1은 옷 선택 시간을 줄이고 싶은 20·30대 남성을 대상으로 모바일 웹 서비스를 제공한다.
  2. V1은 약 17~25일 동안 외부 사용자를 대상으로 24시간 운영하며 별도의 정기 중지 시간을 두지 않는다.
  3. 별도의 개발 서버 없이 하나의 운영 환경에서 배포, 부하테스트, 실제 사용자 운영을 함께 수행한다.
  4. FE·BE·AI는 서비스별 중단 배포 방식을 사용하며, 배포 중 실제 사용자가 겪은 실패와 지연도 사용자 SLI에 포함한다.
  5. V1·V2 예상 트래픽은 인프라 용량 설계와 부하테스트 조건으로 사용하고, 운영 SLI는 실제 발생한 유효 사용자 요청으로 계산한다.
  6. 부하테스트, 헬스체크, E2E 요청은 실제 사용자 요청과 식별하여 별도로 집계한다.
  7. V1 SLO 대상은 회원 기능, 대화형 쇼핑 에이전트, AI 가상 피팅으로 한정하고 주기적 크롤러와 상품 최신성은 제외한다.
  8. 현재 서비스는 단일 EC2와 Single-AZ RDS 및 외부 AI API에 의존하므로 단일 장애 지점과 공급자 장애 가능성을 전제로 한다.
  9. 일반 웹·API 요청은 Nginx 로그를 중심으로 측정하고, 채팅과 가상 피팅은 Spring·FastAPI의 애플리케이션 이벤트 로그를 중심으로 측정한다.
  10. 다음 장에서는 이 범위를 바탕으로 핵심 사용자 여정을 확정하고 가용성, 일반 API 응답시간, 채팅 첫 응답시간, 가상 피팅 성공률과 완료시간의 SLI·SLO를 정의한다.

2. 핵심 사용자 여정

핵심 사용자 여정(Critical User Journey, CUJ)은 사용자가 서비스에서 이루려는 목표를 단계 순서로 정리한 것이다. SLI는 개별 API가 아니라 이 여정이 성공했는지를 기준으로 정한다. 사용자는 "로그인 API가 200을 반환했다"가 아니라 "추천을 받았다", "옷을 입어 봤다"로 서비스를 평가하기 때문이다.

2.1. 핵심 사용자 여정 목록

ID 여정 사용자 목표 중요도 중요도 근거
CUJ-1 시작하기 가입하고 내 정보·전신사진을 등록한다 상 모든 여정의 입구. 채팅·찜·피팅 API가 모두 인증을 요구한다(401)
CUJ-2 대화로 추천 받기 조건을 말하면 맞는 옷을 추천받는다 최상 기능 1. 서비스 목표 문장의 핵심 동작
CUJ-3 가상 피팅으로 입어 보기 추천받은 옷을 내 사진에 입혀 본다 상 기능 2

2.2. 여정별 상세

모든 여정에 대해 다섯 가지를 정리한다.

  • 흐름: 사용자가 거치는 단계와 해당 API
  • 사용자가 느끼는 성공
  • 실패로 느끼는 상황
  • 실패로 세지 않는 상황: 시스템이 올바르게 거절한 경우
  • 측정 시 주의점

API 경로의 출처는 현재 작성된(2026-09-27 기준) 모두 팀 API 명세서이다.

CUJ-1. 시작하기

단계 사용자 동작 API (명세서 행 이름)
1 이메일로 회원가입한다 POST /api/v1/auth/signup (회원가입)
2 로그인한다 POST /api/v1/login (로그인). 응답의 profileCompleted로 기본정보 등록 여부를 확인한다
3 전신사진을 올리고 검증받는다 POST /api/v1/full-body-image/validate (전신사진 검증). 200 응답에 valid와 fullBodyImageUrl이 있다
4 이름·나이·키·몸무게를 등록한다 POST /api/v1/members/me/profile (최초 기본정보 등록)
배경 로그인 상태를 유지한다 POST /api/v1/auth/refresh (Access Token 재발급)

사용자가 느끼는 성공: 가입부터 기본정보 등록까지 막힘 없이 끝나고, 서비스 첫 화면에 들어간다.

실패로 느끼는 상황

  • 서버 오류(500)로 가입·로그인·사진 검증·기본정보 등록이 안 된다.
  • 사진 검증 응답이 오지 않는다.
  • 로그인이 예고 없이 풀린다(Access Token 재발급 실패).

실패로 세지 않는 상황: 시스템이 올바르게 거절한 경우이므로 가용성 실패로 세지 않는다.

  • 회원가입 형식 오류(400 INVALID_EMAIL_FORMAT, INVALID_PASSWORD_FORMAT)
  • 중복 이메일(409 EMAIL_ALREADY_EXISTS)
  • 잘못된 이메일 또는 비밀번호(401 INVALID_CREDENTIALS)
  • 사진 검증 거절(400): FACE_NOT_DETECTED, FULL_BODY_NOT_DETECTED, BLURRY_IMAGE, DARK_IMAGE, MULTIPLE_PEOPLE_DETECTED, BODY_OCCLUDED, INVALID_IMAGE_FORMAT, IMAGE_SIZE_EXCEEDED(10MB 초과)

측정 시 주의점

  • 멀쩡한 사진이 반복해서 거절되는 문제는 가용성이 아니라 정확성(correctness) 영역이다. 사용자는 실패로 느끼지만, 요청 자체는 정상 처리됐기 때문이다.

CUJ-2. 대화로 추천 받기

단계 사용자 동작 API (명세서 행 이름)
1 원하는 조건을 입력한다 (최대 500자) POST /api/v1/chat/messages (메시지 전송). 201 응답에 requestId가 포함된다
2 응답을 기다린다 GET /api/chat/stream (AI 채팅 응답 실시간 연결, SSE)
3 AI가 조건을 요약해 "이렇게 추천해드릴까요?"라고 묻는다 BE가 AI의 POST /api/v1/chat/stream(SSE)을 호출한다. 응답 이벤트는 message_chunk, done
4 확인하면 추천 상품 카드를 받는다 추천 상품에 상품명, 가격, 구매 URL, 찜 여부, 피팅 후보 여부가 담긴다
5 지난 대화를 다시 본다 GET /api/v1/chat-rooms/{chatRoomId} (특정 채팅방 조회)

사용자가 느끼는 성공: 메시지를 보낸 뒤 AI의 첫 글자가 나타나기 시작하고, 끊기지 않고 끝까지 출력되며, 추천 상품 카드가 보인다.

실패로 느끼는 상황

  • 첫 응답이 오지 않는다.
  • AI가 "응답을 생성하지 못했어요"라고 안내한다(AI_RESPONSE_FAILED).
  • 스트림 연결 자체가 실패한다(CHAT_STREAM_CONNECTION_FAILED, 500).
  • 출력 중간에 연결이 끊긴다.

실패로 세지 않는 상황

  • 메시지 미입력(400 INVALID_CHAT_MESSAGE)
  • 500자 초과(400 CHAT_MESSAGE_LENGTH_EXCEEDED)
  • 다른 사람의 대화방 접근(403 CHAT_ROOM_ACCESS_DENIED)
  • 없는 대화방(404)

측정 시 주의점

  • HTTP 상태 코드로는 성공 여부를 판단할 수 없다. 채팅 요청은 제한 시간 안에 첫 message_chunk가 도착하고, 해당 답변이 애플리케이션이 정의한 done 이벤트(구현 여부 미정)로 정상 완료된 경우에만 성공으로 판정한다.
  • 메시지 전송과 응답 수신을 한 건으로 묶는다. 한 번의 질문이 요청 두 개(메시지 전송 POST, 스트림 GET)로 나뉘므로, requestId로 둘을 연결해 하나의 사용자 요청으로 센다.
  • 추천의 품질은 이 문서의 SLO 대상이 아니다. 추천이 사용자 취향에 맞는지, 리뷰 근거가 정확한지는 CUJ-1과 같은 이유로 정확성 영역이다.

CUJ-3. 가상 피팅으로 입어 보기

단계 사용자 동작 API (명세서 행 이름)
0 (전제) 전신사진이 등록돼 있다 CUJ-1의 3단계
1 추천 카드에서 피팅 후보에 담는다 POST /api/v1/fitting-candidates (피팅 목록에 상품 추가)
2 상의·하의를 고른다 `GET /api/v1/fitting-candidates?itemType=TOP
3 피팅을 요청한다 POST /api/v1/fitting-jobs (가상피팅 생성 요청). 상·하의 중 최소 1개 필요. 201 응답의 상태는 PROCESSING
4 결과를 기다린다 GET /api/v1/fitting-jobs/{fittingJobId} (가상피팅 작업 상태 조회). 상태는 PROCESSING, COMPLETED, FAILED 중 하나
5 (내부) 착용 이미지를 생성한다 BE가 AI의 POST /api/v1/sync-fitting을 호출한다. AI는 Runware 플랫폼의 Pruna API를 쓴다
6 결과를 본다 GET /api/v1/fitting-jobs/{fittingJobId}/result (임시 피팅 결과 조회). 만료 시각(expiresAt)이 있다

사용자가 느끼는 성공: 피팅을 요청하면 결과 이미지가 나타나고, 이미지가 정상적으로 열린다.

실패로 느끼는 상황

  • 작업이 실패로 끝난다(FAILED, 실패 사유 VTON_FAILED).
  • 작업이 PROCESSING에서 끝나지 않는다.
  • 결과 이미지가 열리지 않는다.

실패로 세지 않는 상황

  • 상품을 고르지 않고 요청한 경우(400 FITTING_PRODUCT_REQUIRED).
  • 생성 중에 결과를 조회한 경우(409 FITTING_JOB_NOT_COMPLETED). 폴링 구조에서는 정상적으로 반복되는 응답이다.
  • 이미 저장한 결과를 다시 저장한 경우(409).

측정 시 주의점

  • 성공과 완료시간을 작업 단위로 잰다. 성공 여부는 작업 1건이 COMPLETED로 끝났는지로 판정한다. 완료시간은 작업 생성부터 COMPLETED까지로 잰다. 둘 다 BE의 작업 상태 변경 기록을 기준으로 하며, HTTP 응답시간으로는 잴 수 없다.
  • 상태 조회 요청은 일반 API 가용성에서 제외하거나 별도로 집계한다. 폴링은 피팅 1건당 여러 번 반복되므로, 그대로 두면 가용성 수치가 부풀려진다.

3. SLI/SLO 정의표

3.1. 공통 규칙

  1. 성공 판정 (HTTP 요청)

    V1 핵심 API 가용성 SLI는 Nginx access log의 HTTP 상태 코드를 기준으로 판정한다. 측정 대상 요청 중 2xx·3xx 응답은 좋은 이벤트로, 5xx 응답은 나쁜 이벤트로 판정한다. 4xx 응답은 좋은 이벤트와 나쁜 이벤트 모두에서 제외한다.

    일반적으로 4xx는 잘못된 입력이나 인증 실패와 같은 클라이언트 책임의 오류를 의미한다. 그러나 Nginx access log에 기록된 상태 코드만으로는 정상적인 사용자 입력 오류와 프론트엔드 버그로 발생한 잘못된 요청을 구분하기 어렵다.

    예를 들어 사용자가 비밀번호를 잘못 입력해 발생한 401은 정상적인 사용자 오류지만, 프론트엔드가 필수 필드를 누락해 반복적으로 발생시킨 400은 서비스 품질 문제일 수 있다. 두 경우 모두 Nginx에는 4xx로 기록되므로, V1에서는 이를 가용성의 성공이나 실패로 자동 분류하지 않는다.

    따라서 4xx는 가용성 SLI 계산의 분자와 분모에서 제외한다. 다만 프론트엔드 요청 오류와 인증 문제를 확인할 수 있도록 상태 코드 및 API 경로별 발생량을 별도의 운영 지표로 집계한다.

    2xx, 3xx
    → 서버가 요청에 응답했으므로 가용성 측면에서는 정상 처리로 본다.
    
    5xx
    → 서버 내부 문제이므로 실패로 본다.
    
    타임아웃·연결 실패
    → 사용자가 응답을 받지 못했으므로 실패로 본다.
    

    https://sre.google/workbook/slo-engineering-case-studies/

    https://sre.google/workbook/slo-document/

  2. 성공 판정 (채팅·피팅) HTTP 상태가 아니라 이벤트와 작업 상태로 판정한다(2장 CUJ-2, CUJ-3)

  3. 측정 기간 7일 롤링 윈도우로 판정하고, V1 전체 21일(9/28~10/18) 누적값을 함께 보고한다.

    • Workbook은 범용 간격으로 4주 롤링 윈도우를 권하면서, 기간을 정수 주 단위로 잡아 매번 같은 수의 주말이 들어가게 하라고 한다.
    • 동시에 짧은 윈도우일수록 빠르게 결정할 수 있다고 말한다.

    V1 기간이 3주라 4주 윈도우는 한 번도 채워지지 않는다. 그래서 정수 주 단위 중 가장 짧은 7일을 판정 기준으로 쓴다.

  4. 최소 이벤트 수 규칙 측정 기간의 전체 이벤트 수가 1 ÷ (1 − SLO)보다 적으면 SLO 달성 여부를 판정하지 않고, 건수만 보고하고, 데이터 부족으로 표시한다.

    • SLO 99%라면 100건, SLO 95%라면 20건이다.
    • 이 수보다 적으면 실패 1건만으로 에러 버짓 전체가 사라지기 때문이다. 판정이 서비스 상태가 아니라 운에 좌우된다.
    • Workbook도 요청 수가 매우 적은 대상은 운 좋게 100%가 나오거나, 실패 한 번이 큰 비율을 차지해 매우 낮은 값이 나오는 식으로 변동이 크다고 지적한다.
  5. 에러 버짓 에러 버짓은 1 − SLO다. 예를 들어 SLO가 99%이고 7일간 유효 요청이 1,000건이면 최대 10건의 나쁜 이벤트가 에러 버짓에 해당한다.

3.2. SLI/SLO 정의표

SLI-1: 요청을 받아 정상적으로 응답했는가?
SLI-2: 정상 응답이 충분히 빨랐는가?
SLI-3: 접수된 채팅이 실제 답변 완료로 이어졌는가?
SLI-4: 접수된 가상 피팅이 실제 결과 생성으로 이어졌는가?
ID CUJ SLI 측정 단위 전체 이벤트 좋은 이벤트 계산식 잠정 SLO 측정 기간 주 데이터 출처 제외 조건 근거·상태
SLI-1 CUJ-1, CUJ-4, CUJ-2·3의 요청 접수 핵심 API 가용성 HTTP 요청 1건 Nginx에 기록된 /api/ 유효 요청 2xx·정상적인 3xx 좋은 ÷ 전체 99.9% 7일 롤링 (+ V1 21일 누적) Nginx access log (CloudWatch Logs) 헬스체크, 테스트 트래픽, GET /api/chat/stream(SSE), 피팅 상태·결과 조회, /api/ 외 경로 목표값: 가정 / 측정: 로그 필드 확인 필요
SLI-2 SLI-1과 같음 핵심 API 응답시간 HTTP 요청 1건 SLI-1의 전체 이벤트 중 2xx·정상적인 3xx 0.5초 이내에 응답한 요청 좋은 ÷ 전체 95%가 0.5초 이내 AND 99%가 1초 이내 7일 롤링 (+ V1 21일 누적) Nginx access log (CloudWatch Logs) SLI-1 제외 조건 + 전신사진 검증 API 기준시간: 벤치마크 / 비율: 가정 / 측정: 로그 필드 확인 필요
SLI-3 CUJ-2 채팅 응답 성공 대화 요청 1건 (requestId 1개) 접수된(201) 메시지 전송 요청 첫 message_chunk를 7초 안에 받고 done으로 정상 종료 좋은 ÷ 전체 95% 7일 롤링 (+ V1 21일 누적) Spring 채팅 이벤트 로그 테스트 계정, 400으로 거절된 메시지(접수 안 됨), 사용자 이탈(규칙은 4장) 기준시간: 벤치마크 / 비율: 가정 / 측정: 미구현
SLI-4 CUJ-3 가상 피팅 성공 피팅 작업 1건 (fittingJobId 1개) 생성된(201) 피팅 작업 제한 시간 T 안에 COMPLETED 좋은 ÷ 전체 95% (T는 미정) 7일 롤링 (+ V1 21일 누적) Spring 피팅 작업 상태 기록 테스트 계정, 400 FITTING_PRODUCT_REQUIRED(작업 미생성) T: 미정 / 비율: 가정 / 측정: 기록 형식 확인 필요
항목 현재 기능 명세 SLI 측정 상태 용도
requestId 있음 연결 로그 미구현 메시지 전송부터 SSE 종료까지 동일한 요청으로 묶는다.
message_chunk 있음 첫 수신 시각 기록 미구현 AI 답변의 첫 내용이 시작된 시점을 판단한다.
done 있음 종료 결과 기록 미구현 SSE 답변이 오류 없이 정상 종료됐는지 판단한다.
fittingJobId 있음 작업 생명주기 로그 미구현 피팅 생성부터 완료까지 동일한 작업으로 묶는다.
PROCESSING, COMPLETED, FAILED 있음 집계 가능한 종료 로그 미구현 피팅 작업의 최종 성공 여부를 판단한다.
제한 시간 T 없음 미정 피팅이 몇 초 안에 완료되어야 좋은 이벤트인지 정하는 SLO 기준이다.

SLI-1 — 핵심 API 가용성

가입, 로그인, 찜, 채팅 접수, 피팅 접수처럼 대부분의 사용자 여정이 API 요청에서 시작되기 때문에 필요하다.

채팅 POST가 5xx로 실패하면 requestId가 만들어지지 않으므로 SLI-3에서 측정할 수 없다. 피팅 작업 생성이 5xx로 실패하면 fittingJobId가 만들어지지 않으므로 SLI-4에서도 측정할 수 없다. 이런 초기 요청 실패를 SLI-1이 담당한다.

SSE와 피팅 상태 polling을 제외한 이유는 요청 수와 사용자 작업 수가 일치하지 않기 때문이다. 피팅 작업 하나를 확인하기 위해 상태 API를 10번 호출하면 HTTP 요청은 10건이지만 사용자가 수행한 피팅 작업은 1건이다. 채팅과 피팅의 최종 성공 여부는 각각 SLI-3과 SLI-4에서 작업 단위로 측정한다.

4xx는 사용자 입력 오류, 인증 상태, FE 요청 오류 등 여러 원인이 섞여 있고 Nginx 상태 코드만으로 책임 주체를 정확하게 구분하기 어렵다. 따라서 V1에서는 4xx를 가용성 SLI 계산에서 제외한다.

전체 이벤트
= 테스트·헬스체크 요청을 제외한
  핵심 API의 2xx·3xx·5xx·타임아웃 요청

좋은 이벤트
= 2xx 또는 정상적인 3xx로 완료된 요청

나쁜 이벤트
= 5xx, upstream 연결 실패, upstream 타임아웃

제외 이벤트
= 4xx, SSE, 피팅 상태 polling, 테스트·헬스체크 요청

SLO 99.9%

99.9%는 EC2와 RDS의 SLA를 곱해서 얻은 값이 아니라, LookDDak의 핵심 사용자 여정을 막힘없이 완료하기 위해 필요한 요청 성공률을 기준으로 정한다.

LookDDak의 신규 사용자는 서비스를 사용하기 전 다음과 같은 API 단계를 연속해서 통과해야 한다.

회원가입 → 로그인 → 전신사진 검증 → 기본정보 등록

각 요청의 성공률이 99.0%라면, 네 요청이 모두 성공할 확률은 단순 계산상 다음과 같다.

0.99 × 0.99 × 0.99 × 0.99
≈ 96.1%

즉, 다른 오류가 전혀 없다고 가정해도 신규 사용자 약 100명 중 4명은 온보딩 과정에서 한 번 이상 서버 오류를 경험할 수 있다. 첫 사용 과정에서 발생한 실패는 채팅과 가상 피팅을 사용하기도 전에 사용자를 이탈시킬 수 있으므로, 99.0%는 LookDDak의 핵심 API 목표로는 느슨하다.

요청당 성공률을 99.9%로 설정하면 같은 네 단계가 모두 성공할 확률은 다음과 같다.

0.999 × 0.999 × 0.999 × 0.999
≈ 99.6%

이는 신규 사용자 1,000명 중 약 996명이 네 단계에서 서버 오류를 경험하지 않는 수준이다. 실제 장애는 서로 독립적이지 않으므로 정확한 사용자 성공률을 의미하지는 않지만, API 목표가 사용자 여정에 미치는 영향을 설명하는 참고 계산으로 사용한다.

V1의 설계 트래픽은 피크 약 3.92 RPS이며, 기존 용량 분석에서 추정한 애플리케이션 처리 한계 약 24 RPS보다 낮다. 정상적인 V1 트래픽에서는 약 6.1배의 처리 여유가 있으므로, 과부하가 아닌 일반 요청에서 0.1%를 넘는 서버 오류를 지속적으로 허용할 근거가 부족하다.

따라서 다음을 V1 목표로 정한다.

유효한 핵심 API 요청 1,000건 중 서버 책임의 실패는 최대 1건까지 허용한다.

SLI-2 — 핵심 API 응답 시간

SLI-1이 “응답했는가”를 본다면 SLI-2는 응답은 했지만 너무 느리지 않았는가를 측정한다. 가용성과 속도를 분리해서 보기 위해 지표가 필요하다.

전신사진 검증 API, 채팅 SSE 및 가상 피팅 상태 polling은 일반 API 응답시간 SLI에서 제외한다.

  • 전신사진 검증은 이미지 크기와 AI 검증시간의 영향을 받는다.
  • 채팅 SSE는 첫 응답시간과 스트림 정상 종료 여부를 SLI-3에서 측정한다.
  • 피팅 상태 polling은 동일한 작업에서 반복 요청되므로 요청 수가 사용자 작업 수와 일치하지 않는다.
  • 가상 피팅 완료시간은 SLI-4에서 작업 단위로 측정한다.

SLI-2는 가입, 로그인, 기본정보 등록, 회원정보 조회, 채팅 접수, 피팅 접수 등 일반적인 JSON API를 대상으로 한다.

4xx는 정상 처리 요청보다 빠르게 종료되는 경우가 많아 전체 응답시간을 실제보다 좋게 만들 수 있다. 따라서 SLI-2는 유효한 요청 중 2xx 또는 정상적인 3xx로 완료된 요청만을 대상으로 한다.

전체 이벤트
= SLI-2 대상 핵심 API 중
  2xx 또는 정상적인 3xx로 완료된 유효 요청

빠른 응답
= Nginx request_time이 0.5초 이하인 요청

매우 느리지 않은 응답
= Nginx request_time이 1초 이하인 요청

0.5초는 사용자 반응성에 관한 벤치마크와 서비스 특성을 참고해 정한 잠정적인 내부 기준이다. 사용자 반응시간 연구에서는 인간의 지각 능력과 관련된 지연 한계를 다음과 같이 정리한다.

  • 0.1초: 사용자가 시스템이 즉시 반응한다고 느끼는 한계
  • 1초: 지연을 알아채더라도 생각의 흐름이 끊기지 않는 한계
  • 10초: 사용자의 주의를 대화에 붙잡아 둘 수 있는 한계

1초는 사용자의 생각 흐름이 유지되는 상한에 가까운 값이지만, 핵심 API의 서버 응답시간 기준으로 사용하기에는 다소 느슨할 수 있다고 판단했다. 브라우저 렌더링과 네트워크 지연은 서버 응답 이후에도 추가되므로, 서버에서 이미 1초를 사용하면 사용자가 실제로 느끼는 전체 대기시간은 1초보다 길어진다.

따라서 V1에서는 0.1초의 즉각적인 반응 기준과 1초의 흐름 유지 기준 사이인 0.5초를 좋은 응답의 기준으로 정한다.

패션 커머스에서 속도는 사용자의 탐색과 구매 행동에 직접적인 영향을 줄 수 있다. Deloitte 연구에서는 모바일 사이트 속도가 0.1초 개선됐을 때 리테일 전환율은 8.4%, 평균 주문 금액은 9.2% 증가한 결과를 제시한다. V1의 직접적인 목적이 구매 전환은 아니지만, 패션 상품을 탐색하는 서비스에서 느슨한 응답시간 기준을 사용하는 것은 사용자 경험 측면에서 위험할 수 있다.

https://www.deloitte.com/ie/en/services/consulting/research/milliseconds-make-millions.html

https://www.nngroup.com/articles/response-times-3-important-limits/

다만 해당 연구는 브라우저 화면 전체의 반응시간을 대상으로 하므로 서버 응답시간과 완전히 같지는 않다. 따라서 부하테스트와 실제 운영 데이터를 확보한 뒤 달성 가능성과 사용자 경험의 상관관계를 검토해 조정해야 한다.

SLO(2단계로 설정)

SLO-2A
전체 유효 요청의 95% 이상이 0.5초 이내
= p95 ≤ 0.5초

SLO-2B
전체 유효 요청의 99% 이상이 1초 이내
= p99 ≤ 1초

두 조건을 모두 만족해야 SLI-2를 달성한 것으로 판정한다.

1단계 — p95 ≤ 0.5초

0.5초는 대부분의 사용자가 경험하는 일반 요청에 적용하는 기준이다. 사용자가 버튼을 누른 뒤 실제 화면이 변경될 때까지는 서버 처리 이외에도 Nginx, DB 처리, 응답 전송, FE 상태 갱신 등의 작업이 필요하다.

Nginx 기준 서버 응답에 이미 1초가 소요되면 FE 처리와 화면 렌더링을 포함한 전체 체감시간은 1초보다 길어진다. 따라서 서버 구간의 95%를 0.5초 이내로 제한해 네트워크와 브라우저 처리에 사용할 시간 여유를 확보한다.

현재 SLI-2에서는 이미지 검증, SSE, 가상 피팅 처리처럼 시간이 오래 걸리는 작업을 제외한다. 남은 API는 대부분 작은 JSON 요청과 RDS 조회·저장으로 구성된다.

https://learn.microsoft.com/en-us/azure/well-architected/performance-efficiency/performance-targets

2단계 — p99 ≤ 1초

p95만 관리하면 나머지 5%의 요청이 지나치게 느려도 SLO를 통과할 수 있다. 이 경우 p95 목표는 달성하지만 일부 사용자는 명확한 지연을 반복적으로 경험한다. 따라서 거의 모든 요청이 사용 흐름을 유지할 수 있는 1초 안에 끝나도록 p99 기준을 추가한다. 로그인과 같은 기능은 비밀번호 검증과 DB 조회가 포함되어 LookDDak의 일반 API 중 상대적으로 느릴 가능성이 있으므로, 1초는 꼬리 지연의 상한으로 사용할 수 있다.

Google SRE의 공개 예시도 하나의 임계치만 사용하지 않고 두 단계로 API 지연을 관리한다.

https://sre.google/workbook/slo-document/

SLI-3 — 채팅 응답 성공

사용자가 메시지를 보낸 뒤 AI 응답이 제때 시작되고 끝까지 완료됐는지를 측정한다.

전체 이벤트:
201로 접수되어 requestId가 생성된 채팅 요청

좋은 이벤트:
7초 안에 첫 message_chunk가 발생하고
오류 없이 done으로 종료된 요청

SLI:
좋은 채팅 수 / 접수된 전체 유효 채팅 수

잠정 SLO:
95% 이상

채팅은 SSE 연결이 생성되거나 HTTP 200이 반환됐다는 사실만으로 성공했다고 판단할 수 없다. 연결이 정상적으로 시작됐더라도 AI 답변이 늦게 시작되거나, 일부 내용만 전달된 뒤 스트림이 중단될 수 있기 때문이다.

message_chunk는 AI 답변의 일부 내용을 전달하는 SSE 이벤트다. 첫 message_chunk가 전송됐다는 것은 애플리케이션이 사용자에게 실제 답변 내용을 전달하기 시작했다는 의미다. 이때 연결 유지용 빈 이벤트나 메타데이터는 첫 응답으로 계산하지 않고, 화면에 표시할 수 있는 첫 번째 유효한 콘텐츠만 계산한다

done은 모든 답변 전송이 정상적으로 완료됐음을 나타내는 종료 이벤트다. 첫 응답이 빨랐더라도 done 전에 오류나 타임아웃으로 스트림이 종료되면 사용자는 답변이 중간에 끊긴 것으로 경험하므로 나쁜 이벤트로 판정한다.

첫 응답 기준 7초

7초는 외부 LLM 서빙 사례에서 직접 가져온 수치가 아니라 LookDDak의 초기 측정 결과를 기반으로 설정한 잠정적인 서버 측 기준이다. 개발 환경에서 실제 채팅 요청의 첫 응답이 대략 4~6초에 발생한 점을 고려하여, 외부 API 응답 편차와 Spring·FastAPI 내부 처리시간을 포함할 수 있도록 7초를 상한으로 정한다.

다만 현재 측정이 소수의 수동 테스트에 기반한 경우 통계적인 p95를 의미하지 않는다. 따라서 운영 환경과 유사한 프롬프트 길이, 응답 길이 및 동시 요청 조건으로 부하테스트를 수행한 뒤 첫 응답시간의 p50·p95·p99를 확인해야 한다. 테스트 결과 p95가 7초를 지속적으로 초과한다면 모델, 프롬프트, 호출 구조를 개선하거나 기준시간을 재검토한다.

잠정 SLO 95%

95%는 OpenAI가 보장하는 SLA가 아니라 LookDDak V1의 초기 내부 목표다. LookDDak의 핵심 기능은 사용자의 조건을 대화로 수집하고 상품을 추천하는 것이므로, 채팅은 단순한 부가 기능이 아니라 핵심 사용자 여정에 해당한다.

다만 이 SLI의 나쁜 이벤트에는 답변이 중간에 실패한 경우뿐 아니라 7초를 약간 초과했지만 최종 답변은 정상적으로 제공된 경우도 포함된다. 외부 AI API를 사용하는 스트리밍 응답에는 일정한 지연 편차가 발생할 수 있으므로, V1에서는 전체 유효 채팅 요청의 95%가 제한 시간과 정상 종료 조건을 모두 만족하도록 잠정 목표를 설정한다.

단, 95%만 관리하면 느린 응답과 실제 스트림 실패가 하나의 수치에 섞일 수 있다. 따라서 done 정상 종료율은 별도의 운영 지표로 관리하고, 99% 미만으로 내려갈 경우 채팅 기능의 신뢰성 문제로 판단한다.

SLI-4 — 가상 피팅 성공

사용자가 요청한 가상 피팅 결과가 제한 시간 안에 생성되고, 실제로 조회 가능한 이미지로 제공됐는지를 작업 단위로 측정한다.

전체 이벤트:
201로 생성되어 fittingJobId가 발급된 유효한 피팅 작업

좋은 이벤트:
작업 생성 후 T 안에 COMPLETED가 된 작업

SLI:
T 안에 사용 가능한 결과가 생성된 피팅 작업 수
/
생성된 전체 유효 피팅 작업 수

잠정 SLO:
95% 이상

가상 피팅은 하나의 사용자 작업이 생성 요청, AI 이미지 생성, 결과 저장, 상태 조회 등 여러 HTTP 요청으로 구성된다. 또한 사용자가 작업 상태를 확인하기 위해 상태 조회 API를 여러 번 호출할 수 있으므로 HTTP 요청 수를 기준으로 성공률을 계산하면 실제 사용자 작업 수보다 요청 수가 과도하게 집계된다.

따라서 피팅 생성 요청에서 발급되는 fittingJobId를 기준으로 하나의 피팅 작업을 처음부터 끝까지 추적한다. 상태 조회를 위해 발생한 폴링 요청은 SLI-4의 전체 이벤트에 포함하지 않는다.

좋은 이벤트 판정

피팅 작업은 작업 생성 후 T 안에 COMPLETED 상태가 된 경우 좋은 이벤트로 판정한다.

T 이내 COMPLETED
→ 좋은 이벤트

T 초과 후 COMPLETED
→ 결과 생성에는 성공했지만 완료시간 기준을 위반한 나쁜 이벤트

FAILED
→ 결과 생성에 실패한 나쁜 이벤트

T가 지났지만 PROCESSING
→ 제한 시간 안에 완료되지 못했으므로 나쁜 이벤트

이때 COMPLETED는 단순히 외부 AI가 이미지를 생성했다는 의미가 아니라, 생성된 결과를 LookDDak이 저장하여 사용자에게 제공할 준비를 마쳤다는 의미로 정의한다.

잠정 SLO 95%

95%는 Runware가 보장하는 공식 SLA가 아니라 LookDDak V1의 초기 내부 목표다.

가상 피팅은 외부 이미지 생성 모델에 의존하고 입력 이미지 크기, 선택한 의류 수, 외부 작업 대기열 및 네트워크 상태에 따라 처리시간이 달라질 수 있다. 또한 LookDDak에서는 사용자가 생성 중 다른 화면으로 이동할 수 있으므로 일부 긴 꼬리 지연을 허용할 수 있다.

이를 고려하여 V1에서는 전체 유효 피팅 작업의 95%가 T 안에 사용 가능한 결과 이미지로 완료되는 것을 잠정 목표로 설정한다.

4. 측정 규칙과 구현 계획

4장에서는 SLI/SLO 측정에 필요한 데이터를 실제 시스템에서 어떻게 만들고 계산할지를 정의한다.

예를 들어 3장에서 “핵심 API 요청의 99.9%가 성공해야 한다”고 정해도, 요청 상태 코드가 로그에 없다면 실제 성공률을 계산할 수 없다. 따라서 4장에서는 Nginx가 요청마다 상태 코드와 처리시간을 기록하도록 설정하고, CloudWatch에서 이를 계산하는 방법을 정의한다.

현재 데이터 상황은 다음과 같다.

필요한 데이터 쓰이는 SLI 현재 상태
Nginx 요청 로그 SLI-1, SLI-2 CloudWatch로 수집 중
테스트 요청 식별 값 전체 미구현
채팅 SSE 이벤트 SLI-3 requestId, message_chunk, done은 기능 명세에 존재
채팅 측정용 이벤트 로그 SLI-3 접수 시각·첫 chunk 시각·종료 결과를 연결한 로그 미구현
피팅 작업 상태 SLI-4 fittingJobId, COMPLETED, FAILED는 기능 명세에 존재
피팅 측정용 이벤트 로그 SLI-4 생성·완료 시각과 최종 상태를 연결한 로그 미구현
집계 쿼리와 주간 보고 전체 미구현

따라서 현실적인 현재 수준의 구현 계획은 다음과 같다.

대상 현재 상태 측정 가능 조건
SLI-1 핵심 API 가용성 조건부 측정 가능 Nginx JSON 로그 적용 및 상태 코드 필드 검증
SLI-2 핵심 API 응답시간 조건부 측정 가능 request_time 기록 및 두 단계 집계 쿼리 검증
SLI-3 채팅 응답 성공 계측 추가 필요 Spring 채팅 접수·종료 이벤트 로그 구현
SLI-4 가상 피팅 성공 계측 추가 필요 Spring 피팅 생성·종료 이벤트 로그 구현

기존 수집 경로인 컨테이너 stdout/stderr → Docker awslogs → CloudWatch Logs를 사용한다.

4.1. Nginx 로그형식

Nginx는 브라우저 요청이 Next.js와 Spring으로 전달되기 전에 거치는 공통 입구다. Nginx가 요청을 받아 Spring 또는 Next.js에게 전달 후 다시 Nginx가 최종 응답을 사용자에게 전송하고, 요청 결과를 access log에 기록한다.

Nginx가 access log를 JSON 형식으로 출력하면 CloudWatch Logs Insights가 status, request_time, uri 등을 개별 필드로 인식할 수 있다. 이를 이용해 다음과 같은 조건으로 요청을 필터링하고 집계한다.

status >= 500
→ 서버 오류 요청 조회

request_time <= 0.5
→ 0.5초 이내 응답 조회

test_type = "-"
→ 실제 사용자 요청만 조회
{
  "time": "2026-10-01T12:00:00+09:00",
  "nginx_request_id": "3f2a9b41e1",
  "host": "lookddak.com",
  "method": "GET",
  "uri": "/api/v1/members/me",
  "status": 200,
  "request_time": 0.184,
  "upstream_status": "200",
  "upstream_response_time": "0.176",
  "user_agent": "Mozilla/5.0",
  "test_type": "-"
}

LookDDak이 일반 HTTP API의 측정 기준점으로 Nginx를 사용하는 이유는 다음과 같다.

  1. 사용자에게 반환된 최종 상태 코드를 기록한다.

    Spring 내부 로그에는 Spring이 직접 처리한 결과가 남는다. 하지만 Nginx는 Spring이 반환한 응답뿐 아니라, Spring에 연결하지 못하거나 응답을 기다리다 타임아웃이 발생한 경우까지 기록할 수 있다.

    Spring이 정상적으로 응답
    → 2xx·3xx·4xx·5xx 기록
    
    Nginx가 Spring에 연결하지 못함
    → 사용자에게 502 반환
    
    Nginx가 Spring의 응답을 기다리다 타임아웃
    → 사용자에게 504 반환
    

    사용자는 Spring 내부에서 어떤 문제가 발생했는지가 아니라 브라우저가 최종적으로 어떤 응답을 받았는지를 경험한다. 따라서 사용자에게 반환된 최종 상태 코드를 기록하는 Nginx 로그를 가용성 측정 기준으로 사용한다.

  2. 모든 일반 API를 같은 방식으로 측정할 수 있다.

    회원가입, 로그인, 회원정보 조회, 찜, 채팅 접수, 피팅 접수 등 일반 API 요청은 모두 Nginx의 /api/ 경로를 거쳐 Spring으로 전달된다.

    각 Spring API에 별도의 측정 코드를 구현하면 API마다 로그 형식이나 측정 시점이 달라질 수 있다. 반면 Nginx에서는 모든 /api/ 요청에 동일한 로그 형식을 적용할 수 있으므로 API별 구현 차이 없이 같은 기준으로 상태 코드와 응답시간을 측정할 수 있다.

Nginx 설정

log_format slo_json escape=json '{'
  '"time":"$time_iso8601",'
  '"nginx_request_id":"$request_id",'
  '"host":"$host",'
  '"method":"$request_method",'
  '"uri":"$uri",'
  '"status":$status,'
  '"request_time":$request_time,'
  '"upstream_status":"$upstream_status",'
  '"upstream_response_time":"$upstream_response_time",'
  '"user_agent":"$http_user_agent",'
  '"test_type":"$lookddak_test_type"'
'}';

access_log /dev/stdout slo_json;
필드 의미 필요한 이유
time 요청 처리가 끝난 시각 조회할 7일 기간을 정한다.
nginx_request_id HTTP 요청별 고유 ID Nginx와 Spring 로그를 연결하고 장애 요청을 추적한다.
host 요청이 들어온 도메인 lookddak.com 요청과 다른 호스트·리다이렉트 요청을 구분한다.
method GET·POST 등의 HTTP 메서드 반복 GET 요청 등을 제외한다.
uri 쿼리 문자열을 제외한 요청 경로 API·SSE·피팅·헬스체크 경로를 구분한다.
status 사용자에게 반환된 최종 HTTP 상태 코드 SLI-1을 계산한다.
request_time Nginx가 요청을 받은 뒤 응답 전송을 마칠 때까지 걸린 시간 SLI-2를 계산한다.
upstream_status Spring·Next.js가 Nginx에 반환한 상태 코드 Nginx와 백엔드 중 어디에서 오류가 발생했는지 확인한다.
upstream_response_time Spring·Next.js의 응답을 기다린 시간 전체 지연 중 백엔드가 차지한 시간을 분석한다.
user_agent 브라우저 또는 요청 프로그램 정보 봇이나 비정상 요청을 확인할 때 보조적으로 사용한다.
test_type 테스트 요청 구분값 부하테스트와 E2E 요청을 실제 사용자 통계에서 제외한다

https://nginx.org/en/docs/http/ngx_http_log_module.html

https://nginx.org/en/docs/http/ngx_http_core_module.html

Nginx 로그와 Spring 로그를 연결할 수 있도록 Nginx가 생성한 요청 ID를 Spring에 전달한다. 다만 이부분은 별도로 Spring이 X-Request-ID를 읽어 traceId로 로그에 기록하도록 구현이 필요하다.

proxy_set_header X-Request-ID $request_id;
add_header X-Request-ID $request_id always;

SLI-1 계산

SLI-1은 실제 사용자의 핵심 API 요청 중 서버가 정상적으로 응답한 요청의 비율을 계산한다.

계산하기 전에 Nginx 로그에서 SLI-1 대상 요청만 선택한다. 계산은 Cloudwatch log insights(대규모 로그 데이터를 빠르고 대화식으로 검색, 필터링 및 분석할 수 있는 CloudWatch Logs의 내장 분석 도구)로 수행한다.

fields
    host,
    method,
    uri,
    status,
    request_time,
    test_type
| filter host = "lookddak.com"
| filter uri like /^\/api\//
| filter test_type = "-"
| filter uri != "/api/health"
| filter uri != "/api/chat/stream"
| filter not (
    method = "GET"
    and uri like /^\/api\/v1\/fitting-jobs\//
  )
/api/만 선택
→ 화면과 정적 파일 요청을 제외한다.

test_type = "-"
→ 테스트 요청을 제외한다.

/api/health 제외
→ 헬스체크 요청을 제외한다.

채팅 stream 제외
→ SSE는 SLI-3에서 측정한다.

피팅 작업 GET 제외
→ 반복되는 폴링 요청을 제외한다.

4xx는 좋은 이벤트와 나쁜 이벤트 모두에서 제외한다.

| filter (
    (status >= 200 and status < 400)
    or status >= 500
  )
| stats
    count(*) as total,
    sum(if(status >= 200 and status < 400, 1, 0)) as good,
    sum(if(status >= 500, 1, 0)) as bad
| fields
    total,
    good,
    bad,
    good * 100.0 / total as sli1
전체 이벤트
= 2xx·정상적인 3xx·5xx 요청 수

좋은 이벤트
= 2xx·정상적인 3xx 요청 수

나쁜 이벤트
= 5xx 요청 수

SLI-1
= 좋은 이벤트 ÷ 전체 이벤트 × 100

SLI-2 계산

SLI-2는 2xx·정상적인 3xx로 완료된 일반 API 요청만 대상으로 한다. 4xx·5xx, 전신사진 검증, 채팅 SSE 및 피팅 폴링 요청은 제외한다.

filter host = "lookddak.com"
  and uri like /^\/api\//
  and test_type = "-"
  and status >= 200
  and status < 400
  and uri != "/api/health"
  and uri != "/api/chat/stream"
  and not (
    method = "GET"
    and uri like /^\/api\/v1\/fitting-jobs\//
  )
  and uri != "/api/v1/full-body-image/validate"
  and uri != "/api/v1/validation/body-image"
| stats
    count(*) as total,
    sum(if(request_time <= 0.5, 1, 0)) as within_500ms,
    sum(if(request_time <= 1.0, 1, 0)) as within_1s,
    pct(request_time, 95) as p95,
    pct(request_time, 99) as p99
| fields
    total,
    within_500ms * 100.0 / total as slo_2a_rate,
    within_1s * 100.0 / total as slo_2b_rate,
    p95,
    p99
SLI-2A
= 0.5초 이내 정상 응답 수
  ÷ 전체 정상 응답 수
  × 100

목표
= 95% 이상
= p95 ≤ 0.5초
SLI-2B
= 1초 이내 정상 응답 수
  ÷ 전체 정상 응답 수
  × 100

목표
= 99% 이상
= p99 ≤ 1초

두 조건을 모두 만족해야 SLI-2를 달성한 것으로 판정한다.

4.2. 테스트 요청 구분 방법

부하테스트와 E2E 요청은 실제 사용자보다 훨씬 많이 발생할 수 있다. 이를 사용자 요청과 섞으면 SLO가 사용자가 경험한 품질이 아니라 테스트 결과를 나타내게 된다.

따라서 테스트 요청에는 다음 전용 헤더를 추가한다.

X-LookDDak-Test: load
X-LookDDak-Test: e2e
X-LookDDak-Test: internal

Nginx는 $http_x_lookddak_test 변수로 해당 헤더 값을 읽는다.

헤더 없음
→ test_type="-"

X-LookDDak-Test: load
→ test_type="load"

X-LookDDak-Test: e2e
→ test_type="e2e"

X-LookDDak-Test: internal
→ test_type="internal"

사용자 요청에는 이 헤더가 없으므로 Nginx 로그의 test_type이 -가 된다. 실제 사용자 SLI는 test_type = "-"인 요청만 계산한다.

요청 종류 구분 방법 사용자 SLI 처리
실제 외부 사용자 테스트 헤더가 없다. 포함한다.
부하테스트 X-LookDDak-Test: load 제외한다.
E2E·계측 검증 X-LookDDak-Test: e2e 제외한다.
팀 내부 테스트 X-LookDDak-Test: internal 제외한다.

E2E와 팀 내부 테스트에 헤더를 강제하려면 다른 파트의 도구와 코드까지 수정해야 한다. V1에서는 공식 집계를 외부 사용자 모집 시작 시점부터 시작하여, 그 전에 수행한 테스트 데이터를 시간 범위로 제외한다.