TIL 114 - Error Handling

김영현·2024년 12월 31일
1

TIL

목록 보기
124/129

오류(Error)

개발을 하다보면 다양한 오류와 마주치게된다. 개중 개발자가 잘못 작성하여 일어난 참사인 컴파일 오류가 있고, 프로그램이 돌아가다 예측하지 못한 상황에서 마주하게되는 런타임 오류가 있다.

컴파일오류는 잡기 쉽다. 당장 틀린 문법을 수정하기만 하면 되니까.

그러나 런타임 오류는 컴파일 시점에 알지 못해 프로그램을 박살내곤 한다.

이렇게 프로그램이 박살나면, 다시 프로그램을 가동하기 전까지 멈춘다.

따라서 런타임 오류를 개발자가 처리해주어야 한다.


오류 처리하기(Error Handling)

JS에서는 오류를 처리하는 방법은 간단하다.

try{
	eat();
} catch(err){
	console.log('에러가 발생했어용', err);
}

만약 eat()함수에서 에러가 발생하면, catch(err)절에 걸리게된다.
try-catch절을 가지고 처리할 오류는 뭐가 있을까?
보통 오류를 처리하게 되는 상황은 다른 애플리케이션과 상호작용할때 일어난다.

  • 서버측에서 db와 통신할때
  • 클라이언트측에서 서버와 통신할때
  • 더 있긴할듯?🤔

이처럼 예상 가능한오류를 먼저 처리하면 프로그램이 죽는걸 방지할 수 있다.

예시로 한번 보자.

예측 가능한 오류

간단한 서버측 db쿼리 함수가 있다.

const getFruitsService = async() => {
	return await db.collection('fruits').find();
}

//controller...

해당 함수는 서버측에서 작동하며, db에 접근하여 과일들을 가져온다.
이때 예측 가능한 오류는 뭐가 있을까?

  1. db서버가 죽어있다.
  2. 예측 불가능한 오류가 존재할 수 있다.
  3. 과일들이 없다.

1번은 충분히 예측 가능한 오류다.
2번 또한 말장난 같지만, 예측 불가능한 오류가 발생할 것이라는 걸 통신이라는 관점에서 충분히 예측 가능하다.
3번은 사실 비즈니스 관점마다 다를 것이다. 따라서 명확한 오류라고 보기는 어렵긴 하다.

그렇다면 위 함수를 한 번 리팩토링 해보자.

const getFruitsService = async() => {
	try{
    	const fruits = await db.collection('fruits').find();
      	if(!fruits) throw new Error('과일이 없습니다');
      
      	return fruits;
    } catch (err){
    	return {error:true, message: err.message};
    }
}

//controller...
const getFruitsController = async (req, res) => {
    const fruits = await getFruitsService();
    let httpStatus = 200;
    
    if (fruits.error) {
        httpStatus = 404;  // 데이터가 없는 경우 404 반환
    }
    
    res.writeHead(httpStatus, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(fruits));
};

이전보다 안전한 코드가 되었다. 또한 controller는 에러가 있을때 http 코드만 수정하고 무조건 fruits를 반환한다. 여기에 어떤 값이 들었는지는 알 필요 없으니...

이렇게 오류를 처리하면 방치하는 것 보다 낫다만, 실제 api 연결은 이보다 복잡하기 마련이다.

복잡한 오류 처리하기(CustomError Class)

예를들어 과일목록에 새로운 과일을 추가하는 기능을 만들어야한다고 가정해보자.

요구사항은 다음과 같다.

  1. 과일에 대한 정보는 이름과 가격이 있다. 이는 필수다.
  2. 과일은 중복해서 들어갈 수 없다.

만약 위와같은 요구사항(비즈니스)이 있을때 어떻게 에러를 처리하면 좋을까? 일단 생각나는 대로 작성해보자.

// 서비스 계층
const createFruitService = async (fruitData) => {
    const { name, price } = fruitData;

    // 1. 파라미터 검증 (누락된 경우)
    if (!name || !price) {
        throw new Error('이름과 가격은 필수입니다.');
    }

    // 2. 과일 중복 체크
    const existingFruit = await db.collection('fruits').findOne({ name });
    if (existingFruit) {
        throw new Error('이미 존재하는 과일입니다.');
    }

    try {
        // 3. 과일 추가
        const result = await db.collection('fruits').insertOne({ name, price });
        return result;
    } catch (err) {
        throw new Error('과일 추가 중 데이터베이스 오류 발생');
    }
};

// 컨트롤러 계층
const createFruitController = async (req, res) => {
    let httpStatus = 201;
    let result;

    try {
        const fruitData = JSON.parse(req.body);  // JSON 파싱
        result = await createFruitService(fruitData);
    } catch (err) {
        // 4. 다양한 에러 처리
        if (err.message === '이름과 가격은 필수입니다.') {
            httpStatus = 400;  // 파라미터 누락
            result = { error: err.message };
        } else if (err.message === '이미 존재하는 과일입니다.') {
            httpStatus = 409;  // 중복 데이터
            result = { error: err.message };
        } else if (err.message === '과일 추가 중 데이터베이스 오류 발생') {
            httpStatus = 500;  // DB 삽입 실패
            result = { error: err.message };
        } else {
            httpStatus = 500;  // 알 수 없는 서버 오류
            result = { error: '알 수 없는 서버 에러' };
        }
    }

    res.writeHead(httpStatus, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(result));
};

총 4개의 오류를 Error클래스를 이용하여 처리하였다. 이때문에 에러를 문자열과 동등연산하여 구별한다.
누가 봐도 좋지 않은 방법이다. 실제로 MDN에서는 사용자 정의 에러타입을 이용하여 에러의 파생 오류 정의를 고려하는 방법을 추천한다.
또한 스택오버플로우에서 진행한 심도 깊은 논의도 한 번 살펴보면 좋다.

위 문서 둘을 읽었다는 가정 하에 리팩토링을 해보자.

// 서비스 계층
const createFruitService = async (fruitData) => {
    const { name, price } = fruitData;

    // 1. 파라미터 검증 (누락된 경우)
    if (!name || !price) {
        throw new ValidationError('이름과 가격은 필수입니다.');
    }

    // 2. 과일 중복 체크
    const existingFruit = await db.collection('fruits').findOne({ name });
    if (existingFruit) {
        throw new ConflictError('이미 존재하는 과일입니다.');
    }

    try {
        // 3. 과일 추가
        const result = await db.collection('fruits').insertOne({ name, price });
        return result;
    } catch (err) {
        throw new DatabaseError('과일 추가 중 데이터베이스 오류 발생');
    }
};

// 컨트롤러 계층
const createFruitController = async (req, res) => {
    let httpStatus = 201;
    let result;

    try {
        const fruitData = JSON.parse(req.body);  // JSON 파싱
        result = await createFruitService(fruitData);
    } catch (err) {
        // 4. 다양한 에러 처리
         const { status, error } = handleErrors(err);
      	 httpStatus = status;
         result = { error };
    }

    res.writeHead(httpStatus, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(result));
};

//다양한에러를 받아 분기로 처리하는 함수
const handleErrors = (err) => {
    if (err instanceof ValidationError) {
        return { status: 400, error: err.message };
    }
    if (err instanceof ConflictError) {
        return { status: 409, error: err.message };
    }
    if (err instanceof DatabaseError) {
        return { status: 500, error: err.message };
    }
    return { status: 500, error: '알 수 없는 서버 에러' };
};

// 커스텀 에러 클래스 정의
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = this.constructor.name; // 클래스 이름을 this.name에 할당
        this.stack = (new Error()).stack; // 스택 추적 추가!
    }
}

class ConflictError extends Error {
    constructor(message) {
        super(message);
        this.name = this.constructor.name;
        this.stack = (new Error()).stack; 
    }
}

class DatabaseError extends Error {
    constructor(message) {
        super(message);
        this.name = this.constructor.name; 
        this.stack = (new Error()).stack; 
    }
}

다양한 오류를 각 커스텀 클래스로 정의하여 오류의 종류 판별과 재사용성을 증가시켰다.
뿐만 아니라 handleErrors()함수가 각 오류의 분기 처리를 담당하게 함으로써 try-catch절의 코드 양을 감소시키고 각 분기에 대한 대응을 쉽게 변경할 수 있게 되었다.


마치며

애플리케이션 간 통신을 할때 발생한 예측 가능한 오류들은 사실 커스텀 클래스를 이용하면 처리가 그리 어렵지 않아보인다.
그렇다면 서버측과 클라이언트측서로 오류의 정의를 잘 공유하는 것이 핵심 아닐까?
에러 처리는 생각보다 더 귀찮은 작업이 아닐까 싶다.

profile
모르는 것을 모른다고 하기

0개의 댓글