← 일하는 방식 · 해결한 문제

6단계로 푼 문제 8개

한 줄 = 문제 · 판단 · 결과. 줄을 누르면 상세 기록으로 이동.

  1. 01 포트 설정 충돌로 롤링 업데이트가 멈추고, 실패가 성공으로 보고된 문제 위협 4개로 분리해 각각 차단, 성공 기준 = 서빙 중인 이미지 태그 실측 배포 ~9.5 → ~2.5분
  2. 02 Pub/Sub 발행 유실로 관제 화면이 실제 상태와 어긋나는 문제 이벤트 전달 대신 상태 수렴(revision · 공유 checkpoint · snapshot) 마지막 유실도 checkpoint로 복구
  3. 03 재시도 요청의 중복 실행으로 증빙 · 스텝 이벤트가 이중 저장되는 문제 요청 UUID 대신 기존 행 잠금 + 현재 도메인 상태로 판정 재전송해도 중복 저장 없음
  4. 04 요청마다 새 커넥션을 여는 키 조회로 업로드 25%가 타임아웃된 문제 실패 시각으로 connect 실패 특정, 키 조회 캐시 + single-flight + 커넥션 재사용 키 조회 50 → 1회
  5. 05 상태 snapshot 부재로 화면과 알림의 계획 판정이 갈린 문제 NULL 처리 대신 두 경로를 공통 상태 재료로 통합 화면 · 알림 같은 판정
  6. 06 에이전트 구현 비중 증가로 리뷰만으로 아키텍처 규칙을 지킬 수 없게 된 문제 규칙을 AST 구조 테스트로 만들어 배포 게이트에 연결 위반 시 파이프라인에서 중단
  7. 07 프론트엔드 의존으로 서버 변경 검증이 늦어지는 문제 두 역할로 서버 API를 바로 실행하는 테스트 콘솔 Flow Lab 구축 로딩 6.2 → 0.9초 · 요청 14 → 3회
  8. 08 계약 공백을 에이전트가 추측으로 채우는 문제 용어집 → 폴더 규칙 → 계약 확정 → 구현 → show-me · 리뷰 · 시나리오 테스트 → 연동 전 테스트 지시 단위: 파일 수정 → 이슈 구현

2026.06 – 현재팀 프로젝트백엔드 설계 · 구현 · 운영서버 커밋 1,700+

SPOS 서버 철도 전력 관제 작업 관리 백엔드

  • 문제 01–06이 생긴 시스템
  • 단전 · 급전 작업의 계획 · 결재 · 현장 실행 · 감사 서버
  • 관제 데스크와 현장 안전관리자 앱이 같은 상태를 실시간으로 공유
  • 사내 폐쇄망외부 클라우드 · 레지스트리 · 오케스트레이터 없음
  • replica 1스케일아웃으로 장애를 숨길 수 없음
  • 관제 업무배포가 상태를 깨면 곧 안전 문제
  • 현장 망응답 유실과 재전송이 일상

Python · FastAPI · SQLAlchemy(async) · PostgreSQL + pgbouncer · Redis · APScheduler · Docker Swarm · Traefik · GitLab CI · Tavern

판단

MSA 대신 role 기반 모듈러 모놀리스 — 코드베이스 1 · 이미지 1 · 서버 4 SERVICE_ROLE 하나로 앱 조립, 라우터 등록은 선언 테이블. 서버 추가 = 테이블 한 줄 추가. 서비스 분리 비용은 개인 MSA 실험에서 먼저 확인.
app:<commit-sha>단일 Docker 이미지
  • WEB Router유일한 진입점 · JWT 게이트 · 헤더 화이트리스트 프록시
  • Auth로그인 · versioned JWT · 마스터키 단독 보관
  • WAS작업 · 작업계획 · 전력 · SSE · 알림
  • File봉투 암호화 업로드 · 아카이브 배치
@dataclass(frozen=True)
class RouterSpec:
    module_path: str
    prefix: str
    service_roles: frozenset[str]  # 이 라우터를 갖는 role

for spec in get_router_specs_for_role(role):
    app.include_router(load(spec), prefix=spec.prefix)

문제 01배포SEV

포트 설정 충돌로 롤링 업데이트가 멈추고, 실패가 성공으로 보고된 문제

  • 사내 폐쇄망: 오케스트레이터 없음, replica 1
  • 무중단 배포 전환 cutover 이틀간 사고 9건(SEV 포함)
  • 최대 사고: 백엔드 앱 배포 잡이 사흘 치 머지 내내 성공, 컨테이너는 무변경
배포 시뮬레이터 · 단일 노드 · replica 1

앱 서비스가 host 포트를 직접 열고, 모든 서비스가 start-first로 교체됩니다.

노드
host :443 앱 소유
엣지 (Traefik)
구 태스크7f3a2c1Running
신 태스크b81e9d4—
  • DB
  • Redis
  • 키
  • 스토리지
  1. 이미지 빌드 per-commit 태그
  2. 마이그레이션 expand-only one-shot
  3. 롤링 업데이트
  4. 수렴 확인 —
  5. 파이프라인 —
서빙 중인 태그 7f3a2c1 머지 커밋 b81e9d4 배포 전

‘배포 실행’을 누르세요. 변경 전 구성에서 파이프라인이 무엇을 보고하는지 보세요.

판단

무중단 배포 위협 4개
스키마 비호환 → DDL 탐지 CI 게이트 · 스케줄러 중복 → Redis 리더 락 · 파이프라인 간섭 → 배포 직렬화 · 재기동 → start-first + readiness
잡 성공 ≠ 반영
성공 기준을 잡 exit code에서 “서빙 이미지 태그 = 머지 커밋 SHA” 실측으로 변경. 수렴 체크 타임아웃은 hard-fail
같은 병리 재발
개별 패치 대신 “host 포트를 가진 서비스는 모두 stop-first” 술어 진실표 테스트

결과 배포 ~9.5분 → ~2.5분 · 사고 9건 근본 원인과 재발 방지 조치

사고 9건의 원인과 조치 — 전체 기록 펼치기

사고 대부분: 신 체계 결함이 아니라 compose 시절 잔재(붙어 있던 컨테이너, 옛 이름을 참조하는 env, floating 태그, 상대경로)와의 충돌. 로컬 실증은 메커니즘만 검증, 실 호스트의 잔재는 검증 범위 밖.

#증상근본 원인조치와 봉인
SEV1 1cutover 배포 실패로 진입 서버 다운잔존 컨테이너의 네트워크 점유 + 삼켜진 에러(|| echo)로 실패할 배포에 진입잔존 컨테이너 강제 분리 → 실패 시 hard-fail. 순서까지 회귀 테스트
2엣지 신규 태스크 영구 Pending, 배포는 “성공”host 포트 + start-first + 단일 노드 = 포트 데드락host 포트를 쥔 서비스만 stop-first로 분리
3엣지가 모든 요청에 404프록시 특정 버전대가 Docker API 버전을 구버전으로 고정로컬에서 버전별 최소 재현으로 격리 → 패치 버전까지 명시한 태그 고정
4배포 “성공”, 서빙 코드는 그대로고정 이미지 태그 → 오케스트레이터가 no-op 처리per-commit 이미지 태그
5파일 서버 readiness 503 재시작 루프env가 compose 시절 컨테이너 이름을 참조공유 env 대신 스택 파일 override — “토폴로지 지식은 스택이 소유”
6로그 유실, 저장 경로 회귀stack deploy는 env 파일을 보간하지 않음배포 스크립트가 필요한 값을 export, 규칙을 런북에 명시
7영속 데이터가 CI 워크스페이스에상대경로 bind — git clean에 유실 가능named volume·절대경로 + 볼륨 목록을 진실표 테스트로 봉인
SEV 8백엔드 앱 사흘 치 배포 무반영, 잡은 매번 green2번의 병리가 백엔드 앱에서 재발 + 수렴 체크 타임아웃이 실패를 은폐타임아웃 hard-fail → 전 노드 엣지 도입, “host 포트를 가진 모든 서비스는 stop-first”를 술어 진실표로 봉인
9게이트웨이 경유 SSE 전멸(200 + 0 bytes)스트리밍 응답 반환 직후 finally가 클라이언트를 조기 close정리를 응답 BackgroundTask로 이동, 실패 테스트 먼저

readiness 게이트 효과: DNS 결함이 운영 업로드 실패가 아니라 배포 시점 503으로 드러남.

배포 시간: Swarm 전환 후 ~9.5분 → 마이그레이션 one-shot 잡 단일화 + 배포 잡 DAG 병렬화로 ~2.5분. 병렬화 근거 = “expand-only 마이그레이션” 계약.

문제 02실시간 동기화

Pub/Sub 발행 유실로 관제 화면이 실제 상태와 어긋나는 문제

  • 관제 데스크와 현장 앱이 같은 상태를 실시간으로 공유
  • Redis Pub/Sub는 영속 로그가 아님
  • 마지막 발행 하나가 유실되고 이후 변경이 없으면, 이벤트 번호 차이로도 누락 감지 불가
동기화 시뮬레이터 · power 채널
DB · 정본 r12 구간 A · 급전
Redis Pub/Sub checkpoint · 5초 (시연은 2.5초)
관제 화면 r12 구간 A · 급전 일치

‘다음 발행 유실’을 켜고 상태를 한 번만 바꿔 보세요. 그다음 checkpoint를 끄고 다시 해 보세요.

판단

마지막 유실 감지 불가
이벤트 전달 대신 상태 수렴. 정본은 DB, Pub/Sub는 빠른 신호
연결 수만큼 폴링
워커 안 구독자가 checkpoint 타이머와 DB 조회 1회를 공유(5초)
복구 경로
첫 SSE 이벤트 = snapshot → 최초 접속과 복구를 같은 경로로. 큐 256 프레임 초과 · Redis 재구독 · checkpoint 오류 시 연결 종료 후 재연결

결과 마지막 발행이 유실돼도 다음 checkpoint가 차이를 찾아 snapshot으로 복구

주장하지 않는 것 exactly-once 전달, Last-Event-ID replay, 5초 내 복구 보장.

이벤트 계약 — 전체 기록 펼치기
이벤트의미클라이언트 계약
sync.snapshot현재 전체 집합과 revision기존 집합을 교체하고 서버 scope를 채택
power.delta전원·협의의 확정 최종값중복·과거 revision은 무시, 다음 연속 revision만 적용
plans.delta포함된 계획의 공개 상태해당 객체 전체 교체, 생략된 계획은 유지
sync.checkpointPrimary의 현재 revision적용값보다 앞서면 복구. checkpoint로 적용 revision을 올리지 않음
sync.resync집합·기준일 변경 등 전체 복구 요구재연결 후 snapshot 재수신
  • 배열 의미snapshot 빈 배열 = 전체 비었음, delta 항목 생략 = 기존 값 유지
  • 15초 ping연결 생존 신호. 업무 상태 정합성 근거로 쓰지 않음

문제 03재시도 · 멱등성

재시도 요청의 중복 실행으로 증빙 · 스텝 이벤트가 이중 저장되는 문제

  • 현장 망: 응답 유실과 재전송이 일상
  • 앱에 지수 백오프 재시도(0.5 → 1 → 2초) 도입 후, 응답이 유실된 제출이 다시 실행됨
  • 증빙 · 스텝 이벤트 중복 저장, 결재 대기 건 교체
재시도 시뮬레이터 · 스텝 제출
현장 앱
서버
증빙 행
0
스텝 이벤트
0
앱이 받은 응답
—

    응답이 두 번 유실되는 상황입니다. 가드를 끄고도 실행해 보세요.

    판단

    중복 판정 기준
    요청 UUID 대신 현재 도메인 상태. 제출 경로가 이미 잡던 계획 행 잠금(FOR NO KEY UPDATE) 안에서, 권한 · 순서 · 반려 · 계약 검증 뒤에 판정 → 새 저장소 없음, 기존 오류 우선순위 유지
    업로드 burst
    multipart 본문을 읽지 않은 상태에서 수용 결정. 워커당 동시 5 · 대기 85 · 최대 30초 FIFO, 초과 시 429 + 대기 깊이 기반 Retry-After
    약속 범위
    모든 POST의 exactly-once가 아니라 “같은 제출인지”만. 지난 phase 재제출은 계속 실패, 내용 변경은 별도 정정 · 수정 API

    결과 같은 제출이 다시 와도 증빙 · 스텝 이벤트 재저장 없음, 결재 대기 건 유지

    요청 유형별 서버 처리 — 전체 기록 펼치기
    요청서버 처리
    같은 phase에서 이미 COMPLETED인 일반 스텝 재제출권한·순서·계약 검증 뒤, 증빙·이벤트 저장 본문을 실행하지 않고 완료 응답
    같은 사용자의 phase 재제출미결재 PENDING이 있고 이후 초안 변경이 없으면 현재 상태 반환. PENDING 교체 없음 — 그 사이 관제사가 결재해도 대상이 어긋나지 않음
    이미 지난 phase의 스텝 재제출기존 순서 검증 실패 유지. 멱등 가드로 무조건 200을 돌려주지 않음
    내용 정정별도 정정·수정 경로

    구현 리뷰에서 발견: 운영 예외 처리기가 Retry-After 헤더를 버림 → 수정 + 운영 응답 경로 회귀 테스트 추가.

    문제 04파일 파이프라인 · 부하

    요청마다 새 커넥션을 여는 키 조회로 업로드 25%가 타임아웃된 문제

    • 현장 증빙(사진 · 음성): 서버 디스크 평문 저장 금지, 무결성 감사 필요 → 클라이언트 봉투 암호화
    • 프론트 팀과의 동시 업로드 부하 테스트에서 약 25%가 503
    실패 시각의 지문 · 도식
    관찰한 패턴의 도식(실제 로그 아님). 성공은 분산, 실패는 모두 connect 타임아웃 2초에 집중.
    수정 전 · 요청마다 조회50회
    수정 후 · TTL 캐시 + single-flight1회

    원인

    1. 응답 암호화용 활성 키를 매 요청 Auth 서버에 HTTP로 조회. 복호용 키만 캐시
    2. 조회마다 HTTP 클라이언트를 새로 생성 → 커넥션 풀 없이 매번 TCP 연결
    3. burst로 Auth 서버 accept 큐 지연 → 신규 connect가 2초 타임아웃에서 일제히 실패

    판단

    원인 추적
    “처리가 느리다” 가정 대신 실패 시각 분포 확인. 느린 처리는 실패 시각이 흩어지고, connect 타임아웃 값에 몰리면 커넥션 수립 실패
    매 요청 키 조회
    활성 키 TTL 캐시(60초) + single-flight. 동시 요청의 조회를 1회로 묶음
    커넥션 풀 없음
    모듈 공유 클라이언트 재사용, keepalive는 엣지 idle timeout보다 짧게. 게이트웨이 프록시도 앱 수명 공유 클라이언트로 전환, 스트리밍 응답 정리 계약 유지

    결과 동시 50 요청의 키 조회 50회 → 1회

    평문을 남기지 않는 업로드 설계 — 전체 기록 펼치기
    1. 클라이언트평문 sha256 → AES-256-GCM 봉투 암호화. 봉투 헤더에 래핑 키 메타 · nonce → 파일 하나로 자기 완결
    2. 파일 서버키 언래핑은 Auth 서버에 위임, 마스터키 미보유. 파일 호스트가 털려도 저장된 암호문 복호 불가
    3. 검증메모리에서만 복호 → 평문 해시 재계산 · 대조 → MIME 검증. 검증 순서는 값 객체 생성자가 단독 소유(parse, don’t validate)
    4. 저장암호문 해시를 주소로 쓰는 content-addressed 저장, 임시 파일 → rename 원자적 쓰기. 중복 최종 방어선 = DB 부분 유니크 인덱스
    5. 아카이브BackgroundTasks 대신 응답 경로 밖 매시 배치(배포 · 재시작에도 유실 없음). 원격 보관소 전송 → 재읽기 해시 검증 → 로컬 삭제, 다운로드 시 자동 회수
    해시대상역할
    checksum평문 sha256파일의 identity. 클라이언트가 선언해도 서버가 복호 후 다시 계산해 대조. 중복 업로드 판정 키
    blob sha256암호문 sha256저장 주소이자 아카이브 무결성 검증 키

    문제 05상태 해석

    상태 snapshot 부재로 화면과 알림의 계획 판정이 갈린 문제

    • 등록 직후 계획: 저장된 상태 snapshot 없음
    • 화면 조회는 이벤트 · 초안에서 상태 복원, 전원 변경 알림은 빈 snapshot을 그대로 판정해 제외
    운영일 = KST 09:00 ~ 익일 09:00 (달력 날짜 아님)
    08:30–09:30

    작업 시작 < 운영일 종료 AND 작업 종료 > 운영일 시작

    판단

    상태 해석 경로 2개
    NULL 처리 한 줄 대신 공통 상태 재료 + 도메인 조립기로 통합
    용도별 대상 차이
    엔진은 공유, 대상 집합은 분리. 화면 = 완료 · 취소 표시, 알림 = 제외, 급전 안전 판정 = 화면에 없는 전날 작업 포함

    결과 화면과 알림이 같은 상태 재료로 판정

    문제 06아키텍처 규칙

    에이전트 구현 비중 증가로 리뷰만으로 아키텍처 규칙을 지킬 수 없게 된 문제

    • 구현의 상당 부분을 코딩 에이전트에 위임
    • “컨벤션을 아는 사람이 리뷰로 잡는다”는 전제가 성립하지 않음
    구조 테스트 · 배포 게이트
    $ pytest src/tests -q

    판단

    규칙을 둘 곳
    문서 · 리뷰 대신 AST 분석 테스트. 배포 게이트에서 차단
    • 컨트롤러 월권 금지DTO 정의, commit · rollback · flush 호출
    • import 방향utils → 도메인 금지, models ↛ pydantic, dtos ↛ sqlalchemy
    • 순수 도메인 봉인판정 모듈의 ORM · DB · HTTP import 금지
    • 은퇴한 계약 재사용 금지레거시 식별자를 코드 · DTO 양쪽에서 거부, 은퇴한 read-model 접근 시 테스트 실패
    • 판정 지식 복제 탐지판정은 진실표 단일 소유, 서비스에서 재선언하면 AST가 탐지(2벌째부터 개입)

    결과 사람 · 에이전트 구분 없이, 규칙 위반은 파이프라인에서 중단(리뷰까지 가지 않음)