![[Clean Code] 7. 오류 처리](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ2nMk%2FbtsE6IL2I5O%2Fkkm9vhearxIKKRx2tZ3atK%2Fimg.png)
본 게시글은 도서, Clean Code를 읽고 정리한 글입니다.
Clean Code(클린 코드) | 로버트 C. 마틴 - 교보문고
Clean Code(클린 코드) | 프로그래머, 소프트웨어 공학도, 프로젝트 관리자, 팀 리더, 시스템 분석가에게 도움이 될 더 나은 코드를 만드는 책『Clean Code(클린 코드)』은 오브젝트 멘토(Object Mentor)의
product.kyobobook.co.kr
1. 오류 코드보다 예외를 사용하라
기본적으로 옛날(예외를 지원하지 않던 시절)에는 오류를 오류코드로서 반환하는 방법이 전부였고, 지금은 대부분의 프로그래밍 언어에서 예외를 지원한다.
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// 커스텀 오류 코드를 통한 오류 처리
if(handle != DeviceHandle.INVALID) {
...
if(record.getStatus() != DEVICE_SUSPENDED) {
...
}
}
}
}
위 방법을 사용하면 위 코드를 호출하는 쪽에서 복잡해진다.
함수를 호출한 후에 바로 오류 코드가 발생하는지 확인해야 한다. 그리고 이러한 과정은 개발자들이 쉽게 까먹어버리기도 한다.
이러한 단점을 예외가 해결해준다.
// 예외 활용
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
...
}
코드도 깔끔하다.
2. Try-Catch-Finally 문 부터 작성하라
try-catch-finally 문은 블록 들로 이루어져 있다. 그리고 이 블록들은 곧 범위를 정의하는 것을 의미한다.
DataBase의 트랜잭션의 개념처럼도 보이는데, try 블록 내부에서 어떤 무슨 일이 생기더라도 catch 블록 내부에서는 프로그램을 일관성있게 유지하도록 해야 한다는 것이다.
// 파일이 없으면 예외를 던지는지 확인하는 단위 테스트
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file");
}
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
stream.close();
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}
위 코드는 FileInputStream 생성자가 던지는 FileNotFoundException을 잡아내어, 단위 테스트는 성공할 것이다.
이처럼 try-catch 문을 활용할 때, 예외의 범위를 좁히면 더욱 정확한 예외 처리가 가능해진다.
이러한 try-catch 문을 테스트할 때는 강제로 예외를 일으키는 TestCase를 작성하고, 테스트를 통과하도록 하는 것을 권장한다. 그렇게 테스트 코드를 먼저 짜게 되면, try-catch 문을 좀 더 활용하도록 코드를 작성하게 된다.
3. Unchecked Exception(미확인 예외)를 사용하라
어떤 시스템이 있다 해보자.
이 시스템의 최상위 함수가 하위 함수를 호출하고, 그 하위 함수는 더 하위인 함수를 호출한다.
이러한 시스템에서 만약 최하위 함수에 throws 절을 추가해야 한다고 해보자.
그럼 이 최하위 함수를 호출하게 되는 상위의 모든 함수들은 각 함수들의 catch문에 예외처리를 추가하거나, 함수 선언부에 throws 절을 추가해야 하는 연쇄적인 수정이 발생한다. 그리고 이는 객체지향의 특성, 캡슐화를 깬다.
이러한 현상들을 보아, 별도의 throws 절을 처리해주지 않아도 되는 Unchecked Exception(미확인 예외)를 사용하는 것을 권장한다.
Checked Exception (확인된 예외) 종류 | 설명 |
IOException | 파일 입출력 관련 예외 |
SQLException | DB 작업 관련 예외 |
ClassNotFoundException | 클래스를 찾지 못할 때 발생 |
InterruptedException | Thread가 Interrupt 될 때 발생 |
ParseException | 날짜 또는 시간을 Parsing 할 때 발생할 수 있는 예외 |
Unchecked Exception (미확인 예외) 종류 | 설명 |
RuntimeException | 런타임 시 발생하는 일반적 예외의 부모 클래스 |
NullPointerException | Null 참조로 인해 발생 |
ArrayIndexOutOfBoundsException | 배열 인덱스가 범위를 벗어날 때 발생 |
ArithmeticException | 산술 연산 중 발생할 수 있는 예외 |
ClassCastException | 클래스 형변환에서 발생 |
IllegalArgumentException | 메소드에 전달되는 인수가 잘못된 경우 발생 |
4. 예외에 의미를 제공하라
예외의 이름, 호출스택 만으로는 정확한 상황 파악이 힘들 수 있다.
그렇기에 예외를 던질 때는 전후 상황을 충분히 덧붙여야 한다.
오류 메시지에 관련 정보를 자세히 담아서 함께 예외를 던지자.
예를 들어 실패한 연산 이름과 실패 유형 등을 언급한다.
또 로그 기능을 활용한다면 catch 블록에서 오류를 로그에 기록하자.
5. 호출자를 고려하여 예외 클래스를 정의하라
사실 오류라는게 정말 다양하고 끝도 없다.
그래서 이 예외를 작성하는 사람 입장에서는 이해가 될 수 있으나, 이를 받아들이는 호출자의 입장에서는 어려울 수 있다.
// 오류를 형편없이 분류한 사례
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception", e);
} finally {
...
}
위 코드의 경우, 예외에 대응하는 방식이 예외 유형과는 무관하게 동일하다.
따라서 이런 경우 코드를 간결하게 만들어주자. 호출하는 라이브러리 API를 감싸서 예외 유형 하나를 반환하는 방식이다.
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
...
// 코드의 간결화를 위한 감싸기용 클래스
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
예외 처리를 호출하는 호출자 입장에서는 얼마나 간단해졌는가!
이처럼 외부 API를 사용할 때는 감싸기 기법이 좋다.
외부 API를 감쌈으로서 외부 라이브러리와 프로그램 사이의 의존성을 줄여주고, 다른 라이브러리로 변경하더라도 그 비용이 적다.
또한 테스트를 하기도 간편하고, 굳이 외부 라이브러리의 방식을 활용하지 않고 자사 프로그램이 사용하기 편리한 API를 정의할 수 있게 된다.
6. null을 반환하지 마라
public void registerItem(Item item) {
if(item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if(registry != null) {
Item existing = registry.getItem(item.getID());
if(existing.getBillingPeriod().hasRetailOwner()
existing.register(item);
}
}
}
위 코드는 뭐가 문제일까?
모든 조건을 null이 아닐때로 퉁쳐버린다.
만약 예상하지 못한 곳에서 문제가 생겨 null 값이 나오면?
그 때의 처리는 전혀 없다.
애초에 null을 반환할 가능성을 내포한 코드는 호출자에게 문제를 떠넘겨버리는 것이다.
List<Employee> employees = getEmployees();
for(Employee e : employees)
totalPay += e.getPay();
//
public List<Employee> getEmployees() {
if(.. no employee ..)
return Collections.emptyList();
}
이런 코드가 NullpointerException이 발생할 확률을 줄여주며, 깔끔하다.
7. null을 전달하지 마라
위에서 말한 null을 반환하지 않는 것도 중요하지만, null을 전달하는 것은 절 대 금지다.
public class MetricsCaluclator {
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
....
}
만약 인수로 들어오는 Point가 null이라면 당연히 nullPointerException이 발생한다.
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
위 코드는 NullPointerException을 해결할 뿐만 아니라, InvalidArgumentException 예외도 처리해준다.
'Software Engineering > Clean Code' 카테고리의 다른 글
[Clean Code] 9. 단위 테스트 (0) | 2024.03.12 |
---|---|
[Clean Code] 8. 경계 (0) | 2024.02.21 |
[Clean Code] 6. 객체와 자료 구조 (1) | 2024.01.25 |
[Clean Code] 5. 형식 맞추기 (0) | 2024.01.24 |
[Clean Code] 4. 주석 (1) | 2024.01.23 |
개발자가 되고 싶어요.