NLP Blog

4. 실용주의 편집증

|

Tip 36. 여러분은 완벽한 소프트웨어를 만들 수 없다

  • 실용주의 프로그래머는 자기 자신 역시 믿지 않는다. 어느 누구도, 심지어는 자기 자신도 완벽한 코드를 작성할 수 없을을 알기 때문에 실용주의 프로그래머는 자신의 실수에 대비한 방어책을 마련한다.

Topic 23. 계약에 의한 설계

상식과 정의만큼 사람을 놀라게 하는 건 없다 - Ralph Waldo Emerson

  • 정직한 거래를 보장하는 최선의 해법 중 하나는 계약 이다.
  • 소프트웨어 모듈이 서로 소통하는 것을 돕기 위해 계약을 사용할 수 있을까? 그렇다.

DBC (Design By Contract)

  • Eiffel이라는 언어에서 Bertrand Meyer가 창시
  • 프로그램의 정확성을 보장하기 위해 소프트웨어 모듈의 권리와 책임을 문서화하고 합의
  • 소프트웨어 시스템의 모든 함수와 메서드는 뭔가를 한다. 그 뭔가를 시작하기 전에 해당 함수는 세상의 상태에 대해 어떤 전제 조건을 갖고 있을 테고, 루틴이 끝난 후에는 세상의 상태가 어떠할 것이라고 선언할 수 있을 것
  • 선행 조건 precondition
    • 루틴이 호출되기 위해 참이어야 하는 것. 즉, 루틴의 요구사항
    • 루틴의 선행 조건이 위반된 경우에는 루틴이 호출되어서는 안됨
    • 제대로 된 데이터를 전달하는 것은 호출하는 쪽의 책임
  • 후행 조건 postcondition
    • 루틴이 자기가 할 것이라고 보장하는 것. 즉, 루틴이 완료되었을 때 세상의 상태
    • 루틴에 후행 조건이 있다는 것은 곧 루틴이 종국에는 종료될 것 이라는 것 (무한 반복은 허용x)
  • 클래스 불변식 class invariant
    • 호출자의 입장에서 볼 때는 이 조건이 언제나 참인 것을 클래스가 보장함
    • 루틴의 내부 처리 도중에는 불변식이 참이 아닐 수도 있지만, 루틴이 끝나고 호출자로 제어권이 반환되는 시점에는 불변식이 참이 되어야 한다.
  • 루틴과 그 루틴을 호출하려는 코드 간의 계약 : 만약 호출자가 루틴의 모든 선행 조건을 충족한다면 해당 루틴을 종료 시 모든 후행 조건과 불변식이 참이 되는 것을 보장
  • 이런 개념을 더 잘 지원하는 언어 : Clojure - spec library
(defn accept-deposit [account-id abount]
  { :pre [ (> amount 0.00)
          (account-open? account-id) ]
    :post [ (contains? (account-transactions account-id) %) ]}
  "입금을 처리하고 거래 id를 반환"
  ;; 이런 저런 처리를 수행...
  ;; 새로 거래를 만들어서 반환.
  (create-transaction account-id :deposit amount))
  • Elixir에는 guard clause가 존재

Tip 37 계약으로 설계하라

  • Lazy 코드를 강조하고 싶음 - 시작하기 전에 자신이 수용할 것은 엄격하게 확인하고, 내어 줄 것에 대해서는 최소한도를 약속하자
  • 무엇이든 수용하고 결과로는 무엇이든 다 준다고 계약에 쓰여 있다면, 정말이지 많은 코드를 작성해야 함

클래스 불변식과 함수형 언어

  • Eiffel은 객체지향 프로그램 언어였으므로 클래스 불변식이라는 용어가 사용되었지만, 진짜로 의미하는 것은 상태(state)다.
  • 함수형 언어에서는 보통 상태를 함수에 넘긴 후 바뀐 상태를 결과로 받는다.

DBC와 테스트 주도 개발

  • TDD에서도 DBC는 필요하다
  • DBC가 테스트에 비해 가지는 장정
    • DBC는 테스트 환경 구성이나 mock이 필요벗다
    • DBC는 모든 입력값에 대해 성공과 실패를 정의한다 (테스트는 하나가 한 가지 경우만 다룸)
    • TDD와 다른 테스트는 빌드 과정 중 테스트할 때만 수행됨, DBC와 단정문은 영원하다 (설계, 개발, 배포, 유지보수 전체)
    • TDD는 테스트 중인 코드 내의 내부 불변식을 확인하는 것에 초점을 두지 않는다. 그보다는 공개 인터페이스를 확인하는 블랙박스 방식에 더 가까움
    • DBC는 방어적 프로그래밍보다 더 효율적이고 더 DRY하다. 방어적 프로그래밍에서는 아무도 데이터를 검증하지 않는 상황에 대비하기 위해 모든 사람이 데이터를 검증한다.

DBC 구현

단정문

  • 계약의 문서화보다는 컴파일러가 직접 확인해주는 것이 좋다
  • 몇몇 언어에서는 조건문을 실행 시점에 확인하는 단정문 Assertion 을 사용해서 부분적으로나마 DBC를 흉내낼 수 있다.
  • 하지만 단정문만을 이용해서 완벽하게 DBC를 지원하기는 힘듦
    • 객체 지향에서의 상속 계층을 따라 단정문이 밑으로 전파되도록 할 수 없을 것 (계약이 자동적으로 집행되지 않음)
    • 이전 값이라는 개념이 내장되어 있지 않음
    • 일반적인 런타임 시스템과 라이브러리가 계약을 지원하도록 설계되지 않아서 이런 코드를 호출할 때는 검사가 이루어지지 않는다

DBC와 일찍 멈추기

  • DBC는 “일찍 작동을 멈춰라” 라는 개념과 잘 어울림
  • 단정문이나 DBC 방식을 사용하여 선행 조건과 후행 조건, 불변식을 검증하면 더 일찍 멈추고, 문제에 대한 보다 정확한 정보를 알려줄 수 있음
  • ex) sqrt 함수는 매개 변수를 양수로 제한하는 DBC precondition이 필요 (오류를 찾기 쉬움)
  • C, C++은 결과로 그냥 NaN이 나옴… (오류를 훨씬 나중에 찾게 될 가능성이 있음)

의미론적 불변식

  • 바뀌지 않을 간단한 법칙
  • 도메인 주도 개발의 도메인과 비슷한 것일까?

동적 계약과 에이전트

  • 에이전트는 자신이 따르길 원치 핞는 요구를 거절할 자유가 있다. 계약을 재협상할 자유도 있다.
  • 하지만 수동으로 계약을 만들 수 없다면 자동화하는 것은 꿈도 꿀 수 없다. 그러니 나중에 소프트웨어를 설계하게 되면 계약 역시 설계하도록 하라.

관련 항목

  • 항목 24. 죽은 프로그램은 거짓말을 하지 않는다
  • 항목 25. 단정적 프로그래밍
  • 항목 38. 우연에 맡기는 프로그래밍
  • 항목 42. 속성 기반 테스트
  • 항목 43. 바깥에서는 안전에 주의하라
  • 항목 45. 요구 사항의 구렁텅이

Topic 24. 죽은 프로그램은 거짓말을 하지 않는다

  • ‘그런 일은 절대 일어날 리 없어’ 라는 사고에 빠지기 쉽다. 하지만 뭐든지 잘못될 수 있음
  • 모든 오류는 정보를 준다. 일단 그놈의 오류 메시지 좀 읽어라.

잡은 후 그냥 놓아주는 것은 물고기 뿐

  • 어떤 개발자는 모든 예외를 catch나 rescue로 잡은 후 로그 메시지를 좀 찍은 다음 다시 예외를 발생시키는 것이 좋은 상식이라고 여기는 듯 하다.
  • 하지만 실용주의 프로그래머는 하지 않는다
    • 애플리케이션 코드가 오류 처리 코드 사이에 묻히지 않음
    • 코드의 결합도를 높이지 않음

Tip 38. 일찍 작동을 멈춰라

망치지 말고 멈춰라

  • 가능한 한 빨리 문제를 발견하면 좀 더 일찍 시스템을 멈출 수 있으니 더 낫다.
  • Erlang을 만든 Joe Armstrong은 방어적 프로그래밍은 시간 낭비다. 그냥 멈추는게 낫다! 라고 함
  • 있을 수 없는 일이 발생했다는 것을 코드가 발견했다면 프로그램은 더는 유효하지 않다고 할 수 있다. 이 시점 이후로 하는 일은 모두 수상쩍은 게 된다. 되도록 빨리 종료할 일이다.
  • 일반적으로 죽은 프로그램이 끼치는 피해는 이상한 상태의 프로그램이 끼치는 피해보다 훨씬 적은 법이다.

관련 항목

  • 항목 20. 디버깅
  • 항목 23. 게약에 의한 설계
  • 항목 25. 단정적 프로그래밍
  • 항목 26. 리소스 사용의 균형
  • 항목 43. 바깥에서는 안전에 주의하라

Topic 25. 단정적 프로그래밍

자기 비난에는 사치성이 있다. 우리가 자신을 비난할 때, 다른 사람은 우리를 비난할 권리가 없다고 우리는 느낀다. - Oscar Wilde

Tip 39. 단정문으로 불가능한 상황을 예방하라.

단정과 부작용

  • 문제를 발견하려노 넣은 코드가 오히려 새로운 문제를 낳을 수 있다. 책의 코드를 참조하라
  • 디버깅 행위가 디버깅하려는 시스템의 행동을 바꿔버리는 일종의 Heisenbug 적인 문제

단정 기능을 켜 둬라

  • 단정문에 관한 흔한 오해
    • 단정은 코드를 느리게 만든다.
    • 단정은 일어나서는 안 되는 일들을 검사하기 때문에 코드 속에 버그가 있을 때만 단정 검사가 실패할 것이다.
    • 일단 코드가 테느스되고 배포된 다음에는 더 이상 단정이 필요하지 않다.
    • 그러니 코드 실행이 빨라지도록 단정을 꺼버려야 한다.
    • 단정은 디버깅 도구일 뿐이다.
  • 두 가지 명백히 틀린 가정
    • 테스트가 모든 버그를 발경한다는 가정
    • 낙관주의자들은 여러분의 프로그램이 험한 세상에서 돌아간다는 사실을 잊는다.
  • 프로그램을 출시할 때 단정 기능을 꺼 버리는 것은 줄타기 곡예를 하면서 연습으로 한 번 건너 봤다고 그물 없이 건너는 것과 비슷

관련 항목

  • 항목 23. 계약에 의한 설계
  • 항목 24. 죽은 프로그램은 거짓말을 하지 않는다
  • 항목 42. 속성 기반 테스트
  • 항목 43. 바깥에서는 안전에 주의하라

Topic 26. 리소스 사용의 균형

촛불 하나를 켜는 건 곧 그림자도 하나 던지는 거란 말이다. - Ursula K. Le Guin

  • 우리는 코딩할 때 언제나 리소스를 관리한다. - 메모리, 트랜잭션, 스레드, 네트워크 연결, 파일, 타이머 …
  • 리소스를 할당하고, 사용한 다음, 해제한다.

Tip 40. 자신이 시작한 것은 자신이 끝내라

  • 168p ~ 170p 예제 참고
  • 잘 모르겠을 땐 언제나 스코프를 줄이는 편이 낫다.

Tip 41. 지역적으로 행동하라.

중첩 할당

  • 리소스를 할당한 순서의 역순으로 해제하라. 이렇게 해야 한 리소스가 다른 리소스를 참조하는 경우에도 참조를 망가트리지 않는다
  • 코드의 여러 곳에서 동일한 구성의 리소스들을 할당하는 경우에는 언제나 같은 순서로 할당해야 교착 deadlock 가능성을 줄일 수 있다.

객체와 예외

  • 할당과 해제 사이의 균형은 객체 지향 클래스의 constructor와 destructor를 연상시킴
    • 파이썬의 경우 with문으로 할당과 해제를 관리 할 수 있음
      • class는 __enter____exit__으로
      • def는 contextlib.contextmanageryield
  • 객체 지향 언어로 프로그래밍을 한다면 리소스를 클래스 안에 캡슐화하는 것이 유용할 수 있음
  • 특정 유형의 리소스가 필요할 때마다 그 클래스의 객체를 생성하면 됨
    • 그 객체가 스코프를 벗어나거나 가비지 컬렉터가 객체를 수거해 가면 객체의 소멸자가 클래스 안에 들어 있는 리소스를 해제

균형 잡기와 예외

  • 예외가 던져진 경우 예외 발생 이전에 할당된 모든 것이 깨끗이 청소된다고 어떻게 보장할 수 있을까?
    • 변수 스코프를 사용한다. 예를 들어 C++나 Rust의 스택 변수가 있음
    • try~catch 블록에서 finally절을 사용
  • C++나 Rust 같은 언어의 일반적인 스코프 규칙에서는 함수나 블록 종료, 예외 등으로 변수가 스코프를 벗어나면 변수의 메모리가 해제 됨.
  • 하지만 예외가 있다

나쁜 예외 처리 방식

  • 174p 예제 참조

리소스 사용의 균형을 잡을 수 없는 경우

  • 동적인 자료 구조를 사용하는 프로그램에서 리소스 할당 기본 패턴이 맞지 않는 경우가 있음
    • ex) 한 루틴에서 메모리의 일정 영역을 할당한 다음 어떤 더 큰 구조에 그것을 연결한 후, 한동안 그래도 쓰는 식 (python dictionary of dictionary?)
  • 이런 경우의 요령은 메모리 할당에 대한 의미론적 불변식을 정하는 것
  • 한군데 모은 자료 구조 안의 자료를 누가 책임지는지 정해 놓아야 함
  • 자료 구조에서 최상위 구조의 메모리 할당을 해제할 경우 어떻게 처리해야 할까?
    • 최상위 구조가 자기 안에 들어 있는 하위 구조들을 해제할 책임을 진다. 하위 구조들은 또다시 재귀적으로 자기 안에 들어 있는 자료들을 해제할 책임을 지고, 이런 식으로 반복된다.
    • 최상위 구조가 그냥 할당 해제된다. 최상위 구조가 참조하던 하위 구조들은 연결이 끊어져서 다른 곳에서 참조하지 않는 다면 외톨이가 된다.
    • 최상위 구조가 하나라도 하위 구조를 가지고 있으면 자신의 할당 해제를 거부한다.

균형을 점검하기

  • 실용주의 프로그래머는 자신을 포함해서 아무도 믿지 않음. 언제나 정말로 리소스가 적절하게 해제되었는지 실제로 점검하는 코드를 작성하는 것을 좋아한다.
  • 리소스의 종류별로 wrapper를 만들고 그 래퍼들이 모든 할당과 해제 기록을 보관함
  • ex) 계속 실행 중인 상태에서 들어오는 요청을 처리하는 서버 프로그램
    • 프로그램의 주 처리 루프 맨 위에 다음 요청이 도착하기를 기다리는 단일한 지점 존재
    • 이 지점은 직전 요청을 처리하는 동안 리소스 사용량이 증가하지 않았는지 검사하기에 좋은 장소
  • Memory leaks 검사 도구도 활용 가능

관련 항목

  • 항목 24. 죽은 프로그램은 거짓말을 하지 않는다
  • 항목 30. 변환 프로그래밍
  • 항목 33. 시간적 결합 깨트리기

Topic 27. 헤드라이트를 앞서가지 말라

예측은 힘들다. 특히 미래에 대해서는 - Lawrence “Yogi” Berra

  • 소프트웨어 개발에서 우리의 “헤드라이트”는 제한되어 있다.
  • 우리는 너무 먼 미래는 내다볼 수 없고, 정명에서 벗어난 곳일수록 더 어둡다

Tip 42. 작은 단계들을 밟아라. 언제나.

  • 언제나 신중하게 작은 단계들을 밟아라. 더 진행하기 전에 피드백을 확인하고 조정하라.
  • 피드백의 빈도를 여러분의 제한 속도라고 생각하라. ‘너무 큰’ 단계나 작업은 하지 않게 될 것이다.
  • 여기서 피드백이란?
    • REPL의 결과는 API나 알고리즘을 여러분이 제대로 이해하고 있는지 피드백을 줌
    • 단위 테스트는 직전에 고친 코드에 대한 피드백을 줌
    • 사용자 데모 및 사용자와의 대화는 기능이나 사용성에 대한 피드백음 줌
  • 몇 시간이나 며칠 정도의 미래 너머는 경험에 기반한 추측을 벗어난 무모한 억측의 영역
    • 몇 달 후의 완료 일정을 추정하기
    • 미래의 유지 보수나 확장 가능성을 미리 고려하여 설계하기
    • 사용자의 미래 요구 사항 예측하기
    • 미래에 어떤 기술을 쓸 수 있을지 추측하기
  • 하지만 위의 것들은 어떻게든 해내야한다. 이를 위해 우리는 억측을 하기 보다는 “바꾸기 쉽게” 프로그램을 만들어야 한다.

블랙 스완

  • 나심 니콜라스 탈레브는 그의 책 «블랙 스완»에서 역사상 중대한 사건은 모두 다 세간의 이목을 끌고, 예측하기 어렵고, 드문 사건들로부터 발생하는데, 이 사건들은 일반적(normal - 정규 분포) 예상을 넘어서는 것이라고 상정
  • 이런 outlier는 통계적으로는 드물더라도 그 여파는 훨씬 큼

Tip 43. 예언하지 말라.

관련 항목

  • 항목 12. 예광탄
  • 항목 13. 프로토타입과 포스트잇
  • 항목 40. 리팩터링
  • 항목 41. 테스트로 코딩하기
  • 항목 48. 애자일의 핵심
  • 항목 50. 코코넛만으로는 부족하다

Chapter4-map

Comments