[함수형 자바스크립트]5.복잡성을 줄이는 디자인 패턴


함수형 패러다임이 다른 패러다임보다 에러를 더 깔끔하게 처리한다는 의견이 많다.

프로그램 실행 중 예외가 나거나 네트워크 연결이 끊기는 등 예기치 않은 사고로 인해 데이터가 null, undefined가 되는 경우 골치아픈 문제들이 발생할 수 있다. 이런 에러처리를 위해 개발자들은 아주 많은 리소스를 사용한다. 그렇게 덩치가 커진 코드는 수수께끼 같은 코드가 되기 마련이다.

모나드라는 기법을 사용하여 이 문제에 대해 다뤄보자.

명령형 에러 처리의 문제점

명령형 코드는 대부분 try-catch 구문으로 에러를 처리한다. 하지만 try-catch를 사용하면 다른 함수와 합성/체이닝을 할 수 없고 코드 설계에서도 어려움을 갖는다.

함수형 프로그램은 왜 예외를 던지지 않을까?

명령형에서 예외를 던지는 함수의 특징은 다음과 같다.

  • 다른 함수형 장치처럼 합성이나 체이닝을 할 수 없다.
  • 예외를 던지는 행위는 함수 호출에서 빠져나갈 구멍을 찾는 것이므로 예측가능한 값을 지향하는 참조 투명성에 위배된다.
  • 예기치 않게 스택이 풀리면 함수 호출 범위를 벗어나 전체 시스템에 영향을 미치는 사이드이펙트를 일으킨다.
  • 에러를 조치하는 코드가 당초 함수를 호출한 지점과 동떨어져 있어서 비지역성원리에 위배된다. 에러가 나면 함수는 지역 스택과 환경에서 벗어난다.
  • 함수의 단일 반환값에 신경 써야 할 에너지를 catch 블록을 선언해 예외를 붙잡아 처리하는데 낭비하게 되어 호출자의 부담이 가중된다.
  • 다양한 에러 조건을 처리하는 블록들이 중첩되어 사용하기 어렵다.

그렇다면 함수형 프로그래밍에서는 예외를 완전히 없애야할까?

Null 체크

함수가 null을 반환하면 이 함수를 부른 호출자는 null체크를 해야 하는 부담을 떠안게된다.

함수자

try-catch와 비슷하게 잠재적인 위험을 가진 코드를 컨테이너로 감싸면서 함수형 에러처리를 할 수 있다.

불안전한 값을 감싼다

래퍼함수를 만들어서 에러가 날지 모를 값을 감싸는 것이다. 이 경우 값에 직접 접근할 수 없어 identity를 사용해야한다.

모나드를 응용한 함수형 에러 처리

모나드는 제이쿼리를 사용해봤다면 비슷한 개념이다. 선택자가 잘못된 값을 선택해도 예외를 던지는게 아니라 빈 제이쿼리 객체에 메서드를 적용하는 걸 생각하면 된다.

제어흐름에서 데이터흐름으로

모나드는 다음 인터페이스를 준수해야한다.

  • 형식생성자: 모나드형을 생성한다.
  • 단위 함수: 어떤 형식의 값을 모나드에 삽입한다.
  • 바인드함수: 연산을 서로 체이닝한다.
  • 조인연산: 모나드 반환함수를 다중합성할때 중요하게 쓰인다.
// Wrapper Monad

class Wrapper {
    constructor(value) { // 형식 생성자
        this._value = value;
    }

    static of(a) { // 단위 함수
        return new Wrapper(a);
    }

    map(f) { // 바인드 함수
        return Wrapper.of(f(this._value));
    }

    join() { // 조인 연산
        if(!(this._value instanceof Wrapper)) {
            return this;
        }
        return this._value.join;
    }

    get() {
        return this._value;
    }

    toString() {
        return `Wrapper (${this._value})`;
    }
}

Maybe와 Either 모나드로 에러 처리

모나드는 유효한 값을 감싸기도 하지만 값이 없는 상태, null이나 undefined를 모형화할 수 있다. 함수형 프로그래밍에서는 Maybe/Either로 아래와 같은 일들을 처리한다.

  • 불순 코드 격리
  • null 체크 로직 정리
  • 예외를 던지지 않음
  • 함수 합성 지원
  • 기본값 제공 로직을 한곳에 모음

null 체크를 Maybe로 일원화

Maybe 모나드는 Just, Nothing 두 하위형으로 구성된 빈 형식으로, 주로 null 체크로직을 효과적으로 통합한다.

  • Just(value): 존재하는 값을 감싼 컨테이너를 나타낸다.
  • Nothing(): 값이 없는 컨테이너 또는 추가 정보없이 실패한 컨테이너를 나타낸다.

Maybe 모나드는 DB쿼리, 컬렉션에서 값을 검색하거나 서버에 데이터를 요청하는 등 결과가 불확실한 호출을 할 때 자주사용한다.


const safeFindObject = R.curry((db, id) => Maybe.fromNullable(find(db, id)));

const safeFindStudent = safeFindObject(DB('student'))

const address = safeFindStudent('444-44-4444').map(R.prop('address'));

address; // Just(Address(...)) or Nothing
const country = R.compose(getCountry, safeFindStudent);
const getCountry = (student) => student
    .map(R.prop('school'))
    .map(R.prop('address'))
    .map(R.prop('country'))
    .getOrElse('존재하지 않는 국가입니다'); // 어느 하나라도 Nothing이면 다른 연산은 건너뛴다.

함수 승급

모든 함수마다 일일이 모나드를 부착해야할까? 이런 작업을 간략화하기위해 lift라는 함수 승급기법이 있다.

const lift = R.curry((f, value) => Maybe.fromNullable(value)).map(f);

const safeFindObject = R.compose(lift(console.log), findObject);

Either로 실패 복구

Either는 Maybe와 조금 다르다. Either는 절대로 동시에 발생하지 않는 두 값을 논리적으로 구분한 자료구조이다.

  • Left(a): 에러 메시지 또는 예외 객체를 담는다.
  • Right(b): 성공한 값을 담는다.

즉 실패하면 왼쪽을 보고 성공하면 오른쪽을 보면 된다.

const safeFindObject = R.curry((db, id) => {
    const obj = find(db, id);
    if(obj) {
        return Either.of(obj);
    }
    return Either.left(`ID가 ${id}인 객체를 찾을 수 없습니다.`);
})

const findStudent = safeFindObject(DB('student'));
findStudent('444-44-4444').getOrElse(new Student());

const errorLogger = _.partial(logger, 'console', 'basic', 'MyErrorLogger', 'Error');
fineStudent('444-44-4444').orElse(errorLogger);

IO 모나드로 외부 자원과 상호작용

DOM을 읽고쓰는 예제를 보자.

const read = (document, selector) => {
    () => document.querySelector(selector).innerHTML;
}

const write = (document, selector) => {
    return (val) => {
        document.querySelector(selector).innerHTML = val;
        return val;
    }
}

// document를 미리 할당하도록 부분적용

const readDom = _.partial(read, document);
const writeDom = _.partial(write, document);

const changeToStartCase = IO.from(readDom('#student-name'))
    .map(_.startCase)
    .map(writeDom('#student-name'));

changeToStartCase.run();

run()을 할때서 한번에 실행되기 때문에 읽기쓰기 중간에 불순한 일이 생길 수 없다.

모나드 체인 및 합성

모나드는 사이드이펙트를 억제하므로 합성 가능한 자료구조로 활용할 수 있다.

const liftIO = (val) => {
    return IO.of(val)
}

const getOrElse = R.curry((message, container) => container.getOrElse(message));

const showStudent = R.compose(
    map(append('#student-info')),
    liftIO,
    getOrElse('Can not find student'),
    map(csv),
    map(R.props(['ssn', 'firsename', 'lastname'])),
    chain(findStudent),
    chain(checkLengthSsn),
    lift(cleanInput)    
)




2023년 새해에는 성장하고 함께하고 싶다면?

Pre A 단계 이상의 스타트업 C 레벨들이 모여서 커뮤니티를 만들었습니다. 같이 스터디하고 친해질 일잘러를 찾습니다.




© 2017. by isme2n

Powered by aiden