📌 핵심 내용
소프트웨어가 작동을 계속할 수 있는 오류와 작동을 계속할 합리적인 방법이 없는 오류로 구분하고 오류를 적절하게 처리되도록 논의해보자.
JS는 동적 타입 언어(Dynamically typed languages)
로 모든 에러가 컴파일 단계가 아닌 런타임 환경에서 발생한다. TS를 사용해 컴파일 환경에서 타입과 관련한 에러를 잡을 수 있다.
대부분의 오류는 한 코드가 다른 코드를 호출해 발생한다.
아래의 코드는 유저로부터 받은 입력값을 번호만 추출해 전화번호를 반환하는 함수이다.
const getUserPhoneNumber = (phoneNumber: string) => {
if (!isValidPhoneNumber(phoneNumber)) {
// ...
}
}
getUserPhoneNumber('01012341234'); // 01012341234
getUserPhoneNumber('010-1234-1234'); // 01012341234
getUserPhoneNumber('010-12typo34'); // ?
getUserPhoneNumber
함수를 호출할 때 이미 우리는 함수 내부에서 입력값에 대한 유효성을 검사하기를 기대한다. 하지만 호출하는 곳에서는 해당 함수에서 모든 입력값에 대해 유효성 검사가 이루어지는지 알 수 없다. 만약 호출하는 곳에서 입력값에 대해 검증을 통해 혹시 모를 에러를 피할 수도 있을 것 같다.
대부분은 호출 시 오류가 발생한다는 것을 사전에 알 수 있는 방법이 없는 경우가 많다.
이 경우에는 함수 내부에서 입력값의 검증을 통해 호출하는 곳에서는 유효성에 대해 신경쓰지 않도록 하는게 나을 것 같다. 중복 코드 또한 줄어들테니까...!
책에서는 신속하게 실패하기(failing fast)
와 요란하게 실패하기(failing loudly)
를 강조한다.
신속한 실패
는 가능한 오류가 발생한 지점에서 가까운 곳에서 오류를 나타내는 기법이다.
복구할 수 있는 오류의 경우, 오류를 안전하게 복구할 수 있는 기회를 제공할 수 있고
복구할 수 없는 오류의 경우, 개발자가 신속하게 파악해 해결하는 기회를 제공하는 이점이 있다.
그리고 요란한 실패
는 서버에 오류를 기록하거나 오류가 발생할 때 프로그램을 중단시키는 것으로 버그가 발생한 위치를 정확히 알 수 있다는 이점이 있다.
위에서도 언급했다시피 오류를 숨기거나 넘기는 것을 최대한 지양해야 한다.
오류를 숨기는 코드로는 기본값 반환
, 널 객체 패턴
, 아무 조치도 없음
이 있는데 이 세 가지는 신속한 실패와 요란한 실패에 위반하는 내용이다.
const getAccountBalance = (userId: number) => {
const userAccount = accountStore.lookup(userId);
if(!userAccount) {
return 0;
}
// ...
return userAccount.balance;
}
만약 네트워크 오류로 인해 특정 회원의 정보에 액세스할 수 없는 경우, 위와 같이 0
이라는 기본값을 반환한다면 회원은 굉장히 당황할 것이다...! 차라리 '네트워크 장애'라는 메시지를 보여주면 더 나을 것이다.
기본값 반환과 비슷하지만 의미 없는 빈 리스트나 빈 객체를 반환하는 것을 말한다.
const getInvoices = (userId: number) => {
const userAccount = accountStore.lookup(userId);
if(!userAccount) {
return [];
}
// ...
return userAccount.invoices;
}
먼저 위 예제와 같이 논리적으로 적합하지 않다. 빈 리스트 반환으로 에러 없이 동작은 하겠지만 이를 조회의 오류로 표현하는 것을 옳지 않다.
const getInvoices = (userId: number) => {
const userAccount = accountStore.lookup(userId);
if(!userAccount) {
return;
}
// ...
return userAccount.invoices;
}
또는
const fetchAccountInfo = async (userId: number): Promise<AccountInfo> => {
try {
const response = await fetch(`/api/customers/${userId}`);
// ...
return accountInfo;
} catch (error) {
// 텅.
}
}
이런 코드를 작성할까 싶지만, 오류가 발생했음에도 불구하고 처리하지 않아 나중에 버그가 발생할 가능성이 매우 높은 코드라고 볼 수 있다.
그럼 오류는 어떻게 처리하는 것이 좋을까?
복구가 불가능한 경우에는 더 높은 계층에서, 즉 에러를 핸들링하기 쉬운 곳에서 처리하며 오류를 잠재적으로 복구 가능한 경우에는 호출하는 곳에 오류를 알려 처리하도록 하는 것이 일반적이다.
책에서 설명하는 오류 전달 방법에는 명시적
, 암시적
방법이 존재한다.
명시적
으로 오류를 전달하면 코드 계약에서 오류가 발생할 가능성을 파악할 수 있기 때문에 오류를 모르고 넘어가는 방법은 거의 없을 것이다.
반대로 암시적
인 방법은 코드를 호출하는 쪽에 오류를 신경쓰지 않도록 해 적극적인 코드 계약을 읽는 등의 적극적인 노력이 필요하다.
📢 그래서 책에서는
명시적
인 오류 전달 방식을 적극 권장한다. 필자는 지속적인 관리가 이루어지지 않는 문서화의 우려에암시적
인 방식은 바람직하지 않다고 판단하고 있었다.
'함수를 호출하는 곳에서 값을 얻을 수 없음을 알릴 뿐만 아니라 값을 얻을 수 없는 이유까지 알려주면 유용하다. '라는 글이 있었다.
이를 리절트 유형
을 사용해 구현할 수 있는데 swift
, rust
, F#
과 같은 언어들은 리절트를 지원하지만 안타깝게도 JS는 지원하지 않는다.
하지만 fp-ts
의 Either
로 명시적으로 오류의 원인을 전달해볼 수 있다. 이런 게 있는지 몰랐는데 정말 신기했다. (내용 공유해주셔서 감사합니닷)
Either를 간단히 설명하자면,
Either<E, A>
에서 E는 검사할 값이며 A에는 E가 유효하지 않았을 때 반환할 값을 작성해 유효성 검사 규칙을 구현할 수 있다. 여기서는 A를 에러 메시지로 활용했다.
다음 코드는 비밀번호 유효성 검사를 위한 코드로 최소 6자 이상, 특수 문자를 포함해야 한다는 규칙을 검증한다.
import type { Either } from 'fp-ts/lib/Either';
import { left, right } from 'fp-ts/lib/Either';
import { chain } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
const minLength = (password: string): Either<string, string> =>
s.length >= 6
? right(s)
: left('최소 6자 이상 입력해주세요.');
const specialSymbol = (password: string): Either<string, string> =>
/[`~!@@#$%^&*|₩₩₩'₩";:₩/?]/gi.test(password)
? right(password)
: left('특수문자를 포함해 입력해주세요.');
const validatePassword = (password: string): Either<string, string> =>
pipe(minLength(s), chain(specialSymbol));
console.log(validatePassword('ab'));
// => left('최소 6자 이상 입력해주세요.')
console.log(validatePassword('abcdef'));
// => left('특수문자를 포함해 입력해주세요.')
암시적 방법으로는 매직값
반환이 있다. 매직값은 함수의 반환 타입에는 올바르기 때문에 동작은 하겠지만 개발자 혼자 특별한 의미를 부여한 값이므로 함수가 호출되는 곳에서 매직값이 반환될 수 있다는 것을 알아야한다.
const getSquareRoot = (value) => {
if (value < 0) {
return -1;
}
return Math.sqrt(value);
}
추가적으로 주석을 통해 매직값 반환에 대해 알려야 하며, 오류를 알리는 좋은 방법은 아니다.
그리고 그동안 JAVA
언어만 나오다가 반갑게 JS
내용이 나왔다!
프로미스
를 사용한 오류 전달은 암시적인 방법이다. 오류가 발생하고 프로미스가 거부될 수 있음을 알려면 프로미스를 생성하는 함수의 구현 세부 사항을 알아야 하기 때문이다.
개인적으로 이번 장이 분량이 많아 조금 어렵게 느껴졌다. 모든 것을 이해하지 못해 아쉽지만 에러 핸들링에 대해 관심을 갖게 되는 계기가 되었다. 역시나 스터디를 통해 읽으니 여러 사람들을 통해 많은 것을 배울 수 있어서 좋았다.