[JS] 커스텀 에러

Pakxe·2022년 11월 28일
0

JavaScript

목록 보기
7/16
post-thumbnail

개발을 하다보면 이미 알려진 에러들 ReferenceError, SyntaxError 등 말고도 다른 에러인 상황이 많이 발생한다.

흔히 웹사이트에서 볼 수 있는 NotFoundError(404..) 같은 에러는 js에서 기본으로 제공해주지 않는다.

따라서 직접 에러 클래스를 만드는 것이 필요한데, 이를 커스텀 에러라고 한다.
앞선 글에서 적었듯이 보통의 에러 객체는 name, message(가능하면 stack까지) 프로퍼티를 가져야하며 다른 프로퍼티를 추가해도 된다.
예를 들어 HttpError면 statusCode 프로퍼티를 만들고 404나 500 같은 숫자를 값으로 지정할 수 있다.

throw의 인수에는 아무런 제약이 없다. 어떤 자료형이든 가능하다. 따라서 커스텀 에러 클래스가 반드시 Error를 상속할 필요는 없다. 하지만 obj instanceof Error를 사용해서 에러 객체를 식별할 수 있다는 장점이 생기므로, Error를 상속받아 에러 객체는 것이 낫겠다.

애플리케이션 크기가 커진다면 우리가 만드는 커스텀 에러 클래스들은 자연스레 계층 구조를 형성하게 된다. HttpTimeoutError는 HttpError를 상속받는 식으로 말이다.

Error 객체의 내부는?

우리가 Error객체를 상속해 커스텀 객체를 만든다고 하면, 상속할 객체의 내부 사정정도는 알고 있어야 한다.

Error객체의 내부는 다음과 같이 생겼다.

class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
    this.stack = <call stack>;  // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
  }
}

Error를 상속해 커스텀 에러를 작성해보자

class ValidationError extends Error {
	constructor(message) {
    	super(message);
        this.name = "ValidationError";
    }
}

function test() {
	throw new ValidationError('에러 발생');
}

try {
	test();
} catch (err) {
	alert(err.message); // 에러 발생
    alert(err.name); // ValidationError
   	alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}

자바스크립트에서는 자식 생성자 안에서 super를 반드시 호출해야한다. 이를 통해 message 프로퍼티는 부모 생성자에서 설정되게 된다.

실제 상황에서 사용해보자

class ValidationError extends Error {
	construnctor(message) {
      	super(message);
      	this.name = "ValidationError";
    }
}

function readUser (json) {
  	let user = JSON.parse(json);
  
  	if (!user.age) throw new ValidationError('No field: age');
    if (!user.name) throw new ValidationError('No field: name');
	
  	return user;
}

// try catch 와 readUser를 함께 사용
try {
  	let user = readUser('{'age': 22}');
} catch (e) {
  	if (e instanceof ValidationError) alert('Invalid data: ' + e.message);
  	else if (e instanceof SyntaxError) alert('JSON Syntax Error: ' + e.message);
  	else throw e; // 모르는 에러는 다시던지기
}

이제 try catch 에서 커스텀 에러 ValidationError와 SyntaxError를 둘 다 처리할 수있다. 이 과정에서 instanceof로 에러 유형을 확인했다.
하지만 e.name === 'ValidationError'를 사용해서 확인해도 된다.

그런데 에러 유형 확인은 e.name 보다는 instanceof를 사용하는게 좋다. 나중에 ValidationError를 확장해서 다른 확장 에러를 만들게 되는데, 이때 instanceof는 새로운 상속 클래스에서도 동작하기 때문이다.

catch 블럭에서는 유효성 검사와 문법 오류만 처리하고, 다른 종류의 에러는 밖으로 던진다. catch에 알려지지 않은 에러가 있다면 반드지 다시 던지기를 수행하자.

더 깊게 상속하기

앞에서 만든 ValidationError 클래스는 많이 포괄적이다. age 프로퍼티에 문자열이 들어가는 경우와 같은건 처리할 수 없다, 따라서 더 깊게 상속해 다른 커스텀 에러를 만들어보자.

class ValidationError extends Error {
	construnctor(message) {
      	super(message);
      	this.name = "ValidationError";
    }
}

class PropertyRequiredError extends ValidationError {
  	constructor(property) {
      	super('No property: ' + property);
      	this.name = 'PropertyRequiredError';
      	this.property = property;
    }
}

function readUser (json) {
  	let user = JSON.parse(json);
  
  	if (!user.age) throw new PropertyRequiredError('age');
    if (!user.name) throw new PropertyRequiredError('name');
	
  	return user;
}

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No property: name
    alert(err.name); // PropertyRequiredError
    alert(err.property); // name
  } else if (err instanceof SyntaxError) {
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // 알려지지 않은 에러는 재던지기 합니다.
  }
}

새로 만든 PropertyRequiredError 클래스는 사용하기 쉽다. 프로퍼티 명만 전달해주면 되기 때문이다. 이 메세지를 읽기 좋게 가공하는건 클래스가 수행하게 됐다.

그런데 귀찮은 부분이 있다

this.name = 'PropertyRequiredError';를 매번 작성해줘야하는 걸까? 기본으로 제공되는 에러 클래스들 Error, SyntaxError 등과 같은 클래스는 이 이름 자체로 this.name을 설정한다. 커스텀 에러도 가능할까?

우리는 '기본 에러' 클래스를 만들고 커스텀 에러들이 이 클래스를 상속받게 하면 된다. 기본 에러 생성자에 this.name = this.constructor.name을 추가하면 된다.

class MyError extends Error {
	constructor (message){
    	super(message);
        this.name = this.constructor.name;
    }
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("No property: " + property);
    this.property = property;
  }
}

// 제대로 된 이름이 출력됩니다.
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

에러가 엄청 많아지면?

더 깊게 상속하기 목차에서 쓰인 코드를 보면 오류들을 if 조건문으로 나눠서 처리하고 있다. 그런데 에러 종류가 더 많아지면 그때마다 매번 조건문을 작성해야하니 매우 귀찮은 일이된다.

사실 우리가 필요로 하는 정보는 '데이터를 읽을 때' 에러가 발생했는지에 대한 여부다. 왜 에러가 발생했는지에 대한 자세한 설명은 보통 필요없다. 필요한 경우에만 제공하는 것이다.

이런 에러 처리 기술은 예외 감싸기(wrapping exception)라고 부른다.

예외 감싸기

예외 감싸기는 다음과 같은 순서로 진행된다.

  1. '데이터 읽기' 와 같은 포괄적인 에러를 대변하는 새로운 클래스 ReadError를 만든다.
  2. 함수 readUser에서 발생한 ValidationError, SyntaxError 등의 에러는 readUser 내부에서 잡고 이때 ReadError를 생성한다.
  3. ReadError 객체의 cause 프로퍼티엔 실제 에러에 대한 참조가 저장된다.

이렇게 예외 감싸기 기술을 써 스키마를 변경하면 readUser를 호출하는 코드에선 ReadError만 확인하면 된다. 데이터를 읽을 때 발생하는 에러 종류 전체를 확인하지 않아도 된다는 것이다. 추가 정보가 필요한 경우엔 cause 프로퍼티를 확인하면 된다.

class ReadError extends Error {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
    this.name = this.constructor.name;
  }
}

class ValidationError extends Error {}
class PropertyRequiredError extends ValidationError {}

function validateUser(user) {
  if (!user.age) throw new PropertyRequiredError('age');
  if (!user.age) throw new PropertyRequiredError('name');
}

function readUser(json) {
  let user;

  try {
    user = JSON.parse(json);
  } catch (err) {
    if (err instanceof SyntaxError) throw new ReadError('Syntax Error', err);
    else throw err;
  }

  try {
    validateUser(user);
  } catch (err) {
    if (err instanceof ValidationError) {
      throw new ReadError('Validation Error', err);
    } else throw err;
  }
}

try {
  readUser('{잘못된 형식의 json}');
} catch (e) {
  if (e instanceof ReadError) {
    alert(e);
    alert(`original error: ${e.cause}`);
  } else throw e;
}

이제 readUser는 Syntax, Validation 에러가 발생한 경우 해당 에러 자체를 다시 던지는 것이 아닌 ReadError를 던지게 된다.

이렇게되면, readUser를 호출하는 바깥 코드에서는 insteadof ReadError 하나만 확인하면 되므로 에러 분기문을 여러개 만들 필요가 없다.

예외 감싸기라는 이름은 여러 에러들을 ReadError 에 하나로 모아 처리하기 때문에 붙여졌다. 이런 기법은 객체지향 프로그래밍에서 널리 쓰이는 패턴이다.

profile
내가 꿈을 이루면 나는 또 누군가의 꿈이 된다.

0개의 댓글