커스텀 에러와 에러 확장

커스텀 에러와 에러 확장

개발하다면 자체 에러를 만드는 것이 직관적이기 때문에 필요할 때가 있다.

직접 에러 클래스를 만든다면 name, message 프로퍼티를 만들어야 하고 가능하다면 stack 프로퍼티도 지원해야 한다. 물론 추가 프로퍼티 사용은 무엇이든 가능하다.

throw 인수엔 무엇이든 사용 가능해서 꼭 Error 객체를 상속받아야 하는것은 아니지만, Error 객체를 상속받으면 obj instance Error를 사용하여 에러 여부를 확인하는것이 가능하기 때문에 그냥 만드는 것보다 Error 객체를 상속받아서 만드는것을 추천한다.


에러 확장하기

user 정보를 읽는 readUser(json)을 만들것이다.

readUser(json)JSON.parse() 를 내부적으로 사용할거라 형식에 맞지 않으면 SyntaxError발생.

하지만 user라면 반드시 가져야 할 name 이나 age 같은 속성이 없을때는 이런 에러를 던지면 안된다. 따로 데이터를 검증할 것인데 이때 발생하는 에러를 validationError라고 만들것이다.

먼저 Error를 상속 받기 전에 어떤 객체인지 살펴보자.

1
2
3
4
5
6
7
8
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
this.stack = <call stack>; // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
}
}

상속 받기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}

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

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

message 프로퍼티는 부모 생성자의 의해 설정된다.

name 프로퍼티는 ‘Error’ 로 설정되는데 원하는 이름으로 재설정 해준다.

이제 readUser(json) 안에서 ValidationError를 사용해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ValidationError extends Error {
constructor(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": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 합니다. (**)
}
}

에러 유형에 맞게 처리해 주었다.

에러 유형은 instanceof 말고도 err.name으로 확인 가능하다.


더 깊게 상속하기

ValidationError는 너무 포괄적이니 필요한 프로퍼티가 없는 경우에 상세한 에러를 만들어서 상속 하도록 한다.

PropertyRequiredError 만들기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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) {
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; // 알려지지 않은 에러는 재던지기 합니다.
}
}

Error 클래스를 상속받아 직접 커스텀한 에러들은 this.name 을 수동으로 할당해 주고 있다.

이런 방법은 상당히 귀찮은 작업이 될 수 있다.

이런걸 피하기 위해서는 기본 에러 클래스를 만들고 상속 받으면 된다.

기본 에러 클래스는 this.name = this.constructor.name 을 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

예외 감싸기

readUser 가 점점 커지면 에러도 다양해 질 것이고 VallidationError 는 이런 에러를 포함해야 할 것이다.

그런데 이렇게 에러를 추가하다보면 처리하는 곳에서 분기별로 처리하는 코드를 늘려야 할까?

대부분의 경우 그렇지 않다. 데이터를 읽었을때 에러 발생 여부만 알려주고 필요할때 세부 내용을 알려주는 방버을 사용한다.

wrapping exception을 해보자.

  1. ReadError 만들기
  2. ValidationError, SyntaxError 등의 에러는 readUser 내부에서 잡고 이때 ReadErrorㄹㄹ 생성.
  3. ReadError 객체의 cause 프로퍼티엔 실제 에러에 대한 참조 저장.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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;
}
}

이런 기법은 객체 지향 프로그래밍에서 널리 쓰이는 패턴임.

과제1
내장된 SyntaxError 클래스를 상속하는 FormatError 클래스를 만들어 봅시다.

만들어진 클래스에서 message, name, stack를 참조할 수 있어야 합니다.