본문으로 건너뛰기
고급약 27분

저하 상태에서 확인할 것들 — 타임아웃·재시도·중복·정합성

네트워크가 나빠졌을 때 제품이 어떻게 행동해야 하는지를 여덟 가지 체크포인트로 정리하고, 타임아웃 예산·지수 백오프·지터·멱등키·재시도 예산을 검증 가능한 항목으로 다듬는다.

  • #타임아웃
  • #재시도
  • #백오프
  • #멱등성
  • #중복요청
  • #결함리포트

개념

이 모듈의 본론

레슨 1부터 6까지는 전부 상태를 만드는 방법 이었다. 조건을 정의하고, 도구를 고르고, 적용을 확인하고, Limited Service를 재현했다.

이 레슨이 본론이다. 그래서 무엇을 볼 것인가.

네트워크 테스트는 '느려지는지'가 아니라 '느려졌을 때 제품이 어떻게 행동하는지' 를 본다.

조건을 걸어놓고 "아, 느리네요" 하고 끝내는 테스트를 아주 많이 본다. 그건 도구 동작 확인이지 제품 테스트가 아니다.

저하 상태에서 확인할 8가지

① 타임아웃 정책     몇 초 뒤 어떤 메시지를 보여주는가. 무한 로딩은 없는가
② 재시도 동작       자동 재시도 횟수·간격이 통제되는가. 수동 수단이 있는가
③ 중복 처리         응답 지연 중 연타 시 중복 결제·중복 등록이 나는가
④ 부분 로딩         일부만 로드된 화면에서 레이아웃이 무너지지 않는가
⑤ 오프라인 처리     캐시 화면·안내 배너가 뜨고 입력값이 보존되는가
⑥ 데이터 정합성     업로드 중 끊긴 뒤 서버와 클라이언트 상태가 일치하는가
⑦ 회선 전환         Wi-Fi ↔ 셀룰러 전환 시 세션이 유지되는가
⑧ 자원 소모         재시도 폭주로 발열·배터리·데이터 소모가 과도한가

여덟 개 중 ③과 ⑥이 돈과 직결된다. 중복 결제와 주문 유실이 여기서 나온다. 나머지는 사용자 경험 문제지만, 이 둘은 사고다.

① 타임아웃 정책 — 예산으로 생각한다

타임아웃은 하나가 아니다

"타임아웃 30초"라고만 말하는 팀이 많은데, 실제로는 여러 단계가 있다.

DNS 해석      도메인 → IP
   +
TCP 연결      connect timeout
   +
TLS 핸드셰이크
   +
요청 전송 ~ 첫 바이트 수신   read timeout
   +
본문 전체 수신
   ─────────────────────────
   = 전체(total) timeout

각 단계 타임아웃의 합이 전체 타임아웃보다 크면, 전체 값은 의미가 없어진다. 반대로 연결 타임아웃만 짧게 잡고 읽기 타임아웃을 안 걸면 응답이 영원히 안 와도 계속 기다린다.

QA가 확인할 것:

  • connect / read / total 타임아웃이 각각 설정되어 있는가
  • 그 합이 사용자가 기다릴 수 있는 시간 안에 들어오는가
  • 화면마다 다른 값을 쓰는가 (전부 같은 값이면 설계 부재를 의심)

화면별 예산 잡기

사용자가 대기 중인 화면 (로그인, 결제 확인)
   → 10~15초. 그 이상은 사용자가 먼저 포기한다
 
목록·상세 조회
   → 15~20초. 부분 로딩으로 먼저 보여줄 수 있으면 더 짧게
 
백그라운드 동기화
   → 30~60초. 사용자가 안 보고 있으므로 길어도 된다
 
파일 업로드
   → 전체 타임아웃보다 '진행률 정체' 로 판단하는 편이 낫다
     (예: 30초간 전송 바이트가 안 늘면 실패 처리)

"타임아웃이 60초인데 화면에는 스피너만 있다"는 조합이 최악이다. 값 자체보다 그 시간 동안 사용자에게 무엇을 보여주는가 가 더 중요하다.

② 재시도 — 설계 없이 하면 장애를 키운다

나쁜 재시도가 서버를 죽인다

서버가 잠깐 불안정해졌다. 클라이언트 수만 대가 동시에, 즉시, 무한히 재시도한다. 서버는 회복할 틈이 없다. 이게 재시도 폭주(retry storm) 다.

[나쁜 재시도]
실패 → 즉시 재시도 → 실패 → 즉시 재시도 → ...
  모든 클라이언트가 같은 순간에 몰린다
 
[좋은 재시도]
실패 → 1초 대기 → 실패 → 2초 → 실패 → 4초 → 포기
  + 각 대기에 무작위 편차를 섞는다 (지터)
  + 클라이언트마다 다른 순간에 재시도하게 흩어진다

지수 백오프와 지터

delay = min(cap, base × 2^attempt) × random()
 
base    첫 대기 (예: 1초)
2^n     시도할수록 배로 늘린다
cap     상한 (예: 30초). 무한히 늘어나지 않게
random() 지터 — 이게 없으면 모두가 같은 순간에 재시도한다

지터가 없는 지수 백오프는 반쪽짜리다. 간격은 벌어지지만 여전히 모든 클라이언트가 같은 타이밍에 몰린다.

QA가 검증할 재시도 항목

  • 최대 재시도 횟수 가 있는가 (보통 3~5회). 무한 재시도는 결함이다
  • 간격이 늘어나는가 (고정 간격이면 백오프가 없는 것)
  • 간격에 무작위성이 있는가 (여러 번 측정해 값이 다른지 본다)
  • 4xx는 재시도하지 않는가 — 400·401·403·404는 다시 보내도 같은 결과다
  • 429·5xx만 재시도하는가
  • Retry-After 헤더를 존중하는가
  • 재시도가 여러 계층에서 중복되지 않는가 (SDK + 앱 코드가 각각 3회면 실제로는 9회)

마지막 항목이 놓치기 쉽다. HTTP 클라이언트 라이브러리가 이미 재시도를 하고 있는데 앱 코드가 또 감싸면 곱해진다. 재시도는 한 계층에서만 한다.

어떻게 측정하는가

레슨 4의 프록시가 여기서 가장 유용하다.

1. 특정 API를 항상 503으로 반환하게 설정
2. 앱에서 그 기능을 실행
3. 프록시 세션 목록에서 같은 요청이 몇 번 왔는지 센다
4. 각 요청의 도착 시각 차이를 계산한다
   → 1초, 2초, 4초처럼 늘어나는가?
   → 매번 정확히 같은 값인가? (지터 없음)

요청 도착 시각을 적어 간격을 계산하는 것 이 재시도 정책 검증의 실체다. 코드를 안 봐도 동작으로 확인할 수 있다.

③ 중복 처리 — 가장 비싼 결함

왜 저하 상태에서 중복이 나는가

사용자: [주문하기] 클릭
  → 응답이 5초째 안 온다
  → "안 눌렸나?" 하고 다시 클릭
  → 또 안 온다 → 한 번 더
 
서버: 주문 3건 생성

요청은 갔는데 응답이 안 온 상태 가 핵심이다. 사용자도 앱도 성공 여부를 모른다. 여기에 자동 재시도까지 겹치면 더 늘어난다.

방어 수단

수단어디서확인 방법
버튼 비활성화클라이언트연타해도 요청이 1건인가 (프록시로 센다)
멱등키 (Idempotency-Key)클라이언트 → 서버같은 키로 재요청 시 1건만 생성되는가
서버 중복 검사서버짧은 시간 내 동일 요청 거부

클라이언트 버튼 비활성화만으로는 부족하다. 자동 재시도는 버튼과 무관하게 나가고, 네트워크 레벨 재전송도 있다. 상태를 바꾸는 요청 (결제·주문·등록)에는 멱등키가 있어야 한다.

멱등키 검증 절차

1. 레슨 4의 프록시로 요청을 가로채 Idempotency-Key 헤더가 있는지 확인
2. 있다면: 같은 요청을 그대로 재전송(replay)한다
   → 서버가 새로 만드는가, 기존 결과를 돌려주는가?
3. 없다면: 그 자체가 결함 후보다
4. 키가 재시도마다 새로 생성되지는 않는지 확인
   → 재시도인데 키가 바뀌면 멱등성이 깨진다

4번이 실무에서 자주 발견되는 버그다. 멱등키를 넣긴 했는데 재시도할 때마다 새 키를 만들어서 아무 효과가 없는 구현.

④⑤ 부분 로딩과 오프라인 처리

부분 로딩

저하 상태에서는 일부만 도착한 화면 이 오래 유지된다. 정상 속도에서는 찰나라 눈에 안 띄던 문제가 드러난다.

  • 이미지가 안 온 자리에 자리표시자(placeholder)가 있는가, 아니면 레이아웃이 밀리는가
  • 목록 일부만 로드됐을 때 스크롤이 튀지 않는가
  • 값이 없는 필드가 null 이나 undefined 로 화면에 노출되지 않는가
  • 일부 실패한 섹션만 별도로 재시도할 수 있는가

세 번째 항목은 저하 상태에서만 잡히는 전형적 결함이다.

오프라인 처리

레슨 6의 레시피 C·D와 겹친다. 핵심은 둘이다.

  • 캐시된 내용이라도 보여주는가, 아니면 빈 화면인가
  • 입력값이 보존되는가

⑥ 데이터 정합성 — 조용히 어긋난다

가장 늦게 발견되는 문제

업로드 중에 연결이 끊겼다. 클라이언트는 실패로 처리했는데 서버는 받았다. 또는 반대. 화면과 서버가 다른 상태를 믿고 있는 상태 다.

이건 저하 테스트에서 의도적으로 만들지 않으면 절대 안 나온다.

[정합성 시험 절차]
1. 업로드·등록 작업을 시작한다
2. 전송 중간에 연결을 끊는다 (레슨 5의 loss 100% 또는 업링크 차단)
3. 클라이언트 화면 상태를 기록한다
4. 연결을 복구한다
5. 서버 상태를 직접 확인한다 (관리자 화면, DB, API 조회)
6. 두 상태가 일치하는가?
7. 일치하지 않으면 앱이 그것을 발견하고 화해(reconcile)하는가?

7번까지 봐야 한다. 어긋나는 것 자체는 피하기 어렵다. 중요한 건 어긋났을 때 앱이 알아채고 맞추는가 다.

확인할 케이스

  • 업로드 중 끊김 → 서버에 부분 파일이 남는가
  • 등록 중 끊김 → 새로고침하면 등록된 것으로 보이는가
  • 삭제 중 끊김 → 목록에서는 사라졌는데 서버에는 남아 있는가
  • 결제 중 끊김 → 결제는 됐는데 주문은 없는 상태가 가능한가

마지막은 반드시 확인해야 하는 케이스다.

⑦⑧ 회선 전환과 자원 소모

회선 전환

사용자는 이동한다. Wi-Fi에서 셀룰러로, 셀룰러에서 Wi-Fi로 바뀐다. 이때 IP가 바뀐다.

  • 전환 시 세션이 유지되는가. 재로그인을 요구하지 않는가
  • 진행 중이던 업로드·다운로드가 이어지는가, 아니면 처음부터인가
  • WebSocket·실시간 연결이 재연결되는가
  • 전환 직후 요청이 실패하면 재시도로 복구되는가

재현 방법은 간단하다. 모바일 데이터를 켠 상태에서 Wi-Fi를 껐다 켜면 된다. (레슨 6의 조건 테스트와 달리 이때는 모바일 데이터를 켠다.)

자원 소모

재시도 폭주는 서버만 괴롭히는 게 아니다. 클라이언트도 배터리와 데이터를 태운다.

  • Limited 조건을 10분 유지했을 때 배터리 소모가 정상 대비 몇 배인가
  • 기기가 눈에 띄게 뜨거워지는가
  • 데이터 사용량이 비정상적으로 증가하는가 (실패한 요청도 데이터를 쓴다)
  • 백그라운드로 보낸 뒤에도 재시도가 계속되는가

마지막 항목이 특히 중요하다. 사용자가 앱을 닫았는데도 백그라운드에서 재시도가 도는 앱이 있다. 배터리 소모 리뷰의 원인이 된다.

결함 리포트 — 조건이 없으면 닫힌다

레슨 1에서 예고한 양식이다. 저하 상태 결함은 조건 없이는 재현되지 않으므로 리포트 형식이 특히 중요하다.

제목    [조건식별자] 증상 요약
조건    조건명 + 4축 수치 + 사용한 도구/스크립트
측정    적용 확인 결과 (스크린샷 첨부)
절차    1. ... 2. ... 3. ...
실제    관찰된 동작 (재시도 횟수, 경과 시간 등 수치 포함)
기대    무엇이 일어나야 했는가 + 근거
증거    로그, 프록시 세션 캡처, 화면 녹화

"실제" 칸에 수치를 넣는 것 이 핵심이다. "재시도를 많이 함"이 아니라 "12초간 7회 재시도, 간격 고정 1.7초"라고 적으면 개발자가 바로 코드를 찾아간다.

실무에서 왜 필요한가

중복 결제 사고를 미리 막는다

중복 결제는 환불 처리 비용, CS 응대, 신뢰 손실이 전부 따라오는 사고다. 그리고 거의 항상 네트워크 저하 상황에서 발생한다. 정상 속도에서는 응답이 빨라 사용자가 연타할 틈이 없다.

레시피 A(극단 저속) 조건에서 결제 버튼을 연타해보는 5분짜리 테스트 가 이 사고를 막는다.

앱스토어 리뷰의 상당수가 이 영역이다

"자꾸 멈춰요", "배터리가 빨리 닳아요", "글 쓴 게 날아갔어요" — 전부 이 여덟 가지 체크포인트에 해당한다. 기능은 다 되는데 리뷰가 나쁜 앱은 대개 여기가 약하다.

개발과의 대화가 구체적이 된다

"네트워크 처리가 부실합니다"는 대화가 안 된다. "재시도 간격이 고정 1.7초이고 상한이 없습니다. 지수 백오프와 지터를 넣고 5회로 제한하는 게 어떨까요"는 대화가 된다.

실습 과제

과제 1 — 타임아웃 예산표 만들기

우리 제품의 주요 화면 8개에 대해 다음 표를 채운다.

화면현재 타임아웃관찰된 실제 대기사용자 대기 중?제안 값대기 중 화면

"현재 타임아웃"은 개발에 물어보거나, 레시피 D 조건에서 실제로 재서 채운다. 재는 편이 정확하다.

과제 2 — 재시도 정책 측정

레슨 4의 프록시로 특정 API를 항상 503으로 만들고, 재시도 요청의 도착 시각을 기록한다.

시도도착 시각직전 간격
100:00.0—
2
3

같은 실험을 3회 반복해 간격이 매번 같은지(지터 없음) 다른지(지터 있음) 확인한다.

체크할 것: 최대 횟수, 간격 증가 여부, 지터 유무, 4xx 재시도 여부, Retry-After 존중 여부.

과제 3 — 중복 요청 실험

레시피 A 조건에서 상태를 바꾸는 기능(주문·등록·좋아요 무엇이든)의 버튼을 5회 연타하고, 프록시 세션에서 요청 개수를 센다.

기능클릭 횟수실제 요청 수생성된 데이터 수멱등키 유무

요청 수와 생성 데이터 수가 다르면 서버 방어가 있다는 뜻이다. 둘 다 5라면 결함이다.

과제 4 — 멱등키 replay 테스트

프록시로 상태 변경 요청을 하나 잡아 그대로 재전송 한다.

  • 서버가 새 데이터를 만드는가
  • 기존 결과를 그대로 돌려주는가
  • 오류를 반환하는가

두 번째가 정답이다. 첫 번째면 멱등성이 없는 것이다.

과제 5 — 정합성 시험 4종

본문의 4가지 케이스(업로드·등록·삭제·결제 중 끊김)를 각각 실행하고, 클라이언트 상태와 서버 상태를 비교한다. 서버 상태는 관리자 화면이나 API로 직접 확인한다.

어긋난 케이스마다 "앱이 이걸 알아챌 방법이 있는가" 를 함께 적는다.

과제 6 — 자원 소모 측정

Limited 조건을 10분간 유지하고 앱을 켜둔 채 다음을 기록한다.

항목정상 조건 10분Limited 10분배수
배터리 소모 %
데이터 사용량
기기 온도 체감

앱을 백그라운드로 보낸 뒤에도 측정 해서 재시도가 계속되는지 본다.

과제 7 — 결함 리포트 다시 쓰기

과거에 "재현 불가"로 닫힌 네트워크 관련 결함을 하나 찾아(없으면 과제 1~6에서 발견한 것으로) 본문의 양식대로 다시 작성한다.

조건 식별자, 4축 수치, 측정 스크린샷, 수치가 포함된 관찰 결과를 반드시 넣는다.

자가 체크리스트

  • 저하 상태 체크포인트 8가지를 나열할 수 있다
  • 여덟 개 중 어느 둘이 돈과 직결되는지 알고 이유를 설명할 수 있다
  • 타임아웃이 connect·read·total로 나뉘는 이유와 각각의 역할을 안다
  • 화면 성격별로 타임아웃 예산을 다르게 잡는 기준을 설명할 수 있다
  • 재시도 폭주가 무엇이고 왜 장애를 키우는지 설명할 수 있다
  • 지수 백오프 공식과 지터의 역할을 설명할 수 있다
  • 4xx를 재시도하면 안 되는 이유를 안다
  • 재시도가 여러 계층에서 곱해지는 문제를 설명할 수 있다
  • 프록시로 재시도 간격을 측정하는 절차를 수행할 수 있다
  • 저하 상태에서 중복 요청이 생기는 메커니즘을 설명할 수 있다
  • 멱등키가 왜 필요하고 어떻게 검증하는지 안다
  • 재시도마다 멱등키가 바뀌면 왜 무의미한지 설명할 수 있다
  • 정합성 시험 7단계를 수행하고 화해 동작까지 확인할 수 있다
  • 회선 전환 시 확인할 항목 4가지를 말할 수 있다
  • 저하 상태 결함 리포트에 반드시 들어갈 항목을 말할 수 있다

흔한 실수

조건만 걸고 "느리네요"로 끝낸다. 이 모듈 전체에서 가장 흔한 실수다. 도구 동작을 확인한 것이지 제품을 테스트한 것이 아니다.

타임아웃 값만 보고 화면을 안 본다. 30초가 적절한지 아닌지는 그 30초 동안 사용자가 무엇을 보는지에 달려 있다.

재시도 횟수를 눈으로 세려 한다. 화면에는 "재시도 중"만 나온다. 프록시 세션이나 로그에서 실제 요청 수를 세야 한다.

버튼 비활성화를 중복 방어로 충분하다고 본다. 자동 재시도와 네트워크 재전송은 버튼과 무관하다. 상태 변경 요청에는 서버 측 방어가 필요하다.

멱등키가 있다는 사실만 확인하고 동작은 안 본다. 헤더가 붙어 있어도 서버가 무시하거나, 재시도마다 새 키를 만들면 아무 효과가 없다.

정합성 시험을 안 한다. 만들기 번거롭고 확인도 번거로워서 건너뛴다. 그런데 가장 늦게, 가장 비싸게 발견되는 문제가 여기서 나온다.

회선 전환 테스트에서 모바일 데이터를 끈다. 레슨 6의 습관이 남아서 그런데, 회선 전환은 둘 다 켜야 재현된다.

자원 소모를 안 본다. 기능이 다 되므로 통과로 기록한다. 그런데 사용자는 "배터리가 닳는다"로 리뷰를 쓴다.

참고 자료

  • Exponential Backoff & Jitter: Stop Retry Storms — 백오프 공식과 지터가 없을 때 벌어지는 일
  • Exponential backoff and retry storms: jitter, retry budgets — 재시도 예산(전체 트래픽 대비 재시도 비율 상한) 개념
  • API Retry Mechanism: How It Works + Best Practices — 재시도해야 할 상태 코드와 하면 안 되는 상태 코드
  • 이 모듈 레슨 4 프록시 스로틀링 — 재시도 측정과 멱등키 replay에 쓰는 도구
  • 이 모듈 레슨 6 Limited Service 재현 레시피 — 여기 체크포인트를 적용할 조건을 만드는 법
  • 모듈 6 결함 관리와 보고 — 재현 가능한 결함 리포트의 일반 원칙
  • 모듈 12 API·계약 테스트 — 멱등성과 계약 위반 응답에 대한 클라이언트 방어

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
SDK — 소프트웨어 개발 도구 모음
어떤 기능을 쓰려고 할 때 필요한 도구와 코드를 묶어 둔 꾸러미.
HTTP — 하이퍼텍스트 전송 규약
웹에서 요청과 응답을 주고받는 약속. "무엇을 달라"고 보내면 "여기 있다" 또는 "그런 건 없다"가 돌아온다.
IP — 아이피 주소
인터넷에 연결된 기기마다 붙는 번호.
CS — 고객 지원 · 칩 선택 신호
이 커리큘럼에서 두 가지로 쓰인다. ① 고객 문의와 불만을 직접 받는 조직 ② 여러 부품이 한 선을 나눠 쓸 때 "지금은 네 차례" 라고 하나를 고르는 신호. 앞뒤에 문의·고객이 있으면 ①, 통신·배선 이야기면 ②다.
API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.

용어집 전체 보기