본문으로 건너뛰기
피드

머큐리가 하스켈 200만 줄로 핀테크 백엔드를 굴리는 법

backend 약 19분

핀테크 기업 머큐리가 200만 줄 규모의 하스켈 코드베이스를 실제 금융 서비스에서 어떻게 운영하는지 풀어낸 글이다. 핵심은 '순수함' 자체가 아니라 위험한 동작을 타입과 인터페이스 경계 안에 가두고, 조직의 운영 지식을 컴파일러가 읽을 수 있는 형태로 남기는 데 있다. Temporal, OpenTelemetry, 함수 레코드, 도메인 에러 모델링 같은 실전 패턴이 꽤 구체적으로 나온다.

  • 1

    머큐리는 2025년에 거래액 2,480억 달러를 처리했고, 하스켈 코드만 약 200만 줄을 운영 중이다.

  • 2

    하스켈의 타입 시스템을 정답 증명 도구보다 운영 지식 보존 장치로 본다.

  • 3

    트랜잭션, 이벤트 발행, 상태 전이처럼 실수하면 돈이나 규제 문제가 터지는 부분은 타입으로 강하게 막는다.

  • 4

    Temporal을 도입해 크론 잡과 데이터베이스 기반 상태 머신을 내구 실행 워크플로로 대체했다.

  • 5

    운영 가능한 라이브러리를 만들려면 계측, 로깅, 내부 모듈 같은 탈출구를 제공해야 한다.

머큐리의 숫자부터 꽤 세다

  • 머큐리는 하스켈로 약 200만 줄짜리 백엔드 코드베이스를 굴리는 핀테크 회사임

    • 2025년 거래 처리 규모는 2,480억 달러
    • 연환산 매출은 6억 5,000만 달러
    • 30만 개 이상 비즈니스 고객을 서비스 중
    • 직원은 약 1,500명이고, 엔지니어 대부분은 입사 전 하스켈을 한 줄도 안 써본 제너럴리스트라고 함
  • 글쓴이는 이 상황이 상식적으로는 좀 무서워 보여야 한다고 인정함

    • 돈이 오가는 금융 시스템
    • 빠르게 커지는 조직
    • 하스켈 초보가 계속 합류하는 팀
    • 그런데 실제로는 수년간 운영됐고, 실리콘밸리은행 사태 때 5일 만에 20억 달러 신규 예금이 몰리는 상황도 버텼다고 함
  • 이 글의 핵심은 “하스켈은 아름답다”가 아니라 “운영에서 계속 살아남게 만드는 구조가 뭐냐”에 가까움

    • 타입 시스템은 버그를 없애는 마법이 아니라, 운영 지식을 오래 보존하는 장치로 봐야 한다는 쪽
    • 위키나 슬랙 스레드에 묻힌 암묵지를 컴파일러가 읽을 수 있는 인터페이스로 바꾸는 게 진짜 가치라는 얘기임

중요

> 머큐리의 결론은 꽤 현실적임. 하스켈의 장점은 순수함 그 자체가 아니라, 위험한 일을 좁은 경계 안에 가두고 안전한 길을 기본 경로로 만드는 데 있음.

신뢰성은 “안 터짐”이 아니라 “변화를 흡수함”

  • 머큐리는 신뢰성을 실패 방지만으로 보지 않음

    • 테스트를 쓰고, 버그를 잡고, 나쁜 케이스를 막는 건 당연히 필요함
    • 하지만 진짜 운영 시스템은 데이터베이스가 느려지거나, 워커가 죽거나, 사람이 바뀌어도 어느 정도 흡수할 수 있어야 함
  • 글에서는 이를 “적응 능력(adaptive capacity)”이라고 부름

    • 시스템이 점진적으로 성능 저하를 하느냐
    • 운영자가 지금 무슨 일이 벌어지는지 이해하고 조정할 수 있느냐
    • 아키텍처가 맞는 선택은 쉽게, 틀린 선택은 어렵게 만드느냐가 중요함
  • 빠르게 성장하는 회사에서는 이 문제가 더 날카로움

    • 회사가 매년 2배 성장하면 언제나 동료 절반은 1년 미만 경력자가 된다는 얘기를 인용함
    • 결국 “그건 원래 이렇게 해야 해요” 같은 지식은 순식간에 사라짐
    • 그래서 새로 온 엔지니어가 모듈 타입만 보고도 계약과 제약을 이해할 수 있어야 함

순수함은 성질이 아니라 경계다

  • 글쓴이의 첫 번째 강한 주장은 “하스켈이 순수하다”보다 “순수함은 인터페이스가 강제하는 경계다”라는 쪽임

    • bytestring, text, vector 같은 라이브러리 내부에는 mutable allocation, buffer write, unsafe coercion 같은 위험한 동작이 들어감
    • 그래도 외부에서 순수 함수처럼 쓸 수 있는 이유는 그 위험이 밖으로 새지 않게 타입 경계가 잡혀 있기 때문임
  • 대표 예시로 runST를 듦

    • 내부에서는 변경 가능한 참조와 부수효과가 생김
    • 하지만 rank-2 타입 덕분에 내부 상태를 나타내는 타입 변수가 밖으로 탈출하지 못함
    • 결과만 밖으로 나오고, 변경 가능한 세계는 경계 안에 갇힘
  • 이 원칙은 프로덕션 전체에 적용된다고 봄

    • 데이터베이스 계층은 커넥션 풀, 재시도, mutable state를 가질 수 있음
    • 캐시는 동시성 맵을 쓸 수 있고, HTTP 클라이언트는 circuit breaker와 커넥션 풀을 가질 수 있음
    • 문제는 mutation 자체가 아니라 “어디에 있고, 코드베이스의 몇 퍼센트가 그걸 알아야 하느냐”임

올바른 절차를 타입으로 강제하기

  • 큰 코드베이스에서 자주 터지는 문제는 “순서”와 “빠뜨리면 안 되는 부가 작업”임

    • 트랜잭션 뒤에 감사 로그를 flush해야 함
    • 엔드포인트 호출 전 feature flag를 확인해야 함
    • 알림 이벤트는 데이터베이스 트랜잭션 안에서 큐에 넣어야 함
  • 이런 규칙을 위키에만 적어두면 언젠가 무조건 새어 나감

    • 글에서는 이걸 운영 주문(incantation)이라고 표현함
    • 시니어가 팀을 옮기거나 휴가를 가거나, 새 엔지니어가 규칙의 존재 자체를 모르면 바로 사고 후보가 됨
  • 머큐리식 해법은 “잊을 수 없는 API”를 만드는 것임

    • 예를 들어 데이터베이스 write와 이벤트 발행이 반드시 함께 일어나야 한다면, 둘을 따로 호출하는 함수를 열어두지 않음
    • record, emit, commit 같은 연산을 하나의 Transact 흐름 안에 넣고, 커밋 경로가 이벤트 발행을 포함하게 설계함
    • 그러면 개발자는 올바른 절차를 외우는 게 아니라, 그냥 제공된 문 하나로만 나가게 됨
  • 중요한 건 수학적으로 거창한 증명이 아니라 평범하게 바쁜 엔지니어가 실수하기 어렵게 만드는 것임

    • 특히 돈, 감사 로그, 규제, 정산처럼 조용히 틀리면 크게 터지는 영역에서는 이게 진짜 값어치를 함

Temporal로 수제 상태 머신을 줄임

  • 금융 시스템의 워크플로는 단일 트랜잭션 안에 얌전히 머물지 않음

    • 결제를 보내고
    • 파트너 응답을 기다리고
    • 원장을 업데이트하고
    • 고객에게 알리고
    • 취소, 타임아웃, 네트워크 무응답, 워커 크래시까지 처리해야 함
  • 머큐리는 예전에는 데이터베이스 기반 상태 머신, 크론 잡, 백그라운드 워커, 여기저기 흩어진 재시도 로직으로 이걸 처리했다고 함

    • “동작은 했지만 fragile했다”는 평가
    • 운영 사고의 비중도 컸다고 함
  • 그래서 Temporal을 내구 실행(durable execution) 프레임워크로 도입함

    • 워크플로는 일반적인 순차 코드처럼 작성함
    • Temporal이 각 단계를 이벤트 히스토리에 기록함
    • 워커가 중간에 죽으면 다른 워커가 결정적인 prefix를 재생해서 상태를 복원하고 이어서 실행함
    • 재시도, 타임아웃, 취소, 에러 처리는 각 팀이 매번 대충 다시 만들지 않고 플랫폼이 담당함
  • 하스켈과도 개념적으로 잘 맞는다고 봄

    • Temporal workflow는 이벤트 히스토리에 대한 순수 함수처럼 볼 수 있음
    • 같은 입력이면 같은 명령 시퀀스를 만들어야 하는 determinism 요구사항이 있음
    • 실제 부수효과는 activity에 격리되고, workflow는 오케스트레이션만 맡음
sequenceDiagram
    participant 워크플로
    participant 이벤트히스토리
    participant 워커
    participant 액티비티
    participant 외부서비스
    워크플로->>이벤트히스토리: 단계와 명령 기록
    워커->>워크플로: 결정적 상태 재생
    워크플로->>액티비티: 부수효과 실행 요청
    액티비티->>외부서비스: 결제/파트너 호출
    외부서비스-->>액티비티: 응답 또는 실패
    액티비티-->>워크플로: 결과 반환
    워크플로->>이벤트히스토리: 다음 상태 저장
  • 머큐리는 hs-temporal-sdk도 오픈소스로 공개함
    • Temporal의 공식 Core SDK는 Rust로 되어 있고, Haskell SDK는 FFI로 감싼 구조
    • 워크플로, 액티비티, 워커를 Haskell 네이티브 API로 정의할 수 있게 만든 것

도메인은 전송 계층에 끌려다니면 안 됨

  • 글에서 꽤 공감 가는 예시가 HTTP status code exception임

    • 처음에는 HTTP handler 안에서만 쓰니까 409 Conflict를 던져도 이상하지 않음
    • 그런데 그 코드가 크론 잡, 큐 워커, Temporal workflow에서 재사용되기 시작하면 갑자기 이상해짐
    • 크론 잡이 409를 던지는 건 호출자가 있는 웹 요청과 전제가 완전히 다르기 때문임
  • 해결은 도메인 에러를 도메인 타입으로 모델링하는 것임

    • 잔액 부족은 InsufficientFunds
    • 중복 요청은 DuplicateRequest
    • 파트너 타임아웃은 PartnerTimeout
    • HTTP 응답, 워커 재시도 전략, 로그 메시지는 각 경계에서 얇게 변환함
  • 이 분리를 빨리 할수록 싸고, 늦게 할수록 추해짐

    • 나중에는 HTTP 예외를 비즈니스 로직으로 catch하는 코드가 생김
    • 그 순간부터 abstraction이 경계를 탈출한 상태가 됨

타입에 너무 많이 넣는 것도 비용이다

  • 글은 타입 인코딩을 강하게 권하면서도 과용을 꽤 세게 경계함

    • 타입에 넣은 불변식은 런타임 비용이 아니라 인지 비용과 변경 비용을 만든다고 봄
    • 요구사항은 변하고, 타입은 그 변화를 모든 호출자에게 전파함
  • 타입으로 강제할 만한 것은 “조용한 데이터 오염”을 막는 불변식임

    • 이벤트 없이 트랜잭션이 커밋됨
    • 감사 로그 없이 결제가 처리됨
    • 의미적으로 불가능한 상태 전이가 겉보기에는 정상 데이터처럼 저장됨
    • 이런 건 장애가 늦게 발견되므로 타입으로 막을 가치가 큼
  • 반대로 즉시 크게 실패하는 건 런타임 체크와 좋은 에러 메시지로 충분할 수 있음

    • 잘못된 JSON 경계
    • 명백한 assertion 실패
    • 바로 500으로 드러나는 케이스
  • 머큐리 내부에도 GADT, type family, phantom type을 쓰는 복잡한 라이브러리가 있다고 함

    • 다만 그 복잡성은 작은 모듈 안에 캡슐화함
    • 사용하는 쪽 API는 평범한 함수 몇 개처럼 보이게 만든다는 게 핵심
    • 타입 수준 증명 부담을 제품 엔지니어에게 흘려보내면 그건 추상화가 아니라 팀 생산성 저하로 돌아옴

💡

> 글의 기준은 단순함. 틀리면 돈이 잘못 움직이거나 규제 문제가 생기면 타입으로 막고, 그냥 크게 실패해서 빨리 보이는 문제면 런타임 체크도 실용적인 선택임.

관측 가능성은 나중에 뿌리는 양념이 아님

  • 머큐리는 “보이지 않는 코드는 운영할 수 없다”는 입장을 강하게 냄

    • Haskell은 monkey patching이 없음
    • 런타임에 라이브러리 내부 HTTP 클라이언트를 바꿔치기해 timing을 찍는 식의 탈출구가 없음
    • Rust도 비슷한 제약이 있지만, Rust 생태계는 tower middleware 패턴이 꽤 자리를 잡았다고 비교함
  • Haskell 라이브러리가 concrete top-level function만 노출하면 계측이 힘들어짐

    • 사용자가 wrapper module을 만들고 “제발 다들 이걸 import하자”에 기대야 함
    • 글쓴이는 이런 희망을 아키텍처 패턴으로 보지 않음. 맞는 말임
  • 선호하는 패턴은 함수 레코드(records of functions)임

    • sendRequest :: Request -> IO Response 하나를 바로 노출하는 대신, HttpClient 레코드 안에 sendRequest, getManager 같은 필드를 둠
    • 그러면 호출자가 특정 함수를 감싸서 tracing, timeout, fault injection, retry, mock을 붙일 수 있음
    • 라이브러리 소스 수정 없이 운영 환경에 맞게 behavior를 조합할 수 있음
  • WAI의 Middleware = Application -> Application도 좋은 예로 듦

    • behavior를 변환하는 함수이기 때문에 합성이 쉬움
    • 인터셉터도 각 필드가 endomorphism이면 Semigroup, Monoid로 합칠 수 있음
    • 머큐리는 retargeting, OpenTelemetry, Sentry, SQL application name, logging context, statement timeout 같은 인터셉터를 mconcat으로 합치는 식으로 쓴다고 함
  • effect system도 대안으로 언급함

    • effectful, polysemy, fused-effects, cleff 같은 선택지가 있음
    • operation을 effect로 정의하고 production, test, tracing interpreter를 갈아끼울 수 있음
    • 다만 타입 레벨 effect list와 handler stack, 어려운 타입 에러가 따라오므로 함수 레코드보다 진입장벽은 높음

라이브러리 작성자에게 하는 현실적인 부탁

  • 패키지 작성자는 OpenTelemetry 계측 지점을 제공해 달라고 요청함

    • 특히 IO가 중요한 작업 주변에 span 몇 개만 있어도 프로덕션 운영자는 큰 도움을 받음
    • hs-opentelemetry-api는 SDK가 초기화되지 않으면 inert하게 동작하도록 설계됐다고 설명함
  • 라이브러리 코드에서 직접 로그를 stdout이나 stderr로 쓰지 말라는 얘기도 나옴

    • 애플리케이션마다 구조화 로그 파이프라인, 수집기, observability stack이 다름
    • 라이브러리가 로그 목적지를 결정하면 운영 환경 결정을 침범하는 셈임
    • callback, logger parameter, log message data type을 제공하고 애플리케이션이 라우팅하게 해야 함
  • .Internal 모듈 노출도 권장함

    • 논란이 있는 조언이라는 건 인정함
    • 내부 모듈이 사실상 API가 되어 리팩터링을 어렵게 만들 수 있기 때문임
    • 그래도 사용자가 fork하거나 vendoring하는 것보다는, 안정성 경고가 붙은 internal module을 쓰는 편이 나을 때가 많다고 봄

하스켈도 타협 없이 굴러가진 않음

  • 글은 하스켈 생태계의 불편한 부분도 꽤 솔직하게 적음

    • unsafePerformIO는 실제 라이브러리 내부에서 쓰임
    • bytestring, text도 내부에서 mutable buffer를 만들고 freeze하는 식의 최적화를 함
    • 타입이 모든 걸 말해주진 않으므로, 문서화와 리뷰, 테스트로 보완해야 함
  • “컴파일되면 맞다”는 감각도 위험하다고 못 박음

    • 작은 순수 코드에서는 꽤 맞을 수 있음
    • 하지만 IO-heavy 코드, 외부 시스템 연동, 의미론적 버그에는 타입만으로 부족함
    • 금액을 센트로 파싱했는지 달러로 파싱했는지, partner API가 null과 omitted field를 어떻게 구분하는지는 타입이 알려주지 않음
  • Hackage의 일부 라이브러리에 테스트가 거의 없다는 문제도 언급함

    • 프로덕션에서는 이런 미검증 가정을 자기 계층의 integration test로 보완해야 함
    • 결국 하스켈 운영은 순수함의 승리가 아니라, 타협이 어디 있고 왜 필요한지 계속 관리하는 일에 가깝다는 결론임

그래서 하스켈을 쓸 가치가 있나

  • 글쓴이는 “첫날부터는 아니다”라고 답함

    • Next.js나 Rails처럼 배터리 포함 개발 경험을 바로 주지는 않음
    • 필요한 라이브러리가 없거나, 있어도 한 사람이 spare time에 유지하는 경우가 있음
    • 에러 메시지도 가끔 괴롭다고 함
  • 채용 문제는 생각보다 다르게 본다고 함

    • 머큐리 CTO는 “백엔드 하스켈 엔지니어가 머큐리에서 가장 채용 쉬운 역할”이라고 말한 적이 있음
    • 하스켈 일자리에 대한 수요가 공급보다 많아, 일반적인 채용 역학이 뒤집힌다는 설명
    • 경험자뿐 아니라 좋은 제너럴리스트를 뽑아 6~8주 교육으로 생산성을 내게 한다고 함
  • 진짜 채용 리스크는 인원 수가 아니라 성향이라고 봄

    • 하스켈은 정확성과 추상화에 진심인 사람을 끌어들임
    • 이건 장점이지만, 통제되지 않으면 모든 문제를 논문식 타입 레벨 설계로 풀려는 생산성 리스크가 됨
    • 그래서 문화적으로 “타입 시스템은 전동 공구지 종교가 아니다”라는 실용주의를 키워야 한다고 말함
  • 투자 회수는 몇 년이 아니라 몇 달 단위로 온다고 주장함

    • 동적 타입 코드베이스에서 몇 주 걸릴 리팩터링이 하스켈에서는 타입 변경 후 컴파일러가 누락 지점을 다 알려줘 몇 시간 안에 끝날 수 있음
    • 새 엔지니어가 타입 시그니처만 보고 계약을 이해하는 것도 빠르게 커진 조직에서는 생존 수단임
    • 금융 서비스에서는 데이터 무결성 버그 비용이 고객 불만이 아니라 규제와 타인의 돈으로 측정되기 때문에 이 차이가 더 커짐

기술 맥락

  • 머큐리가 고른 핵심 선택은 “하스켈로 금융 백엔드를 짜자”가 아니라, 위험한 운영 절차를 타입과 모듈 경계 안에 가두자는 쪽이에요. 결제, 원장, 감사 로그처럼 조용히 틀리면 큰일 나는 영역에서는 사람이 기억하는 규칙보다 컴파일러가 강제하는 경로가 훨씬 오래 가거든요.

  • Temporal 도입도 같은 맥락이에요. 결제 플로우는 한 번의 데이터베이스 트랜잭션으로 끝나지 않고, 외부 파트너 응답, 재시도, 타임아웃, 워커 크래시까지 따라오잖아요. 머큐리는 이걸 크론 잡과 수제 상태 머신으로 계속 관리하기보다, 이벤트 히스토리와 재생 모델을 가진 플랫폼에 맡긴 거예요.

  • 관측 가능성 이야기가 길게 나오는 이유도 실무적이에요. Haskell은 런타임 monkey patching으로 라이브러리 내부를 대충 갈아끼우기 어렵기 때문에, 애초에 함수 레코드나 effect system처럼 감쌀 수 있는 표면을 열어둬야 해요. 그래야 OpenTelemetry span, timeout, fault injection을 나중에 붙일 수 있거든요.

  • 타입을 어디까지 쓸지도 결국 운영 비용의 문제예요. 머큐리는 돈이 잘못 움직이거나 규제 불변식이 깨지는 곳에는 GADT나 phantom type 같은 복잡한 도구도 쓰지만, 그 복잡성을 작은 내부 모듈에 가둬요. 제품 엔지니어가 매일 만지는 API까지 어려워지면, 안전을 사려다가 팀 전체 변경 속도를 팔아버리는 셈이니까요.

이 글의 재미는 '하스켈 좋다'가 아니라 '하스켈을 회사가 망하지 않게 쓰려면 어디까지 타협해야 하나'에 있다. 타입 안정성, 관측 가능성, 조직 성장 문제가 한 덩어리로 연결된다는 점에서 백엔드 엔지니어라면 언어 취향과 무관하게 읽을 만하다.

댓글

댓글

댓글을 불러오는 중...

backend

비동기 러스트, 아직 MVP 상태에서 못 벗어났다는 꽤 아픈 지적

글쓴이는 비동기 러스트(async Rust)가 서버와 마이크로컨트롤러를 모두 커버하는 멋진 모델이지만, 컴파일러가 만드는 상태 기계가 아직 너무 비싸다고 지적한다. 특히 임베디드나 WASM처럼 바이너리 크기가 중요한 환경에서는 불필요한 panic 경로, 상태, 중복 MIR이 실제 비용으로 튄다.

backend

30살 된 FastCGI가 아직도 리버스 프록시 백엔드 프로토콜로 더 낫다는 주장

HTTP를 리버스 프록시와 백엔드 사이 프로토콜로 쓰는 관행이 desync 공격과 신뢰 헤더 문제를 계속 만든다는 글이다. 저자는 FastCGI가 1996년 나온 오래된 프로토콜이지만 명시적 프레이밍과 신뢰 정보 분리 덕분에 이 구간에서는 HTTP보다 안전한 선택일 수 있다고 주장한다.

backend

삼성SDS, 삼성전기 SAP ERP 클라우드 전환 — 다운타임 140시간을 34시간으로 줄였다

삼성SDS가 삼성전기의 차세대 SAP ERP 클라우드 전환 프로젝트를 완료. 국내 최초 RISE with SAP 프리미엄 서플라이어 기반 사례이고, Downtime Optimized Conversion 적용으로 8.5TB HANA DB 전환 다운타임을 76% 단축. DVM으로 DB 용량 35% 축소, 업무 효율 25% 이상 개선.

backend

월 $20 인프라로 MRR $10K 회사 여러 개 돌리는 법

VPS 하나, Go 바이너리, SQLite, 로컬 GPU, GitHub Copilot 조합으로 월 $20 이하의 인프라 비용으로 MRR $10K 넘는 회사를 여러 개 운영하는 개발자의 실전 플레이북. AWS 없이도 충분히 확장 가능한 아키텍처를 구축할 수 있음을 구체적 수치와 코드로 보여줌.

backend

소프트웨어 개발자를 위한 USB 입문 — 유저스페이스 드라이버 직접 만들기

커널 코드 없이 libusb를 사용해 유저스페이스에서 USB 드라이버를 작성하는 방법을 안드로이드 Fastboot 프로토콜을 예시로 설명하는 튜토리얼. USB 엔드포인트 유형, 열거(enumeration) 과정, 컨트롤/벌크 전송의 동작 원리를 실습 중심으로 다룸.