null 참조는 (...) 10억 달러짜리 실수다.
by 찰스 앤터니 리처드 호어

Maybe, Just, Nothing 객체

export class Maybe {
  static just(a) {
    return new Just(a);
  }
  static nothing() {
    return new Nothing();
  }
  static fromNullable(a) {
    return a !== null ? Maybe.just(a) : Maybe.nothing();
  }
  static of(a) {
    return this.just(a);
  }
  get isNothing() {
    return false;
  }
  get isJust() {
    return false;
  }
}

export class Just extends Maybe {
  _value: any;
  constructor(value) {
    super();
    this._value = value;
  }

  get value() {
    return this._value;
  }
  map(f) {
    return Maybe.fromNullable(f(this._value));
  }
  getOrElse() {
    return this._value;
  }
  filter(f) {
    Maybe.fromNullable(f(this._value) ? this._value : null);
  }
  chain(f) {
    return f(this._value);
  }
  toString() {
    return `Maybe.Just(${this._value})`;
  }
}

export class Nothing extends Maybe {
  _value: any;
  map(f) {
    return this;
  }
  get value() {
    throw new TypeError("Nothing 값을 가져올 수 없습니다.");
  }
  getOrElse(other) {
    return other;
  }
  filter(f) {
    return this._value;
  }
  chain(f) {
    return this;
  }
  toString() {
    return `Maybe.Nothing`;
  }
}

왜 필요할까?

일단 개념에 얽매이지 말고, 이게 왜 필요할까 그리고 실질적인 사용 예시로서 생각해보면 Maybe, Either 이런걸 뭘 위해 사용할까? 라는 측면에서 접근해보자. 함수형 프로그래밍의 핵심은 함수의 합성에 있다고 할 수 있다(pipe, compose 등을 배웠었다). 이 때, 전에 Tuple, Curry, Partial에 대해서 공부했던게 기억이 나는데, 그걸 배운 이유는 함수의 합성을 하는 중에 A라는 함수의 리턴값을 B라는 함수의 매개변수에 딱 알맞게(타입, 인자의 개수 etc) 넣기 위해서 였다. Tuple은 인자의 개수를 맞추기 위한거였고, Curry, Partial도 유사하다. 인자가 여러개인 함수를 컨트롤 하기 위해 curry는 하나하나씩 넣도록 실행을 지연시키는 로직이었고, Patial은 curry보다 유연하게 넣고 싶은 만큼(?) 넣는 로직이었다. 그럼 다시 돌아와서, Maybe, Either 이런 함수자 형태는 어떤 쓸모가 있을까?. 그건 바로 리턴 데이터와 매개변수의 매칭 필요성이 없어진다는 것이 아닐까?

  • 리턴 데이터와 매개변수의 매칭 필요성이 없어진다?.
type Inform = {
  name: string;
  age: number;
};

export function getRandomBinary(): Inform | null {
  return Math.random() < 0.5 ? { name: "yongki", age: 32 } : null;
}
export const congratulate = (obj: Inform) =>
  alert(`${obj.name}님 축하드립니다!`);
export const checkAge = (obj: Inform) => (obj.age > 31 ? "GOLD" : "BRONZE");

getRandomBinary 라는 함수는 50%의 확률로 inform 데이터를 리턴하거나 null 을 리턴한다. 각각 당첨과 꽝이라고 정의해보자. 그리고, 이 결과를 바탕으로 당첨이 됐으면, 축하한다는 alert 문과 나이에 따라 GOLD or BRONZE를 화면에 보여주도록 하고, 꽝이면 꽝을 최종적으로 보여주고자 한다. 이 때, getRandomBinary의 리턴 타입을 보면 Inform | null 이렇게 된다. 그럼 함수의 합성의 관점에서 봤을 때 getRandomBinary와 congratulate, checkAge를 합성하려면 getRandomBinary가 리턴하는 값에 따라 후속 함수들에(합성할) 일종의 조치를 취해놔야 null 참조에 따른 에러를 막을 수 있다. 예를 들어, getRandomBinary의 리턴값을 매개변수로 받아서 congratulate를 바로 실행한다고 하면,

export const congratulate = (obj: Inform) =>
  alert(`${obj.name}님 축하드립니다!`);

TypeError: Cannot read properties of null (reading 'name')에러가 날 것이다(obj에는 null이 들어갈 것이기에). 그리고 Typescript로 개발중이라면 애초에 이렇게 개발할수도 없다. 무조건 null 체킹을 하는 로직을 더해야한다.

하지만, Maybe를 쓰면 이러한 문제를 피해갈 수 있고, null에 따른 예외 상황도 굳이 if-else 분기처리를 직접하지 않아도 할 수 있다. 비교를 위해 if-else처리를 한 것과 Maybe로 처리한 것을 둘다 작성해본다.

  • Maybe Version
type Inform = {
  name: string;
  age: number;
};
export function getRandomBinary(): Inform | null {
  return Math.random() < 0.5 ? { name: "yongki", age: 32 } : null;
}
export const congratulate = (obj: Inform) =>
  alert(`${obj.name}님 축하드립니다!`);
export const checkAge = (obj: Inform) => (obj.age > 31 ? "GOLD" : "BRONZE");

  Maybe.fromNullable(getRandomBinary())
        .map(tap(congratulate))
        .map(checkAge)
        .getOrElse("꽝")
  • if-else Version
export function getRandomBinary2(): Inform2 {
  return Math.random() < 0.5 ? { name: "yongki", age: 32 } : null;
}
export const congratulate2 = (obj: Inform2): Inform2 => {
  if (obj !== null) {
    alert(`${obj.name}님 축하드립니다!`);
    return obj;
  } else {
    return obj;
  }
};
export const checkAge2 = (obj: Inform) => {
  if (obj !== null) {
    return obj.age > 31 ? "GOLD" : "BRONZE";
  } else {
    return "꽝";
  }
};

export const checkReward = pipe(getRandomBinary2, congratulate2, checkAge2);

위와 같이 계속해서 null 체크를 해줘야한다. 이걸 편하게, 심플하게 대체해주는게 Maybe의 역할이다. Maybe를 쓰면 뒤에 null을 참조하는 함수를 거칠 필요가 없어진다. null을 체킹하는 순간 이미 Nothing을 계속해서 리턴하기 시작하므로, 뒤의 함수는 실행조차 되지 않기 때문이다.

장점 정리

  • null 체크 로직을 알아서 처리
  • 예외를 던질 필요가 없음
  • 함수 합성을 유려한 흐름으로 지원

이렇게 에러를 '명확히 정의(=null
이면 에러인 것임 -> Nothing으로 처리해버림)' 하면 생기는 장점들이 Maybe의 장점들이라고 할 수 있다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글