fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • langgraph 페이지로 이동
    • 사람용 CLI와 AI 에이전트용 CLI는 설계가 다르다
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code 메모리: CLAUDE.md와 .claude/rules를 규칙으로 쓰는 법
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
    • OpenClaw는 context와 memory를 어떻게 관리하나 — 나만의 에이전트를 구성하는 법
    • OpenClaw vs Hermes Agent — 갈아탈까 고민하며 정리한 비교
    • 스킬 문서를 신경망처럼 학습시킨다 — Microsoft SkillOpt 분석
  • ai 페이지로 이동
    • agent 페이지로 이동
    • [초안] AI 제품 백엔드 안정성 — 지연·비용·권한·관측·도구 실패·폴백/재시도/사람 에스컬레이션
    • [초안] LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • apartment 페이지로 이동
    • 구리 럭키아파트 24평 인테리어 레퍼런스 모음
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] Event Sourcing과 CQRS — 상태가 아니라 변화를 저장한다는 발상
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리
    • [초안] F&B 주문/매장/픽업 상태머신 설계
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 대규모 회원과 메가 프로모션을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — 디지털 채널 백엔드 관점
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • [초안] 운영 데이터 정합성 장애 대응 — 결제 취소 누락과 중복 적재 런북
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS는 어떻게 안전한가 — TLS, 인증서, 그리고 termination
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
    • [초안] Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • mlops 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/network/Connection reset by peer…
system

Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다

한 OCR API 서비스를 운영하다가 모니터링 알림을 받았다. 클라이언트(curl)에서는 호출이 실패했는데, 서버 쪽에서는 정상 처리되고 사용량(미터링)까지 집계된 건이었다. 클라이언트가 본 에러는 curl exit 56 / OpenSSL SSLread: Connection reset by peer (errno 104). 그래서 답해야 했던 질문은 하나였다...

2026.06.24·8 min read·16 views

한 OCR API 서비스를 운영하다가 모니터링 알림을 받았다. 클라이언트(curl)에서는 호출이 실패했는데, 서버 쪽에서는 정상 처리되고 사용량(미터링)까지 집계된 건이었다. 클라이언트가 본 에러는 curl exit 56 / OpenSSL SSL_read: Connection reset by peer (errno 104).

그래서 답해야 했던 질문은 하나였다. 이 연결을 끊은 건 클라이언트일까, 서버일까?

이걸 추적하면서 다시 확인한 사실은, 많은 사람이 무심코 넘기는 지점이었다. 리버스 프록시는 연결을 "통과"시키지 않는다. 홉마다 완전히 별개의 TCP 연결이다. 이 한 가지를 제대로 알면 Connection reset by peer 한 줄이 "어느 박스가 끊었는지"를 거의 단정해준다.

증상 — 클라이언트는 실패, 서버는 성공

상황을 정리하면 이랬다.

  • 클라이언트: curl exit 56, SSL_read: Connection reset by peer (errno 104) 로 응답을 못 받음
  • 서버: 같은 시각 요청을 정상 수신, 모델 추론도 약 980ms에 성공, 사용량 집계까지 완료
  • 서버 로그에는 그 요청에 대한 에러가 없음

클라이언트는 "실패"라는데 서버는 "성공"이라고 한다. 둘 다 거짓말을 하는 게 아니다. 둘은 서로 다른 연결을 보고 있었을 뿐이다.

curl exit 56과 errno 104가 정확히 무슨 뜻인가

두 신호를 분해하면 방향이 보인다.

  • curl exit 56은 CURLE_RECV_ERROR다. 데이터를 받는(read) 도중 실패했다는 뜻이다. 즉 클라이언트는 요청을 다 보내고 응답을 기다리며 읽던 상태였다.
  • errno 104는 ECONNRESET이다. 소켓이 TCP RST 패킷을 수신했다는 뜻이다. "by peer"는 상대편이 RST를 보냈다는 의미다.

여기서 이미 한 가지가 갈린다. 클라이언트가 스스로 끊었다면 자기 read에서 "reset by peer"가 날 리 없다. 그냥 자기가 닫을 뿐이다. 클라이언트가 타임아웃으로 포기했다면 curl은 exit 28 (operation timeout)을 낸다. exit 56 + ECONNRESET이 아니다.

클라이언트는 RST를 받은 쪽이다. 끊은 주체가 아니다. 응답을 기다리다 연결이 강제로 끊긴 피해자다.

RST와 FIN은 다르다 — 강제 중단 vs 정상 종료

"서버가 정상 응답을 했다"는 말과 이 에러는 사실 양립하기 어렵다. TCP에서 연결을 닫는 방법이 두 가지인데, 둘의 의미가 완전히 다르기 때문이다.

  • FIN: 정상 종료. "할 말 다 했으니 닫는다." 클라이언트는 이걸 깨끗한 EOF로 받는다. 정상 응답을 다 받은 뒤의 종료가 이쪽이다.
  • RST: 강제 중단. "이 연결 지금 즉시 버린다." 타임아웃이나 중간 장비의 강제 종료에서 전형적으로 나온다.

클라이언트가 받은 건 EOF가 아니라 RST였다. 그러니 최소한 응답이 클라이언트까지 정상적으로 전달되지는 않았다는 것만은 확실하다. 서버 내부에서 처리가 끝났다는 사실과는 별개다.

리버스 프록시는 연결을 통과시키지 않는다

핵심은 여기다. 흔히 클라이언트부터 백엔드까지 하나의 파이프가 쭉 이어진다고 생각하지만, L7 리버스 프록시(API Gateway, ingress 등)는 들어온 연결을 자기 선에서 종료(terminate)하고, 뒤로는 완전히 새로운 TCP 연결을 맺는다.

그래서 실제로는 하나의 연결이 아니라 여러 개의 독립된 연결 구간이 존재한다.

각 구간은 자기만의 소켓, 자기만의 SYN/FIN/RST, 자기만의 타임아웃을 가진 서로 다른 TCP 연결이다. 클라이언트는 오직 첫 프록시(API Gateway)하고만 연결을 맺고 있고, 그 뒤의 ingress나 백엔드 앱과는 소켓 자체가 없다. 주소도 모르고 연결도 없다.

그래서 RST는 한 구간 안에서만 작동한다

RST는 TCP 패킷이고, 그게 오가는 한 연결 구간 안에서만 의미가 있다. 이걸 시간 순으로 보면 이번 사건이 한눈에 들어온다.

정리하면 이렇다.

  • 백엔드 앱이 끊으면 연결③에 RST를 보낸다. 받는 쪽은 ingress다.
  • ingress가 끊으면 연결②에 RST를 보낸다. 받는 쪽은 게이트웨이다.
  • 클라이언트에게 직접 RST를 보낼 수 있는 건 연결①의 상대, 즉 API Gateway뿐이다.

뒤쪽 박스가 연결을 끊어도 클라이언트 소켓에 직접 RST를 꽂는 건 물리적으로 불가능하다. 소켓이 없으니까. 뒤쪽의 끊김은 거기서 끝나고, 게이트웨이가 그 상황을 받아 클라이언트에게 무엇을 돌려줄지 새로 결정한다. 보통은 502 Bad Gateway나 504 Gateway Timeout 같은 HTTP 응답으로 변환하거나, 자기 판단으로 클라이언트 연결에 RST를 보낸다.

TLS 종료점이 "클라이언트의 peer"를 정한다

Connection reset by peer에서 peer가 누구인지는 TLS가 어디서 끝나는지로 정해진다. 클라이언트의 HTTPS 연결은 그 호스트네임의 인증서를 쥐고 있는 쪽에서 종료된다. 그게 API Gateway라면, 클라이언트의 TLS peer는 게이트웨이다.

이번 건에서 백엔드가 받은 요청 헤더를 보면 x-forwarded-proto: http, x-scheme: http처럼 내부 구간이 평문 HTTP였다. 앞단에서 TLS를 종료하고 안쪽은 HTTP로 전달하는 전형적인 구성이다. 즉 클라이언트가 받은 RST는 TLS를 종료한 API Gateway가 보낸 것으로 좁혀진다. 뒤쪽 박스는 클라이언트의 TLS peer가 아니므로 후보에서 빠진다.

그럼 누가 끊었나 — 확정과 미확정을 나눠야 한다

여기서 두 질문을 분리하는 게 중요하다.

질문답
클라이언트에 RST를 보낸 주체API Gateway (TLS peer라 구조적으로 확정)
클라이언트가 먼저 끊었나아니다 (RST를 받은 쪽, recv 에러)
백엔드 앱이 클라이언트를 직접 끊었나불가능 (별개 연결, 소켓 없음)
왜 게이트웨이가 끊었나미확정 — 게이트웨이 access log 필요

"왜"를 확정하려면 게이트웨이의 요청 단위 access log가 필요하다. 거기서 두 가지를 보면 갈린다.

  • HTTP status가 504(upstream timeout) → 게이트웨이가 느린 백엔드를 타임아웃으로 끊은 것
  • 499(client closed) → 클라이언트가 먼저 닫은 것

이번엔 그 로그가 보존되지 않아 "왜"는 게이트웨이 담당에 문의하는 것으로 정리했다. 하지만 "누가 끊었나(=게이트웨이가 보냈다)"는 위 구조만으로 이미 단정할 수 있었다. 추적이 막힌 지점과 단정할 수 있는 지점을 섞지 않는 게 중요하다.

로그가 없어도 토폴로지와 TCP 의미만으로 "끊은 손"은 특정된다. "왜 끊었는지"와 "누가 끊었는지"는 다른 질문이고, 필요한 증거도 다르다.

만약 게이트웨이가 L4 passthrough였다면

여기엔 전제가 하나 있다. 게이트웨이가 L7에서 TLS를 종료한다는 것. 만약 게이트웨이가 L7 종료가 아니라 L4 passthrough(TCP/TLS를 그대로 흘려보냄)였다면 이야기가 달라진다. L4는 패킷 헤더만 보고 전달하므로, 뒤쪽에서 발생한 RST가 클라이언트까지 전파될 수도 있다. 그 경우 "끊은 손"은 게이트웨이로 좁혀지지 않는다.

그래서 추적의 첫 단추는 클라이언트의 TLS가 어디서 끝나는가를 먼저 확정하는 것이다. 인증서가 어느 박스에 있는지가 곧 추적의 출발점이다.

더 큰 교훈 — 부수효과는 응답 전달과 분리될 수 있다

이 사건이 단순한 네트워크 잡음이 아니었던 이유는, 그 와중에 미터링(사용량 집계)이 확정됐다는 점이다.

미터링은 "모델 추론 성공" 시점에 서버 내부에서 확정된다. 그 뒤 응답이 클라이언트에 전달되는지와는 무관하다. 그래서 응답이 전송 도중 RST로 끊겨도, 서버는 이미 처리에 성공했고 사용량을 더한 상태다.

이건 분산 시스템에서 흔히 밟는 함정이다. 서버가 본 성공과 클라이언트가 본 성공이 갈릴 수 있고, 부수효과(과금, 사용량, 외부 호출)가 클라이언트의 응답 수신을 기다리지 않고 먼저 커밋되면, "클라이언트는 실패했는데 과금은 됐다" 같은 상태가 만들어진다. 응답 전달까지 성공해야 부수효과를 확정할지, 아니면 처리 성공만으로 확정하고 전달 실패는 별도로 보정할지는 설계에서 명시적으로 정해야 한다. 그냥 두면 이번처럼 둘이 조용히 어긋난다.

그럼 이 과금은 보상해야 하나

당연히 "전달 실패면 환불해야지" 싶고, 그래서 처음 떠올린 건 게이트웨이 실패 직후 보상 트랜잭션을 발행하는 그림이었다. 그런데 이건 대부분 안 쓰고, 못 쓴다. 이유는 지식이 컴포넌트별로 쪼개져 있기 때문이다.

  • 미터링을 커밋한 주체는 백엔드 앱이다. 그런데 앱은 자기 관점에서 성공했고, 전달이 실패했다는 사실을 모른다.
  • 전달 실패를 아는 주체는 게이트웨이다. 그런데 게이트웨이는 그 요청에 미터링이라는 부수효과가 있었다는 걸 모른다.

"부수효과를 안다"와 "전달 실패를 안다"를 동시에 쥔 컴포넌트가 없다. 게다가 끊김은 앱 바깥 전송 계층에서 일어났고, API Gateway는 보통 실패한 요청마다 과금 시스템을 콜백해주는 훅을 제공하지 않는다. 그래서 게이트웨이에 보상을 물리는 설계는 현실적으로 성립하기 어렵다.

실무에서는 보통 이렇게 푼다.

  • 과금 정책을 먼저 정한다. "추론이 실제로 돌았으면 과금"인지 "전달 성공까지 돼야 과금"인지는 코드 트릭이 아니라 비즈니스 결정이다. 전자라면 이번 건은 정당한 과금이고 보상이 불필요하다. 후자라면 전달 실패분은 정의상 환불 대상이 된다.
  • 사후 정산(reconciliation)으로 보정한다. 네트워크 경계를 넘는 exactly-once는 사실상 불가능하므로, 과금은 보통 at-least-once로 일단 집계하고 나중에 맞춘다. 미터링 레코드와 전달 결과를 request-id로 대조해 "성공+과금됐는데 전달 실패한 요청"을 배치로 뽑아 크레딧/환불한다. 실시간 보상이 아니라 결과적 정합성으로 간다.
  • 멱등키로 이중 과금을 막는다. 클라이언트가 같은 idempotency key로 재시도하면 서버가 중복 집계를 거른다. 이번 고아 과금과는 다른 축이지만 같이 챙겨야 한다.

여기서 걸리는 게 사후 정산의 전제다. 미터링 기록과 전달 결과를 묶어서 대조하려면 게이트웨이의 요청 단위 로그(504인지 499인지)가 있어야 하는데, 그게 없으면 보상을 하려 해도 무엇을 보상할지 목록조차 못 만든다. 앞 섹션에서 본 관측성 공백이 정확히 여기서 발목을 잡는다.

보상은 "어떻게 되돌리나"보다 "되돌릴 대상을 어떻게 식별하나"가 먼저다. 식별이 안 되면 보상 로직은 짤 수가 없다.

마지막으로 우선순위. 이 RST는 포화로 응답이 느려져 게이트웨이 타임아웃이 터진, 빈도 낮은 엣지 케이스다. 이럴 때 실시간 보상 트랜잭션을 정교하게 짜는 비용은 절감액보다 크고, 보상 자체의 실패라는 새 실패 모드까지 늘린다. 그래서 보통은 원인 제거(포화·타임아웃부터 줄여 발생 빈도를 떨어뜨림)와 관측 가능하게 만들어 사후 정산하는 쪽이 먼저고, 남는 소량은 고객 지원 크레딧으로 흡수한다. 정교한 실시간 보상은 대개 과한 해법이다.

정리하며

Connection reset by peer 한 줄에서 출발해 여기까지 왔다. 다음에 비슷한 걸 만나면 나는 이 순서로 본다.

  1. curl/에러 코드로 클라이언트가 받은 건지 보낸 건지부터 가른다 (RST 수신 vs 자발적 종료)
  2. 클라이언트의 TLS가 어디서 끝나는지 확인한다 (= peer = 끊은 후보)
  3. 프록시 홉을 별개 연결로 그려놓고, RST가 닿을 수 있는 구간을 좁힌다
  4. "누가 끊었나"(구조로 확정)와 "왜 끊었나"(로그 필요)를 분리한다
  5. 그 사이에 커밋된 부수효과(과금·외부 호출)가 있는지 따로 점검한다
  6. 부수효과가 있다면 되돌릴 방법보다 되돌릴 대상을 식별할 수 있는지(request-id로 대조 가능한지)를 먼저 확인한다

연결이 끊긴 자리를 찾는 것보다, 끊을 수 있었던 자리가 어디인지를 토폴로지로 좁히는 게 먼저였다.

on this page
  • 01증상 — 클라이언트는 실패, 서버는 성공
  • 02curl exit 56과 errno 104가 정확히 무슨 뜻인가
  • 03RST와 FIN은 다르다 — 강제 중단 vs 정상 종료
  • 04리버스 프록시는 연결을 통과시키지 않는다
  • 05그래서 RST는 한 구간 안에서만 작동한다
  • 06TLS 종료점이 "클라이언트의 peer"를 정한다
  • 07그럼 누가 끊었나 — 확정과 미확정을 나눠야 한다
  • 08만약 게이트웨이가 L4 passthrough였다면
  • 09더 큰 교훈 — 부수효과는 응답 전달과 분리될 수 있다
  • 10그럼 이 과금은 보상해야 하나
  • 11정리하며

이런 글도

  • L2(스위치)와 L3(라우터)의 역할 차이
    - 같은 Subnet -> L2 스위치만으로 통신 가능 - 다른 Subnet -> 반드시 L3 라우터를 거처야 통신 가능 왜 이렇게 되어있을까? 그 이유는 MAC 주소와 IP 주소의 역할이 완전히 다르기 때문이다 !layer2-vs-layer3 - L2는 MAC 주소 기반으로 동작 - 같은 Subnet = 같은 동네 - 예: 192.168.1.0/24 서브...
    🔌 system
    system
    2026.01.30
  • IP Subnet
    예: 192.168.1.23 이 주소는 사실 두 부분으로 구성돼 있음: 1. 네트워크(Network): 어떤 동네인가? 2. 호스트(Host): 그 동네 안의 어떤 집인가? > 즉, [네트워크 부분] + [호스트 부분] 으로 되어 있음 그런데, IP 주소만 보면 어디까지가 핑계고 어디까지가 집 번호인지 모름 그래서 등장하는 게 CIDR 표기 예: 192.1...
    🔌 system
    system
    2026.01.30
  • L4와 VIP(Virtual IP Address)
    TCP/UDP 레벨에서 트래픽을 보고 판단하는 Load Balancer L4가 판단할 떄 보는 정보는 4가지 - Source IP - Source Port - Destination IP - Destination PORT > 즉, 패킷의 헤더만 보고 어디로 보낼지를 결정하는 것 - 1. 트래픽 분산(로드밸런싱) - 예: - text VIP(20.30.40.5...
    🔌 system
    system
    2026.01.30

댓글 (0)