Rust로 배우는 모나드의 개념

July 28, 2025 차정윤 조회수 10,660

저희 회사는 전 세계에서 가장 많은 Android 디바이스를 개발 및 생산하고 있습니다. 다양한 신규 모델을 개발하고 이미 출시된 모델의 OS 업그레이드까지 수년간 빈틈없이 지원하려면 강력한 내부 소프트웨어 개발 인프라가 필수적입니다. 따라서 저희 팀은 웹 브라우저만으로 원격에서 수천 대의 디바이스를 사용할 수 있는 서비스형 개발 인프라를 전 세계 개발자들에게 제공하고 있습니다. 디바이스 수가 많다 보니 디바이스를 직접 제어하는 노드 머신의 수도 수백 대에 달합니다. 이러한 노드 머신의 상태를 효율적으로 관리하기 위해 Rust 기반의 에이전트를 직접 개발하게 되었고 새로운 언어를 사용하는 과정에서 많은 시행착오와 깨달음이 있어 이를 공유하고자 합니다. 이번 블로그에서는 Rust 언어로 함수형 프로그래밍의 주요 개념인 ‘모나드(Monad)’를 알기 쉽게 설명해 보겠습니다. 모나드가 왜 중요하고 Rust에서 어떻게 활용될 수 있는지 함께 탐구해보며, 강력한 함수형 프로그래밍 도구를 장착해 보시는 건 어떨까요?

들어가며

혹시 ‘모나드’라는 용어를 들어보신 적이 있나요? 함수형 프로그래밍을 공부하다 보면 반드시 마주치게 되는 개념인데, 처음 접하는 분들은 ‘이게 도대체 뭐지?’라고 생각하실 수도 있습니다. 실제로 많은 개발자들이 처음에는 이런 반응을 보입니다.

하지만 모나드는 생각보다 어렵지 않고 여러분이 이미 Rust 코드를 작성하면서 자연스럽게 사용해 오던 패턴들을 좀 더 체계적으로 정리한 것이라고 보시면 됩니다. Option이나 Result를 사용해서 .map()이나 .and_then()을 체이닝해본 경험이 있다면 이미 모나드를 사용하고 계신 겁니다.


이 글에서 배울 내용

• 모노이드(Monoid): 결합 가능한 연산의 기초

• 펑터(Functor): 값을 변환하는 방법

• 엔도펑터(Endofunctor): 같은 범주 내 변환

• 어플리커티브 펑터(Applicative Functor): 함수를 적용하는 방법

• 모나드(Monad): 순차적 연산을 체이닝하는 방법


이들이 어떻게 연결되어 있고 실제로 어떤 문제를 해결하는지 함께 알아보겠습니다.

1. 모노이드(Monoid): 결합의 기초

그럼 차근차근 시작해 볼까요? 먼저 모노이드부터 살펴보겠습니다.

‘모노이드’라고 하니 생소하게 들릴 수 있는데 우리가 일상에서 매우 자주 사용하는 개념입니다. 예를 들어 문자열을 이어붙이거나, 숫자를 더하거나, 리스트를 합치는 것들 말이죠.

수학적으로 설명하면 결합법칙을 만족하는 이항 연산과 항등원을 가진 구조입니다. 이렇게 말하면 어려워 보이지만 실제로는 간단합니다. 모노이드는 쉽게 말해 다음과 같은 구조라고 생각하시면 됩니다.


• 여러 원소를 순서대로 합칠 수 있음(결합법칙)

• 아무것도 없는 상태(빈 값)가 있음


문자열로 예를 들자면 "안녕" + "하세요"처럼 합칠 수 있고 빈 문자열 ""이 존재하는 구조입니다.


모노이드의 정의


실제 예제들

문자열 모노이드


숫자 덧셈 모노이드


모노이드의 특징

1. 결합법칙: (a + b) + c = a + (b + c)

2. 항등원: empty + a = a + empty = a

이런 구조는 리스트 합치기, 문자열 연결, 숫자 덧셈 등에서 자연스럽게 나타납니다.


모노이드 사용 예시

2. 펑터(Functor): 값을 변환하는 방법

이제 펑터에 대해 이야기해볼 차례입니다.


‘펑터’라는 용어도 처음 들으면 어렵게 느껴질 수 있지만, 사실은 여러분이 이미 잘 알고 있는 개념입니다. Rust에서 Option이나 Vec에 .map()을 사용해 보았다면 그때 이미 펑터를 사용하고 계셨던 것입니다.

펑터를 간단히 설명하면 **값을 담고 있는 상자(컨테이너)**가 있을 때 그 상자를 열지 않고도 안에 있는 값에 함수를 적용할 수 있게 해주는 방법입니다.

예를 들어 선물 상자가 있다고 가정해 봅시다. 상자를 열어서 선물을 꺼내고 뭔가 작업을 한 다음에 다시 포장하는 대신, 마법처럼 상자 밖에서 함수를 적용하면 안의 내용물이 변환되는 것입니다. 흥미로운 개념이지 않나요?



펑터의 정의


실제 예제들

Option 펑터


Result 펑터


펑터의 특징

1. 값 보존: fmap은 컨테이너의 구조를 유지합니다.

2. 함수 적용: 상자 안의 값에만 함수를 적용합니다.

3. 에러 처리: None이나 Err는 그대로 유지합니다.


펑터 사용 예시

3. 엔도펑터(Endofunctor): 같은 범주 내 변환

이제 엔도펑터라는 개념을 살펴보겠습니다.


‘엔도(Endo)’라는 접두사를 들어보신 적이 있나요? 의학에서 내시경을 ‘엔도스코프(endoscope)’라고 부르는데, 여기서 ‘엔도’는 그리스어로 ‘내부의’, ‘같은 곳의’라는 뜻입니다.

마찬가지로 엔도펑터도 같은 ‘범주’ 내에서만 변환이 일어나는 펑터를 말합니다.

프로그래밍 언어에서 엔도펑터는 단일 타입 파라미터를 가진 제네릭 타입으로 이해할 수 있습니다. Rust에서는 `Option<T>`, `Vec<T>`, `Box<T>`, `Arc<T>` 등 대부분의 제네릭 타입이 엔도펑터에 해당합니다.



엔도펑터 예제들


엔도펑터의 특징

• F<A> → F<B>(F는 같은 타입 생성자)

• 구조 보존: 컨테이너의 구조는 그대로 유지

• 모나드의 기초: 모나드는 엔도펑터의 특별한 경우

4. 어플리커티브 펑터(Applicative Functor): 함수를 적용하는 방법

이제 좀 더 고급 개념인 어플리커티브 펑터를 살펴보겠습니다.


‘어플리커티브 펑터’도 이름은 복잡하지만 생각보다 어렵지 않습니다.

일반적인 펑터는 ‘상자 안의 값’에 ‘일반 함수’를 적용하는 것인데, 어플리커티브 펑터는 여기서 한 단계 더 나아갑니다. 함수 자체도 상자 안에 담긴 상황에서 어떻게 적용할지를 다루는 것입니다. 예를 들어보겠습니다.


일반 펑터:

• 상자 안의 값: Some(5)

• 일반 함수: |x| x * 2

• 결과: Some(10)


어플리커티브 펑터:

• 상자 안의 값: Some(5)

• 상자 안의 함수: Some(|x| x * 2)

• 결과: Some(10)


여기서 중요한 점은 함수 자체도 상자 안에 들어있다는 것입니다. 어플리커티브 펑터만의 독특한 특징이죠. 덕분에 함수와 값이 모두 불확실한 상황(실제 값이 존재할 수 있거나 에러 발생 가능성이 있는 경우)에서도 안전하게 연산을 수행할 수 있습니다.



어플리커티브 펑터의 정의


실제 예제들



Option 어플리커티브 펑터



간단한 사용 예제



어플리커티브 펑터 사용 예시


어플리커티브 펑터의 특징

1. 함수 적용: F<A>와 F<A→B>를 조합하여 F<B> 생성

2. 병렬 처리: 여러 값을 동시에 조합 가능

3. 에러 전파: 하나라도 실패하면 전체 실패

참고: Rust에서는 주로 map과 and_then을 사용하므로 어플리커티브 펑터는 이론적 배경으로만 이해하시면 됩니다.

5. 모나드(Monad): 순차적 연산을 체이닝하는 방법

드디어 우리가 기다려온 주인공, 모나드의 등장입니다!


지금까지 모노이드, 펑터, 엔도펑터, 어플리커티브 펑터를 차례대로 살펴봤는데요. 사실 이 모든 것들이 모나드를 이해하기 위한 준비 과정이었습니다.

모나드가 무엇인지 한 문장으로 설명하면 **이전 단계의 결과를 보고 다음에 무엇을 할지 결정할 수 있는 똑똑한 체이닝 방법**입니다.

예를 들어 다음과 같은 상황을 가정해 보겠습니다.


1. 사용자 ID로 사용자 정보를 찾는다. → 사용자가 있을 수도, 없을 수도 있음

2. 사용자가 있다면 권한을 확인한다. → 권한이 있을 수도, 없을 수도 있음

3. 권한이 있다면 데이터를 가져온다. → 성공할 수도, 실패할 수도 있음


각 단계가 성공해야만 다음 단계로 진행할 수 있고, 중간에 하나라도 실패하면 전체가 실패해야 하는 상황입니다. 이런 **조건부 연쇄 실행**을 우아하게 처리하는 것이 바로 모나드의 힘입니다!



모나드의 정의


실제 예제들


Option 모나드



Result 모나드


모나드의 특징

1. 순차적 실행: 이전 연산의 결과가 다음 연산의 입력

2. 에러 전파: 중간에 실패하면 전체 체인이 실패

3. 조건부 실행: 값이 있을 때만 다음 연산 실행

6. 개념들 간의 관계

지금까지 살펴본 여러 개념들이 서로 어떻게 유기적으로 연결되어 있는지 알아보고 각각의 역할과 관계를 체계적으로 정리해보겠습니다. 이를 통해 전체적인 그림을 더 선명하게 그려볼 수 있을 것입니다.


계층 구조

• 모나드는 어플리커티브 펑터를 확장합니다.

• 어플리커티브 펑터는 펑터를 확장합니다.

• 엔도펑터는 펑터의 특별한 경우입니다.

• 모노이드는 별도의 대수적 구조입니다.


각각이 해결하는 문제

1. 모노이드: 여러 값을 결합하는 방법

2. 펑터: 값을 변환하는 방법

3. 엔도펑터: 같은 컨테이너 내에서 값을 변환하는 방법

4. 어플리커티브 펑터: 함수를 적용하는 방법

5. 모나드: 순차적 연산을 체이닝하는 방법

7. 모나드의 장점

이쯤에서 모나드가 좋다는 것은 알겠지만 구체적으로 무엇이 좋은지 궁금해 하실 수 있습니다. 기존 방식도 충분히 잘 동작하는데 굳이 새로운 패턴을 배워야 할까 생각되실 수 있습니다. 하지만 실제로 사용해보면 정말 많은 장점이 있습니다!

특히 에러 처리가 복잡한 프로젝트나 여러 단계의 데이터 변환이 필요한 경우 모나드 없이는 코드가 상당히 복잡해집니다. 실제 경험을 바탕으로 모나드의 핵심 장점들을 보여드리겠습니다.


1. 에러 처리의 단순화


2. 가독성 향상


3. 조합 가능성

8. 실제 Rust에서의 모나드

Rust에서 찾아볼 수 있는 모나드의 대표적인 예는 다음과 같습니다. 이들은 Rust의 표준 라이브러리에서 제공되는 기본 타입들로, 실제 코드에 매우 자주 사용되며 깔끔하고 체계적인 에러 처리와 데이터 변환을 가능하게 합니다.



Option 모나드



Result 모나드



Iterator 모나드


마무리

모노이드에서 시작해 펑터, 엔도펑터, 어플리커티브 펑터를 거쳐 마침내 모나드까지 함께 차근차근 따라와주셔서 감사합니다. 처음에는 ‘이게 무슨 개념인가?’ 싶었던 것들이 이제는 조금 친숙하게 느껴지시나요?


핵심 정리

1. 모노이드: 결합 가능한 연산의 기초

2. 펑터: 값을 변환하는 방법

3. 엔도펑터: 같은 범주 내 변환

4. 어플리커티브 펑터: 함수를 적용하는 방법

5. 모나드: 순차적 연산을 체이닝하는 방법


실무 활용

• 에러 처리: Result 모나드로 깔끔한 에러 처리

• 널 안전성: Option 모나드로 null pointer error 예방

• 비동기 처리: Future 모나드로 복잡한 비동기 로직 단순화

• 파싱: Parser 모나드로 복잡한 파싱 로직 구성


모나드는 처음에 어렵게 생각될 수 있지만 우리가 일상에서 자주 사용하는 패턴을 수학적으로 정리한 것입니다. Rust의 강력한 타입 시스템과 함께 사용하면 더욱 안전하고 읽기 쉬운 코드를 작성할 수 있습니다.

어려운 수학 이론 대신 Rust가 제공하는 풍부한 예제와 함께 함수형 프로그래밍의 세계에 한 걸음 더 들어가보시는 건 어떨까요? 


다음 단계

이제 어느 정도 모나드의 기본 사항을 이해했다면 다음과 같은 주제를 탐구해보실 것을 추천합니다.

• 모나드 변환자(Monad Transformers): ‘여러 개의 모나드를 어떻게 조합할까?’라는 궁금증이 생기신다면 추천합니다.

• 렌즈(Lenses)와 프리즘(Prisms): 복잡한 데이터 구조를 우아하게 조작하는 방법을 배울 수 있습니다.

• 카테고리 이론의 다른 개념들: 코모나드, 애로우 등 좀 더 고급 개념들도 있습니다.




저자

차정윤

S/W인프라개발그룹(MX)

이메일 문의하기