
모나드는 공통된 인터페이스 규약을 가진 데이터 처리의 표준 모델입니다.
각 모나드(IO, List, Promise 등)는 용도가 완전히 다르지만, 내부 로직을 처리하는 방식은 동일한 메커니즘을 공유합니다.
flatMap 등의 연산을 수행해도 여전히 래퍼 상태를 유지합니다.if-null이나 try-catch 같은 노이즈가 사라집니다.함수형 프로그래밍에서 우리가 다루는 대부분의 펑터는 사실 엔도펑터입니다.
일반 펑터: 타입 를 로 바꾸면서, 컨텍스트 자체도 에서 로 바꿀 수 있는 변환입니다.
엔도펑터: 컨텍스트 를 그대로 유지한 채 내부 타입만 로 바꿉니다.
(예: List<T> -> List<R>)
이 '유지성' 덕분에 함수를 무한히 이어 붙이는 합성이 가능해집니다.
모나드는 엔도펑터의 성질을 포함하며, 여기에 중첩된 컨텍스트를 평평하게 펴주는 능력이 추가된 구조입니다.
모나드의 핵심 연산인 flatMap은 "세계 속에 중첩된 세계"를 해결하는 알고리즘입니다.
Monad<Monad<R>>이라는 중첩 구조가 됩니다.Left가 발생하면 이후의 모든 차원 연산을 무시하고 에러를 유지.Left는 흡수원 역할을 하여, 한번 발생하면 이후의 모든 flatMap 연산을 무효화합니다.
sealed interface Either<L, R> {
class Left<L>(val v: L) : Either<L, Nothing>
class Right<R>(val v: R) : Either<Nothing, R>
fun <R2> flatMap(block: (R) -> Either<L, R2>): Either<L, R2> = when (this) {
is Left -> this // 에러 발생 시 이후 연산 스킵
is Right -> block(v)
}
}
then 사실상의 flatMap은 비동기 상태를 관리하며, 이전 비동기가 끝나야 다음 비동기 차원으로 진입하도록 동기화 정책을 내포합니다.
모나드를 사용하는 궁극적인 이유는 순수 비즈니스 로직과 구동 메커니즘을 분리하기 위함입니다.
현실적인 마이크로서비스나 대규모 시스템에서는 에러 타입이 제각각입니다. 이를 합타입 계층 구조로 묶어 해결합니다.
// 1. 공통 터미널 정의
sealed interface BC1 { interface Terminal : BC1 }
sealed interface CommonFail : BC1.Terminal { object Timeout : CommonFail; object Network : CommonFail }
// 2. 도메인 전용 실패 정의
sealed interface Agg1Rej : BC1.Terminal { class InvalidEmail(val msg: String) : Agg1Rej }
// 3. 파이프라인 구성
aggregate1Entry(email)
.flatMap { validate(it) } // Either<BC1.Terminal, Email>
.flatMap { createOrder(it) } // 에러가 발생해도 BC1.Terminal이라는 공통 분모로 유지됨
.effect { result ->
when(result) {
is Right -> handleSuccess(result.v)
is Left -> when(val err = result.v) {
is CommonFail -> ...
is Agg1Rej -> ...
}
}
}
파이프라인이 길어질수록 앞 단계의 데이터가 뒷 단계에서 필요할 때가 있습니다.
이를 도메인 모델에 억지로 넣으면 모델이 오염됩니다.
map { it to extraInfo }를 통해 데이터를 Pair나 Data Class로 묶어서 전달.