320x100
320x100

복구 가능한 오류와 불가능한 오류를 구분하기

: 어떤 예외는 상시로 발생해서 무시할 수 있고, 어떤 예외는 무시하면 안되기 때문에 이들을 구분해야한다

 

- 복구 가능한 오류

: 시스템 외적인 요소로 발생하는 치명적이지 않은 오류

: 사용자의 오입력, 네트워크 오류, 서비스적으로 중요하지 않은 작업의 오류

: 사용자에게 문제 원인을 인지할 수 있게 해줘야한다

: 로그 레벨을 warn으로 로깅한다

 

- 복구 불가능한 오류

: 별도의 조치 없이 시스템이 자동으로 복구할 수 있는 방법이 없는 경우

: Out Of Memory, StackOverflow, 시스템 레벨의 오류, 개발자의 잘못구현된 코드

: 로그 레벨을 error로 두고 트레이스를 남긴 뒤 임계치를 초과하면 개발자에게 알람을 보내도록 구성해야한다

 

 

 

 

 

 

null, -1, 빈 문자열 등 특수 값을 예외로 사용하지 않기

// bad
function divideWrong(a: number, b: number): number {
    if (b === 0) {
        return -1;  // 오류를 나타내는 대신 -1 반환
    }
    return a / b;
}

// good
function divideRight(a: number, b: number): number {
    if (b === 0) {
        throw new Error("Division by zero is not allowed.");  // 오류를 throw
    }
    return a / b;
}

: 예외는 Exception 으로만 처리해야한다

 

 

 

 

 

 

 

문자열을 throw 하지 않기

// bad
throw '유저 정보를 받아오는데 실패했습니다.';

// good
throw new Error ('유저 정보를 받아오는데 실패했습니다.');
throw new NotFoundResourceException ('유저 정보를 받아오는데 실패했습니다.');

: 문자열을 throw 하면 메시지 내용으로만 오류를 구분해야하기 때문에 자세한 스택 트레이스를 위해 Error 객체를  throw 해야한다

 

 

 

 

 

 

 

추적 가능한 예외

// bad 
throw new IllegalArgumentException('잘못된 입력입니다.');

// good 
throw new IllegalArgumentException(`사용자 ${userId}의 입력(${inputData})가 잘못되었다.`);

: 오류 메시지에 어떠한 값을 사용하다가 실패했는지, 실패한 작업의 이름과 실패 유형을 남겨야한다

: 로그로 남길 내용과 사용자에게 노출할 메시지는 분리되어야 한다

 

 

 

 

 

 

 

의미를 담고있는 예외

// bad
class CustomException extends Error {}

function connectToDatabase() {
    throw new CustomException("Connection failed because of invalid credentials.");
}


// good
class InvalidCredentialsException extends Error {}

function connectToDatabase() {
    throw new InvalidCredentialsException("Failed to connect due to invalid credentials.");
}

: 예외의 이름은 그 예외의 원인과 내용을 정확하게 반영해야한다

: 코드를 읽는 사람이 예외 이름만 보고도 해당 예외가 왜 발생했는지 어느정도 추측할 수 있어야 한다

 

 

 

 

 

 

Layer에 맞는 예외

: 여러 계층으로 구성된 소프트웨어 아키텍처에서 각 계층에 맞게 적절한 예외와 던지를 방법을 정의해야한다

: 오류를 더 쉽게 추적하고 각 계층에서 발생하는 오류의 본질에 따라 적절한 처리를 할 수 있도록 해야한다

// Data Access Layer
function fetchUserData(userId: string): any {
    // ...
    throw new DataAccessException("Failed to fetch user data from database.");
}

// Business Logic Layer
function getUserProfile(userId: string): any {
    try {
        const userData = fetchUserData(userId);
        // ... some business logic
    } catch (error) {
        if (error instanceof DataAccessException) {
            throw new BusinessLogicException("Error processing user profile.");
        }
    }
}

// Presentation Layer
function displayUserProfile(userId: string): void {
    try {
        const profile = getUserProfile(userId);
        // ... display logic
    } catch (error) {
        if (error instanceof BusinessLogicException) {
            throw new PresentationException("Error displaying user profile.");
        }
    }
}

// 글로벌 Error Handler
try {
    displayUserProfile("someUserId");
} catch (error) {
    if (error instanceof PresentationException) {
        console.error("UI Error:", error.message);
    } else {
        console.error("Unknown error:", error.message);
    }
}

 

 

 

 

 

 

 

예외 계층 구조 만들기

// bad
class DuplicatedException extends Error {}

class UserAlreadyRegisteredException extends Error {}


// good
class ValidationException extends Error {}

class DuplicatedException extends ValidationException {}

class UserAlreadyRegisteredException extends ValidationException {}

: 수많은 Custom Exception을 기준과 용도에 맞게 분류하고 그에 맞게 일관된 처리 방법을 적용한다

 

 

 

 

 

 

외부의 예외를 감싸기

// bad
function order() {
  const pay = new Pay();
  try{
      pay.billing();
      database.save(pay);
  } catch (e) {
      logger.error(`pay fail`, e);
  }
}

// good
function billing() {
  try {
    pay.billing();
  } catch (e) {
    throw new BillingException (e);
}

function order() {
  const pay = new Pay();
  pay.billing(); 
  
  try{
    database.save(pay);
  } catch (e) {
    pay.cancel();  
  }
}

: 외부 SDK, API를 통해 발생하는 예외들은 하나로 묶어서 처리한다

: 서로 다른 서비스 간 의존성을 분리해야한다

 

 

 

 

 

 

 

다시 throw 한다면 잡지 않기

// bad
function something() {
  try {
    // 비즈니스 로직
  } catch (e) {
    throw e
  }  
}

// good
function something() {
  // 비즈니스 로직
}

: catch 절에서 아무 작업도 없이 throw를 하는 코드는 불필요한 코드이다

 

 

 

 

 

 

무분별한 catch 금지

// bad
function fetchDataFromAPI() {
    // 가상의 API 호출
    if (/* 데이터가 없는 경우 */) {
        throw new NoDataFoundError();
    }
    // 데이터 반환
    return data;
}

function display() {
  try {
    const data = fetchDataFromAPI();
    process(data);
  } catch (error) {
      if (error instanceof NoDataFoundError) {
          displayEmptyState();
      } else {
          displayGenericError();
      }
  }
}

// good
function fetchDataFromAPI() {
  // 가상의 API 호출
  if (/* 데이터가 없는 경우 */) {
    return null;
  }
  
  // 데이터 반환
  return data;
}

function display() {
  const data = fetchDataFromAPI();

  if (!data) {
    displayEmptyState();
    return;
  }

  process(data);
}

: 예외는 실제 오류나 예기치 않은 상황을 처리하기 위해서만 사용되어야 한다

: 위 코드를 보면 catch 내에서 다른 함수를 호출해서 다른 작업을 수행하는데, 굳이 예외로 볼 것이 아니라면 catch를 쓰지말고 return 값으로 판단하게 해야한다

 

 

 

 

 

 

 

가능한 늦게 예외를 처리한다

: 예외를 throw 하자마자 잡지말고 가능한 가장 최상위 계층에서 처리한다

: 예외를 던지를 함수가 여러 곳에서 호출한다면 모든 곳에서 일일이 예외처리를 하지 말고 글로벌 핸들러 혹은 미들웨어 등 최상위 계층에서 예외를 처리하면 코드의 가독성과 재사용성이 향상된다

 

 

 

 

 

 

 

Reference

 

좋은 예외(Exception) 처리

좋은 예외 처리는 견고한 프로그램을 만들고, 좋은 사용자 경험을 줄 수 있다. 예외 처리를 통해 애플리케이션이 예기치 않게 종료되는 것을 방지하고, 갑작스런 종료 대신 사용자는 무엇이 잘

jojoldu.tistory.com

 

300x250
728x90