본문 바로가기

함수형 프로그래밍

함수형 자바스크립트 (6-1) - 함수자, 모나드를 이용한 예외처리

예외 처리

우리가 흔히 아는 예외 처리 방식은 try-catch 문을 이용하는 것이죠. 이는 위험한 코드를 try 문으로 보호하고, 해당 코드에서 던지는 에러를 catch에서 처리하는 방식으로, 명령형 프로그래밍 방식입니다. try-catch는 함수형 프로그래밍에서 사용하기에는 몇가지 문제점들이 있습니다. 

  1. 함수 합성이나 체이닝을 할 수 없습니다.
  2. 예외를 던지는 행위는 예측 가능한 값을 지양하는 참조 투명성 원리에 위배됩니다.
  3. 에러 조건을 처리하는 부분이 복잡해집니다.

따라서 함수형 프로그래밍에서는 다른 방식으로 예외를 처리합니다. 바로 모나드를 이용하는 것입니다.

 

함수자, 모나드

우선 함수자(Functor)와 모나드(Monad)에 대해 알아보겠습니다. 함수자와 모나드는 모두 값을 감싸는 컨테이너화를 통해 값을 외부 접근으로부터 보호하고, 연산자를 통해 값에 접근하도록 하는 방식을 사용하는 디자인 패턴을 의미합니다.

 

우선 함수자에 대해 알아보겠습니다. 예를 들어 함수자 방식으로 값을 감싸는 Wrapper라는 클래스를 작성해보겠습니다.

class Wrapper {
  constructor(value) {
    this._value = value 
  }

  map(f) {
    return new Wrapper(f(this._value))
  }
}

함수자를 한 문장으로 설명하자면, 'map' 메서드를 구현한 자료구조라고 할 수 있습니다. 해당 클래스 Wrapper는 값을 생성자로 받아서 클래스로 감싸고, map 메서드는 함수를 감싼 값에 함수를 적용한 후 다시 Wrapper로 반환값을 감싸 반환합니다. 이처럼 동일한 형태의 컨테이너로 값을 mapping해서 반환하는 것, 이걸 우리는 함수자라고 합니다. 우리는 이미 함수자를 사용하고 있습니다. 바로 Array인데요. Array의 map 메서드를 이용하면 새로운 각 원소에 함수를 적용시킨 새로운 배열을 생성할 수 있습니다.

 

함수자를 이용하는 이유는 무엇일까요? 함수자는 콘텍스트를 제공하여 값을 바꾸지 않은 상태로 안전하게 값을 꺼내어 연산을 수행할 수 있도록 해줍니다. 예외 처리 등 구체적인 작업은 함수자를 이용한 여러가지 모나드형을 통해 할 수 있습니다.

 

그렇다면 Monad는 무엇일까요? 우선 함수자 사용의 한계에 대해 알아보겠습니다. 학생 정보를 db에서 찾고 학생의 주소를 반환하는 함수를 위에 함수자 Wrapper를 이용해서 반환값을 안전한 콘텍스트로 만든다고 가정해봅시다. 다음과 같이 작성할 수 있습니다.

// ****** 유틸 함수 ******

// 함수 합성하는 함수
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

// 객체에서 속성을 꺼내는 함수
const getProp = property => obj => obj[property]


// ******* 합성에 사용할 함수들 *******

// DB에서 학생 정보를 찾아서 Wrapper로 감싼 값을 반환하는 함수
const findStudent = db => stundentId => new Wrapper(db.find(studentId))

// 학생 Wrapper에서 주소를 꺼내 Wrapper로 감싼 값을 반환하는 함수
const getAddress = studentWrapper => new Wrapper(studentWrapper.map(getProp('address'))


// ****** 최종적으로 사용할 함수 ******
const getStudentAddress = compose(getAddress, findStudent(DB('student'))

해당 방식을 이용하면 에러 처리 코드는 감출 수 있지만, 실행 결과는 예상과 다릅니다. 

const identity = x => x

const result = studentAddress('444-44-4444') // Wrapper(Wrapper(address))

// 값을 꺼내기 위해선 identity 함수를 두번 적용...
const adderess = result.map(identity).map(identity)

값은 Wrapper로 이중으로 감싸져 있습니다. 값을 꺼내기 위해서는 identity 함수도 두번 적용해야 합니다. 이런 방식은 함수가 길어지고 Wrapper의 수가 많아질 수록 귀찮아집니다. 이를 해결하기 위해서, 우리는 Monad를 사용합니다.

 

Monad는 특별한 형태의 함수자로, flatMap을 구현한 함수자입니다. Monad Wrapper 클래스를 보겠습니다.

class Wrapper {
  constructor(value) {
    this._value = value
  }
  
  map(f) {
    return new Wrapper(f(this.value))
  }
  
  flatMap(f) {
    if (this._value instanceof Wrapper) {
      return new Wrapper(this._value.flatMap(f))
    }
    
    return this.map(f)
  }
}

flatMap은 Wrapper 껍질을 벗겨주는 역할까지 해주는 map이라고 보시면 됩니다. Monad를 사용하여 다시 코드를 작성해보겠습니다.

// ****** 유틸 함수 ******

// 함수 합성하는 함수
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x)

// 객체에서 속성을 꺼내는 함수
const getProp = property => obj => obj[property]


// ******* 합성에 사용할 함수들 *******

// DB에서 학생 정보를 찾아서 Wrapper로 감싼 값을 반환하는 함수
const findStudent = db => stundentId => new Wrapper(db.find(studentId))

// 학생 Wrapper에서 주소를 꺼내 Wrapper로 감싼 값을 반환하는 함수
const getAddress = studentWrapper => new Wrapper(studentWrapper.map(getProp('address'))


// ****** 최종적으로 사용할 함수 ******

const getStudentAddress = compose(getAddress, findStudent(DB('student'))

그 후 값을 꺼내는 것은 매우 쉽습니다.

const identity = x => x

const result = studentAddress('444-44-4444') // Wrapper(Wrapper(address))

// 값을 꺼내기 위해선 identity 함수를 한번만 적용!
const adderess = result.flatMap(identity)

이 것이 모나드, 그리고 flatMap의 장점입니다. 다음 글에서는 모나드를 인터페이스를 실제로 구현한 Maybe, Either 모나드 형들을 사용해서 예외처리하는 법을 알아보겠습니다.