TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 22 READS

작은 프로그램이 곧 단순한 것은 아니다 — 유닉스 철학의 착각

작은 프로그램이 곧 단순한 것은 아니다 — 유닉스 철학의 착각
SOURCE IMAGE · HACKER NEWS

개발자들이 흔히 쓰는 '단순함(simplicity)'이라는 말에는 오해가 하나 숨어 있다. 프로그램이 짧고 작으면 단순하다고 여기는 습관이다. jyn.dev에 올라온 에세이 'Simple Is Not Small'은 이 통념을 정면으로 반박한다. 필자는 자기 회사가 코드 커버리지 관련 버그 하나를 9개월 동안 붙잡고 디버깅한 경험을 발표한 뒤, 동료 Predrag로부터 "이런 대서사시급 디버깅이 애초에 필요 없도록 도구를 어떻게 만들어야 하느냐"는 질문을 받았다고 한다. 그의 답은 단순함을 우선하라는 것이었지만, 여기서 말하는 단순함은 우리가 흔히 떠올리는 '작음'과 전혀 다른 개념이다.

작지만 얽혀 있는 코드

필자는 파일의 단어 빈도를 세는 두 프로그램을 비교한다. 하나는 여러 유닉스 명령을 파이프로 이어붙인 짧은 한 줄짜리 스크립트다. 단어 경계를 줄바꿈으로 바꾸고, 대문자를 소문자로 낮추고, 정렬한 뒤 개수를 세어 빈도순으로 보여준다. 대부분의 사람이 '단순하다'고 부르는 바로 그 모습이다. 다른 하나는 Clojure로 작성한, 이름과 고차 함수가 조금 더 많이 등장하는 버전이다.

차이는 작은 변경을 요구했을 때 드러난다. 결과를 파일에 등장한 원래 순서대로 출력하고 싶다고 하자. Clojure에서는 단어의 순서를 담은 시퀀스와 단어별 빈도를 담은 맵을 각각 두고, 시퀀스를 돌면서 맵을 조회하면 그만이다. 반면 Bash 파이프라인에서는 임시 파일 여러 개와 불투명한 정규식, sort, join의 복잡한 춤을 춰야 한다. 원인은 명확하다. 유닉스에는 빈도를 직접 세는 도구가 없어 sort | uniq -c 조합을 쓰는데, uniq는 인접하지 않은 중복 줄을 감지하지 못하므로 반드시 먼저 정렬해야 한다. 즉 이 조합은 '집계'라는 작업을 '정렬'에 묶어버린다. 정렬과 집계를 분리하기 어려운 이유가 바로 이 결합에 있다.

단순함의 진짜 정의

Rich Hickey는 강연 'Simple Made Easy'에서 단순함(simple)의 어원을 'sim-plex', 곧 '한 가닥으로 꼬인 것'으로 풀이했다. 반대말 'com-plex'는 여러 가닥을 함께 땋은 상태다. 필자는 모호함을 피하려고 complex를 '결합(coupled)'이라는 말로 바꿔 쓴다. 이 언어로 보면 유닉스 파이프라인의 정체가 드러난다. 그것은 작지만 결합되어 있다. 흔히 인용되는 "한 가지 일을 잘하는 프로그램을 만들라"는 유닉스 철학은 단순함에 관한 것으로 이해되지만, 실제로는 크기에 관한 원칙이다. 유닉스 도구는 작을 뿐 단순하지 않다.

반대 극단도 있다. Google Drive for Desktop은 플랫폼별 파일 감시기, 방대한 사내 코드베이스, 스트리밍·동기화 네트워크 클라이언트, 충돌 해결 로직에 의존하는 거대한 프로그램이다. 그러나 사용자에게는 오히려 단순하게 느껴진다. 설치하고, 감시할 폴더를 지정하고, 로컬에 둘지 클라우드에 둘지 정하면 나머지는 알아서 처리한다. 프로그램이 복잡해지는 것은 크기 때문이 아니라, 서로 얽힐 필요가 없는 기능들이 결합될 때다.

표현과 검증을 떼어놓기

결합의 또 다른 사례로 필자는 Rust의 struct와 map을 든다. struct는 어떤 필드가 존재하는지 타입 검사기가 알기 때문에 값에 바로 접근할 수 있고, map은 그렇지 않아 unwrap 호출이 필요하다. 그러나 struct는 런타임 정보를 잃는다. map은 순회가 쉽지만 struct의 필드를 순회하려면 프로시저 매크로 같은 수단을 동원해야 한다. Rust에서 struct가 타입 검사를 고정된 데이터 표현에 묶어버린 탓이다. 하나를 얻으려면 다른 하나를 포기해야 한다.

Clojure는 이를 분리한다. 그곳에서 struct는 곧 맵이며, 타입을 정의하는 대신 맵이 가질 수 있는 필드를 주석으로 표시한다. Malli 같은 라이브러리로 런타임에 검사하되, 그 주석 자체가 들여다볼 수 있는 값이라 스키마를 문서로 변환하는 함수를 직접 만들 수도 있다. 타입 안전성이나 순회를 포기하지 않으면서 말이다. 데이터 표현과 타입 검사를 분리했기에 가능한 일이다. Typed Racket은 매크로를 써서 같은 효과를 컴파일 시점에 낸다.

작음은 자원의 문제, 단순함은 언제나

작게 만드는 것이 정당한 경우도 분명히 있다. PDP-11 위에서 돌아가던 시절의 Brian Kernighan처럼, 혹은 한 달에 몇 시간밖에 낼 수 없는 오픈소스 관리자처럼, 투입할 자원이 부족할 때는 작게 유지하는 것이 합리적이다. 그러나 작음과 단순함은 다르다. 프로그램을 언제 단순하게 만들어야 하느냐는 질문에 필자는 '항상'이라고 답한다. 필요 없는 결합을 끌어들여 얻는 이점은 거의 없고, 유지보수는 어려워지며 사용자에게는 덜 유연해진다.

다만 단순함은 공짜가 아니다. 좋은 정신적 모델과, 가르치기 어려운 '취향'이 필요하다. 때로는 어려운 것을 감내하고 제대로 만들어야 한다. CSS와 SQL은 원하는 결과를 선언적으로 기술하면 브라우저 엔진이나 데이터베이스 런타임이 방법을 알아서 찾아주는 고도로 분리된 도구다. 이 수준의 분리를 떠받치기 위해 SQLite에는 수백 인년(person-year), Chrome의 렌더러 Blink에는 수만 인년이 투입됐다. 어떤 영역에서는 그만한 대가를 치러야 비로소 분리된 프로그램을 쓸 수 있다. 물론 모든 프로그램에 그런 투자가 정당한 것은 아니고, 기술적 난제는 코드가 어떻게 동작해야 하는지 몽상하기 좋아하는 사람을 끌어들이는 함정이 되기도 한다.

실무자에게 남는 것

주목할 대목은 결말이다. 9개월을 잡아먹은 커버리지 파이프라인은 문제를 해결한 뒤 오히려 노드가 더 늘어 커졌다. 그런데도 더 단순해졌다. 데이터플로 그래프의 각 부분 사이에 숨어 있던 의존성이 줄었기 때문이다. 코드 라인 수나 도구 개수를 줄이는 것이 곧 개선이라는 등식은 성립하지 않는다는 뜻이다. 한국의 실무자에게 이 글이 던지는 실질적 질문은 명확하다. 지금 쓰는 파이프라인이나 스크립트에서 서로 얽힐 이유가 없는데도 묶여 있는 부분, 예컨대 정렬과 집계처럼 하나를 건드리면 다른 하나가 딸려오는 지점이 어디인가. 다만 글은 취향을 어떻게 기르는지, 시스템을 수직 통합하면서도 결합을 피하는 구체적 방법은 다음 글로 미뤄 두었다는 한계도 있다. 결합을 걷어내는 감각은 결국 개별 코드베이스 안에서 스스로 벼려야 할 몫으로 남는다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://jyn.dev/simple-is-not-the-same-as-small/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...