
서론
오류를 나누는 기준은 여러가지가 있을 수 있습니다. 개발자의 관점에서 에러는 처리의 대상이며 상황에 따른 적절한 처리를 위해 소프트웨어가 계속 작동할 수 있는 오류와 작동할 수 없는 오류로 구분할 수 있습니다.
이 포스팅에서는 에러를 복구 가능에러와 복구 불가능 에러로 구분하고 각 에러의 처리 방식에 대한 이야기를 나눠보겠습니다.
복구 가능한 오류

시스템 전체에 영향을 미칠만큼 치명적이지 않으며 호출하는 곳에서 오류를 복구하는 것이 합리적인 경우입니다.
대표적인 예로 로그인폼에 패스워드를 잘못입력하는 경우가 있습니다. 이 경우 앱을 멈추는 대신에 사용자에게 다시 비밀번호 입력을 유도하는 것이 더 이상적입니다. 일반적으로 복구 가능한 오류의 예에는 다음과 같습니다.
1.네트워크 오류
외부 서비스에 연결할 수 없는 경우 몇 초 동안 기다렸다가 다시 시도하거나 사용자 권한이 필요한 요청이라면 사용자에게 권한을 확인하도록 요청하는 것이 좋습니다.
2.중요하지 않은 작업의 오류
서비스 이용에 대한 통계를 기록하는 부분에서 오류가 발생한다면 시스템이 계속 실행되어도 사용자의 이용에는 문제가 없을 것입니다.
복구할 수 없는 오류

오류의 영향 범위가 광역적이거나 오류를 복구하고 사용자 경험을 유지할 방법이 없는 경우입니다.
이 경우 최선의 방법은 오류를 기록하고 사용자에게 적절한 오류에 대한 UI를 제공하는 것일 것입니다.
대부분 오류에 대한 처리를 상위 계층에서 이뤄진다.
오류가 발생하면 일반적으로 더 높은 계층으로 오류를 전달해야 합니다.
오류 처리를 상위 계층에 위임함으로써 하위 모듈의 가독성 및 유지보수성을 높일 수 있고 오류에 대한 중앙 관리를 통해 오류 기록이나 사용자 알림등의 일관성있는 처리를 할 수 있습니다. 그렇기 때문에 일반적으로 오류를 숨기는 대신에 상위 계층으로 전달하는 것이 좋습니다. 오류를 숨기게 될 경우 다음과 같은 문제가 발생할 수 있습니다.
- 호출하는 쪽에서 복구하고자 할 수도 있는 오류를 숨기면 호출하는 쪽에서 오류로부터 복구할 수 있는 기회를 잃게 됩니다. 정확하고 의미있는 오류 메세지를 표시하거나 다른 동작을 하는 대신 잘못된 일이 일어날 수 있음을 전혀 알지 못하게 되고 이것은 소프트웨어가 의도한 대로 작동하지 않은 가능성이 크다는것을 의미합니다.
- 복구할 수 없는 오류를 숨기면 버그의 조기 대응 가능성을 차단하게 됩니다.
오류를 감춰지는 경우에 대한 예시를 보여드리겠습니다.
이런 방법은 위와 같은 이유로 일반적으로 바람직한 방법이 아닙니다.
기본값 반환
아래 코드에서 fetchBalance 함수는 계좌 잔고를 조회하는 요청에서 에러가 발생시 0을 반환하고 있습니다.
이 경우 10억의 계좌 잔고가 있는 사용자가 0원이된 계좌화면을 볼 수 있기 때문에 좋은 처리라 할 수 없습니다.
function fetchBalance(accountId) {
try {
const response = fetch(`https://bank-api.com/balance/${accountId}`);
const data = response.json();
return data.balance;
} catch (error) {
return 0;
}
}
const accountId1 = "123456";
// 에러가 발생한 경우 0이 반환된다.
console.log(fetchBalance(accountId1));
null 반환
이 경우 null을 반환하기 때문에 명시적으로 데이터를 조회하지 못했음을 호출하는 코드에 전달할 수 있다는 점에서 개선되었다할 수 있습니다. 하지만 만약 조회하지 못한 이유가 여러가지일 경우 호출하는 쪽에서 이를 알 수 없기 때문에 한계가 존재합니다.
function fetchBalance(accountId) {
try {
const response = fetch(`https://bank-api.com/balance/${accountId}`);
const data = response.json();
return data.balance;
} catch (error) {
return null;
}
}
const accountId2 = "789012";
console.log(fetchBalance(accountId2));
아무것도 하지 않음
이번엔 북마크 요청을 하는 로직을 살펴보겠습니다.
이 경우 요청에 성공하더라도 반환값이 없을 수 있는데요. 아래 코드에서 catch절에서 아무런 처리를 하지 않고 있기 때문에 성공과 실패에 대한 구분이 불가능한 상황입니다. 호출측에서는 실패의 경우에도 성공으로 간주할 수 있습니다.
async function bookmark(productId) {
try {
await apiClient.bookmarkProduct(productId);
} catch (error) {
// 아무 처리도 하지 않음
}
}
const productId1 = "123456";
bookmark(productId1);
오류를 전달하는 방법
이제 우리는 오류가 발생한 경우 하위 모듈에서 상위 계층으로 오류를 전달함으로써 오류에 대한 판단을 위임해야 할 필요성에 대해서 알게 됐습니다. 이제 오류를 전달하는 방법에 대해서 이야기를 나눠보겠습니다.
오류는 전달하는 방법은암시적 방법과 명시적 방법으로 나눠 볼 수 있습니다.
명시적 방법
코드를 호출하는 쪽에서 오류가 발생할 수 있음을 인지하도록 합니다. 그것을 처리하든 다시 상위 계층으로 위임하든 호출하는 쪽에서 이를 결정할 수 있습니다.
암시적 방법
코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류의 존재를 모를 수 있는 경우입니다. 이 케이스의 경우 문서나 하위 모듈의 코드 세부사항을 읽어서 오류의 발생 가능성을 확인해야 합니다.
암시적 오류 전달
제곱근을 구하기 위한 getSquareRoot함수입니다. javasript에서 Math.sqrt를 통해 쉽게 계산할 수 있지만 설명을 위한 코드임을 참고 부탁드립니다. 아래 코드에서는 음수가 매개 변수로 전달될 경우 NegativeNumberError 에러를 반환하고 있는데요 이는 암묵적이며 코드의 상세 구현이나 주석을 확인하지 않으면 알아차릴 수 없는 부분입니다. 따라서 에러에 대한 예외처리가 누락될 가능성을 내포하고 있습니다.
class NegativeNumberError extends Error {
private erroneousNumber: number = -1
constructor(value: number) {
super(`${value}은 음수입니다.`)
this.erroneousNumber = value
}
getErroneousNumber() {
return this.erroneousNumber
}
}
/**
* 제곱근을 반환한다.
* @throws value가 음수인 경우 NegativeNumberError가 발생한다.
*/
function getSquareRoot(value: number):number {
if (value < 0) {
throw new NegativeNumberError(value)
}
return Math.sqrt(value)
}
eslint-plugin-jsdoc을 예외가 존재할 경우 @throws태그를 달 것을 eslint를 통해 알려줄 수 있는 플러그인입니다.
한가지 아쉬운점은 @throws태그가 있는 경우 호출하는 코드에서 예외처리가 필요함을 알려주지 못한다는 점에서 한계가 있었습니다.
eslint-plugin-jsdoc
JSDoc linting rules for ESLint.. Latest version: 48.5.0, last published: 6 days ago. Start using eslint-plugin-jsdoc in your project by running `npm i eslint-plugin-jsdoc`. There are 1032 other projects in the npm registry using eslint-plugin-jsdoc.
www.npmjs.com
Result 유형을 이용한 명시적 오류 전달
타입스크립트 환경에서 에러를 명시적으로 전달할 수 있는 방법중 하나로 Result 유형 타입이 있습니다.
반환값에 대한 일괄된 형식을 통해 에러를 명시할 수 있는 장점이 있지만 프로젝트 전반에 걸쳐 예외에 대한 표현을 통일해야 하기 때문에 팀내의 합의가 필요할 것 같습니다. Result타입을 통해 명시적으로 getSquareRoot를 개선해보겠습니다.
class Result<V, E> {
private value: V | null = null
private error: E | null = null
protected constructor(value: V | null, error: E | null) {
this.value = value
this.error = error
}
static ofValue<V, E>(value: V): Result<V, E> {
return new Result<V, E>(value, null)
}
static ofError<V, E>(error: E): Result<V, E> {
return new Result<V, E>(null, error)
}
isError(): this is E extends null ? never : ErrorResult<null, E> {
return this.error !== null
}
isSuccess(): this is V extends null ? never : SuccessResult<V, null> {
return this.isError() === false
}
getValue() {
return this.value
}
getError() {
return this.error
}
}
class SuccessResult<V, E> extends Result<V, E> {
constructor(value: V) {
super(value, null)
}
getValue(): V {
return this.getValue()
}
}
class ErrorResult<V, E> extends Result<V, E> {
constructor(error: E) {
super(null, error)
}
getError(): E {
return this.getError()
}
}
function getSquareRoot(value: number): Result<number, NegativeNumberError> {
if (value < 0) {
return Result.ofError(new NegativeNumberError(value))
}
return Result.ofValue(Math.sqrt(value))
}
function main() {
const result = getSquareRoot(10)
if (result.isSuccess()) {
return result.getValue() + 1
}
}
개선된 코드에서 getSquareRoot이 예외가 존재할 수 있음이 명시됐으며 호출 계층인 main함수에서 에러를 명시적으로 전달받고 처리할 수 있게 되었습니다.
정리
오류의 처리를 처리를 위해 하위 계층에서 상위 계층으로 오류를 전달하는 것이 좋으며 이를 위한 방법으로 암시적 방법과 명시적 방법이 있음을 알아봤습니다. 타입스크립트 환경에서 react를 이용해 웹앱을 개발하는 저는 암시적 에러 전달로 인해 에러 처리가 누락될 수 있는 문제를 크게 공감하진 못했던 것 같습니다. 왜나면 리액트에서 로직은 크게 렌더링 로직과 비랜더링 로직으로 구분되면서 사용자 경험에 치명적 영향을 주는 문제는 렌더링 에러이고 이 영역에서는 ErrorBoudary로 오류 범위를 제한하고 있으니까요. 네트워크 요청같은 대표적인 비랜더링 로직의 경우 자연스럽게 글로벌 영역까지 도달 하도록 흘려보내면 unhandlePromise로 센트리에 수집되고요. 하지만 특정 함수에서 에러를 반환하는 시나리오에 대해 구분이 필요할 때는 명시적 에러 전달 방법이 효율적일 것 같다는 생각이 들었습니다.
'programming' 카테고리의 다른 글
[설계] 오용하기 어려운 코드 설계하기 (0) | 2024.06.30 |
---|---|
[설계] 단일 책임 원칙에 근거한 모듈 설계 (0) | 2024.06.28 |

서론
오류를 나누는 기준은 여러가지가 있을 수 있습니다. 개발자의 관점에서 에러는 처리의 대상이며 상황에 따른 적절한 처리를 위해 소프트웨어가 계속 작동할 수 있는 오류와 작동할 수 없는 오류로 구분할 수 있습니다.
이 포스팅에서는 에러를 복구 가능에러와 복구 불가능 에러로 구분하고 각 에러의 처리 방식에 대한 이야기를 나눠보겠습니다.
복구 가능한 오류

시스템 전체에 영향을 미칠만큼 치명적이지 않으며 호출하는 곳에서 오류를 복구하는 것이 합리적인 경우입니다.
대표적인 예로 로그인폼에 패스워드를 잘못입력하는 경우가 있습니다. 이 경우 앱을 멈추는 대신에 사용자에게 다시 비밀번호 입력을 유도하는 것이 더 이상적입니다. 일반적으로 복구 가능한 오류의 예에는 다음과 같습니다.
1.네트워크 오류
외부 서비스에 연결할 수 없는 경우 몇 초 동안 기다렸다가 다시 시도하거나 사용자 권한이 필요한 요청이라면 사용자에게 권한을 확인하도록 요청하는 것이 좋습니다.
2.중요하지 않은 작업의 오류
서비스 이용에 대한 통계를 기록하는 부분에서 오류가 발생한다면 시스템이 계속 실행되어도 사용자의 이용에는 문제가 없을 것입니다.
복구할 수 없는 오류

오류의 영향 범위가 광역적이거나 오류를 복구하고 사용자 경험을 유지할 방법이 없는 경우입니다.
이 경우 최선의 방법은 오류를 기록하고 사용자에게 적절한 오류에 대한 UI를 제공하는 것일 것입니다.
대부분 오류에 대한 처리를 상위 계층에서 이뤄진다.
오류가 발생하면 일반적으로 더 높은 계층으로 오류를 전달해야 합니다.
오류 처리를 상위 계층에 위임함으로써 하위 모듈의 가독성 및 유지보수성을 높일 수 있고 오류에 대한 중앙 관리를 통해 오류 기록이나 사용자 알림등의 일관성있는 처리를 할 수 있습니다. 그렇기 때문에 일반적으로 오류를 숨기는 대신에 상위 계층으로 전달하는 것이 좋습니다. 오류를 숨기게 될 경우 다음과 같은 문제가 발생할 수 있습니다.
- 호출하는 쪽에서 복구하고자 할 수도 있는 오류를 숨기면 호출하는 쪽에서 오류로부터 복구할 수 있는 기회를 잃게 됩니다. 정확하고 의미있는 오류 메세지를 표시하거나 다른 동작을 하는 대신 잘못된 일이 일어날 수 있음을 전혀 알지 못하게 되고 이것은 소프트웨어가 의도한 대로 작동하지 않은 가능성이 크다는것을 의미합니다.
- 복구할 수 없는 오류를 숨기면 버그의 조기 대응 가능성을 차단하게 됩니다.
오류를 감춰지는 경우에 대한 예시를 보여드리겠습니다.
이런 방법은 위와 같은 이유로 일반적으로 바람직한 방법이 아닙니다.
기본값 반환
아래 코드에서 fetchBalance 함수는 계좌 잔고를 조회하는 요청에서 에러가 발생시 0을 반환하고 있습니다.
이 경우 10억의 계좌 잔고가 있는 사용자가 0원이된 계좌화면을 볼 수 있기 때문에 좋은 처리라 할 수 없습니다.
function fetchBalance(accountId) {
try {
const response = fetch(`https://bank-api.com/balance/${accountId}`);
const data = response.json();
return data.balance;
} catch (error) {
return 0;
}
}
const accountId1 = "123456";
// 에러가 발생한 경우 0이 반환된다.
console.log(fetchBalance(accountId1));
null 반환
이 경우 null을 반환하기 때문에 명시적으로 데이터를 조회하지 못했음을 호출하는 코드에 전달할 수 있다는 점에서 개선되었다할 수 있습니다. 하지만 만약 조회하지 못한 이유가 여러가지일 경우 호출하는 쪽에서 이를 알 수 없기 때문에 한계가 존재합니다.
function fetchBalance(accountId) {
try {
const response = fetch(`https://bank-api.com/balance/${accountId}`);
const data = response.json();
return data.balance;
} catch (error) {
return null;
}
}
const accountId2 = "789012";
console.log(fetchBalance(accountId2));
아무것도 하지 않음
이번엔 북마크 요청을 하는 로직을 살펴보겠습니다.
이 경우 요청에 성공하더라도 반환값이 없을 수 있는데요. 아래 코드에서 catch절에서 아무런 처리를 하지 않고 있기 때문에 성공과 실패에 대한 구분이 불가능한 상황입니다. 호출측에서는 실패의 경우에도 성공으로 간주할 수 있습니다.
async function bookmark(productId) {
try {
await apiClient.bookmarkProduct(productId);
} catch (error) {
// 아무 처리도 하지 않음
}
}
const productId1 = "123456";
bookmark(productId1);
오류를 전달하는 방법
이제 우리는 오류가 발생한 경우 하위 모듈에서 상위 계층으로 오류를 전달함으로써 오류에 대한 판단을 위임해야 할 필요성에 대해서 알게 됐습니다. 이제 오류를 전달하는 방법에 대해서 이야기를 나눠보겠습니다.
오류는 전달하는 방법은암시적 방법과 명시적 방법으로 나눠 볼 수 있습니다.
명시적 방법
코드를 호출하는 쪽에서 오류가 발생할 수 있음을 인지하도록 합니다. 그것을 처리하든 다시 상위 계층으로 위임하든 호출하는 쪽에서 이를 결정할 수 있습니다.
암시적 방법
코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류의 존재를 모를 수 있는 경우입니다. 이 케이스의 경우 문서나 하위 모듈의 코드 세부사항을 읽어서 오류의 발생 가능성을 확인해야 합니다.
암시적 오류 전달
제곱근을 구하기 위한 getSquareRoot함수입니다. javasript에서 Math.sqrt를 통해 쉽게 계산할 수 있지만 설명을 위한 코드임을 참고 부탁드립니다. 아래 코드에서는 음수가 매개 변수로 전달될 경우 NegativeNumberError 에러를 반환하고 있는데요 이는 암묵적이며 코드의 상세 구현이나 주석을 확인하지 않으면 알아차릴 수 없는 부분입니다. 따라서 에러에 대한 예외처리가 누락될 가능성을 내포하고 있습니다.
class NegativeNumberError extends Error {
private erroneousNumber: number = -1
constructor(value: number) {
super(`${value}은 음수입니다.`)
this.erroneousNumber = value
}
getErroneousNumber() {
return this.erroneousNumber
}
}
/**
* 제곱근을 반환한다.
* @throws value가 음수인 경우 NegativeNumberError가 발생한다.
*/
function getSquareRoot(value: number):number {
if (value < 0) {
throw new NegativeNumberError(value)
}
return Math.sqrt(value)
}
eslint-plugin-jsdoc을 예외가 존재할 경우 @throws태그를 달 것을 eslint를 통해 알려줄 수 있는 플러그인입니다.
한가지 아쉬운점은 @throws태그가 있는 경우 호출하는 코드에서 예외처리가 필요함을 알려주지 못한다는 점에서 한계가 있었습니다.
eslint-plugin-jsdoc
JSDoc linting rules for ESLint.. Latest version: 48.5.0, last published: 6 days ago. Start using eslint-plugin-jsdoc in your project by running `npm i eslint-plugin-jsdoc`. There are 1032 other projects in the npm registry using eslint-plugin-jsdoc.
www.npmjs.com
Result 유형을 이용한 명시적 오류 전달
타입스크립트 환경에서 에러를 명시적으로 전달할 수 있는 방법중 하나로 Result 유형 타입이 있습니다.
반환값에 대한 일괄된 형식을 통해 에러를 명시할 수 있는 장점이 있지만 프로젝트 전반에 걸쳐 예외에 대한 표현을 통일해야 하기 때문에 팀내의 합의가 필요할 것 같습니다. Result타입을 통해 명시적으로 getSquareRoot를 개선해보겠습니다.
class Result<V, E> {
private value: V | null = null
private error: E | null = null
protected constructor(value: V | null, error: E | null) {
this.value = value
this.error = error
}
static ofValue<V, E>(value: V): Result<V, E> {
return new Result<V, E>(value, null)
}
static ofError<V, E>(error: E): Result<V, E> {
return new Result<V, E>(null, error)
}
isError(): this is E extends null ? never : ErrorResult<null, E> {
return this.error !== null
}
isSuccess(): this is V extends null ? never : SuccessResult<V, null> {
return this.isError() === false
}
getValue() {
return this.value
}
getError() {
return this.error
}
}
class SuccessResult<V, E> extends Result<V, E> {
constructor(value: V) {
super(value, null)
}
getValue(): V {
return this.getValue()
}
}
class ErrorResult<V, E> extends Result<V, E> {
constructor(error: E) {
super(null, error)
}
getError(): E {
return this.getError()
}
}
function getSquareRoot(value: number): Result<number, NegativeNumberError> {
if (value < 0) {
return Result.ofError(new NegativeNumberError(value))
}
return Result.ofValue(Math.sqrt(value))
}
function main() {
const result = getSquareRoot(10)
if (result.isSuccess()) {
return result.getValue() + 1
}
}
개선된 코드에서 getSquareRoot이 예외가 존재할 수 있음이 명시됐으며 호출 계층인 main함수에서 에러를 명시적으로 전달받고 처리할 수 있게 되었습니다.
정리
오류의 처리를 처리를 위해 하위 계층에서 상위 계층으로 오류를 전달하는 것이 좋으며 이를 위한 방법으로 암시적 방법과 명시적 방법이 있음을 알아봤습니다. 타입스크립트 환경에서 react를 이용해 웹앱을 개발하는 저는 암시적 에러 전달로 인해 에러 처리가 누락될 수 있는 문제를 크게 공감하진 못했던 것 같습니다. 왜나면 리액트에서 로직은 크게 렌더링 로직과 비랜더링 로직으로 구분되면서 사용자 경험에 치명적 영향을 주는 문제는 렌더링 에러이고 이 영역에서는 ErrorBoudary로 오류 범위를 제한하고 있으니까요. 네트워크 요청같은 대표적인 비랜더링 로직의 경우 자연스럽게 글로벌 영역까지 도달 하도록 흘려보내면 unhandlePromise로 센트리에 수집되고요. 하지만 특정 함수에서 에러를 반환하는 시나리오에 대해 구분이 필요할 때는 명시적 에러 전달 방법이 효율적일 것 같다는 생각이 들었습니다.
'programming' 카테고리의 다른 글
[설계] 오용하기 어려운 코드 설계하기 (0) | 2024.06.30 |
---|---|
[설계] 단일 책임 원칙에 근거한 모듈 설계 (0) | 2024.06.28 |