복구 가능한 오류와 불가능한 오류를 구분하기
: 어떤 예외는 상시로 발생해서 무시할 수 있고, 어떤 예외는 무시하면 안되기 때문에 이들을 구분해야한다
- 복구 가능한 오류
: 시스템 외적인 요소로 발생하는 치명적이지 않은 오류
: 사용자의 오입력, 네트워크 오류, 서비스적으로 중요하지 않은 작업의 오류
: 사용자에게 문제 원인을 인지할 수 있게 해줘야한다
: 로그 레벨을 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
'Programming > TypeScript' 카테고리의 다른 글
타입스크립트 데코레이터 (5) | 2024.01.13 |
---|---|
프로젝트 내에서 타입 관리하기 (0) | 2023.12.28 |
타입스크립트 클린코드 (0) | 2023.12.28 |
nodejs / typescript / express API 문서 자동화 도입기 (실패) (0) | 2023.12.28 |
자바스크립트에 타입스크립트 적용 (0) | 2023.11.20 |