1P by GN⁺ | ★ favorite | 댓글 1개
  • 조건 분기는 호출자 쪽으로 끌어올리고 반복 처리는 배치 연산 쪽으로 밀어 넣는 두 가지 코드 구조화 규칙이 핵심임
  • if를 위로 올리면 전제조건을 타입이나 assert로 강제하기 쉬워지고, 복잡한 제어 흐름을 한곳에 모아 중복 조건과 죽은 분기를 찾기 쉬움
  • enum을 만든 뒤 곧바로 다시 match하는 구조는 같은 조건을 분기, 데이터 구조, 재분기로 반복하므로 foo(x)bar(y) 호출로 단순화할 여지가 있음
  • for를 아래로 내리면 개별 객체 반복보다 배치 연산이 기본 단위가 되어 시작 비용 상각, 처리 순서 변경, 벡터화와 struct-of-array 최적화가 가능해짐
  • 조건을 반복문 밖으로 빼면 핫 루프의 분기를 줄일 수 있고, TigerBeetle처럼 제어 평면의 의사결정 비용을 데이터 평면의 배치 처리로 상각하는 구조에도 이어짐

if를 위로 올리기

  • 함수 안의 if 조건은 먼저 호출자로 옮길 수 있는지 살펴볼 대상임
    • 예시는 Option<Walrus>를 받아 None이면 반환하는 함수보다 Walrus를 직접 받는 함수가 더 나은 형태라고 봄
    • 전제조건을 함수 내부에서 확인한 뒤 “아무것도 하지 않기”보다, 호출자가 확인하고 타입이나 assert로 성립을 강제하는 편이 명확함
  • 전제조건 검사를 위로 올리는 리팩터링은 호출 경로를 따라 전파될 수 있으며, 결과적으로 전체 검사 횟수를 줄일 수 있음
  • 제어 흐름과 if는 복잡도를 높이고 버그의 원인이 되기 쉬움
    • if를 위로 올리면 복잡한 분기 로직은 한 함수에 모이고, 실제 작업은 직선적인 하위 루틴으로 내려감
    • 복잡한 제어 흐름은 파일 전체에 흩어질 때보다 한 화면의 한 함수에 있을 때 중복과 죽은 조건을 발견하기 쉬움
  • dissolving enum” 리팩터링은 분기를 데이터 구조로 바꾼 뒤 다시 분기하는 패턴을 없애는 방식임
    • f()가 조건에 따라 E::Foo(x) 또는 E::Bar(y)를 만들고, g(e)가 다시 matchfoo(x) 또는 bar(y)를 호출하면 같은 조건이 여러 형태로 반복됨
    • 조건을 main()으로 끌어올리면 if condition { foo(x) } else { bar(y) } 형태로 단순해짐

for를 아래로 내리기

  • 데이터 지향 관점에서는 프로그램과 핫 패스가 대개 많은 객체를 다루므로, 객체들의 배치를 도입하고 배치 연산을 기본 사례로 삼음
    • 개별 객체용 스칼라 버전은 배치 연산의 특수 사례가 됨
    • for walrus in walruses { frobnicate(walrus) }보다 frobnicate_batch(walruses)가 더 나은 형태임
  • 주된 이점은 성능
    • 배치 전체를 한 번에 다루면 시작 비용을 상각할 수 있음
    • 처리 순서를 유연하게 바꾸고, 모든 엔티티의 한 필드를 먼저 처리하는 벡터화나 struct-of-array 방식도 가능함
    • 극단적인 사례로 Vectorized Interpreters Talk가 연결됨
  • 흥미로운 예로 FFT 기반 다항식 곱셈이 등장함
    • 여러 점에서 다항식을 동시에 평가하는 방식이 개별 점 평가를 여러 번 수행하는 것보다 빠를 수 있음
  • iffor 규칙은 함께 적용될 수 있음
    • 좋은 형태는 if condition 바깥에서 분기별로 for walrus in walruses를 수행하는 구조임
    • 나쁜 형태는 for 루프 안에서 매번 if condition을 평가하는 구조임
    • 조건 재평가를 피하고, 핫 루프에서 분기를 제거하며, 벡터화를 가능하게 할 수 있음
  • 이 패턴은 마이크로 수준과 매크로 수준 모두에 적용됨
    • TigerBeetle의 아키텍처는 데이터 평면에서 객체 배치를 동시에 처리해 제어 평면의 의사결정 비용을 상각하는 구조임
    • for 조언의 주된 동기는 성능이지만, jQuery처럼 컬렉션 단위로 동작하는 방식은 표현력에도 도움이 될 수 있음

댓글과 토론

Hacker News 의견들
  • 처음엔 이 글이 이렇게 반발을 받는 게 의외였지만, 이게 데이터 지향 설계 조언이라는 걸 떠올리니 이해됨
    이 포럼의 많은 사람들처럼 나도 대부분은 업무용 웹 앱을 만들고 있고, 그런 맥락에서는 이 조언이 헛소리처럼 보일 수 있음
    일상 업무에서 명령어 캐시를 신경 쓸 일이 없다면 이 조언은 대체로 무시해도 된다는 휴리스틱이 맞아 보임
    이 조언이 언제 중요해지는지 감을 잡고 싶다면 Mike Acton의 “Typical C++ Bullshit”을 찾아보면 좋고, 이 글은 그 내용을 더 이해하기 쉽게 정리한 느낌임
    Casey Muratori의 문제의식에는 꽤 공감하지만, 대부분의 업무용 소프트웨어는 성능보다 변경 가능성과 정확성, 즉 “시간에 걸친 프로그래밍”을 최적화해야 함

    • 이 조언은 객체 지향 프로그래밍에도 여전히 꽤 적용 가능하다고 봄
      개발자들은 복잡한 비즈니스 로직을 클래스 안의 작은 private 메서드들로 쪼개 DRY하게 만들려 하는데, “if를 위로 올려라”는 분기 로직이 여러 메서드에 흩어지지 않게 해줌
      “for를 아래로 내려라”도 중요함. 많은 호출 흐름이 결국 비싼 데이터베이스 쿼리로 끝나는데, 상위의 반복문이 하위에서 여러 번 DB 호출을 만들 때가 흔함
      그 반복은 SQL의 where 절이나 join으로 대체할 수 있는 경우가 많고, 집계나 필터링도 객체를 잔뜩 꺼내 반복하기보다 DAO 가까이 밀어 넣는 편이 DB 근처에서 최적화하기 좋음
      다만 모든 설계 원칙처럼 교리처럼 적용하지 말고 의식적으로 판단해야 함
    • 데이터 지향 설계는 늘 사람들을 자극하는 것 같음. 기존의 지배적인 객체 지향 접근법 중 많은 부분이 틀렸다는 함의를 주기 때문으로 보임
      변경 가능성과 성능을 서로 반대되는 것으로 보는 건 아쉬움. 실제로 둘이 충돌한다는 설득력 있는 증거는 아직 못 봤음
    • 변경 가능성, 정확성, 성능은 서로 배타적이지 않고 오히려 자주 함께 움직임
      성능에서 가장 중요한 면 중 하나는 작게 만드는 것임. 작은 코드, 작은 자료 구조, 적은 실행 명령어 수가 핵심이고, “명령어 캐시를 생각한다”는 것도 본질적으로는 작은 코드를 쓰는 일임
      코드가 작을수록 버그가 들어갈 공간이 줄고, 한 번에 이해하기 쉬우며, 테스트 커버리지를 확보하기도 좋아 정확성에 유리함. 변경 가능성도 코드가 작을수록 변경이 작아짐
      물론 병렬화, 캐싱, 저수준 최적화처럼 코드를 복잡하게 만드는 최적화도 있지만, 이는 성능 최적화의 일부일 뿐이고 진지하게 성능을 신경 쓰는 프로그래머라면 프로파일링과 분석 없이 그런 일을 하지 않음
      if/forfor/if는 기능적으로 크게 다르지 않아도 하나가 더 빠를 수 있으니, 비용 없이 제대로 쓸 수 있다면 굳이 느린 쪽을 고를 이유가 없음
    • John Ousterhout의 말을 들어보면, 실무에서 변경 가능성·정확성은 처음 생각하는 것만큼 성능과 충돌하지 않음
      단순한 프로그램은 호출 스택이 짧고 복잡하면서 비싼 추상화를 피하므로 메모리를 덜 쓰고 더 빠르게 도는 경향이 있음
      최고 수준의 성능을 추구하면 프로그램이 복잡해질 수 있지만, 진짜 단순함은 대부분 합리적인 성능을 가져다줌
      “for를 아래로 내리기”는 주로 데이터 지향 조언이지만, “if를 위로 올리기”는 프로그램을 더 단순하게 만드는 쪽에 가까움. 정확히는 소스 코드의 지역성을 높이는 이야기이고, 분기 로직을 한곳에 모으는 게 핵심임: https://loup-vaillant.fr/articles/source-of-readability
    • 업무용 앱에도 두 조언은 꽤 적용된다고 봄
      특히 “반복문을 아래로 내려라”는 CRUD 앱에서 강력함. 가능한 한 생성과 갱신을 일괄 처리하면 CPU 병목보다 훨씬 큰 시간을 아끼는 경우가 많음
      items.map(insertToDb/postToServer)insertToDb/postToServer(items)의 차이는 거의 모든 경우에 몇 자릿수 차이가 남
      이런 최적화로 작업 시간이 초·분 단위에서 밀리초 단위로 줄어드는 걸 봤고, API도 더 깔끔해지고 로그도 읽기 쉬워지는 경우가 많았음
  • 경험이 쌓일수록 너무 많은 프로그래머가 작은 단위의 “예쁜 코드”에는 신경 쓰지만, 전체 코드베이스 설계에는 충분히 신경 쓰지 않는다고 느낌
    간결하고 잘 다듬어진 함수는 좋지만, 이런 글은 PR이나 토론에서 생산성 없는 자전거 창고 논쟁으로 번질 수 있음
    함수가 좀 지저분한지, iffor를 어디에 두는지, mapfilter를 쓰는지는 함수 이름이 적절하고, 인터페이스와 타입이 표현적이며, 목적이 명확하고, 문서화가 되어 있고, 부작용을 과도하게 쓰지 않는다면 크게 신경 쓰지 않음

    • “if를 위로 올리라”는 건 자전거 창고 논쟁이 아니라, 오히려 말한 것처럼 결정해야 할 아키텍처 선택에 가까움
      입력값이 있을 때 어디서 검증할지 생각해보면, 이 휴리스틱에 따르면 입력을 받는 가장 위쪽에서 검증해야 함
      이는 코드를 이해하는 데도 도움이 되고, 증명 관점에서도 전제 조건이 위로 전파되도록 해야 하므로 기술적으로도 필요함
      첫 번째 조언은 확실히 사소한 논쟁이 아니고, 두 번째는 조금 더 애매함
    • 보안 일을 하면서 FAANG 기준 L7~L9 정도의 기술 리드와 아키텍트들을 많이 접했는데, “올바른 설계”에 가장 집착하는 사람들은 보통 경력 5~10년 차인 경우가 많았음
      주니어가 발등을 찍지 않게 하려 하지만, 복잡성의 비용과 코드가 장기적으로 어떻게 변하는지에 대한 감각은 아직 충분하지 않은 경우가 많음
      반대로 20년 넘게 해보면 대부분의 기술적 요소보다 단순함을 더 높게 평가하게 됨
      “얼마나 일찍 분기할까”, “이 코드가 실제로 해야 하는 일은 무엇인가” 같은 질문이 장기적으로 가장 가치 있는 답을 주는 편임
      추상화와 캡슐화에 관한 질문은 오히려 앞서 말한 종류의 논쟁으로 이어지기 쉽고, “큰 그림”만 보는 사람들의 코드에서 보안 문제가 가장 많이 나오는 것도 실제로 그 잘 설계된 코드베이스가 무엇을 하는지 잘 이해하지 못하기 때문임
    • 이런 사소해 보이는 원칙들이 미리 정해져 있으면, PR과 토론에서 같은 유형의 소모적 논쟁은 줄어들 수 있음
      데이터 지향 설계가 잘 맞는 성능 민감 소프트웨어에서는 이런 작은 단위의 것들에 더 신경 써야 함. 컴파일러 최적화가 그런 식으로 동작하기 때문임
      그러면 게임의 규칙이 바뀜. 문장의 의미가 중요해지고, 자기 설명적인 코드가 더 의미 있어지며, 코드 주석은 추론을 위한 용도로만 남고, “문서”는 명세와 사용자 설명서에 가까워짐
    • 반대도 많이 봤음. 과설계된 대성당을 설계하는 건 좋아하면서, 알고리즘의 저수준 효율성은 생각하지 않는 사람들도 많음
    • 한 가지 이유는 프로그래밍 언어에서 함수보다 훨씬 큰 컴포넌트나 함수와 다른 방식으로 연결되는 컴포넌트를 의미 있게 표현할 방법이 부족하기 때문임
      기껏해야 함수들을 묶을 수 있고, 그 다음부터는 알아서 해야 함
  • “if를 위로 올리기”에는 함수 정의 안에서 전제 조건과 사후 조건이 직접 보이지 않고, 각 호출 지점에서 확인해야 한다는 단점이 있음
    여러 사람이 참여하는 큰 프로젝트에서는 이런 함수가 의도한 맥락 밖에서 재사용되어 버그로 이어질 수 있음
    계약 프레임워크를 쓰면 해결할 수 있지만, 조건을 계약과 코드에 두 번 쓰게 되고 의존 타입도 마찬가지임
    특정 맥락에 속한 코드 영역을 태그하고, 그 맥락 안에서만 호출 가능한 함수를 정의하는 방식은 흥미로움
    Python이라면 @requires_context("VALIDATED_XY") 같은 데코레이터와 validated_xy 컨텍스트 매니저로 유효성 검증된 영역에서만 함수를 호출하게 만들 수 있음
    런타임은 그 맥락의 의미를 모르지만, 도구와 테스트를 갖추면 특정 조건이 만족될 때만 원하는 맥락을 세우도록 설계할 수 있음
    Haskell 같은 언어에서는 항등 모나드 같은 것으로 타입 수준에서 강제할 수 있고, 타입 수준 강제가 아니더라도 “unsafe” 코드 영역을 보호하는 흥미로운 방법이 될 수 있음

    • 저자의 논지 중 두 번째 부분을 놓친 것 같음. “전제 조건 검사를 호출자에게 밀고, 타입으로 강제할 수 있다”는 이야기임
      따라서 전제 조건은 여전히 함수 정의에 직접 보이고, 다만 if 문이 아니라 타입 시그니처의 일부로 드러남
      이는 글에서 쓰인 Rust에서 흔한 패턴이고, if 검사와 달리 런타임이 아니라 컴파일 타임에 확인되는 엄격한 전제 조건이라 만족하지 않으면 프로그램이 컴파일되지 않음
    • Python에서도 타입으로 이걸 표현할 수 있음
      예를 들어 do_something(position: ValidatedPosition)처럼 받고, 일반 Position을 검증한 뒤 ValidatedPosition으로 바꿔 넘기면 됨
      실제로는 ValidatedPosition 생성자 안에 검증을 캡슐화하겠지만, 핵심은 Position을 그대로 넘기려 하면 mypy가 잘못된 타입을 넘겼다고 알려준다는 점임
      Python의 타입 검사는 Rust만큼 포괄적이지 않지만, 전달되는 데이터가 적절히 처리되었는지 보장하고 싶을 때 점점 유용하게 쓰고 있음
    • “if를 위로 올리라”는 조언은 전제 조건 검사가 아니라 올바른 코드 경로 선택에 관한 것임
      전제 조건이 있는 함수라면 당연히 함수 시작에서 단언해도 됨. 예를 들어 Java 타입 시스템은 null을 허용하므로 객체가 필요한 함수는 null이 들어오면 예외를 던져야 함
      각 호출 지점은 전제 조건이 참일 때만 함수를 호출할 책임이 있음. 이는 전제 조건의 정의상 당연함
      전제 조건을 어기고 함수를 호출하는 건 호출자의 버그임. 함수 안에서 이를 확인해 정의되지 않은 동작을 막는 코드가 필요할 수는 있지만, 이런 단언과 프로그램의 실제 제어 흐름은 구분해야 하고, 글은 후자에 관한 것임
    • 이 예시의 fn frobnicate(walrus: Walrus)에서는 소유한 Walrus가 아닌 것을 넘기면 프로그램이 컴파일되지 않음
      제네릭이라 해도 인자가 트레이트 경계를 만족해야 하고, 함수 안에서 인자를 어떻게 쓰는지에 따라 컴파일러가 함수 정의에 필요한 경계를 요구함
    • publicprivate의 의도가 특정 맥락에 맞게 코드를 태그하는 역할을 어느 정도 하려던 것 아닌가 싶음
      아니면 public, private, .NET 생태계의 protected가 다루는 영역을 가로지르는 더 구체적인 의미론이 필요할 수도 있음
  • 첫 예시는 iffor 때문이 아니라 다른 이유로 좋지 않음
    일반적으로 어떤 것의 “컨테이너”가 있다면, 컨테이너가 아니라 그 안의 도메인 수준 “대상”에 함수를 작성하는 편이 좋음
    Clojure에서 agent를 쓸 때 agent용 함수를 쓰지 않고, agent가 담을 수 있는 대상용 함수를 작성함
    Elixir에서도 핵심 도메인 함수는 PID가 아니라 내부의 도메인 자료 구조에 동작하고, 필요한 경우 GenServer 호출이 거기에 위임됨
    이렇게 하면 더 유연하고, 핵심 도메인인 “Walrus를 frobnicate하기”와 애플리케이션 관심사인 “Walrus가 있을 수도 없을 수도 있고, 있으면 frobnicate하기”를 더 깔끔하게 나눌 수 있음

    • 주어진 조언은 검증 로직을 너무 위로 띄워 올릴 가능성이 있어 보임
      초기에 검증하는 건 좋지만, 함수가 이상한 오류로 갑자기 터지는 대신 검증 오류를 명확히 내는 것도 중요함
      Haskell은 newtype으로 이 문제를 푸는데, “적절한 검증을 이미 했음을 인증하는 투명한 컨테이너”를 제공하는 방식임
      정말 사람들에게 강조하고 싶은 조언은 “슬픈 if”를 선호하라는 것임. 정상 경로를 여러 겹 중첩하는 것보다, 잘못된 조건들을 위에서 하나씩 검사하고 바로 고치거나 중단하는 코드가 거의 항상 더 읽기 쉽고 유지보수하기 좋음
      사람은 기대한 경우에 집중하려는 본성이 있지만, 코드는 반대로 예외적 경우에 먼저 집중하길 바람. 모든 if는 머릿속 부담을 만들고, 외부 시스템에서 정보를 가져오거나 오류로 조기 종료해야 한다면 감지와 처리가 붙어 있을 때 그 부담을 즉시 내려놓을 수 있음
    • 첫 예시가 walrus.frobnicate()이면 더 낫다는 뜻이라면, 그건 저자가 말하려는 핵심보다는 문법 취향에 가까운 문제가 아닌가 싶음
  • 이게 나쁜 조언이라고까지 하진 않겠지만, 반드시 좋은 조언도 아님
    선택된 언어가 Rust라는 점이 꽤 많은 걸 말해줌. 강한 타입 시스템은 다른 언어에서 필요한 많은 방어적 프로그래밍을 막아줌
    C 프로그래머가 함수로 전달된 포인터의 유효성을 확인하지 않아 NULL 역참조를 일으킨다면, 그런 사람과는 팀을 하고 싶지 않음
    그래서 적어도 일부 if는 확실히 아래쪽에 있어야 하고, 오류가 잘 위로 전파되는 방식이면 좋음
    for에 대해서는 덜 강하게 느끼지만, C에서 배열 인자가 포인터로 decay된다는 점 때문에 반복도 아래가 아니라 위에 있어야 한다고 생각함. 배열이 만들어진 함수에서는 길이를 신뢰할 수 있지만, 인자로 넘긴 함수에서는 그렇지 않음

    • C의 인터페이스는 기대 사항을 신중하게 문서화하고, 딱 그만큼만 검사해야 함
      문서가 강한 타입 시스템을 대신해야지, 런타임 검사가 대신하면 안 됨
      NULL 검사와 기타 방어적 조치로 가득 찬 코드는 훨씬 읽기 어려움
      라이브러리 경계에서는 더 방어적인 검사가 필요하다고 할 수 있고, 이 글도 정확히 그런 검사를 위로 밀라고 말하는 것임
      보안 중요 코드는 다를 수 있지만, 대부분의 경우 우발적인 NULL 역참조는 테스트, 새니타이저, 퍼징으로 잡히면 충분함
    • C에서 NULL은 종종 유효하지만 역참조할 수 없는 포인터 값임
      유효하지 않은 포인터 값을 검사하려면 가능한 수많은 잘못된 값 전체를 검사해야 하고, NULL만 확인한다면 사실상 유효하지 않은 값을 검사하는 게 아님
      함수의 전제 조건이 “매개변수 p는 NULL일 수 없다”라면 검사해도 됨. 하지만 “p는 유효한 포인터여야 한다”가 전제 조건이라면, 적절한 단언 조건을 찾는 데 행운을 빌어야 함
    • 내 휴리스틱은 타입 시스템이 잘못된 값을 막지 못하면, 런타임에서 막는 책임이 있다는 것임
      요즘 T-SQL을 많이 쓰는데, 매개변수나 변수를 NOT NULL로 선언할 수 없음
      그래서 합리적으로 가능한 한 이른 시점, 보통 저장 프로시저 맨 위에서 NULL을 검사하는 게 좋음
      그렇지 않으면 NULL이 호출 계층 깊숙이 예상치 못하게 전파되어 덜 명확한 문제를 만들 수 있음
      다행히 테이블 데이터는 NOT NULL로 선언할 수 있어 이런 버그가 보통 데이터를 망치지는 않지만, 일찍 잡는 편이 훨씬 수월함
      다만 어떤 매개변수 값에 따라 DB에 쓰는 로직이 있고 그 값이 예상치 못하게 NULL이면, 잘못된 값이 쓰이거나 필요한 값이 전혀 쓰이지 않아 사실상 데이터가 손상될 수 있음
      그러니 방어적 프로그래밍이 답임
  • 적절한 맥락 없이 보면 이 조언은 꽤 이상하고, 어쩌면 나쁜 조언일 수도 있음
    반복문과 if 문은 모두 제어 흐름 연산이므로 글의 일부 논지는 별로 말이 되지 않음
    가장 강한 논거는 성능 같지만, 성능은 보통 가장 나중에 신경 쓸 문제여야 하고, 특히 휴리스틱 조언이라면 더 그렇다고 봄
    아쉽게도 저자가 이를 캐치프레이즈로 만들어버렸고, 퍼지지 않기를 바람

    • 성능은 논거일 수는 있지만 강한 논거는 아님
      저자의 예시처럼 개선된 코드를 쓸 수 있다면 조건은 반복문 실행 동안 상수임. 매번 비싼 조건을 평가하는 게 아니라면 분기 예측이 대부분 처리해줌
      조건이 const 값으로 된 불리언 표현식이라면 컴파일러가 알아낼 가능성도 있음
  • 글 전체가 이런 예시로 이어질 줄 알았음: 조건에 따라 walrus를 나눠 frobnicate_batch(fwalrus)transmogrify_batch(twalrus)를 호출하는 방식
    그런데 실제로는 조건 하나로 전체 반복문을 두 갈래로 감싸는 예시로 갔음

  • 소프트웨어 공학 배경에서 온 프로그래머들이 이걸 자주 틀리는 게 놀라움
    나는 과학 분야에서 프로그래밍을 시작했고, 거기서는 이런 것들을 생각하는 게 절대적으로 필요함
    반복문 순서를 잘못 잡는 것만으로 시뮬레이션이 1시간 대신 1주일 걸릴 수 있음
    그런 배경 덕분에 forif의 순서를 적절히 잡아 작은 최적화를 본능적으로 하고, 그렇게 하지 않은 코드는 그냥 틀려 보임

  • 이런 일반 규칙이 실제 코드에 정말 적용될 수 있는지는 확신이 없음
    이런 규칙은 종종 잘못 놓인 교리처럼 보이고, 블로그 글이 휴리스틱이라고 시작하더라도 젊은 프로그래머들이 항상 그렇게 받아들이지는 않음
    몇 주 전 YouTube가 “I'm a never-nester”라는 영상을 계속 추천했는데, 절대 if를 중첩하지 말라는 주장 같았고 꽤 우스꽝스러웠음
    글의 예시처럼 for 안에 if condition이 있는 코드를 “나쁨”으로 표시했지만, 실제로 그런 식으로 작성된 코드 대부분에서는 conditionwalrus에 의존해서 if를 위로 올릴 수 없음
    만약 올릴 수 있다면 같은 조건을 루프마다 다시 평가한다는 게 너무 명백해서 프로그래머들은 자연스럽게 피하려는 경향이 있음
    하지만 주니어나 학생이 교리처럼 들리는 조언을 읽으면, 이를 엄격히 따르려다 더 나쁜 코드를 만들 수 있음

    • “절대 중첩하지 않기”에 대해 특별히 교조적이지는 않지만, 문법이 가능하다는 전제에서 match (condition_a, condition_b)로 네 가지 경우를 나누는 방식이 중첩 if보다 낫지 않은 경우를 경험적으로 본 적이 없음
    • 이건 오히려 if를 “위로 올려야” 하는 좋은 예라고 봄
      코드의 목적은 조건에 따라 walrus에 특정 연산을 수행하는 것인데, 사실 if다형성과 타입 시스템 대신 쓰이고 있음
      왜 walrus에 서로 다른 시나리오에서 호출해야 하는 두 함수가 있어야 하는가? 하나의 함수와 두 타입을 두고, 올바른 타입을 넘기면 되지 않는가?
      현재 구조에서도 위쪽에서 조건에 따라 처리 함수를 선택하고, 아래쪽 반복문에서는 선택된 함수를 매번 호출하게 만들 수 있음
      결정은 가능한 한 이른 시점에 하면 코드 전체에 흩뿌릴 필요가 없음. 코드의 내부는 분기 없이 매번 같은 작업을 수행하고, 출력은 구성 그래프를 통해서만 달라짐
      물론 새로운 생각은 아니고 15년 전에도 이미 오래된 아이디어였음: https://www.youtube.com/watch?v=4F72VULWFvc
    • “GOOD” 리팩터링은 조건이 walrus에 의존하지 않을 때만 동작하고, 그 사실을 명시적으로 드러내는 데 도움이 됨
      여기에 “for를 아래로 내려라”를 다시 적용하면 if condition 안에서 frobnicate_batch(walruses)transmogrify_batch(walruses)를 호출하는 형태가 됨
    • 그런 글들이 존재하는 건 좋다고 봄
      내가 여러 번 마주쳤지만 제대로 표현하지 못했던 문제를 누군가 말로 정리해줬고, 머릿속에 넣어둘 만한 좋은 모델처럼 보임
      동시에 이런 비판도 가치 있음. 교조적인 주니어 개발자가 같이 읽고 조금 더 미묘하게 판단하게 되길 바람
    • 모든 “휴리스틱”과 “모범 사례”에서 배워야 할 핵심은 그 뒤에 있는 이유
      프로그래머는 특히 이런 것을 복사해 기계적으로 적용하면 안 됨
      휴리스틱과 모범 사례를 맹목적으로 적용하는 건 좋은 생각이 아닐 수 있고, 결국 모든 것은 “상황에 따라 다름”
  • 이런 휴리스틱에는 보통 지혜의 티끌이 들어 있지만, 대체로 신입 프로그래머에게서 다시 탈교조화해야 하는 대상이 생김
    너무 엄격하게 따르려 할 때 더 나빠지는 경우는 언제나 많고, “언제 이 조언을 듣지 말아야 할지 알아라”가 사실상 핵심 난이도임

    • 탈교조화는 어차피 필요함
      이런 규칙들은 가지고 놀기에 유용하다고 봄. 받아들여 보고, 끝까지 밀어붙여 보고, 하루나 1년쯤 뒤집어도 보고, 어디까지 가는지 보면 됨
      한계를 배우는 것도 계속 덧칠되는 양피지 위에 쌓이는 또 하나의 재료임
    • 이 글은 더 큰 모음집 안의 코안으로는 유용할 수 있음
      그 코안들 중 일부는 서로 모순되어야 함