ENGINEERING

규칙은 도구로 강제하고, 문제는 원인까지 기록합니다.

큰 시스템은 좋은 의도로 유지되지 않습니다. 아키텍처 규칙은 린터로 검사하고, 설정 드리프트는 push에서 거부하고, 장애마다 "확인된 사실"과 "가설"을 구분해 기록합니다.

DISCIPLINE

지키는 방식 네 가지

01

규칙은 린터가 판정합니다.

101개 아키텍처 규칙을 자체 린터가 정적 검사합니다. 점수를 "준수 인증"으로 쓰지 않습니다.

  • Custom architecture linter
  • 101 rules
  • FAIL / NEEDS_REVIEW / SOURCE_CONFLICT
  • SARIF · JSON output
  • CI gate
02

표준은 실제 코드에 고정합니다.

기준 저장소 16개의 커밋 해시와 발췌를 매니페스트로 관리하고, 문서와 코드의 충돌 19건을 근거와 함께 보존합니다.

  • Reference manifest · pinned SHA
  • Source-conflict registry
  • ADR-gated exceptions
  • Starter kit
03

설정은 생성하고, 드리프트는 거부합니다.

클러스터 설정 38개를 SSOT에서 생성합니다. 손편집으로 어긋나면 push가 거부됩니다. 거부되는지는 실측했습니다.

  • SSOT env matrix
  • Generated-file marker
  • pre-push hook = CI gate
  • Drift injection test
04

health 200은 업무 가용성이 아닙니다.

문제해결 기록 38건은 현상, 확인된 원인, 조치, 검증, 남은 범위를 분리해 적고, 검증하지 않은 범위를 성공으로 쓰지 않습니다.

  • Problem-resolution ledger
  • Fact vs hypothesis
  • Restart count · kernel OOM · real screen
  • Scope of verification stated

CASES

풀어낸 문제 여섯 가지

문제는 쉬운 말로, 해결은 기술 용어로 적었습니다. 현상 · 원인 · 해결 · 검증 전체 기록은 펼쳐서 볼 수 있습니다.

CASE 01

작은 오류 하나가 멀쩡한 조회까지 멈추게 했습니다.

문제한 가지 요청이 거절당했을 뿐인데, 같은 서버로 가는 정상 요청들까지 한꺼번에 실패하기 시작했습니다.

해결공통 서비스 클라이언트의 서킷브레이커 실패 분류(record predicate)를 수정해 4xx는 호출자 오류로 제외하고, 5xx와 timeout만 회로 개방 집계에 포함했습니다.

  • CircuitBreaker failure predicate
  • Failure classification 4xx vs 5xx
  • Shared HTTP client
  • Single-fix propagation
현상 · 원인 · 해결 · 검증 전체 보기
현상
전자도서관에서 도서 · 가격 · 소장 조회가 간헐적으로 500을 냈습니다.
원인
특정 M2M 토큰 발급 요청이 invalid_scope로 400을 받았는데, 구버전 공통 클라이언트는 4xx를 장애로 집계해 같은 인증 서버를 향한 서킷브레이커를 열었습니다. 무관한 정상 호출까지 함께 차단됐습니다.
해결
공통 클라이언트의 실패 분류를 수정해 4xx는 호출자 오류로, 5xx와 timeout만 회로 개방 대상으로 구분했습니다. 모든 서비스가 같은 클라이언트와 Spring Boot starter를 쓰므로 한 번의 수정으로 전체에 반영됐습니다.
검증
토큰 발급 실패 12회가 교대로 발생하는 동안 카탈로그 조회 12/12 성공을 확인했습니다. 무인증 401, 잘못된 audience 401, 부족한 scope 403, 정상 200을 각각 실측했습니다.
CASE 02

고쳐 둔 설정이 다음 날 조용히 사라졌습니다.

문제설정 파일을 손으로 고쳤는데, 자동 생성이 돌 때마다 그 수정이 흔적 없이 지워졌고 같은 사고가 세 번 반복됐습니다.

해결SSOT 환경 매트릭스에서 클러스터 설정을 생성하고 생성물에 표식을 박았습니다. SSOT와 생성물을 한 줄씩 대조하는 게이트를 pre-push 훅과 CI가 같은 스크립트로 실행해, 드리프트가 있으면 push를 거부합니다.

  • SSOT env matrix
  • Generated-file marker
  • pre-push hook · CI gate
  • Drift injection test
  • 38 generated configs
현상 · 원인 · 해결 · 검증 전체 보기
현상
클러스터 설정에 넣은 수정이 다음 재생성에서 사라졌고, 같은 사고가 세 번 반복됐습니다.
원인
생성된 파일과 손편집 파일의 겉모양이 같아 구분할 수 없었고, 기존 검증기는 재생성물과 Deployment 참조 관계만 보고 커밋본과 대조하지 않았습니다. 표식을 붙인 날 값을 바꿔 검증기를 돌렸더니 그대로 통과했습니다.
해결
생성물 머리에 표식을 박고, SSOT 매트릭스(base + cluster overlay)와 생성물을 한 줄씩 대조하는 테스트를 만들어 pre-push 훅과 CI가 같은 게이트 스크립트를 부르게 했습니다. 설치 스크립트는 훅 배선 여부를 확인해 경고합니다. 키를 아예 만들지 않는 '부재' 규약으로 로컬 값이 클러스터에 새어 들어가는 것도 막았습니다.
검증
드리프트를 주입해 push가 거부되는 것을, 원복 후 통과하는 것을 실측했습니다. 생성 대상 38개 전부 일치를 확인했습니다.
CASE 03

"다시 해도 되는 실패"와 "끝난 실패"가 구분되지 않았습니다.

문제발주가 실패했을 때 재시도해야 할 것과 하면 안 되는 것이 같은 상태로 저장되어, 운영자가 잘못 누를 수 있었습니다.

해결상태 기계를 재설계해 자동 집계는 FAILED, 운영자의 명시적 확정만 DELIVERY_FAILED(terminal)로 분리했습니다. 확정 액션은 멱등이고, 종단 상태에서의 RETRY / APPROVE는 409로 거절하며, 늦게 도착한 패키징 결과가 종단 상태를 되돌리지 못하게 가드했습니다.

  • State machine · terminal state
  • Idempotent action endpoint
  • 409 on terminal state
  • Late-event guard
  • Existing scope reuse
현상 · 원인 · 해결 · 검증 전체 보기
현상
패키징이 실패한 발주를 재시도할 수 있어야 하는데, 어떤 실패는 재시도하면 안 되는 상태였고 둘이 같은 값으로 저장됐습니다.
원인
도메인이 실패 집계 때 곧바로 최종 실패 상태를 저장하고 그 상태에서 재시도를 허용했습니다. 중계 서비스도 이를 한 값으로 변환해 구분이 사라졌습니다.
해결
자동 집계는 FAILED, 운영자의 명시적 확정 액션만 DELIVERY_FAILED로 분리했습니다. 확정 뒤 늦게 도착한 패키징 결과는 주문을 되살리지 않고, 확정 요청을 반복해도 상태 · 시각 · 이력이 변하지 않습니다. 새 권한 scope를 만들지 않고 기존 승인 권한을 재사용했으며, 스키마 변경 없이 적용했습니다.
검증
도메인 · API · 게이트웨이 · 중계 서비스 계층별 테스트 35건과 실제 DB를 거치는 HTTP → UseCase → 저장소 통합 테스트 1건을 통과했습니다. 전체 경로에서 확정 후 RETRY / APPROVE가 409로 거절되는 것을 확인했습니다.
CASE 04

배포는 했는데 바뀐 게 하나도 없었습니다.

문제오늘 날짜가 붙은 새 이미지를 배포했는데 내용은 지난번 그대로였습니다.

해결이미지 태그를 날짜와 커밋 해시로 고정하고 빌드에 커밋 지정을 필수화했습니다. 배포 스크립트에 plan(배포본과 main 사이 DB 변경 요구 집계), dry-run, rollback, history를 한 체계로 묶고, 클러스터 작업은 스크립트로만 하도록 정했습니다.

  • Immutable tag · git sha
  • Commit pin required
  • Plan · dry-run
  • Rollback · history
  • Script-only cluster ops
현상 · 원인 · 해결 · 검증 전체 보기
현상
새로 구운 이미지로 배포했는데 변경이 반영되지 않았습니다.
원인
커밋을 지정하지 않으면 배포용 저장소의 현재 HEAD, 곧 지난 배포 커밋으로 빌드됐습니다. 태그의 날짜는 오늘이고 sha는 지난번이었습니다.
해결
빌드 스크립트에 커밋 지정을 필수로 하고, 배포 스크립트에 plan, dry-run, rollback, history를 한 체계로 묶었습니다. 대상 워크로드 목록을 스크립트가 정본으로 들고 있어, 목록에 없는 워크로드나 자동 동기화 앱에는 배포를 막습니다.
검증
이후 배포에서 태그의 sha와 의도한 커밋이 일치함을 상태 조회로 확인하는 절차가 표준이 됐습니다.
CASE 05

모든 점검은 정상인데 화면만 가끔 실패했습니다.

문제서버는 모두 켜져 있고 상태 점검도 통과하는데, 목록 화면이 가끔 열리지 않았습니다.

해결HTTP health 대신 컨테이너 재시작 횟수와 커널 OOM 기록으로 원인을 검증 환경 VM의 메모리 부족으로 특정했습니다. 메모리를 증설하고, 응답이 느린 단 한 경로에만 timeout을 조정했으며, 원인이 아니었던 임시 변경은 되돌렸습니다.

  • Kernel OOM killer
  • Restart count
  • VM memory vs container limit
  • Health ≠ availability
  • Scoped timeout change
현상 · 원인 · 해결 · 검증 전체 보기
현상
검증 환경에서 발주 목록 조회가 간헐적으로 실패했고, 직원 권한 확인까지 실패하기도 했습니다. 모든 컨테이너는 Up이었고 health는 200이었습니다.
원인
VM 메모리 부족으로 커널이 특정 컨테이너를 반복 종료하고 있었습니다. 컨테이너 재시작 횟수와 커널 OOM 기록으로 확인했습니다. 컨테이너 메모리 제한과 애플리케이션 힙 설정은 정상 범위였고, 개별 힙 문제가 아니라 VM 전체 부족이었습니다. 처음 의심한 웹 timeout은 원인이 아니었고, 시도했던 변경은 정확히 되돌렸습니다.
해결
VM 메모리를 증설하고, 응답이 느린 카탈로그 한 경로에만 timeout을 조정했습니다. 다른 API와 인증 경로는 건드리지 않았습니다.
검증
가용 메모리 회복, OOM 재발 없음, 실제 직원 화면에서 카탈로그 23권과 발주 목록 표시를 확인했습니다. 별개 문제인 가격 표시 불일치까지 정상이라고 주장하지 않았습니다.
CASE 06

여러 서버를 거치는 변환을 처음부터 복잡하게 돌릴 수는 없었습니다.

문제파일 하나가 다섯 단계를 거쳐야 하는데, 처음부터 큐 기반 분산 처리로 가면 중간 실패를 추적하고 복구하는 일이 먼저 복잡해질 상황이었습니다.

해결상위 흐름은 문서 1건 = 작업 1건으로 고정하고 fan-out / fan-in을 설계하되, 1단계는 DB 상태 기반 오케스트레이터와 단일 워커 직렬 실행으로 운영했습니다. 작업은 claim / lease / attempt 모델로 at-least-once 재시도와 멱등성을 보장하고, 메시지 큐는 병목 구간에만 점진 도입합니다.

  • Fan-out / Fan-in
  • DB-state orchestrator
  • Single-worker first
  • claim / lease / attempt
  • Outbox → queue (selective)
  • SHA-256 manifest
현상 · 원인 · 해결 · 검증 전체 보기
현상
원본 한 건이 정규화, 문서 DRM, 미디어 DRM, 이미지 암호화, 발행을 거쳐야 하고 각 단계는 서로 다른 서버가 맡습니다.
원인
하위 처리기는 독립적으로 fan-out할 수 있지만, 최종 메타데이터는 모든 결과가 모여야만 완성됩니다. 발행 전 "로컬 진실"과 발행 후 "확정 좌표(stable locator)"를 구분하지 않으면 중간 실패를 복구할 수 없습니다.
해결
상위 흐름은 문서 처리 작업 하나로 추적하고, 1단계는 DB 상태 기반 오케스트레이터와 단일 워커 직렬 실행으로 안정화했습니다. 작업 결과는 claim token · lease 만료 · attempt 버전을 검증한 뒤 저장하며, 같은 UID의 같은 payload는 멱등 응답, 다른 payload는 409 충돌로 처리합니다. 발행은 outbox를 거쳐 메시지 큐로 전달하고 배치 소비자가 처리합니다.
검증
실제 PDF 한 건으로 정규화 → 암호화 파일 생성 → 객체 저장소 발행까지 통과했고, 암호화 파일별 manifest 크기와 SHA-256 일치를 확인했습니다. 운영 LCP 키 등록과 뷰어 열람 검증은 별도 범위로 명시했습니다.

이런 방식으로 일합니다.

여기 적힌 숫자와 사례는 모두 하늘랩 내부 기록에서 가져왔습니다. 고객사 비공개 정보, 제품 버전, 내부 식별자는 제외했습니다.

프로젝트 문의