이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.
책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.
목차
- Checked Exception vs Unchecked Exception
- 꼭 필요한 경우에만 예외를 잡자
- 항상 예외를 체이닝하자
- 단 한번만 복구하자
- 관전-지향 프로그래밍을 사용하자
- 하나의 예외 타입만으로도 충분하다
1. Checked Exception vs Unchecked Exception
두 Exception의 차이는 이 글을 참조해주세요.
unchecked exception
fun length(file: File): Int {
if (!file.exists())
throw IllegalArgumentException("file doesn't exist")
return content(file).length
}
fun use() {
val file = File("/path/file")
length(file) // 예외에 대한 처리를 하지 않을 수 있음
}
unchecked exception은 의미 그대로 코드에서 exception을 체크하지 않아도 되는 exception입니다. 이 경우 개발자는 발생하는 exception에 대해서 무방비해지고 잠재적으로 500 에러를 발생시키는 코드가 탄생하게 됩니다.
checked exception
이 코드는 Java로 작성합니다. Kotlin에서는 throws 키워드가 존재하지 않습니다.
// check exception
public byte[] content(File file) throws IOException {
byte[] array = new byte[1000];
new FileInputStream(file).read(array);
return array;
}
// 방법1
public int length(File file) {
try {
return content(file).length;
} catch (IOException e) {
e.printStackTrace();
}
}
// 방법2
public int length(File file) throws IOException {
return content(file).length;
}
위처럼 checked exception으로 처리했을 경우 개발자는 반드시 exception에 대한 처리를 해야만 합니다. 개발자는 안전하지 않은 메서드를 사용하고 있다는 사실을 알 수 있습니다.
2. 꼭 필요한 경우에만 예외를 잡자
public int length(File file) {
try {
return content(file).length;
} catch (IOException e) {
return 0;
}
}
checked exception을 사용한 코드입니다. 이 코드는 에러가 발생하지 않는 안전한 코드입니다. 이로써 프로그램에 함정카드를 심어놓았습니다. 이 코드에서 exception이 발생할 경우 결과값을 0을 반환하고 있습니다. 코드에서는 exception을 처리하라고 개발자에게 명시했지만 개발자는 무시하고 0 ( 개발한 개발자만이 알 수 있는 숫자 ) 을 반환해버립니다. 이 코드에서 exception이 발생한 경우 정상처리되면 안되지만 강제로 정상처리하여 개발자에게 함정카드 발동이라는 나쁜 선물을 선사하게 됩니다.
반환값으로 0, -1 을 개발자 임의로 반환한다면 null을 반환하는 행위와 동일합니다.
이와 같이 처리할바엔 exception을 상위호출에게 throws 해버리는 것이 훨씬 효율적입니다.
3. 항상 예외를 체이닝하자
public int length(File file) throws Exception {
try {
return content(file).length;
} catch (IOException e) {
throw new Exception("계산이 불가능하다.");
}
}
먼저 체이닝하지 않는 코드를 살펴보겠습니다. 이 코드는 이전에 작성한 코드에서 exception 처리를 새로운 Exception을 생성하여 throw 시키고 있습니다. 이 경우 length() 메서드를 사용하는 곳에서 Exception을 처리할 수 있지만 근본적으로 어디에서 Exception이 발생했는지 찾을 수 없습니다.
Exception은 length() 메서드에서 발생하고 있다. 하지만 근본적인 exception은 content() 메서드에서 발생하고 있다. 개발자는 근본적인 exception을 length() 메서드로 착각한다.
위 코드를 체이닝 방식으로 수정해보겠습니다.
public int length(File file) throws Exception {
try {
return content(file).length;
} catch (IOException e) {
throw new Exception("계산이 불가능하다.", e); // 체이닝 방식
}
}
근본적으로 Exception이 발생한 IOException의 데이터를 새로운 Exception을 생성할 때 넣어주는 방식으로 작성할 수 있습니다.
4. 단 한번만 복구하자
복구는 가장 상위에서 단 한번만 복구하는 것이 가장 좋습니다. 하위에서 발생한 exception은 그 메서드를 사용하고 있는 상위에서 exception을 잡고 체이닝하고 던지고, 다시 상위에서 exception을 잡고 체이닝하고 던지는 방법이 가장 좋습니다.
항상 예외를 잡고, 체이닝하고, 던지고를 반복한 뒤 가장 최상위에서 단 한번만 복구합니다.
5. 관점-지향 프로그래밍을 사용하자
exception을 발생시키는 경우 가끔식 실패한 케이스에 대해서 재시도를 해야할 필요가 있습니다.
public String content() throws IOException {
int attempt = 0;
while (true) {
try {
return http();
} catch (IOException ex) {
if (attempt >= 2) {
throw ex;
}
}
}
}
이 코드는 http()메서를 2번 재시도하고 3번째에 예외를 발생시키는 코드입니다. 하지만 코드가 매우 장황해집니다. 이럴 경우 관점-지향 프로그래밍 AOP 방법을 사용하여 풀 수 있습니다.
위 코드를 AOP 방법으로 수정해보겠습니다.
@RetryOnFailure(attempts = 3)
public String content() throws IOException {
return http();
}
컴파일러는 컴파일 시점에 @RetryOnFailure Annotation을 발견한 후 content() 메서드를 실패, 재시도 코드로 둘러쌉니다. 이렇게 실패, 재시도 코드를 분리하면 중복 코드 또한 제거할 수 있습니다.
6. 하나의 예외 타입만으로도 충분하다
'절대 복구하지 않기' '항상 체이닝하기'와 같은 방법으로 예외를 처리한다면 예외 타입은 하나의 타입만으로도 충분합니다. 예외는 이전에 발생한 예외를 담을 수 있는 예외라면 항상 체이닝하여 가장 최상위까지 예외를 던져 처리할 수 있기 때문입니다.
'Study > object' 카테고리의 다른 글
[엘레강트 오브젝트] 4-3장 final이나 abstract이거나 (0) | 2021.10.13 |
---|---|
[엘레강트 오브젝트] 4-1장 절대 NULL을 반환하지 마세요 (0) | 2021.10.05 |
[엘레강트 오브젝트] 3-7장 인트로스펙션과 캐스팅을 피하세요 (0) | 2021.09.27 |
[엘레강트 오브젝트] 3-6장 부 ctor 밖에서는 new를 사용하지 마세요 (0) | 2021.09.26 |
[엘레강트 오브젝트] 3-5장 절대 getter와 setter를 사용하지 마세요 (0) | 2021.09.25 |