3P by neo 2달전 | favorite | 댓글 1개

파싱, 검증하지 말라

타입 주도 설계의 본질

  • 타입 주도 설계(type-driven design)를 설명하는 간단한 슬로건: 파싱, 검증하지 말라
  • 이 슬로건은 타입 시스템을 활용하여 코드의 안전성과 정확성을 높이는 방법을 의미함

가능성의 영역

  • 정적 타입 시스템은 특정 함수가 구현 가능한지 여부를 쉽게 판단할 수 있게 해줌
  • 예시: foo :: Integer -> Void는 구현 불가능함 (Void 타입은 값을 가질 수 없음)
  • 예시: head :: [a] -> a 함수는 리스트가 비어 있을 경우 정의되지 않음

부분 함수를 전체 함수로 바꾸기

기대 관리
  • head 함수는 리스트가 비어 있을 때 값을 반환할 수 없으므로 Maybe 타입을 사용하여 Nothing을 반환할 수 있게 함
  • 그러나 이는 사용 시 불편함을 초래할 수 있음
기대를 전달하기
  • NonEmpty 타입을 사용하여 비어 있지 않은 리스트를 표현함으로써 head 함수가 항상 값을 반환하도록 보장할 수 있음
  • NonEmpty 타입을 사용하면 불필요한 체크를 제거하고, 타입 시스템을 통해 오류를 컴파일 타임에 잡을 수 있음

파싱의 힘

  • 파싱과 검증의 차이는 정보를 어떻게 보존하느냐에 있음
  • validateNonEmpty 함수는 리스트가 비어 있지 않음을 검증하지만, 정보를 보존하지 않음
  • parseNonEmpty 함수는 리스트가 비어 있지 않음을 검증하고, NonEmpty 타입으로 정보를 보존함

검증의 위험성

  • 검증 기반 접근 방식은 "shotgun parsing"이라는 문제를 초래할 수 있음
  • 이는 프로그램이 입력의 일부를 처리한 후 나머지 입력이 유효하지 않음을 발견하는 상황을 초래할 수 있음
  • 파싱은 프로그램을 두 단계로 나누어, 첫 번째 단계에서 입력의 유효성을 확인하고, 두 번째 단계에서 유효한 입력만을 처리하도록 함

실전에서의 파싱

  • 데이터 타입에 집중하여 함수의 타입 시그니처를 가능한 한 구체적으로 만듦
  • 불법 상태를 표현할 수 없는 데이터 구조를 사용하고, 가능한 한 빨리 데이터를 구체적인 표현으로 변환함
  • 데이터 타입이 코드를 안내하도록 하고, 코드가 데이터 타입을 제어하지 않도록 함
  • m ()를 반환하는 함수는 신중하게 사용해야 함
  • 여러 번에 걸쳐 데이터를 파싱하는 것을 두려워하지 말아야 함
  • 데이터의 비정규화된 표현을 피하고, 필요한 경우 캡슐화를 통해 관리해야 함
  • 검증기를 파서처럼 보이게 만드는 추상 데이터 타입을 사용해야 함

요약, 반성, 관련 읽을거리

  • Haskell의 타입 시스템을 최대한 활용하는 것은 어렵지 않으며, 최신 언어 확장을 사용할 필요도 없음
  • 핵심 아이디어는 "전체 함수 작성"이며, 이는 간단하지만 실천하기 어려울 수 있음
  • 관련 읽을거리로는 Matt Parson의 블로그 포스트 "Type Safety Back and Forth"와 Matt Noonan의 논문 "Ghosts of Departed Proofs"를 추천함

GN⁺의 정리

  • 이 글은 Haskell의 타입 시스템을 활용하여 코드의 안전성과 정확성을 높이는 방법을 설명함
  • 파싱과 검증의 차이를 이해하고, 파싱을 통해 입력의 유효성을 확인하는 것이 중요함을 강조함
  • 타입 시스템을 활용하여 불법 상태를 표현할 수 없는 데이터 구조를 사용하고, 가능한 한 빨리 데이터를 구체적인 표현으로 변환하는 것이 중요함
  • 관련 읽을거리로는 Matt Parson의 블로그 포스트와 Matt Noonan의 논문을 추천함
Hacker News 의견
  • 이 조언과 기사는 매우 유익함

  • 정적으로 타입이 지정된 함수형 언어를 사용하지 않는 사람들에게도 유용함

  • 이 아이디어는 패러다임을 초월함

  • 80~90년대 객체지향 문헌에서도 유사한 개념을 찾을 수 있음, 예를 들어 Design by Contract

  • TypeScript는 런타임에 타입을 세분화하는 방식으로 작성되는 경우가 많음

  • Design by Contract는 Clojure의 spec에 영향을 미쳤을 것임 (Clojure는 동적 언어임)

  • 기본적으로 이는 가정과 보장에 관한 것임 (요구와 제공)

  • 가정이 확인되고 보장이 이루어지면 프로그램의 다른 부분에서 중복된 가정을 다시 확인할 필요가 없음

  • 코드에서 이미 보장된 속성이 다시 확인되는 것을 보면 혼란스러울 수 있음, 이는 코드 이해와 개선을 어렵게 만듦

  • 이 패턴은 현대 C#에서도 잘 작동하며 공간 절약 효과도 있음

    • 예시 코드:
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • 예시 코드:
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • 커널 모드 드라이버에서는 후자를 사용하지 말 것을 권장함
  • 강력한 타입 시스템을 활용하여 오류 사례를 표현할 수 없게 만드는 것이 좋음, 이는 소프트웨어 버그를 줄이는 데 도움이 됨

  • 문제를 생각하고 디자인을 따르는 데 시간이 더 걸리지만, 많은 경우 그 시간이 가치가 있음

  • "Parse, don’t validate"라는 슬로건이 타입 기반 설계를 잘 요약함

  • 개인적으로는 "항상 단일 생성자에서만 유효성 검사를 수행"하는 것이 좋음, 이렇게 하면 유효하지 않은 객체가 전혀 존재하지 않게 됨

  • 객체를 수정하려면 동일한 생성자를 다시 호출하여 새 상태를 구성하는 방식으로 구현해야 함

  • qmail의 섹션 5가 떠오름, 여기에는 "파싱하지 말라"와 "좋은 인터페이스와 사용자 인터페이스가 있다"는 내용이 포함됨

  • 중간 규모의 프로그래밍 수업을 가르친다면 학생들에게 이 제안을 비교하고 대조하는 에세이를 작성하게 할 것임, 각 제안은 배울 점이 있으며 처음에는 모순처럼 보일 수 있음

  • 관련 자료: Richard Feldman의 "Making Impossible States Impossible"

  • 이전 논의:

  • Crowdstrike에 전달됨

  • 2000년대 중반 XML 열풍 때 누군가의 댓글이 떠오름, 많은 조직이 XML을 선택한 이유는 XML이 파서를 제공하기 때문임

  • 파서를 작성하는 것이 어렵지 않고 재미있음에도 불구하고, 사람들이 파서를 작성하지 않으려는 이유를 이해할 수 없음

  • Protocol Buffers의 "required" 키워드가 큰 실수였다는 의견과 반대되는 것인지 궁금함

  • 유연하고 검증되지 않은 파싱과 검증된 파싱 기능을 모두 갖추는 것이 최선일 것임