예외처리 #7

세나정·2023년 3월 7일
0

타입스크립트는 런타임에 발생할 수 있는 예외를 컴파일 타임에도 잡을 수 있도록 최선을 다함

일반적인 패턴 네가지

  • null 반환
  • 예외 던지기
  • 예외 반환
  • Option 타입

- null 반환

사용자의 생일을 입력 받아 Date 객체로 파싱하는 프로그램을 구현해보자

function ask() {
	return prompt ('When is your birthday?')
}

function parse(birthday: string) :Date {
	return new Date(birthday)
}

let date = parse(ask())
console.info('Date is', date.toISOString())

단순한 텍스트 입력창을 사용 했으므로 사용자가 입력한 내용을 검증해야함

입력한 내용을 사용하기 전에 가장 먼저 결과가 null인지 확인

가장 간단히 null을 반환하여 타입시스템이 코드가 두 가지 상황을 모두 처리하는지를 확인

하지만 parse에서 발생하는 에러를 이 방식으로 처리하면 문제가 생긴 원인을 알 수 없기에 개발자 로그를 뒤져도 '알 수 없는 오류가 발생했습니다.' 같은 모호한 에러 메세지만을 보게 될 것
또한, 모든 연산에서 null을 확인해야 하므로 연산을 중첩하거나 연결할 때 코드가 지저분해짐

- 예외 던지기

문제가 발생하면 null 대신 예외 던지기

에러를 catch로 잡는 것보다 처리하지 않은 에러는 다시 던지는 것이 좋음

또한, 나중에 다른 개발자가 parse나 ask에서 또 다른 형태의 RangeError를 던질 수 있게 하려면 이렇게 아래처럼 커스텀 에러를 사용해서 구체적으로 표현하면 됨

여기서 다른 개발자가 특정 타입의 에러와 기존 RangeError가 던져질 수 있다는 사실을 어떻게 알고 잡아 처리할 수 있는 방법은 함수 이름에 명시하거나 문서화 주석에 정보를 추가하면 됨

근데 코드를 사용하는 개발자에게 성공과 에러 상황 모두를 처리하도록 알려주려면 어떻게 해야 할까?

- 예외 반환

타입스크립트는 자바가 아니기에 throws문을 지원하지 않음 하지만 유니온 타입을 활용하여 비슷하게 흉내낼 수 있음

이제 이 메서드는 세가지 상황을 처리해야 하고 그렇지 않으면 컴파일 타임에 TypeError가 발생함

이상으로 TS의 타입 시스템을 활용하여 다음을 수행함

  • parse의 시그니처에 발생할 수 있는 예외를 나열
  • 메서드 사용자에게 어떤 에러가 발생할 수 있는지를 전달
  • 메서드 사용자가 각각의 에러를 모두 처리하거나 다시 던지도록 강제

다음처럼 명시적으로 처리도 가능

한편 에러를 던지는 연산을 연쇄적으로 호출하거나 중첩하면 코드가 지저분해진다는 단점이 있음, T | Error1을 반환하는 함수를 이용하는 모든 호출자 함수는 두 가지 선택지 중 하나를 고를 수 있음

  1. 명시적으로 Error1을 처리
  2. T (성공상황)을 처리하고 Error1은 호출자 함수의 사용자가 처리하도록 전달 하지만, 이 방법을 너무 자주 사용하면 오히려 최종 사용자가 처리해야할 에러의 종류가 크게 늘어날 수 있음

- Option 타입

특수 목적 데이터 타입을 사용해 예외를 표현
이 방식은 값과 에러의 유니온을 반환하는 방법에 비해 단점이 있지만 에러가 발생할 수 있는 계산에 여러 연산을 연쇄적으로 수행할 수 있음

어떤 특정 값을 반환하는 대신 값을 포함하거나 포함하지 않을 수도 있는 컨테이너를 반환한다는 것이 Option 타입의 핵심

컨테이너는 자체적으로 몇 가지 메서드를 제공
값을 포함할 수 있다면 어떤 자료구조로도 컨테이너를 구현할 수 있음

다음은 배열로 구현한 모습

Option도 에러가 발생한 이유를 알려준다기 보다 그냥 좀 잘못됐음을 알려만 줌
언제든 실패할 수 있는 여러 동작을 연쇄적으로 수행할 때 Option의 진가가 발휘됨
prompt는 항상 성공하고 parse는 실패할 수 있다고 가정
그런데 propmt도 실패할 수 있다면? 이때 또 다른Option을 이용해 처리할 수 있음

하지만 여기서 에러는 Date의 배열 (Date[])를 Date의 배열의 배열 (Date[][])로 매핑했기 때문에 Date의 배열로 평탄화를 해줘야함

이렇게 여러 코드로 나뉜다면 타입이 많은 정보를 제공하지 않고 무슨 일이 일어나는지를 한눈에 볼 수 없기 때문에

우리가 하려는 작업을 컨테이너에 담아서 상황을 개선하면 된다

이때 컨테이너는 대상 값을 이용해 연산을 수행하는 방법과 그 결과를 얻어내는 방법을 드러내는 역할을 함

Option 타입을 다음 처럼 정의

  • Option은 Some<T>와 None이 구현하게 될 인터페이스
  • Option은 타입이기도 하고 함수이기도 함, 타입 관점에서는 단순히 Some과 None의 슈퍼타입을 뜻함

  1. Option<T>는 Some<T>와 None이 공유하는 인터페이스
  2. Some<T>는 연산에 성공하여 값이 만들어진 상황을 나타냄 앞서 사용했던 배열처럼 Some<T>는 결과값을 포함
  3. None은 연산이 실패한 상황

배열 기반 구현과 비교

이 옵션으로 할 수 있는 것

  • flatMap 비어있을 수도 있는 Option을 연산에 연쇄적으로 수행하는 수단

  • getOrElse Option에서 값을 가져옴

  • flatMatp은 T타입의 값을 받는 f를 인수로 받아 U 타입의 값을 포함하는 Option을 반환, flatMap은 Option의 값을 인수로 건네 f를 호출한 다음 새로운 Option<U>를 반환함

  • getOrElse는 T 타입의 값을 기본적으로 받은 다음, Option이 빈 None이면 기본값을 반환하고 Option이 Some<T>이면 Option 안의 값을 반환

이제 두 클래스에 메서드를 구현해 넣어보자

  1. Some<T>에 flatMap을 호출하면 인수로 전달된 f를 호출해 새로운 타입의 Option을 만들어냄
  2. Some<T>에 getOrElse를 호출하면 Some<T>를 반환
  3. None은 계산 실패를 의미하므로 flatMap을 호출하면 항상 None을 반환
  4. None에서 getOrElse를 호출하면 기본 값으로 제공한 값을 그대로 반환

None의 매핑 결과는 항상 None이며 Some<T>의 매핑 결과는 Some<T>나 None이 된다는 사실을 알 수 있다

이러한 것을 이용하여 우리는 오버로드를 통해 더 구체적인 타입을 줄 수 있다 (결과가 어떻게 나오는지)

이제 Option을 만드는데 사용할 함수만 구현하면 된다
Option을 인터페이스로 작성했기 때문에 함수의 이름은 같아도 됨 (타입스크립트는 타입과 값을 별도의 네임스페이스로 관리하기 때문)

  1. Option에 null이나 undefined를 전달하면 None을 반환
  2. Some<T>이라면 사용자가 전달한 그 값을 반환
  3. 마지막으로 오버로드 된 두 시그니처의 상위 경계를 직접 계산
    T | null | undefined의 상위 경계는 어쨌든 T이다. None과 Some<T>의 상위 경계는 Some<T>이므로 이미 정의한 Option<T>로 표현할 수 있다

이제 null일 수 있는 값에도 안심하고 연산을 넣을 수 있음
다음은 실사용 예임생일 예를 가져다가 해도 가능

ask() // Option<String>
	.flatMap(parse) // Option<Date>
    .flatMap(date => new Some(date.toISOSting())) // Option<string>
    .flatMap(data => new Some('Date is ' + date)) // Option<string>
    .getOrElse('에러임') // String

하지만 Option의 단점은 None을 반환하기 때문에 정확히 어떤 이유로 에러가 발생했는지 알 수 없다는 것이고
한 가지 더로 Option을 사용하지 않는 애들과는 호환되지 않는 것이 단점이다.

그렇지만 Option을 사용할 때 오버로드 기능을 추가하면 Option을 기본 지원하는 언어를 포함한 대부분의 언어로는 표혀날 수 없는 일도 해낼 수 있도 오버로드된 호출 시그니처를 활용하여 Some과 None으로만 제한한 코드를 훨씬 안전하게 만들 수 있다.


요약

  • 어떤 작업이 실패했음을 단순하게 알리거나 (null, Option) 실패한 이유와 관련된 정보를 제공 (예외를 던지거나 반환)
  • 가능한 모든 예외를 사용자가 명시적으로 처리하도록 강제하거나 (예외 반환) 에러 처리 관련 코드를 더 적게 구현 (예외 던지기)
  • 에러를 만드는 방법이 필요하거나 (Option) 아니면 단순히 에러가 발생했을 때 처리 (null, 예외)
profile
기록, 꺼내 쓸 수 있는 즐거움

0개의 댓글