# 수백만 줄의 Haskell: Mercury의 프로덕션 엔지니어링

> Clean Markdown view of GeekNews topic #29137. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29137](https://news.hada.io/topic?id=29137)
- GeekNews Markdown: [https://news.hada.io/topic/29137.md](https://news.hada.io/topic/29137.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-05-04T09:47:00+09:00
- Updated: 2026-05-04T09:47:00+09:00
- Original source: [blog.haskell.org](https://blog.haskell.org/a-couple-million-lines-of-haskell/)
- Points: 1
- Comments: 1

## Topic Body

- Mercury는 주석 등을 제외한 약 **200만 줄의 Haskell** 코드베이스로 30만 개 이상 기업에 뱅킹 서비스를 제공하며, 2025년에 2,480억 달러 거래량과 연환산 매출 6억 5,000만 달러를 처리함  
- Mercury의 Haskell 활용 가치는 순수성 자체보다 **운영 지식**을 API와 타입에 담고, 위험한 동작을 좁은 경계 뒤에 두며, 안전한 경로를 쉬운 경로로 만드는 데 있음  
- 신뢰성은 실패를 모두 막는 것이 아니라 시스템이 **변동을 흡수**하는 능력으로 다뤄지며, 타입 시스템은 오류 클래스를 배제하고 제도적 지식을 컴파일러가 강제하는 문서처럼 남겨줌  
- Mercury는 금융 업무 흐름의 재시도, 타임아웃, 취소, 크래시 복구를 위해 [Temporal](https://temporal.io/)을 **durable execution** 프레임워크로 쓰고, Haskell SDK [`hs-temporal-sdk`](https://github.com/MercuryTechnologies/hs-temporal-sdk)를 오픈소스로 공개함  
- Haskell의 프로덕션 가치는 모든 것을 타입에 넣는 데 있지 않고, 데이터 손실·금융 오류·규제 문제로 이어지는 불변식은 타입으로 보호하되 복잡성은 **캡슐화**하고 테스트·문서·코드 리뷰와 함께 운영하는 데 있음  
  
---  
  
### Mercury의 Haskell 운영 규모와 신뢰성 관점  
- Mercury는 주석 등을 제외하고 약 **200만 줄** 규모의 Haskell 코드베이스를 운영함  
- Mercury는 30만 개 이상의 기업에 뱅킹 서비스를 제공하는 핀테크 회사이며, 2025년에 **2,480억 달러** 거래량과 연환산 매출 **6억 5,000만 달러**를 처리함  
- 직원은 약 **1,500명**이고, 엔지니어링 조직은 주로 범용 개발자를 채용하며 대부분은 입사 전 Haskell을 써본 적이 없음  
- 이 시스템은 빠른 성장, SVB 위기로 5일 만에 **20억 달러** 신규 예금이 유입된 상황, 규제 심사, 대규모 금융 시스템의 일반적·비일반적 상황을 거치며 수년간 동작해 옴  
  
### 신뢰성은 실패 방지가 아니라 변동 흡수 능력  
- 전통적 신뢰성 접근은 실패를 열거하고, 검사와 테스트를 추가하고, 버그를 찾는 데 집중하지만 이것만으로는 충분하지 않음  
- Mercury는 신뢰성을 시스템이 **변동을 흡수**하는 능력으로 다룸  
  - 시스템이 우아하게 성능 저하를 겪을 수 있어야 함  
  - 운영자가 시스템을 이해하고 조정할 수 있어야 함  
  - 아키텍처가 올바른 일을 쉽게, 잘못된 일을 어렵게 만들어야 함  
- 빠르게 성장하는 조직에서는 새로 합류한 엔지니어가 모듈을 읽고 이해할 수 있는지, 데이터베이스가 느릴 때 서비스가 함께 무너지는지, 인터페이스 오용을 컴파일러가 잡는지가 실제 운영 질문이 됨  
- 타입 시스템은 단순한 정확성 증명보다 **운영 보조 장치**에 가까움  
  - 특정 오류 클래스를 배제함  
  - 작성자가 떠난 뒤에도 제도적 지식을 컴파일러가 읽을 수 있는 형태로 남김  
  - 위키보다 일관되게 강제되는 문서 역할을 함  
- Mercury의 안정성 엔지니어링은 제품 개발을 늦추는 품질 경찰이 아니라, 기능이 깨졌을 때의 영향을 설계 초기부터 다루는 협업 방식임  
  - 실패 시 폭발 반경  
  - 멱등성이 필요한 작업과 방식  
  - 롤백 형태  
  - 진행 중 작업의 처리  
  - 실패를 흡수하는 시스템과 증폭하는 시스템을 미리 따짐  
  
### 순수성은 언어의 속성이 아니라 인터페이스 경계  
- Haskell의 순수성은 내부에 부작용이 전혀 없다는 뜻이 아니라, **인터페이스가 부작용의 누출을 막는 경계**를 만든다는 의미에 가까움  
- `bytestring`, `text`, `vector` 같은 라이브러리의 순수 함수 뒤에는 가변 할당, 버퍼 쓰기, unsafe coercion 같은 내부 구현이 존재함  
- `ST` 모나드는 계산 안에서 관찰 가능한 제자리 변경과 부작용을 사용하지만, `runST`의 rank-2 타입이 내부에서 만든 가변 참조의 탈출을 막음  
  ```haskell  
  runST :: (forall s. ST s a) -> a  
  ```  
- 내부에서는 명령형 동작이 가능하지만, 외부에는 결과만 나오며 변경 가능 상태는 경계 밖으로 새지 않음  
- 이 원칙은 운영 시스템 전반에 적용됨  
  - 데이터베이스 계층은 내부적으로 연결 풀링, 재시도, 가변 상태를 사용할 수 있음  
  - 캐시는 동시성 가변 맵을 사용할 수 있음  
  - HTTP 클라이언트는 회로 차단기, 연결 풀, 많은 장부 관리를 가질 수 있음  
  - 핵심은 위험한 동작을 좁은 인터페이스로 감싸 오용을 어렵게 만드는 것임  
- 실제 시스템에서 목표는 변경을 완전히 피하는 것이 아니라, 변경이 어디에 있는지 명확히 하고 코드베이스 중 누가 그것을 알아야 하는지 제한하는 것임  
  
### 올바른 일을 쉬운 일로 만들기  
- 대형 코드베이스에서는 정확성이 특정 순서나 보이지 않는 추가 단계에 의존하는 패턴이 자주 생김  
  - 트랜잭션 후 감사 로그를 flush해야 함  
  - 엔드포인트 호출 전 feature flag를 확인해야 함  
  - 알림 enqueue를 데이터베이스 트랜잭션 안에서 해야 함  
- 이런 운영 지식이 위키, 온보딩 문서, 과거 디자인 리뷰, Slack 스레드, 일부 시니어 엔지니어의 기억에만 있으면 빠르게 사라짐  
- Haskell은 이런 절차를 **타입으로 인코딩**해 잊을 수 없게 만들 수 있음  
- 나쁜 방식은 올바른 함수를 쓰라고 부탁하되 우회 경로를 남겨두는 것임  
  ```haskell  
  -- Please use this one, not the other one  
  writeWithEvents :: Transaction -> [Event] -> IO ()  
  
  -- Don't use this directly (but we can't stop you)  
  writeTransaction :: Transaction -> IO ()  
  publishEvents :: [Event] -> IO ()  
  ```  
  - 더 나은 방식은 작업을 실행하는 유일한 경로가 이벤트 발행을 포함하도록 타입을 재구성하는 것임  
  ```haskell  
  data Transact a -- opaque; cannot be run directly  
  record :: Transaction -> Transact ()  
  emit :: Event -> Transact ()  
  
  -- The *only* way to execute a Transact: commit and publish atomically  
  commit :: Transact a -> IO a  
  ```  
- 여기서 타입 시스템은 이벤트에 관한 깊은 정리를 증명하기보다, 올바른 운영 절차를 **가장 쉬운 경로**로 만듦  
- 새 엔지니어가 트랜잭션 작성법을 물으면 타입 시그니처와 공개 API가 답을 제공하고, 시니어 엔지니어가 떠나도 지식이 남음  
  
### 지속 실행과 Temporal  
- 금융 시스템의 업무 흐름은 단일 트랜잭션 안에 머물지 않음  
  - 결제 전송  
  - 파트너 승인 대기  
  - 원장 업데이트  
  - 고객 알림  
  - 취소와 타임아웃 처리  
  - 파트너는 성공했지만 워커가 기록 전 죽은 경우  
  - 네트워크 문제로 응답이 없는 경우  
- 이런 흐름에는 상태, 재시도, 타임아웃, 멱등성, 프로세스 크래시와 배포를 넘어 지속되는 실행이 필요함  
- Mercury는 과거에 데이터베이스 기반 상태 머신, cron 작업, 백그라운드 워커, 코드 곳곳의 재시도와 타임아웃 처리로 이런 프로세스를 조정함  
  - 동작은 했지만 취약했고 이해하기 어려웠으며 운영 사고의 불균형한 원인이 됨  
- [Temporal](https://temporal.io/)은 Mercury의 **durable execution** 프레임워크로, 워크플로를 일반적인 순차 코드처럼 작성하고 플랫폼이 각 단계를 이벤트 히스토리에 기록함  
- 워커가 워크플로 중간에 크래시하면 다른 워커가 결정적 prefix를 replay해 상태를 재구성하고 중단 지점부터 계속함  
- 재시도, 타임아웃, 취소, 오류 처리는 각 팀이 따로 재구현하는 대신 플랫폼이 제공함  
- Temporal workflow는 이벤트 히스토리에 대한 순수 함수와 비슷한 성격을 가짐  
  - replay된 workflow는 원래와 같은 명령 시퀀스를 만들어야 함  
  - 이 결정성 요구는 순수 코드의 같은 입력·같은 출력 제약과 닮아 있음  
  - 부작용은 workflow의 `IO`에 해당하는 **activity**로 격리됨  
- Mercury는 Temporal의 공식 Core SDK를 Rust FFI로 감싼 Haskell SDK [`hs-temporal-sdk`](https://github.com/MercuryTechnologies/hs-temporal-sdk)를 만들고 오픈소스로 공개함  
- Temporal 도입 패턴은 [Temporal Replay conference 발표](https://www.youtube.com/watch?v=FdGMaLcEP2M)에서도 다뤄졌고, Mercury는 취약한 cron·상태 머신 체인을 durable workflow로 대체해 운영 개선을 얻음  
  
### 도메인은 전송 계층이 아니라 비즈니스 언어로 설계  
- 성장한 시스템에서 흔한 실수는 호출 시스템의 개념이 도메인 모델에 새는 것임  
- HTTP 요청 핸들러용으로 작성된 코드가 나중에 cron 작업, 큐 기반 백그라운드 워커, Temporal workflow에서 재사용되면 `StatusCodeException 409 "Conflict"` 같은 HTTP 예외가 비HTTP 맥락으로 전파될 수 있음  
- cron 작업에는 409 응답을 기다리는 호출자가 없으며, 상태 코드는 비즈니스 의미를 잘못된 계층으로 끌고 감  
- 해결책은 **도메인 오류**를 도메인 타입으로 모델링하는 것임  
  - 잔액 부족은 `InsufficientFunds`여야 함  
  - 중복 요청은 `DuplicateRequest`여야 함  
  - 파트너 타임아웃은 `PartnerTimeout`이어야 함  
- 각 경계에는 얇은 변환 계층을 둠  
  ```haskell  
  data PaymentError  
    = InsufficientFunds  
    | DuplicateRequest RequestId  
    | PartnerTimeout Partner  
  
  toHttpError :: PaymentError -> HttpResponse  
  toHttpError InsufficientFunds       = err402 "Insufficient funds"  
  toHttpError (DuplicateRequest _)    = err409 "Duplicate request"  
  toHttpError (PartnerTimeout _)      = err502 "Partner unavailable"  
  
  toWorkerStrategy :: PaymentError -> WorkerAction  
  toWorkerStrategy InsufficientFunds    = Fail "Insufficient funds"  
  toWorkerStrategy (DuplicateRequest _) = Skip  
  toWorkerStrategy (PartnerTimeout _)   = RetryWithBackoff  
  ```  
- 전송 계층 관심사는 가장자리에 있어야 하며, 도메인 모델은 웹 핸들러, CLI, cron 작업, 백그라운드 워커, workflow 엔진 어디서 호출돼도 HTTP 상태 코드를 끌고 다니지 않아야 함  
  
### 타입 인코딩의 비용과 적정선  
- 불변식을 타입에 넣는 것은 강력하지만 **인지 비용**, 경직성, 요구사항 변경 시 어려움을 만든다는 비용이 있음  
- 위반이 데이터 손실, 금융 오류, 규제 문제, 호출 대기 사고로 이어진다면 타입 인코딩 비용이 정당화됨  
- 단지 현재 방식이 그렇다거나, 타입 수준 기법을 적용해 보고 싶다는 이유라면 코드베이스를 바꾸기 어렵게 만들 가능성이 큼  
- ## 너무 많이 인코딩하는 쪽  
  - 불법 상태가 표현 불가능하고 도메인이 타입으로 충실히 모델링됨  
  - 비즈니스 규칙 변경이 50개 모듈을 관통하는 타입 변경으로 이어져 리팩터링이 길어짐  
  - 새 엔지니어가 타입 시그니처를 이해하기 어려워짐  
- ## 아무것도 인코딩하지 않는 쪽  
  - 타입이 `String`, `IO ()`, 최악의 경우 `Dynamic`에 가까워짐  
  - 코드는 바꾸기 쉽지만 계약이 없고, 의미는 기존 작성자의 기억에 의존함  
  - 작성자가 떠나면 시스템이 왜 동작하지 않는지 알기 어려워짐  
- ## 유용한 기준  
  - **조용한 손상**을 막는 불변식은 타입에 넣는 편이 좋음  
    - 이벤트 없이 커밋된 트랜잭션  
    - 감사 로그 없이 처리된 결제  
    - 겉보기엔 가능하지만 의미적으로 불가능한 상태 전이  
  - **크게 실패하는** 불변식은 좋은 오류 메시지를 가진 런타임 검사로 충분할 수 있음  
    - 500 응답  
    - assertion 실패  
    - JSON 경계의 타입 불일치  
  - 전체 도메인을 타입으로 모델링하려는 욕구는 억제해야 함  
    - 도메인에는 예외, 과거 호환 규칙, 서로 충돌하는 규칙, 특정 고객용 특수 동작이 존재함  
  - 타입은 컴파일러만이 아니라 팀을 위한 도구임  
    - 테스트, 문서, 코드 리뷰, 예제, 플레이북과 함께 방어층을 구성해야 함  
  - Mercury 내부에는 GADT, type family, 상태 전이를 추적하는 phantom type 같은 복잡한 타입 수준 장치를 쓰는 라이브러리도 있음  
  - 잘못되면 돈이 잘못 이동하거나 규제 불변식이 깨지는 메커니즘에서는 이런 복잡성이 필요함  
  - 핵심은 복잡성을 **캡슐화**하는 것임  
  - 타입 수준 상태 머신을 구현하는 모듈은 소수의 깊이 이해한 작성자와 충분한 테스트를 가져야 함  
  - 사용하는 쪽의 API는 일반적인 타입을 가진 몇 개 함수처럼 보여야 함  
  - product engineer가 내부의 타입 수준 증명 장치를 모르고도 안전하게 호출할 수 있어야 함  
  - 코드 리뷰에서 다른 모듈을 만지는 PR이 컴파일러를 달래기 위해 복사한 타입 주석으로 가득하다면 추상화가 경계를 넘어 새고 있다는 신호임  
  
### 내성 가능성을 위한 설계  
- 신뢰성이 적응 능력이라면 **내성 가능성**은 그 능력을 얻는 방식 중 하나임  
- 운영자는 볼 수 없는 것을 운영할 수 없고, 팀은 내부가 불투명한 시스템에 적응하기 어려움  
- Haskell에는 monkey patching이 없어 런타임에 라이브러리 내부 HTTP 클라이언트를 바꾸거나 데이터베이스 호출을 OpenTelemetry span을 내는 함수로 교체하기 어려움  
- Rust도 같은 제약을 갖지만 Rust 생태계는 `tower` 미들웨어 패턴에 수렴한 반면, Haskell 생태계는 여러 접근으로 나뉘어 있음  
- 라이브러리가 구체적인 최상위 함수 묶음만 노출하면, 계측하려면 새 모듈로 감싸고 사람들이 원래 모듈 대신 그 모듈을 import하길 기대해야 함  
- ## 함수 레코드  
  - 가장 자주 쓰는 해법은 구체 함수 대신 **함수 레코드**를 노출하는 것임  
    ```haskell  
    -- A concrete module gives you no leverage:  
    sendRequest :: Request -> IO Response  
    -- A record of functions gives you all of it:  
    data HttpClient = HttpClient  
    { sendRequest :: Request -> IO Response  
    , getManager  :: IO Manager  
    }  
    ```  
  - 이 방식이면 `sendRequest`를 타이밍 계측으로 감싸 새 `HttpClient`를 반환할 수 있음  
  - 테스트용 fault injection, mock 교체, 재시도, tracing, 요청 rewrite, tenant별 동작 같은 횡단 관심사를 런타임에 추가할 수 있음  
  - WAI의 `type Middleware = Application -> Application`처럼 동작 변환을 합성 가능하게 만드는 패턴이 운영상 매우 유용함  
- ## `Monoid`로 합성되는 인터셉터  
  - 미들웨어와 interceptor 타입은 대개 `Semigroup`과 `Monoid` 인스턴스를 가질 수 있음  
  - WAI의 `Middleware`는 endomorphism이고, endomorphism은 합성과 `id` 아래 monoid를 형성함  
  - interceptor hook 레코드는 필드별로 합성할 수 있어 tracing, timeout, task queue rewrite 같은 관심사를 별도 배관 없이 `mconcat`으로 합칠 수 있음  
    ```haskell  
    appTemporalInterceptors =  
    mconcat  
      [ retargetingInterceptor  
      , otelInterceptor  
      , sentryInterceptor  
      , sqlApplicationNameInterceptor  
      , loggingContextInterceptor  
      , statementTimeoutInterceptor  
      , teamNameInterceptor  
      , clientExceptionInterceptor  
      , workflowTypeNameInterceptor  
      ]  
    ```  
  - 각 interceptor는 독립 모듈에서 한 가지 관심사만 다루고, `mempty`에서 필요한 필드만 override하며, 순서는 리스트에 명시됨  
- ## 이펙트 시스템  
  - `effectful`, `polysemy`, `fused-effects`, `cleff` 같은 effect system도 다른 경로를 제공함  
  - 사용 가능한 연산을 effect 타입으로 정의하고 production, testing, tracing용 interpreter를 호출 지점에서 바꿀 수 있음  
  - effect를 가로채 메트릭 기록이나 지연 주입 후 실제 handler로 다시 보낼 수 있음  
  - 단점은 타입 수준 effect list, handler stack, 까다로운 타입 오류 같은 장치가 추가된다는 것임  
  - 함수 레코드는 새 엔지니어가 오후 하나면 이해할 수 있을 만큼 단순함  
- ## `persistent`의 긍정적 예  
  - `persistent`의 `SqlBackend`는 `connPrepare`, `connInsertSql`, `connBegin`, `connCommit`, `connRollback` 같은 함수 레코드임  
  - OpenTelemetry 계측을 추가할 때 관련 필드를 감싸 모든 데이터베이스 작업에 tracing span을 붙일 수 있었음  
  - fork 없이, 거의 소스 변경 없이 데이터베이스 계층 가시성을 확보함  
- ## 운영상 어려운 라이브러리  
  - Mercury는 Hackage에 공개된 웹 API 클라이언트 바인딩을 거의 쓰지 않음  
  - 서드파티 바인딩이 구체 함수로 HTTP 호출을 수행하면 tracing, SLO에 맞춘 timeout, 파트너 장애 시뮬레이션, trace의 400ms 공백 설명이 어려워짐  
  - 그래서 직접 클라이언트를 작성하고 처음부터 관측 가능하게 만듦  
- ## 작은 생태계의 비용  
  - 일부 Haskell 라이브러리는 버려진 것은 아니지만, 명확히 책임지고 빠르게 개선하는 주체가 없는 공공 인프라처럼 남아 있음  
  - 오래된 인터페이스가 유지되고, 관측 가능성·경계 설계·운영성에 관한 새로운 설계를 받아들이는 속도가 느릴 수 있음  
  - `http-client`는 직접적으로 HTTP/1.1만 지원하며, 충분히 쓸 만하지만 특정 시점에는 우회가 필요할 수 있음  
  
### 패키지 작성자를 위한 운영상 요구  
- 라이브러리 작성자는 사용자가 소스 수정 없이 동작을 주입할 수 있도록 함수 레코드, effect 타입, callback 같은 **탈출구**를 제공해야 함  
- [`hs-opentelemetry-api`](https://hackage.haskell.org/package/hs-opentelemetry-api)를 의존성으로 추가하고 핵심 `IO` 작업 주변에 span을 두는 것만으로도 production에서 라이브러리를 운영하는 사용자에게 도움이 됨  
  - API 패키지는 breaking change에 보수적이며, 애플리케이션이 OpenTelemetry SDK를 초기화하지 않으면 inert하게 동작하도록 설계됨  
  - 성능 오버헤드는 최소화되어 있고, 사용자 애플리케이션에서 예기치 않은 예외나 logging을 발생시키지 않음  
  - 의존성 footprint는 아직 원하는 만큼 작지 않으며 개선 작업 중임  
- 라이브러리 코드에서 직접 로그를 쓰면 안 됨  
  - logging framework를 import해 `stdout`이나 `stderr`에 직접 쓰는 대신 callback, logger parameter, 호출자가 라우팅할 수 있는 로그 메시지 데이터 타입을 제공해야 함  
  - 로그가 어디로 가는지는 애플리케이션의 운영 환경에 속한 결정임  
  - Mercury는 구조화 로그 파이프라인을 observability stack으로 보내며, 라이브러리가 `stderr`에 직접 쓰면 JSON lines 스트림과 별도 배관이 필요해짐  
- `.Internal` 모듈 노출도 고려할 수 있음  
  - 사용자가 내부 API에 의존해 refactor가 어려워질 수 있다는 우려는 타당함  
  - 하지만 공개 API가 모든 사용 사례를 이미 맞혔다는 확신은 드물게만 정당화됨  
  - 명시적 안정성 경고가 있는 `.Internal` 모듈은 사용자가 패키지를 fork하고 vendoring하는 것보다 나을 수 있음  
  - `containers`, `text`, `unordered-containers`는 Haskell 생태계에서 이런 방식을 쓰는 좋은 예임  
  - 다만 사용자가 조용히 내부 모듈을 써서 필요한 것을 해결하면 공개 API 결함에 대한 feedback이 줄어들 수 있음  
  
### 타입에 넣지 않는 것들  
- 프로덕션 Haskell에도 아름답지 않은 부분이 존재함  
- `unsafePerformIO`는 일상적으로 의존하는 라이브러리 내부에서 쓰임  
  - `bytestring`과 `text`는 내부적으로 가변 버퍼를 할당하고, 쓰고, freeze해 결과를 만듦  
  - 타입은 생성 중 어떤 일이 있었는지 말하지 않음  
  - 경계는 관례, 신중한 reasoning, 코드 리뷰로 유지됨  
- 타입 안전한 대안이 성능이나 복잡성 비용을 지나치게 만들면 직접 이런 타협을 작성할 수도 있음  
  - 타입이 확인하지 않는 불변식을 문서화해야 함  
  - 불편함을 유지하고, 타입 안전한 대안이 실용적이 됐는지 주기적으로 재검토해야 함  
  - production Haskell은 타협 부재가 아니라 타협의 **규율 있는 격리**임  
- Hackage의 많은 Haskell 라이브러리에는 테스트가 적거나 없음  
  - “컴파일되면 동작한다”는 생각은 작은 순수 코드와 강한 타입에서는 가끔 맞을 수 있음  
  - IO-heavy 코드, 외부 시스템 연동, 구조보다 의미에 버그가 있는 코드에는 거의 맞지 않음  
- 타입은 `Either ParseError Transaction`을 반환한다는 것은 말할 수 있지만 다음은 말할 수 없음  
  - `amount` 필드를 센트로 파싱하는지 달러로 파싱하는지  
  - 파트너 API가 생략된 필드와 null 필드를 다르게 해석하는지  
  - 재시도 로직이 윤일의 특정 타이밍 창에서 이중 과금을 일으키는지  
- production에서는 이런 라이브러리 위에 시스템을 만들며, 검증되지 않은 가정을 물려받으므로 자기 계층의 integration test로 보완해야 함  
- orphan instance, 문맥상 total이라고 믿는 partial function, 도달 불가능하다고 약속한 `error`, 어색한 FFI wrapper, 수작업 exception hierarchy 같은 타협도 누적됨  
- 목표는 도덕적 순수성이 아니라, 모든 타협이 어디에 있고 왜 만들어졌으며 제거하면 무엇이 깨지는지 코드 리뷰, 문서, 예제, 테스트로 알 수 있게 하는 것임  
  
### Haskell을 프로덕션에서 쓸 가치  
- Haskell은 첫날부터 빠른 선택은 아님  
  - 현재 생태계는 Next.js나 Rails처럼 batteries-included hot-reloading 개발 환경을 즉시 제공하지 못함  
  - 필요한 라이브러리가 없거나, 있어도 한 사람이 spare time에 유지할 수 있음  
  - 오류 메시지가 매우 난해할 때가 있음  
- 채용 문제는 과장되어 있음  
  - Mercury CTO Max Tagher는 backend Haskell engineer가 Mercury 전체에서 가장 채용하기 쉬운 역할이라고 공개적으로 말한 바 있음  
  - Haskell 일자리에 대한 수요가 공급보다 많아 일반적인 채용 역학이 뒤집힘  
  - Mercury는 Haskell 경험이 깊은 사람과 전혀 없는 사람을 모두 채용하며, 후자는 **6~8주** 교육 프로그램으로 생산성을 갖추게 함  
  - 내일 Haskell 전문가 100명이 필요하다면 채용 풀 문제는 현실적이지만, 좋은 범용 개발자를 뽑아 가르칠 의지가 있다면 덜 현실적임  
- 더 큰 채용 리스크는 풀의 크기가 아니라 **성향**임  
  - Haskell은 정확성과 추상화에 신경 쓰고 논문을 즐겨 읽으며 기존 가정을 의심하는 이상주의자를 끌어들임  
  - 이 강점이 통제되지 않으면 production 책임이 될 수 있음  
  - 데이터베이스 계층을 새로운 타입 수준 관계대수 인코딩으로 다시 쓰려 하거나, throwaway script의 `String` 대신 `Text`를 쓰지 않았다고 merge를 거부하거나, 모든 설계를 최신 논문식 total rewrite로 끌고 가는 태도는 팀을 느리게 함  
- production Haskell에는 **실용주의 문화**가 필요함  
  - 타입 시스템은 전동 공구이지 종교가 아님  
  - 이미 좋은 해법이 있는 문제를 새 메커니즘 발명 기회로 삼는 것은 production에 맞지 않음  
- 수익은 시간이 지나며 나타남  
  - 동적 타입 코드베이스에서 몇 주 걸릴 리팩터링이, 타입 변경 후 컴파일러가 모든 call site를 알려줘 몇 시간에 끝날 수 있음  
  - 새 엔지니어가 타입 시그니처를 읽고 모듈의 계약을 이해할 수 있음  
  - 불가능한 상태가 실제로 표현 불가능해서 production incident가 발생하지 않을 수 있음  
- Mercury는 투자 회수가 몇 년이 아니라 **몇 달 단위**로 나타난다고 봄  
  - 특히 금융 서비스에서는 데이터 무결성 버그 비용이 사용자 불만이 아니라 규제 지적과 타인의 돈으로 측정됨  
  - 타입 시스템이 위험을 제거하지는 않지만, 빠르게 성장하는 코드베이스에서 실수로 위험을 도입하기 어렵게 만드는 도구를 제공함  
- Haskell의 production 가치는 은탄환이나 도덕적 운동이 아니라, 다양한 Haskell 숙련도를 가진 팀도 위험한 장치를 경계 안에 두고, 운영 지식을 보존하며, 안전한 경로를 쉬운 경로로 만들 수 있게 하는 강력한 도구 집합에 있음

## Comments



### Comment 56786

- Author: neo
- Created: 2026-05-04T09:47:00+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47991802) 
* Haskell이 이런 걸 타입으로 강제하는 데 가장 강력한 축에 드는 언어인 건 맞지만, 같은 패턴은 **Rust**와 **TypeScript**에서도 꽤 잘 먹힘  
  User -> LoggedInUser -> AccessControlledLoggedInUser 같은 흐름으로 웹앱에서 반복되는 명백한 **권한 부여 버그**를 막는 방식도 좋아함  
  업계에서는 이 패턴이 엄청나게 덜 쓰이고 있다고 봄
  * 이건 Rust나 TypeScript에만 해당하지 않고, 사실상 거의 모든 언어에서 가능함  
    보안상 이스케이프 전/후 문자열을 구분해야 한다면 동적 타입 언어에서도 Escaped 클래스로 감싸고 `escape(str)->Escaped`, `dangerouslyAssumeEscaped(str)->Escaped` 같은 함수를 둘 수 있음  
    성능 비용이 있으니 절충은 필요하지만 가능함  
    또 다른 방식은 **Application Hungarian**이고, 다만 이건 컴파일러보다 프로그래머의 규율에 더 의존함: [https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...](<https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/>)
  * 이건 타입 시스템 자체보다 **어포던스**의 문제에 가까움  
    예를 들어 C#에서도 충분히 할 수 있지만, 실제 타입 정의보다 시각적 잡음이 더 커지는 식임
  * Rust와 TypeScript도 당연히 **Haskell**의 영향을 크게 받았음  
    다만 “모나드는 무서우니 튜토리얼을 써야겠다” 같은 효과를 피하려고 그걸 굳이 말하지 않고 이름도 다르게 부르는 편임  
    모나드보다는 타입 클래스 같은 쪽 영향이 더 큼
  * TypeScript에서 이게 정말 잘 된다고는 확신이 안 듦  
    **명목 타입**이 없어서 원시 타입을 감싸는 newtype 같은 걸 만들려면 꽤 해키한 주문을 기억해야 함  
    내 경험상 이런 타입 안전성을 강제하는 데는 OCaml이 Rust보다 더 강력했음  
    GADT로 표현력이 더 크고, 다형 변이와 객체 타입/레코드 행 타입으로 편의성도 있으며, 모듈 시스템과 펑터도 있음  
    가비지 컬렉션이면 충분한 영역에서는 Rust의 빌림 검사기 때문에 생기는 추상화 제약과 어려움도 피할 수 있음
  * “**잘못된 상태를 표현 불가능하게 만들라**”는 얘기와 같음: [https://news.ycombinator.com/item?id=40150159](<https://news.ycombinator.com/item?id=40150159>)

* 몇 년간 Haskell로 일하는 걸 정말 좋아했음  
  일부러 찾던 건 아니었지만 기회가 우연히 왔고, 흥미롭고 지적으로 자극적이었음  
  다만 안타깝게도 Haskell만 3년을 쓴 뒤에도 Rust에서의 생산성이 Haskell의 쉽게 **두 배**는 됨  
  Haskell에는 미리 알고 피해야 하는 함정이 더 많고, 작성자에 따라 거의 읽기 전용 언어처럼 소화하기 어려울 때가 있음  
  도구 체인은 종종 Nix와 결합되어 있는데 Nix 자체도 복잡한 괴물이고, 언어 확장은 사방에 퍼져 있는 느낌임  
  Cabal 파일도 별로고, 컴파일러 오류에 익숙해지는 데 시간이 걸림
  * 꽤 놀랍게도 내 경험은 거의 정반대였음  
    마지막 제품에서 백엔드를 Typescript에서 Rust로 옮기기 시작했는데, 크래시에 지쳐서였음  
    지금은 그걸 내가 저지른 가장 큰 기술적 실수 중 하나로 봄. 생산성이 엄청나게 느려졌기 때문임  
    Rust에서만 생긴 시간 낭비 예로는, 데이터베이스 연결을 열고 뭔가 한 뒤 닫는 식의 **고차 함수** 작성이 Haskell, TypeScript, JavaScript, C++, PHP에서는 사소한데 Rust에서는 Rust 전문가 친구들에게 물어봐도 사실상 불가능해서 포기하게 된 일이 있음  
    또 리팩터링을 시도해 하루 종일 타입 오류를 고치다가 최상위 파일에서 오류를 만나고, 알고 보니 설계의 기본 부분 때문에 전체 리팩터링이 불가능하다고 판단해 전부 되돌린 적이 여러 번 있음  
    게다가 Rust는 구체 타입 대신 **인터페이스로 값 사용**하는 일이 상황에 따라 고급 기법과 불가능 사이 어딘가에 있는, 내가 떠올릴 수 있는 유일한 현대 언어임  
    그래서 애플리케이션 코드, 즉 시스템 코드나 라이브러리 코드가 아닌 코드는 대략 Rust로 쓰면 안 된다는 결론에 도달함
  * 생산성이 전반적으로 2배였는지, 아니면 Rust에서 덜 생산적인 부분도 있었는지 궁금함  
    그리고 “읽기 전용”이라는 건 무슨 뜻인지도 궁금함

* 일반적인 인식과는 다르게, Mercury가 Haskell을 선택했고 초기 리더들이 Haskell에서 풍부한 경험을 가졌다는 점이 성공에 적지 않은 역할을 했을 수 있다고 봄  
  Mercury 고객 입장에서 이 회사는 내 도구함의 핵심 회사 중 하나이고, **Haskell 선택**이 그들의 진행, 개발, 전체 여정을 더 좋게 만들었다는 느낌을 지울 수 없음  
  물론 대부분의 언어에 대해 이런 주장을 할 수 있고, Haskell 같은 함수형 언어가 성공 공식이라는 뜻은 아님  
  하지만 “vibe coding”과 LLM 시대 이전에 이런 의도적 결정을 한 건 특히 선견지명이 있어 보이고, 글에서 자세히 다룬 엔지니어링 문화와 결합된 결과라고 봄
  * 오히려 성공 요인은 **스타트업 지향 핀테크 초점**과 실행력일 가능성이 큼  
    나도 좋은 기술 문화를 좋아하지만, 훌륭한 기술 문화를 가진 회사가 나쁜 사업 초점 때문에 죽는 걸 봤음  
    더 나아가 스타트업식 핀테크 문화가 좋은 기술 문화를 낳았을 수도 있음  
    은행으로 출발하지 않았기 때문에, 예컨대 SVB와 달리 그렇게 보수적일 필요도 없었고 끔찍한 고대 기술 스택과 통합할 필요도 없었음  
    Haskell로 성공한 건 기쁘지만 Jane Street와 OCaml처럼, 회사가 믿게 만들고 싶어 하는 것과 달리 언어 선택은 사업 측면에서는 거의 우연에 가깝다고 봄  
    다만 프런트엔드는 뭘 쓰는지 궁금함. 아마 이 Haskell은 전부 백엔드일 것 같음
  * 해당 언어 경험이 없는 **제너럴리스트 채용**도 오히려 도움이 됐을 것 같음  
    새로 온 사람들에게 문화와 스타일을 처음부터 심을 수 있었기 때문임  
    vibe coding 이전이라면 그런 사람들 대부분은 아무 지시 없이 그냥 뛰어들어 해킹하려고 하지는 않았을 것임
  * 앱의 모든 것이 그냥 잘 동작한다는 점을 느꼈음  
    다른 서비스에서 넘어오면 정말 만족스러움

* 절친이 이 회사에서 일하는데, 밖에서 보기에도 **엔지니어링 문화**가 좋아 보임  
  Haskell은 이 일에 맞는 도구이고 강점을 잘 살리고 있다고 보지만, 성공의 상당 부분은 그냥 회사가 전반적으로 잘 운영되기 때문일 수도 있겠다는 생각도 듦
  * 글을 읽으며 받은 느낌도 그랬음  
    이 작성자라면 사실 어떤 언어를 쓰더라도 성공적인 엔지니어링 조직을 운영했을 것 같음
  * 함수형 프로그래밍 언어를 쓰면 더 높은 품질의 인력/지원자 풀이 걸러진다는 흔한 생각과도 배치되지는 않음

* 지금 **Real-World OCaml**을 읽고 있는데, 이미 몇 가지는 알고 있었지만 함수형 프로그래밍을 더 많이 배우는 중임  
  함수형 프로그래밍으로 놀랄 만큼 견고한 소프트웨어 조각을 만들 수 있어 보임  
  하지만 고민도 됨  
  현재 제품 백엔드는 NiceGUI로 동작하고 있고 역할을 잘 해냄  
  코드는 합리적이고 MVVM이며, 가장 중요한 일은 고객별로 웹소켓에 연결해 데이터를 소비하고 분석을 보여주는 것임  
  고객 수는 많지 않을 것이고, 웹사이트 방문자는 수십 명에서 많아야 수백 명 정도일 듯함  
  REPL이나 핫 리로드도 원하지만, 기능이 늘어나면 사용자 관리 패널, 더 많은 분석 등에서 함수형 프로그래밍이 데이터 파이프라인 변환에 잘 맞을 수도 있다는 건 알고 있음  
  다만 Haskell이나 OCaml은 정적 언어임  
  나중에 커지고 확장되면서도 동적인 걸 원한다면 Clojure나 Elixir가 좋은 선택일 것 같음  
  동시에 언젠가 리팩터링이 필요해지면 망가질까 봐 두려움  
  현재는 Python과 Mypy를 쓰고, 프런트엔드는 NiceGUI가 백엔드에서 생성함
  * OCaml은 모르겠지만 Haskell에서는 ghci/`cabal repl`로 개발 중인 웹앱을 매우 빠르게 다시 로드할 수 있음  
    솔직히 많은 Haskell 사용자들이 이걸 잘 활용하지 않는다고 봄

* Scheme, 나중에는 Racket이라는 비교적 비주류 언어로 비슷한 시스템을 작업한 적이 있는데, 규모는 커졌지만 작은 팀이 오랫동안 관리 가능하고 빠른 속도를 유지했음  
  버그는 많이 만들지 않았고, 보통 기능을 아주 빠르게 추가할 수 있었음  
  예를 들어 민감한 데이터를 AWS에 호스팅하기 위한 어떤 인증을 가장 먼저 달성했음  
  가끔은 인기 플랫폼에서는 기성 구성요소로 해결할 일을 처음부터 만들어야 해서 기능 추가가 느릴 때도 있었음  
  하지만 일단 만들고 나면 잘 동작했고, 다시 예전 속도로 돌아갔으며 수십 개 기성 프레임워크의 비대함과 복잡성에 느려지지 않았음  
  관리 가능한 플랫폼을 직접 통제했기 때문에 필요가 생겼을 때 AWS로 빠르게 이동할 수도 있었음  
  시스템에는 처음부터 복잡한 데이터와 웹 상호작용을 위한 **아키텍처 비법**도 있었고, 이게 많은 기능을 빠르게 개발하게 해줬으며 이후에도 똑똑한 방향으로 힘을 실어줬음  
  Haskell 핀테크와 다른 점은 팀 규모가 매우 작았다는 것임  
  한 번에 소프트웨어 엔지니어는 2~3명뿐이었고 운영을 모두 맡는 사람이 있었음  
  그래서 수백 명이 조율하면서 일관된 시스템을 유지해야 하는 어려움은 없었음  
  보통 한 명은 더 기술적이고 아키텍처적인 코드 변경을 맡고, 다른 한 명은 복잡한 프로세스에 대한 방대한 비즈니스 로직 기능을 빠르게 추가했음  
  현재나 가까운 미래의 LLM류 AI 도구를 신중히 쓰면, 소프트웨어 개발에서도 매우 작고 엄청나게 효과적인 팀의 효율을 일부 얻을 수 있을 것 같음  
  떠오르는 모델은 스토리 포인트를 없애려고 거대한 비대를 양산하고 지속 가능성은 남의 문제로 미루는 게 아니라, 소수의 아주 날카로운 사고자들이 시스템을 힘을 실어주면서도 관리 가능한 길에 계속 올려두는 방식임

* 양날의 검임  
  **200만 줄**은 대단한 성취지만, 동시에 상당한 유지보수 부담이기도 함  
  Haskell의 장점은 이론적으로 명확하지만 단점은 직관하기 더 어려움  
  유혹은 모든 것을 타입으로 모델링하는 데 있음  
  코드베이스 자체가 애플리케이션이 아니라 **비즈니스 명세**가 되어버림  
  정책 변경마다 큰 리팩터링이 되고, Haskell의 안전성 덕에 놀랄 만큼 손이 많이 가는 경우도 있음  
  결국 둘 다 가질 수는 없고, 언젠가는 타입에 갇히게 됨  
  Haskell은 특히 이 규모에서 정말 인상적이고 강력하지만 고유한 문제도 가져옴  
  비즈니스 로직을 타입으로 모델링하려는 유혹은 경직된 구조를 만들고, 그 구조가 주는 안전성은 다른 종류의 위험을 보지 못하게 할 수 있음
  * 취향 있는 경험 많은 엔지니어들이 핵심 부분을 만든다면 그 선을 꽤 잘 탈 수 있음  
    전부 가질 수는 없지만 많은 건 가질 수 있음  
    몇 년 전 Jane Street에서 인턴을 했는데, Haskell이 아니라 OCaml이었지만 그 균형을 정말 잘 잡는 것처럼 보였음  
    내재적 복잡성이 높고 신뢰성과 정확성이 사업 존립과 직결되는 영역인데도 놀랄 만큼 빠르게 움직였음  
    돌이켜보면 Jane Street의 핵심은 Stephen Weeks처럼 훌륭한 취향을 가진 경험 많은 **OCaml 프로그래머**를 고용하고, 그들이 처음부터 핵심 라이브러리를 만들고 전체 코드베이스를 이끌게 한 데 있었음  
    안타깝게도 Mercury는 이 부분을 그만큼 잘하지는 못했음
  * TypeScript도 마찬가지임: [https://www.richard-towers.com/2023/03/11/typescripting-the-...](<https://www.richard-towers.com/2023/03/11/typescripting-the-technical-interview.html>)  
    솔직히 튜링 완전한 타입 시스템의 가장 큰 단점은 이론상 컴파일하면 먼지가 되는 애플리케이션을 구현할 수 있다는 것임

* Bellroy의 비슷한 Haskell 성공 사례가 곧 열리는 **Melbourne Compose** 모임 주제임: [https://luma.com/uhdgct1v](<https://luma.com/uhdgct1v>)

* 함수형 프로그래밍에서 내가 겪는 문제는 **디버깅**임  
  더 정확히는 명령형 프로그래밍, 특히 절차형 방식의 강점이라고 봄  
  함수형/선언형 스타일에서는 보통 무언가가 어떻게 만들어지는지가 아니라 어떤 상태여야 하는지를 설명하고, 언어가 모든 것을 조립해 최종 결과를 내게 함  
  모든 걸 제대로 했다면 좋고 더 나을 수도 있지만, 그렇지 않아서 기대한 결과가 안 나오면 버그를 어떻게 찾을지가 문제임  
  C 같은 언어에서는 비교적 단순함  
  한 줄씩 따라가며 각 단계 사이의 실행 상태, 사실상 RAM을 보고 기대와 다르면 그 줄에서 뭔가 잘못된 것이므로 들어가서 그렇게 진행하면 됨  
  함수형 프로그래밍처럼 언어가 상태를 숨기려고 할수록 이건 더 어려움  
  글에서 가장 긴 섹션이 이 문제, 즉 “design for introspection”인 것도 흥미로움  
  작성자는 코드를 디버깅 가능하게 만들기 위해 일부러 많은 노력을 해야 했고, Haskell의 종종 간과되는 실용적 사용에 대한 좋은 통찰을 줌
  * 내 디버깅 요령은 중요도가 조금이라도 있는 모든 코드가 같은 입력에 대해 같은 출력을 반환하게 만드는 것임  
    사소한 코드도 마찬가지임  
    다른 주류 언어는 여기에 근접하지 못함  
    공유 메모리 동시성처럼 그렇게 쓸 수 없는 상황은 트랜잭션을 씀  
    이것도 다른 주류 언어는 근접하지 못함  
    여기에 null 없음, 암묵적 정수 캐스팅 없음 같은 쉬운 이점은 넣지도 않았음  
    Haskell 코드 디버깅이 다른 언어보다 어렵다는 건 완전히 맞음  
    하지만 하위 90%의 발목잡이를 없애면 당연히 그렇게 될 수밖에 없음
  * 함수형 프로그래밍 디버깅은 명령형 프로그래밍과 달리 **REPL 주도**인 경우가 많음  
    물론 함수형에만 고유한 건 아니고, Python이나 JavaScript 같은 대체로 명령형인 언어에서도 Python 셸, 브라우저 콘솔, Node/Deno/Bun 셸, 노트북 등을 첫 디버깅 계층으로 쓰는 일이 많음  
    REPL 중심 디버깅에는 흥미로운 절충이 있음  
    C 같은 언어에서는 전체 프로그램 디버깅과 중단점에서 시작해 문제가 있을 것 같은 정확한 지점을 맞히려는 경우가 많음  
    REPL 중심 세계에서는 프로그램의 구성요소를 REPL에서 직접 더 많이 테스트할 수 있게 만들려 함  
    그래서 모듈/API/타입 경계가 디버깅 가능성과 닮아감  
    C/C++ 같은 명령형 언어보다 이런 경계를 제대로, 쓰기 쉽게 만드는 압력이 더 클 때가 있음  
    반대로 전체 프로그램 우선 디버깅에 비해, 현실의 이상한 시나리오에서 단위 간 복잡한 통합 문제를 분리하기 더 어려워질 때도 있음  
    하지만 REPL 우선 접근은 통합 **표면적**을 최소로 줄이도록 유도하는 경우가 많아, 함수형 언어에서는 명령형 언어에서 보이는 통합 효과가 덜 나타나기도 함  
    함수형 언어가 상태를 숨긴다는 표현은 맞지 않음  
    이 언어들도 명령형 하드웨어에서 실행되고 실제 하드웨어 상태를 다룸  
    어느 지점에서는 두 세계 사이의 번역이 있으며, 아마 생각보다 그렇게 다르지도 않음  
    필요하면 여전히 명령형 중단점과 명령형 디버거로 돌아갈 수 있음  
    그래서 “REPL 주도” 디버깅이라고 부름  
    REPL로 문제 있는 단위, 즉 정확한 모듈/API/함수와 놀라운 출력을 내는 입력을 좁힐 수 있음  
    소스만 보고 버그가 안 보이면 명령형 디버거로 보내 거의 같은 한 줄씩 실행 경험을 볼 수 있고, 추가 맥락을 얻을 수도 있음  
    그 시점에는 이미 REPL로 충분히 좁혀서 단위 자체가 작고 좁기 때문에 좋은 중단점을 고를 필요도 별로 없을 가능성이 큼  
    글의 “design for introspection” 섹션에서 받은 메시지는 잘못 잡은 것 같음  
    그 섹션은 디버깅 가능성이 아니라 **관측 가능성**에 대한 것임  
    로깅/텔레메트리 시스템을 올바르게 연결하고, 테스트 중 가짜를 모킹하고, 개별 라이브러리에 맡기는 대신 재시도/회로 차단기를 시스템 전체 수준에서 추가하는 얘기였음  
    명령형 세계에서도 이건 디버깅 문제가 아니라 의존성 주입, 미들웨어 설치, 공개 API 경계에서 구체 클래스보다 추상 인터페이스를 쓰는 식의 분해 문제임  
    이런 설계 제안은 리팩터링이고, 디버깅 가능성보다는 남의 공개 API에 관측 가능성 미들웨어를 얼마나 쉽게 설치할 수 있는지에 영향을 줌

* Haskell **200만 줄**이 대체 뭘 하고 있을지 상상하기 어려움  
  코드가 정말 많은데, Haskell은 적은 코드로 많은 일을 할 수 있는 “촘촘한” 언어라는 인상이 있음  
  JSON 직렬화/역직렬화, REST API 프레임워크, 로깅 같은 걸 위한 라이브러리가 많아서 그런 걸까 싶음
  * 원문에 따르면, 계측할 수 없는 코드는 신뢰할 수 없다는 게 문제임  
    서드파티 바인딩이 구체 함수로 HTTP 호출을 하면 추적을 추가할 방법도 없고, SLO에 맞춘 타임아웃을 주입할 방법도 없고, 테스트에서 파트너 장애를 시뮬레이션할 방법도 없으며, 추적의 400ms 공백을 이론으로 때려맞히는 것 말고 설명할 방법도 없음  
    그래서 직접 작성함  
    초기에 일이 더 많지만, 직접 만든 클라이언트는 처음부터 그렇게 만들었기 때문에 **관측 가능하도록 구성**됨
  * “촘촘하다”고 부른 성질은 보통 **표현력이 높다**고 함  
    상대적으로 매우 추상적인 생각을 적은 문자로 표현할 수 있다는 뜻임  
    어떤 사람들은 이것을 “고수준”이라고도 부름  
    다만 200만 줄은 처음 들리는 것만큼 많은 코드는 아니라고 봄  
    특히 금융처럼 규제가 많은 영역의 회사이고 몇 년간 쌓인 코드라면 더 그렇음
  * 객관적 지표는 전혀 아니지만, Haskell은 그냥 **종횡비**가 다르다고 느꼈음  
    줄 수는 어느 정도 적을 수 있지만, 단어 수는 더 명령형인 객체지향 언어들과 대체로 비슷함
  * 코드베이스가 실제로 어떤지는 모르지만, Haskell이 간결하다는 평판은 일부는 학계나 범주론 쪽에서 과대표집되기 때문임  
    그쪽에서는 `St M -> C T` 같은 표현도 괜찮지만, 실제 소프트웨어에서는 `TransactionState Debit -> Verified Transaction`처럼 쓰는 게 훨씬 유용함  
    또 다른 부분은 LISP까지 거슬러 올라가는 문화적 요인임  
    사람들이 이해하기 어려운 요령이나 매크로로 줄 수를 아끼려고 지나치게 영리하게 구는 경향이 있음  
    Mercury 같은 금융 회사에서는 이런 방식보다 명확성과 가독성이 장려될 것 같음  
    예컨대 린터가 `>>`와 `>>=`로 한 줄에 쓰는 대신, 모나드 코드를 꼼꼼한 여러 줄짜리 `do` 표현식으로 나누게 할 수도 있음
