들어가기 앞서
이 글은 개발자 필독서인 클린 코드를 읽으며 습득한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
클린 코드와 오류 처리는 연관성이 있다. 흩어져있는 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기가 거의 불가능하며, 오류 처리 코드로 인해 프로그램 논리를 이해하기 어렵다면 클린 코드라 불리기 어렵다.
1. 오류 코드보다 예외를 사용하라
- 과거, 예외를 지원하지 않는 언어는 오류 플래그를 설정하거나 호출자에게 오류 코드를 반환하는 등 오류 처리 및 보고 방법이 제한적이었다.
- 위와 같은 방법은 호출자 코드가 복잡해지는 문제를 초래한다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다.
- 오류 발생 시 예외를 던지면 논리가 오류 처리 코드와 뒤섞이지 않아 호출자 코드가 더 깔끔해지게 된다.
2. Try-Catch-Finally 문부터 작성하라
- try 블록에서 발생하는 일과 무관하게 catch 블록은 프로그램 상태로 일관성 있게 유지해야 한다.
- 따라서, 예외 발생 코드를 작성할 때는 try-catch-finally 문으로 시작한다.
- try 블록에서 발생하는 일과 무관하게 호출자가 기대하는 상태를 정의하기 쉬워진다.
- 단위 테스트 작성시 강제로 예외를 발생시키는 테스트 케이스를 작성 후, 테스트에 통과하는 코드를 작성하는 방법을 권장한다.
- try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 수월하다.
3. 미확인 예외를 사용하라
- 확인된 예외는 OCP(Open Closed Principle)를 위반한다.
- 하위 단계에서 코드를 변경하면 상위 단계 메소드 선언부를 전부 수정해야 한다.
- 모듈 관련 코드가 변경되지 않아도 모듈을 다시 빌드해서 배포해야 한다.
- 확인된 예외는 캡슐화를 깨는 원인이 될 수도 있다.
- 최하위 함수를 변경해 새로운 오류를 던진다고 가정했을 때, 선언부에 throws 절을 추가할 경우 해당 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 한다.
- 확인된 예외가 유용한 경우도 존재한다.
- 중요한 라이브러리를 작성하는 경우, 모든 예외를 잡아야 함
4. 예외에 의미를 제공하라
- 예외를 던질 때는 전후 상황을 충분히 덧붙임으로써, 오류가 발생한 원인과 위치를 파악하기 쉽도록 한다.
- 오류 메시지에 정보를 담아 예외와 함께 던진다.
- 실패한 연산 이름과 실패 유형도 같이 언급한다.
- 로깅 기능을 사용하는 경우, catch 블록에서 오류를 기록하도록 충분한 정보를 공급한다.
5. 호출자를 고려해 예외 클래스를 정의하라
- 오류 정의 시 오류를 잡아내는 방법에 중점을 두도록 한다.
- 감싸기 기법
- 외부 API를 사용할 때 적용할 수 있는 최선의 방법이다.
- 호출하는 라이브러리 API를 감싸면서 예외 유형을 반환한다.
- 외부 라이브러리와 프로그램 사이의 의존성이 대폭 감소한다.
- 감싸기 클래스에서 테스트 코드를 넣어줌으로써 프로그램을 테스트하기 수월하다.
- 특정 업체가 API를 설계한 방식에 의존하지 않고 API를 정의하면 된다.
- 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우는 하나를, 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우는 여러 예외 클래스를 사용한다.
6. 정상 흐름을 정의하라
-
앞선 내용들을 적용하면 비즈니스 논리와 오류 처리가 잘 분리된 코드를 작성할 수 있지만, 오류 감지가 프로그램 언저리로 밀려나게 된다.
-
특수 사례 패턴(SPECIAL CASE PATTERN[Fowler])
클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식
MealExpenses expenses = expenseReportDAP.getMeals(employee.getID());
m_total += expenses.getTotal();
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
}
}
- 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하여 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.
7. null을 반환하지 마라
-
null을 반환하는 코드는 일거리를 초래할 뿐만 아니라 호출자에게 문제를 떠넘긴다.
-
null 반환 대신 예외를 던지거나 특수 사례 객체를 반환하는 방법을 고려해라. ex) 특수 사례 객체
😈 개선 전
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
total += e.getPay();
}
}
😇 개선 후
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if( .. 직원이 없다면 .. ) {
return Collections.emptyList();
}
}
-
코드가 깔끔해질 뿐 아니라 NullPointerExceptions가 발생할 가능성이 감소한다.
8. null을 전달하지 마라
- 정상적인 인수로 null을 기대하는 API가 아니라면 메소드로 null을 전달하는 코드는 최대한 피한다.
- 대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다.
- 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.
- 인수로 null이 넘어오면 코드에 문제가 있다는 소리다.
9. 결론
- 클린 코드는 가독성뿐만 아니라 안정성도 좋아야 한다.
- 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하도록 한다.
- 이를 통해 분리 정도에 따라 독립적인 추론이 가능할 뿐만 아니라 코드 유지보수성도 향상된다.
📖 참고
- 로버트 C. 마틴, 『Clean Code 클린 코드 애자일 소프트웨어 장인 정신』, 박재호·이해영 옮김, 케이앤피북스(2010), p147-158.