2P by GN⁺ | ★ favorite | 댓글 1개
  • Kubernetes의 pv_controller.goPV/PVC 바인딩을 동기화하는 컨트롤러로, 파일 상단부터 “단순화하지 말고 space shuttle style을 유지하라”고 못박음
  • 이 스타일은 모든 if에 대응하는 else를 두고 명백해 보이는 조건도 주석으로 남겨, 검토된 분기와 의도를 코드 안에 드러내려는 방식임
  • 설계의 중심은 pvc.Spec.VolumeNamepv.Spec.ClaimRef로 이어지는 양방향 포인터이며, 트랜잭션 없는 환경에서 경쟁·삭제·사용자 수정·동시 바인딩을 복구 가능하게 다룸
  • 컨트롤러는 PV/PVC 변경 감시, 내부 캐시, 단일 워커 큐, 이벤트 기록, 동적 프로비저닝, CSI 마이그레이션 인터페이스를 엮어 바인딩 상태 전이를 관리함
  • 장황한 분기와 주석은 동작의 업무 지식과 실패 복구 맥락을 보존하기 위한 장치라서, 향후 변경도 같은 스타일을 따라야 함

pv_controller.go의 역할과 작성 원칙

  • pv_controller.go는 Kubernetes persistentvolume 패키지의 PersistentVolumeController 구현 파일임
  • 이 컨트롤러는 PersistentVolumeClaimPersistentVolume의 상태를 맞춤
    • PersistentVolume 변경을 감시하는 캐시 컨트롤러
    • PersistentVolumeClaim 변경을 감시하는 캐시 컨트롤러
    • 두 객체의 변경 이벤트를 바탕으로 PV/PVC 상태 동기화
  • 파일 상단 주석은 이 코드를 단순화하지 말라고 반복해서 경고함
    • 스타일 이름은 space shuttle style
    • 모든 if 문에 대응하는 else를 두는 방식임
    • 단순 에러 체크를 제외하고 모든 분기를 명시하려는 목적임
    • 명백해 보이는 동작도 주석으로 적어 유지보수자가 바인딩 복잡성을 추적할 수 있게 함

space shuttle style을 유지하는 이유

  • 이 컨트롤러는 원래 세 개의 컨트롤러에 나뉘어 있던 작업을 하나로 합친 결과물임
  • PV 서브시스템을 단순화하는 과정에서, 모든 조건을 코드에서 명시적으로 다루는 방식이 필요해짐
  • 그 결과 코드가 장황하고 주석과 분기가 많아 보일 수 있음
  • 이 장황함은 바인딩 동작의 업무 지식과 맥락을 코드에 남기기 위한 장치임
  • 이 파일을 바꿀 때는 space shuttle style을 보존하고, 필요한 경우 같은 방식으로 분기와 주석을 추가해야 함

핵심 설계: PV와 PVC의 양방향 포인터

  • 설계의 중심에는 PV와 PVC 사이의 양방향 포인터가 있음
    • PVC 쪽 포인터: pvc.Spec.VolumeName
    • PV 쪽 포인터: pv.Spec.ClaimRef
  • 이 양방향성은 트랜잭션이 없는 시스템에서 다루기 어렵지만, 장애 상황에서도 정상 동작을 보장하기 위해 필요함
  • rogue HA controller instance가 경쟁 상태를 만들면, 구분할 수 없는 여러 바인딩이 생겨 데이터 손실 가능성이 생길 수 있음
  • 컨트롤러는 기본적으로 active-passive 고가용성 모드에서 동작하도록 설계됨
    • 객체 전이는 active-active HA에서도 동작할 수 있게 설계됨
    • 다만 두 active 컨트롤러가 자주 충돌하면 성능이 낮아질 수 있음

바인딩 방식과 복구 조건

  • 컨트롤러는 양방향 pre-bound 객체를 지원함
    • 특정 PV를 원하는 PVC
    • 특정 PVC를 위해 예약된 PV
  • 바인딩은 두 단계로 진행됨
    • 먼저 PV.Spec.ClaimRef를 수정함
    • 다음으로 PVC.Spec.VolumeName을 수정함
  • 이 과정의 어느 시점에서도 PV나 PVC가 사용자 또는 다른 컨트롤러에 의해 수정·삭제될 수 있음
  • 두 개 이상의 컨트롤러가 서로 다른 볼륨과 클레임을 동시에 바인딩하려 할 수도 있음
  • 컨트롤러는 이런 충돌 상황을 복구할 수 있어야 함

컨트롤러 구조체의 주요 구성

  • PersistentVolumeController는 PV/PVC 동기화에 필요한 lister, informer sync 함수, Kubernetes 클라이언트, 이벤트 기록기, 볼륨 플러그인 관리자 등을 가짐
  • 마지막으로 알려진 PV/PVC 버전은 내부 캐시에 저장됨
    • volumes persistentVolumeOrderedIndex
    • claims cache.Store
  • 이 캐시는 API 서버에 저장한 최신 버전과 etcd 이벤트로 들어온 버전을 함께 반영함
  • 바인딩 하나는 대략 네 개의 이벤트를 만들 수 있음
    • volume.Spec 업데이트
    • volume.Status 업데이트
    • claim.Spec 업데이트
    • claim.Status 업데이트
  • 내부 캐시가 없으면 informer가 오래된 상태를 들고 있을 때 이미 완료된 바인딩을 다시 고치려 할 수 있음
  • 이때 API 서버에 다시 쓰기를 시도하면 이미 저장된 객체와 버전 충돌이 발생할 수 있음

워크 큐와 동시성 제약

  • 컨트롤러는 claim과 volume 처리를 위한 별도 workqueue를 가짐
    • claimQueue
    • volumeQueue
  • 각 큐는 정확히 하나의 worker thread만 가져야 함
  • 특히 syncClaim()은 재진입 가능하지 않음
  • 두 개의 syncClaim()이 동시에 실행되면 다음 문제가 생길 수 있음
    • 서로 다른 두 claim을 같은 volume에 바인딩
    • 하나의 claim을 두 volume에 바인딩
  • 컨트롤러는 API 서버의 버전 에러와 자체 검사로 이런 상황을 복구할 수 있지만, multi-worker 방식은 전체 속도를 낮출 수 있음

syncClaim: PVC 동기화의 진입점

  • syncClaim은 claim이 생성, 업데이트, 주기 동기화될 때 호출되는 주요 메서드임
  • 이 메서드는 이벤트 종류를 구분하지 않음
  • 먼저 PVC에 올바른 migration annotation을 설정하고, 필요하면 API 서버에 업데이트함
  • 이후 AnnBindCompleted annotation 유무에 따라 분기함
    • annotation이 없으면 syncUnboundClaim
    • annotation이 있으면 syncBoundClaim
  • 실제 처리는 가독성을 위해 unbound claim과 bound claim 메서드로 나뉨

checkVolumeSatisfyClaim: PV 요구사항 검사

  • checkVolumeSatisfyClaim은 요청된 PV가 PVC 요구사항을 만족하는지 확인함
  • 검사 조건은 코드에 명시적으로 나열됨
    • PV에 DeletionTimestamp가 있으면 오류
    • PV 용량이 PVC 요청 용량보다 작으면 오류
    • storageClassName이 다르면 오류
    • VolumeAttributesClass feature gate가 켜져 있으면 VolumeAttributesClassName 일치 여부를 검사
    • feature gate가 꺼져 있는데 claim이나 volume에 VolumeAttributesClassName이 있으면 오류
    • volumeMode가 호환되지 않으면 오류
    • access mode가 호환되지 않으면 오류
  • 모든 조건을 통과하면 nil을 반환함

지연 바인딩 PVC의 이벤트 처리

  • emitEventForUnboundDelayBindingClaim은 지연 바인딩 모드의 미바인딩 claim에 정보를 주는 이벤트를 생성함
  • 기본 reason은 WaitForFirstConsumer
  • 기본 메시지는 첫 consumer가 생성되기 전까지 바인딩을 기다린다는 내용임
  • 해당 PVC를 참조하는 아직 스케줄되지 않은 Pod가 있으면 reason이 WaitForPodScheduled로 바뀜
    • Pod가 여러 개면 모든 Pod 이름을 메시지에 포함함
    • volume scheduling에서는 하나의 Pod만 고려되지만 어떤 Pod가 사용되는지 알 수 없어 모든 Pod를 포함함

syncUnboundClaim: 아직 바인딩되지 않은 PVC 처리

  • claim.Spec.VolumeName이 비어 있으면 사용자가 특정 PV를 요구하지 않은 상태임
  • 이 경우 컨트롤러는 claim의 지연 바인딩 모드를 확인하고, findBestMatchForClaim으로 가장 적합한 PV를 찾음
  • 적합한 PV가 없으면 다음 순서로 처리함
    • 기본 StorageClass를 할당할 수 있으면 PVC를 업데이트하고 동기화를 종료함
    • 지연 바인딩이고 아직 provisioning 상태가 아니면 대기 이벤트를 생성함
    • claim에 StorageClass가 있으면 provisionClaim으로 동적 프로비저닝을 시도함
    • 그 외에는 사용 가능한 PV도 없고 StorageClass도 없다는 FailedBinding 이벤트를 기록함
  • 적합한 PV가 있으면 bind를 호출해 PV와 PVC를 바인딩함
    • 성공 시 provision + binding 작업의 metric을 기록하고 timestamp 캐시를 정리함
    • 저장 중 오류가 나면 이후 syncClaim이 바인딩을 마무리함

특정 PV를 요구하는 PVC 처리

  • claim.Spec.VolumeName이 비어 있지 않으면 사용자가 특정 PV를 요청한 상태임
  • 요청한 PV가 캐시에 없으면 PVC 상태를 Pending으로 업데이트하고 나중에 재시도함
  • 요청한 PV가 있고 volume.Spec.ClaimRef가 없으면, PV가 아직 claim되지 않은 상태임
    • checkVolumeSatisfyClaim으로 요구사항을 검사함
    • 요구사항을 만족하지 못하면 VolumeMismatch 이벤트를 기록하고 PVC를 Pending으로 유지함
    • 요구사항을 만족하면 bind를 호출함
  • 요청한 PV가 이미 이 PVC에 claim되어 있으면 bind를 호출해 바인딩을 마무리함
  • 요청한 PV가 다른 claim에 묶여 있으면 다음처럼 처리함
    • claim이 controller에 의해 바인딩된 annotation을 갖고 있지 않으면 FailedBinding 이벤트를 기록하고 Pending으로 둠
    • controller가 바인딩한 것으로 보이는데 다른 claim에 묶여 있으면 “should never happen” 상태로 오류를 반환함

syncBoundClaim: 이미 바인딩된 PVC 처리

  • syncBoundClaimAnnBindCompleted annotation이 있는 PVC를 처리함
  • 이미 바인딩된 claim인데 claim.Spec.VolumeName이 비어 있으면 claim 상태를 ClaimLost로 변경함
    • 이벤트 메시지는 bound claim이 PV 참조를 잃었고 volume의 데이터가 손실됐다는 내용임
  • claim이 가리키는 PV가 존재하지 않으면 역시 ClaimLost로 변경함
    • 이벤트 메시지는 bound claim이 PersistentVolume을 잃었고 데이터가 손실됐다는 내용임
  • PV가 존재하지만 volume.Spec.ClaimRef가 없으면 volume이 unbound 상태가 된 것으로 보고 다시 bind를 호출함
  • PV의 ClaimRef.UID가 claim의 UID와 같으면 정상 바인딩 상태로 보고 bind를 호출함
    • 대부분의 경우 아무 작업도 하지 않는 호출임
  • PV가 다른 claimant를 가리키면 claim phase를 terminal 상태인 Lost로 설정함

syncVolume: PV 동기화의 진입점

  • syncVolume은 volume 생성, 업데이트, 주기 동기화 시 호출되는 주요 메서드임
  • 이벤트 종류는 구분하지 않음
  • 먼저 PV에 올바른 migration annotation과 finalizer를 설정하고 필요하면 API 서버에 업데이트함
  • volume.Spec.ClaimRef가 없으면 사용되지 않는 volume으로 보고 phase를 Available로 설정함
  • ClaimRef가 있지만 UID가 비어 있으면 특정 PVC에 예약된 PV로 보고 phase를 Available로 설정함
    • 해당 PVC가 아직 이 PV에 바인딩되지 않은 상태이며, PVC sync가 처리함

claim을 찾지 못한 PV 처리

  • PV가 claim에 바인딩되어 있으면 컨트롤러는 ClaimRef의 namespace/name으로 PVC를 찾음
  • 캐시에서 PVC를 찾지 못한 경우, 특정 조건에서 추가 확인을 수행함
    • informer cache에서 다시 확인
    • API 서버에서 다시 확인
  • 외부 PV provisioner나 외부 PV binder가 만든 PV에서는 부하가 큰 상황에서 PVC가 아직 로컬 캐시에 동기화되지 않았을 수 있음
  • PVC를 잘못 reclaim하지 않기 위해 이중 확인을 수행함
  • claim이 없다고 판단되면 volume phase를 Released로 바꾸고 reclaimVolume을 실행함
    • 기존 phase가 Failed이면 덮어쓰지 않음
    • reclaim policy가 Retain이면 존재하지 않는 claim을 참조하는 PV라는 로그를 남김

PV와 PVC 연결이 어긋난 경우

  • claim이 존재하지만 claim.Spec.VolumeName이 비어 있으면 PVC가 아직 PV 이름을 갖지 않은 상태임
  • volumeMode가 맞지 않으면 PV와 PVC 양쪽에 VolumeMismatch 이벤트를 기록하고 syncClaim을 건너뜀
  • mismatch가 아니면 claim을 claimQueue에 추가해 syncClaim이 곧 호출되도록 함
    • 이 방식은 provisioned volume의 바인딩을 빠르게 함
  • claim의 Spec.VolumeName이 현재 volume 이름과 같으면 정상 바인딩으로 보고 volume phase를 Bound로 업데이트함
  • claim이 다른 volume에 바인딩되어 있으면 상황에 따라 처리함
    • 동적으로 provisioned된 volume이고 reclaim policy가 Delete이면 Released로 표시하고 reclaimVolume을 실행함
    • controller가 바인딩한 volume이면 unbindVolume로 정리함
    • 사용자가 만든 포인터라면 그대로 두되 unbindVolume을 호출해 phase를 업데이트하고 ClaimRef.UID를 지움

상태 업데이트와 이벤트 발행

  • updateClaimStatus는 PVC status를 API 서버에 저장함
    • phase 변경
    • volume이 없을 때 AccessModes, Capacity, CurrentVolumeAttributesClassName 초기화
    • volume이 있을 때 access mode, capacity, current volume attributes class 이름 업데이트
  • claim이 Bound가 되는 순간에만 capacity를 갱신하는 조건이 있음
    • PVC filesystem size와 PV block device size의 차이가 의도적일 수 있어 이미 bound인 claim의 capacity를 덮어쓰지 않음
  • VolumeAttributesClass feature gate가 켜져 있으면, pending에서 bound로 바뀌는 동안 CurrentVolumeAttributesClassName을 설정함
    • 이후에는 resizer나 admin override가 다뤄야 하며, controller가 계속 설정하면 race condition 가능성이 있음
  • updateClaimStatusWithEventupdateVolumePhaseWithEvent는 실제 status/phase가 바뀐 경우에만 이벤트를 발행함

기본 StorageClass 할당

  • assignDefaultStorageClass는 claim에 storage class가 없을 때 기본 StorageClass를 찾아 할당함
  • 이미 storage class가 있는 claim은 무시함
  • 기본 class가 없으면 업데이트하지 않고 false를 반환함
  • 기본 class가 있으면 claim.Spec.StorageClassName에 class 이름을 설정하고 API 서버에 업데이트함

파일 범위와 명시적 한계

  • GitHub 페이지에 표시된 파일 메타데이터 기준으로 pv_controller.go2038줄, 1864 LOC, 91 KB
  • 제공된 본문에는 파일 앞부분부터 bindVolumeToClaim 함수 시작부까지만 포함되어 있고, 나머지는 raw view 링크로 이어짐
  • 따라서 이 요약은 제공된 코드 본문에 드러난 컨트롤러 구조, 설계 주석, 주요 동기화 분기, 상태 업데이트 로직에 한정됨

댓글과 토론

Hacker News 의견들
  • 이 파일의 코드가 정말 평범한 Go 코드처럼 느껴지는 게 이상한 건지 모르겠음. Go라서 장황하고, 깊은 추상화에 의존하지 않아서 더 길어 보이지만, 코드 자체로는 전형적으로 보임
    추상화는 양날의 검이라서 이런 방식도 괜찮고, 머리말이 없었다면 작성 스타일을 두 번 생각하지 않았을 것 같음. 아마 시스템 소프트웨어보다 기업용 소프트웨어 경험이 많아서 생기는 차이일 수 있음. Kubernetes에 꾸준히 기여하는 사람에게는 이 주석들이 불필요해 보일 수 있지만, 기업 환경에서 먼 미래의 독자가 맥락 없이 읽을 코드라면 이 복잡도에서는 오히려 주석을 더 달았을 것 같음

    • 예전에는 이런 코드가 정상적으로 느껴졌는데, 지난 10년쯤 사이에 많은 사람이 명시성보다 짧음을 더 높게 치는 것 같음
      특히 이런 중요한 코드에서는 명시성을 훨씬 선호함. 여러 조건을 합치고 업무 맥락과 의미를 설명하는 주석을 생략한 코드 때문에, 현재 동작이 의도인지 우연인지 판단할 수 없었던 적이 커리어에서 여러 번 있었음. 이런 방식은 변경에 강한 코드가 아니라 변경을 막는 코드가 되기 쉽고, 적어도 작성자 아닌 사람이 고치기 어렵게 만듦. 불필요한 체스터턴의 울타리를 만드는 건 유지보수성에 반함

    • 이 주석은 아마 코드를 단순화하려다 실패한 뒤, 미래 유지보수자에게 같은 시도를 하기 전에 다시 생각하라는 경고로 추가된 것 같음
      경고를 추가한 커밋은 "Add note about space-shuttle code style"[1]이고, 그 직전 커밋은 "Revert controller/volume: simplify sync logic in syncUnboundClaim"[2]였음

      [1] https://github.com/kubernetes/kubernetes/commit/de4d193d45f6...

      [2] https://github.com/kubernetes/kubernetes/commit/8a1baa4d64ca...

    • 나도 비슷하게 생각하다가, 크게 중첩된 if 문들을 보고 생각이 바뀌었음. 저 부분은 확실히 조기 반환 분기를 만들었을 것 같음
      "동작하게 만들고, 빠르게 만들고, 예쁘게 만들라"에서 첫 단계만 끝내고 "예쁘게 만들기"를 안 한 느낌임. 까다로운 상태 상호작용을 풀 때 이런 식으로 못생기고 주석 많은 코드를 쓴 적은 있지만, 보통 리뷰 전에 조금 정리함. 어쩌면 그냥 파일 맨 위에 "이 코드를 단순화하려 하지 말 것"이라는 큰 배너를 붙이는 게 나을지도 모르겠음. 그래도 확실히 아주 나쁘진 않음

    • 이상할 수는 있어도 혼자는 아님. 나도 이 코드는 완전히 정상적으로 보임. 시스템 신뢰성에 중요하다고 느끼는 구성요소에는 이런 식의 코드와 주석을 쓴 적이 있음
      "주석 없는 코드" 유행에는 동의한 적이 없고, 몇 달이나 몇 년 뒤에 다시 돌아왔을 때 내가 쓴 주석이 미래의 나에게 너무 자주 귀중했음. 이 정도 복잡도의 구성요소에 박힌 논리를 탄탄한 주석 없이 다시 맞춰 보는 건 상상하기 어렵다

    • 특히 모든 if에 대응하는 else 주석이 있다는 설명은 안정적으로 참은 아닌 것 같음. 대응 없는 if 중 상당수는 단순한 if (err != nil) { 검사나 다른 조기 반환이지만, 그런 것들을 제외해도 대응 없는 if가 있어 보임
      다만 기업용 소프트웨어 경험상 추가 주석이 꼭 많은 것도 아니었음. 코드베이스에는 // end if 주석이 역병처럼 있었지만, 실제 설명 주석은 드물었음

  • Space Shuttle 소프트웨어 품질 관련 글: https://archive.is/HX7n4
    발췌하면, 이 소프트웨어가 놀라운 이유는 얼마나 많은 일을 하는지가 아니라 얼마나 잘 동작하는지에 있음. 절대 크래시하지 않고, 재부팅이 필요 없고, 버그가 없으며, 인간이 달성한 수준에서 완벽에 가깝다고 함. 마지막 세 버전은 각각 42만 줄이었는데 오류가 하나씩뿐이었고, 마지막 11개 버전 전체의 오류는 17개였음. 같은 복잡도의 상용 프로그램이라면 오류가 5,000개쯤 있었을 것이라고 함

    • "마지막 세 버전이 각각 42만 줄이고 오류가 하나씩 있었다"는 게 정확히 무슨 뜻인지 궁금함. 세 버전 각각에 버그가 정확히 하나 있었다면, 앞의 두 수정이 작동하지 않았거나 새 버그를 넣었다는 말을 이상하게 표현한 것 아닌가?
    • NASA 방식과 SpaceX 방식이 어떻게 다른지 비교해 보면 흥미로울 것 같음. SpaceX도 유인 임무를 수행했으니 요구사항은 꽤 비슷해 보임
    • 5000 / 17 ≈ 295임. 같은 복잡도의 상용 프로그램은 인시가 295배 적게 들었다고 가정해도 공정한가?
    • Space Shuttle 개발 방법론의 문제는 엄청나게 비싸고 느리면서도 100% 무버그는 아니라는 데 있음
      너무 비싸고 느려서, 현대적인 증명 보조기(proof assistant)로 소프트웨어의 정확성을 증명하는 편이 훨씬 싸고 빠르며 실제로 더 안전할 것임. seL4, CompCert 같은 프로젝트가 어떻게 해야 하는지 보여줌
    • 내가 가장 좋아하는 글 중 하나임. 1996년 인터넷 글이 아직 접근 가능하다는 게 놀라움
  • // KEEP THE SPACE SHUTTLE FLYING.이라는 의도는 이해하지만, 안전 기록이 좋지 않아 더 이상 운용되지 않는 시스템을 주석에서 참조한다는 점은 좀 웃김
    10년쯤 뒤에도 사람들이 Space Shuttle을 좋게 기억할까?

    • Space Shuttle의 안전 문제는 대체로 하드웨어 문제였지 소프트웨어 문제가 아니었음
      1986년 Challenger 사고 보고서에 실린 Richard Feynman의 부록인 "Appendix F - Personal Observations on Reliability of Shuttle" [0]에서는 이렇게 말함

      요약하면, 컴퓨터 소프트웨어 점검 시스템과 태도는 최고 품질이다. Solid Rocket Booster나 Space Shuttle Main Engine 안전 시스템에서 특징적으로 나타나는, 기준을 낮추면서 서서히 스스로를 속이는 과정은 보이지 않는다.

      그는 항공전자 소프트웨어 품질을, Shuttle 같은 대형 복잡 정부 프로젝트도 제대로 엔지니어링될 수 있으며 그 자체로 저품질·위험이 예정된 것은 아니라는 예로 특별히 강조했음

      0: https://www.nasa.gov/history/rogersrep/v2appf.htm

    • 100회가 훨씬 넘는 성공 임무에서 사람과 장비를 우주로 올리고 다시 집으로 데려왔음. 지금도 좋게 보고 있고 앞으로도 그럴 가능성이 큼. 인간의 진보와 순효과라는 면에서는 성공이었음

    • Shuttle을 끝낸 건 나쁜 안전 기록이 아니라 비용과 미래의 안전 저하에 대한 예상이었음
      두 번의 Shuttle 사고로 다른 NASA 재난보다 더 많은 우주비행사가 사망하긴 했지만, 실제로 벌어진 일들의 난이도를 감안하면 안전 기록은 정말 놀라웠음. 코드는 아주 좋아 보임

    • Space Shuttle 상황은 단순히 안전성이 나빴다고 하기보다 더 복잡함. 임무 기준으로 보면 다른 발사체보다 기록이 좋은 편임. Shuttle은 135회 중 2회의 치명적 임무였고, 소련 시절 Soyuz는 66회 중 2회, SpaceShipTwo는 고작 12회 비행 중 1회의 치명적 임무라는 무서울 정도로 나쁜 기록임
      다만 Space Shuttle은 대부분 임무에 필요한 것보다 훨씬 큰 승무원 수용 능력을 가졌음. Apollo나 Soyuz의 3명과 달리 최대 8명까지 태울 수 있었고, 소련/Roscosmos, ESA, CNSA 임무 대부분이 완전 무인 자율 임무였다는 점을 생각하면 위험에 처할 승무원 자체가 없었음. 어쩌면 이 비유는 Kubernetes에 더 잘 맞을지도 모름. 고도로 엔지니어링되고, 강력하며, 다목적이지만 많은 주의를 요구하고, 아마 필요한 것보다 조금 더 많이 쓰이는 시스템임

    • 승객-마일당 기준이라는 가장 흔한 척도로 보면, Space Shuttle은 지금까지 만들어져 비행한 차량 중 가장 안전한 축에 듦
      어린 시절이 정확히 1980년대였던 입장에서 솔직히 묻자면, 어떻게 좋게 기억하지 않을 수 있는지 모르겠음. 너무 어려서 이 프로그램과 그 모든 임무·성과를 전부 회고적으로만 보고, 현재 민간 우주 계약업체 중심의 시대 분위기에 물든 관점만 가진 건가?

  • Richard Hipp가 SQLite 코드를 항공 표준에 맞추는 이야기도 꽤 흥미로움: https://corecursive.com/066-sqlite-with-richard-hipp/#testin...

    DO-178B. 안전 필수 항공 제품을 위한 품질 표준입니다... 테스트는 결과 바이너리 코드의 각 분기 연산이 적어도 한 번은 실행되고, 적어도 한 번은 통과되게 해야 합니다... 주 60시간씩 1년이 걸렸습니다... 엄청난 차이를 만들었습니다. 이후 8~9년 동안 사실상 버그가 없었습니다

  • 이 부분은 TypeScript 코드의 완전성 검사를 떠올리게 함. 항상 쓰려고 노력함
    https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...

    • 더 새로운 satisfies never가 이 용도에 아주 좋음. 취향상 if else 체인을 쓰는 경우에도 편리함

    • ts-pattern을 좋아할 수도 있음

      https://github.com/gvergnaud/ts-pattern

  • 완전히 사소하지 않은 if마다 명시적 else를 붙이는 경우만 놓고 보면, Kubernetes 작성자들이 if/else 블록 대신 구조적 패턴 매칭을 중심으로 설계했다면 이 코드가 얼마나 단순해졌을지 궁금함
    구조적 패턴 매칭을 지원하는 여러 주류 언어에는 매칭이 완전한지 컴파일 시점에 확인하는 도구가 있고, 그것만으로도 코드의 정보 밀도를 높이면서 관용적인 해법이 될 수 있음

  • 2018년 논의: https://news.ycombinator.com/item?id=18772873

  • 코드를 대충 훑어보기만 했지만, 솔직히 그렇게 나빠 보이지 않음. 다르게 했을 부분은 있겠지만 훨씬 더 심한 코드를 많이 봤음
    적어도 이 코드는 하나의 규칙을 따르고 있고, 모든 것이 생각을 거쳐 작성됐으며 이 혼돈에도 나름의 방법이 있다는 인상을 줌. 여러 번 봐 온 스타일 뒤섞임, 게으른 코딩, 비논리적 구조 같은 전형적인 잡탕보다 이런 코드를 언제든 택하겠음

  • 왜 "안전" 관행을 새로 만들면서 문서화된 소프트웨어 공학 모범 사례는 무시하는지 궁금함
    2,000줄짜리 모듈과 200줄짜리 메서드, 3~4단계 if 중첩은 해로운 것으로 여겨짐. 왜가 아니라 무엇을 하는지만 말하는 주석도 유용하지 않고 실제 코드와 어긋나기 쉬움. 불필요한 nil 사용도 보임. 결합도나 단일 책임 원칙 같은 더 깊은 문제로 들어가지 않고도 표면적으로 이런 점들이 보임

    • 이런 것들이 해롭다고 생각한다면 "John Carmack on Inlined Code"를 읽어보길 권함
      http://number-none.com/blow/john_carmack_on_inlined_code.htm...

      "Armadillo 로켓의 비행 제어 코드는 몇천 줄밖에 안 돼서, 메인 tic 함수를 잡고 모든 서브루틴을 인라인하기 시작했다. 실제 추락을 일으킬 수 있는 숨은 버그를 찾았다고 말할 수는 없지만, 여러 번 설정되는 변수 몇 개와 조금 수상해 보이는 제어 흐름 몇 가지를 찾았고, 최종 코드는 더 작고 깔끔해졌다."

      Carmack이 이 접근에서 가치를 찾았다면, 성급히 무시해서는 안 될 것 같음. 후속 코멘트도 볼 만함

      "이 글을 쓴 뒤 몇 년 동안, C/C++에서도 합리적인 범위에서는 순수 함수형 프로그래밍에 훨씬 더 긍정적이 되었다... 감당하기 어려워지면 블록을 순수 함수로 분리하는 방법을 찾아라"

    • 때로는 "다른 방법이 없음(TM)"인 경우가 있음
      임의의 줄 수 제한은 불필요한 파편화를 낳기 쉬움. include, 라이선스, 접착 코드, 주석까지 더하면 접근하기 어려운 스파게티가 됨. 고성능 코드에서 메서드를 200줄로 유지하려고 해 보면, 성능이 이카로스의 비행처럼 추락할 수 있음

      코드의 주석을 읽어보면 이 코드를 단일 모듈로 단순화했고, 접근 가능하고 더 중요하게는 지속 가능하게 만들기 위해 엄청난 양의 노하우를 넣어 둔 것을 볼 수 있음. 언어나 로직을 모르는 사람에게는 코드가 무엇을 하는지 윤곽을 잡아 주는 주석이 매우 유용함. 6개월 뒤에는 자기 코드도 낯설어지므로 본인에게도 유용함

      주석은 코드와 코드베이스의 일부임. 주변 코드를 고치면서 주석을 함께 갱신하지 않으면 코드에 문서 버그를 넣는 것임. 컴파일러가 처리하지 않는다고 해서 기능적 부분이 아닌 것은 아님. 본질적으로 주석은 지식이자 코드 안에 박힌 연구 노트이고, 작성한 코드를 유지보수할 때 실행되는 코드보다 더 가치 있을 수 있음

      모범 사례는 법이나 엄격한 규칙이 아니라 지침임. 코드베이스에 맞을 때 적용해야지, 맹목적으로 따르다가 문제가 있는 코드베이스를 만들면 안 됨. 때로는 규칙을 굽히고 직접 만들어야 하며, 자신이 무엇을 하는지 안다면 완전히 받아들일 수 있음

    • 꽤 오래 이런 "안전한" 방식으로 작성해 봤지만, 조기 반환을 통한 철도식 오류 처리보다 버그를 훨씬 많이 만들었고 고치는 데도 훨씬 오래 걸렸음
      모든 if 블록에 명시적 else를 붙이면 현재 맥락을 기억해야 하는 복잡도가 폭발함. 이 규칙은 "모든 if 조건 블록은 조기 반환하거나, 대응하는 else 블록을 가진다"로 바꾸는 게 합리적이라고 봄. if (cond) { 특수 처리 } 패턴은 조기 반환보다 확실히 훨씬 위험하고 추론하기 어렵게 만듦

    • 단 하나의 정식 모범 사례 묶음 같은 것은 없음
      함수 길이나 파일의 코드 줄 수 자체가 본질적으로 해롭거나 이롭지도 않음. 언어마다 코드를 어떻게 구성하라는 관점은 있지만, 그 어떤 것도 "최선의 관행"이라고 주장할 수는 없음. Go는 많은 작은 파일로 코드를 쪼개는 방식을 선호하는 언어가 아님

    • 200줄짜리 메서드가 본질적으로 잘못된 것은 아님. 내부 코드가 선형적이고 같은 추상화 수준을 유지한다면 최선의 선택일 수 있음
      대안으로 5줄짜리 메서드 40개를 만드는 건 더 나쁠 수 있음. 전체를 이해하려면 여기저기 뛰어다녀야 하고, 호출 순서를 망칠 수도 있음. 선택 가능한 순열이 40! 개나 됨

  • 이런 코드는 선언적·규칙 기반·테이블 구동 시스템으로 옮기기에 이상적인 후보처럼 보임
    그런 방식이 임시방편적인 명령형 if 절 범벅 코드보다 더 이해하기 쉽고 검증하기 쉬움. 이런 종류의 지저분한 코드는 보통 빠진 추상화가 있다는 신호임

    • Go의 이념은 기본적으로 C로 썼을 코드를 어느 정도 직설적으로 옮기듯이 전부 적어 내려가고, 뭔가를 추상화하려 들지 않는 것에 가까움