이번 스터디를 하면서 이런 피드백을 받았다.
피드백을 받게된 근원은 문제조건이다.
이 에러 문자를 같이 출력하기위해 "[ERROR]"만 따로 상수로 만들어 사용했는데
커스텀 에러를 사용한다면 에러를 만들때마다 이런 수고로움이 사라지고 유지보수도 수월해진다고 하셨다.
미션도 끝나고 시간도 여유로워서 천천히 공부를 해보았다.
사실 이전에 사용했던 것처럼 throw "[ERROR] 숫자만 입력할 수 있습니다." 이렇게 사용해도된다
왜냐하면 throw의 인수엔 아무런 제약이 없기 때문에 커스텀에러로 구성하여 사용할 필요가 없다.
그럼에도 만들면 좋은 이유는 커스텀에러에는 큰 장점이 있다.
바로 직관성이다.
우리가 단순히 어떤 작업을 할때 Error라고만 뜬다면 이게 무슨에러인지 어디서 발생한 에러인지 에러가 딱 뜨자마자 파악하기는 어렵다.
에러를 다 읽어봐야 파악이되고 어떨때는 추상적이여서 하나하나 다 뒤져봐야할 때도 있다.
하지만 만약 네트워크 관련 작업 중 에러가 발생했다면 HttpError,
데이터베이스 관련 작업 중 에러가 발생했다면 DbError,
검색 관련 작업중 에러가 발생했다면 NotFoundError가 뜬다면 훨씬 직관적일 것이다.
그렇다면 커스텀 에러는 어떻게 만들어야 할까?
일단 Error를 상속하면서 시작하면 된다.
하지만 꼭 Error를 상속해서 만들지 않아도 된다.
그럼에도 불구하고 Error를 상속받아 커스텀 에러 클래스를 만들게 된다면
obj instanceof Error를 사용해서 에러 객체를 식별할 수 있다는 장점이 생긴다.
이러한 장점 때문에 맨땅에서 커스텀 에러 객체를 만드는 것보다 Error를 상속받아 에러 객체를 만드는 것이 훨씬 낫다.
그럼 Error를 상속받기 전에 Error가 대략적으로 어떻게 생겼는지 부터 알고가자
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
this.stack = <call stack>; //
}
}
Error는 생성자 함수이고 이 생성자 함수가 에러 객체를 생성한다.
Error가 가장 일반적인 생성자 함수이고 이외에 Error로 부터 상속받은 SyntaxError, ReferenceError, TypeError등 총 7가지의 에러 객체를 생성할 수 있는 Error 생성자 함수를 자바스크립트가 제공한다.
이제 여기서 커스텀에러를 만들려면 Error를 상속받으면 된다.
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("에러 발생!");
}
try {
test();
} catch(err) {
console.log(err.message); // 에러 발생!
console.log(err.name); // ValidationError
console.log(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}
(1) 자바스크립트에서는 자식 생성자 안에서 super를 반드시 호출해야 한다. 왜냐하면 message 프로퍼티는 부모 생성자에서 설정되기 때문이다.
(2) 부모 생성자에서는 message 뿐만 아니라 name 프로퍼티도 설정("Error")하기 때문에, (2)에서 원하는 값으로 재설정 한다면 name을 바꿀 수 있습니다.
이제 이렇게 만든 커스텀 에러를 사용해보자
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError"; // 사실 이 예제에서는 this.name을 설정할 필요는 없다.
}
}
// 사용법
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": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
console.log("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
console.log("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 합니다. (**)
}
}
여기서는 우리가 아까 만든 커스텀에러인 ValidationError와 이미 존재하는 SyntaxError가 사용되었다.
즉 이러한 방법으로 서로 다른 에러를 구별해서 에러발생을 시킬 수 있다.
현재 로직에서 에러 유형확인은 instanceof로 확인하고 있는데 err.name을 사용해도 된다.
// (err instanceof SyntaxError) 대신 사용 가능
} else if (err.name == "SyntaxError")
하지만 에러 유형 확인은 err.name보다는 instanceof를 사용하는 게 훨씬 좋다.
왜냐하면 추후에 ValidationError를 확장하여 PropertyRequiredError 같은 새로운 확장 에러를 만들게 되는데,
instanceof는 새로운 상속 클래스에서도 동작하기 때문이다.
앞서 만든 ValidationError 클래스는 너무 포괄적이어서 뭔가 잘못될 확률이 존재한다.
꼭 필요한 프로퍼티가 누락되거나 age에 문자열 값이 들어가는 것처럼 형식이 잘못된 경우를 처리할 수없다.
필수 프로퍼티가 없는 경우에 대응할 수 있도록 조금 더 구체적인 클래스 PropertyRequiredError를 만들면 더 좋다.
PropertyRequiredError엔 누락된 프로퍼티에 대한 추가 정보가 담겨야 합니다.
class ValidationError extends Error {
constructor(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..catch와 readUser를 함께 사용하면 다음과 같습니다.
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
console.log('Invalid data: ' + err.message); // Invalid data: No property: name
console.log(err.name); // PropertyRequiredError
console.log(err.property); // name
} else if (err instanceof SyntaxError) {
console.log('JSON Syntax Error: ' + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 합니다.
}
}
new PropertyRequiredError(property) 처럼 프로퍼티 이름을 전달하기만 하면 된다.
message는 생성자가 알아서 만들어준다.
현재 PropertyRequiredError 생성자 안에서 this.name을 수동으로 할당해 주고있다.
이렇게 매번 커스텀 에러 클래스의 생성자 안에서 this.name을 할당해 주는 것은 귀찮은 작업니다.
이런 번거로운 작업은 '기본 에러' 클래스를 만들고 커스텀 에러들이 이 클래스를 상속받게 하면 해결이 가능하다.
기본 에러의 생성자에 this.name = this.constructor.name을 추가하면 된다.
기본 에러 클래스를 MyError 라고 해보자
MyError를 사용하면 다음과 같이 커스텀 에러 클래스를 간결하게 할 수 있다.
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;
}
}
// 제대로 된 이름이 출력됩니다.
console.log( new PropertyRequiredError("field").name ); // PropertyRequiredError
이렇게 한다면 ValidationError가 훨씬 더 깔끔해진다.
예외 감싸기
함수 readUser는 '사용자 데이터를 읽기' 위한 용도로 만들어졌다.
그런데 사용자 데이터를 읽는 과정에서 다른 오류가 발생할 수 있다.
지금 당장은 SyntaxError와 ValidationError를 사용해 에러를 처리하고 있는데, 앞으로 readUser가 더 커지면 다른 커스텀 에러 클래스를 만들어야 한다.
readUser를 호출하는 곳에선 새롭게 만들어질 커스텀 에러들을 처리할 수 있어야 한다.
그런데 지금 catch 블록 안에 if문 여러개를 넣어 종류를 알 수 있는 에러를 처리하고, 그렇지 않은 에러는 다시 던지기를 해 처리하고 있다.
현재 에러 처리 스키마는 다음과 같다.
try {
...
readUser() // 잠재적 에러 발생처
...
} catch (err) {
if (err instanceof ValidationError) {
// validation 에러 처리
} else if (err instanceof SyntaxError) {
// 문법 에러 처리
} else {
throw err; // 알 수 없는 에러는 다시 던지기 함
}
}
여기에는 두 종류의 에러만 있지만 더 추가될 수가 있다.
그렇다면 추가될때마다 에러 처리 분기문을 매번 추가해야한다면 물어보면 NO
실제 우리가 필요로 하는 정보는 '데이터를 읽을 때' 에러가 발생했는지에 대한 여부이다.
여기에 더해 필요할 때 에러 세부사항에 대한 정보를 얻을 수 있는 방법이 바로 예외 감싸기이다.
예외 감싸기는 다음과 같은 순서로 진행됩니다.
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 = 'ReadError';
}
}
class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
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);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
Syntax에러나 validation 에러가 발생한 경우 해당 에러 자체를 다시 던지기 하지 않고 ReadError를 던지게 된다.
이렇게 되면, readUser를 호출하는 바깥 코드에선 instanceof ReadError 딱 하나만 확인하면 된다.
즉 에러 처리 분기문을 여러 개 만들 필요가 없다.
이 개념을 나의 프로젝트에 적용해 보았다.
적용하기전 나의 코드이다.
이렇게 input값을 check하다 조건에 맞는 값이 아니면 Error를 throw한다.
그것을 최상위에 있는 controller가 try/catch문으로 잡아서 출력해준다.
정말 단순한 로직이다.
만들기도 정말 간편하다.
하지만 위 설명에서 말했듯이 앱의 규모가 커지고 로직이 많아지고 복잡해질수록
이런식의 에러설정은 유지보수를 더 어렵게 할 것이다.
무슨 에러가 발생했는지 분류만 잘해 놓아도 유지보수가 한결 쉬워질 것이다.
필자는 위글과 똑같이 하지는 않고 어느정도 참고해서 변형해보았다.
단순히 Error("~~~")이였던 에러 처리방식을 이제는 ValidationError()라는 커스텀 에러로 바꾸어주었다.
inputCheck에서 ValidationError을 throw을 하면
최상단에 있는 try/catch문이 받아서
errorHandler 함수로 전달해주고
그 error가 ValidationError이면
throw로 바로 종료시키는 대신 printError로 해당 에러를 출력해주고 다시 이전로직으로 돌아가도록했다.
만약 예상치 못한 에러면 치명적이기에 throw 통해 error를 출력과 동시에 앱을 끝내도록 했다.
이제는 이런식으로 에러가 뜨게된다.
stack은 사용자가 굳이 볼 필요가 없어 제외를 시킨다고 조금 손을 보았다.
그냥 출력하면 아래처럼 나온다.
stack을 같이 출력할지 제외할지 선택하면된다.
아래는 커스텀에러 코드들이다.
이번 스터디 피드백으로 커스텀에러라는 것에 대해 공부해보았다.
이렇게 커스텀에러 코드를 만드는 것은 간단하지만 에러 전체과정을 어느정도 이해해야하기에
시간이 조금 걸렸지만 추후에 엄청나게 도움이 많이 될 것 같다.
이런식으로 에러처리를 한다면 리팩토링이나 유지보수 하는 과정이 훨씬 수월해질 것 같다.
여기서 조금 더 공부해서 코드가 더 개선될 여지가 있는지 알아봐야겠다.
이렇게 하나하나 알아가는게 재밌는 것 같다.
다시한번 이런 공부할 기회를 준 스터디원에게 감사하다.
'정보 > The 공부' 카테고리의 다른 글
상속 vs 조합 (0) | 2023.12.22 |
---|---|
[그릇만들기] D + 33 (0) | 2022.11.30 |
스터디하고난후 (0) | 2022.11.17 |
도메인 로직은 무엇일까? (0) | 2022.11.11 |
클린코드 연습하기 ( 함수 분리 / 클래스[객체]분리) (0) | 2022.11.11 |