오류 코드보다 예외를 사용하라

예외를 지원하지 않는 언어는 오류를 처리하고 보고하는 방법이 제한적이었다.

오류 플래그를 설정하거나 호출자에게 오류 코드를 반환하는 방법이 전부였다.

이 방법은 사용하는 호출자 코드가 복잡해진다.

함수 호출 즉시, 오류를 확인해야 하기 때문이다. 불행히도 단계를 잊어버리기 쉽다.

그래서 오류가 발생하면 예외를 던지는 편이 좋다

그러면 호출자 코드가 깔끔해진다.

논리가 오류 처리 코드와 뒤섞이지 않으니까

핵심은 핵심 알고리즘과 오류 처리 알고리즘이 분리 됐다는

 

Try-Catch-Finally문부터 작성하라

try블록에서 무슨 일이 생기더라도 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.

그러므로 예외가 발생할 코드를 짤 때는 무조건 try-catch-finally 문부터 작성하는 게 좋다

 

미확인(unchecked) 예외를 사용하라

현재는 안정적인 소프트웨어를 제작하는 요소로 확인된(Checked) 예외 반드시 필요하지 않다는 사실이 분명해졌다.

확인된(checked) 예외는  OCP(Open Closed Principle) 위반한다.

만약 메서드에서 확인된 예외를 던졌는데 catch블록이 단계 위에 있다면 사이 메서드는 모두 선언부에 해당 예외를 선언부에정의해야 한다. 만약 하위 단계에서 발생 예외가 변경되면 전부 수정해야 할 것이다.

 

대규모 시스템에서 호출이 일어나는 방식을 상상해보라.

최상위 함수가 아래 함수를 호출한다. 아래 함수는 아래 함수를 호출한다. 단계를 내려갈수록 호출하는 함수 수는 늘어난다.

이제 최하위 함수를 변경해 새로운 오류를 던진다고 가정하자.

확인된 오류를 던진다면 함수는 선언부에 throws 절을 추가해야 한다.

그러면 변경한 함수를 호출하는 함수 모두가 catch 블록에서 새로운 예외를 처리하거나 선언부에 throw 절을 추가해야 한다는 말이다.

결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다.

throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.

때로는 확인된 예외도 유용하다. 아주 중요한 라이브러리를 작성한다면 모든 예외를 잡아야 한다. 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다.

 

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그래야 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

자바는 모든 예외에 호출 스택을 제공한다. 하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다.

 

오류 메시지에 정보를 담아 예외와 함께 던진다. 메시지에 실패한 연산 이름과 실패 유형도 언급한다. 혹은 로깅 기능을 사용한다면 catch에서 충분한 정보를 가진 로그에 남기는 게 좋다.

호출자를 고려해 예외 클래스를 정의하라

오류를 분류하는 방법은 수없이 많다.

오류가 발생한 위치로 분류가 가능하다.

예시, 오류가 발생한 컴포넌트로 분류한다

유형으로도 분류가 가능하다

, 디바이스 실패, 네트워크 실패,…

오류를 정의할 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

예를 들어, API를 사용하는데, API가 발생시킬 수 있는 예외를 전부 catch 문으로 나열했다.

catch 문을 처리하는 방법이 거의 동일하다면, API를 감싼 래퍼 클래스를 만들어 하나의 예외로 변환하는 것을 들 수 있다.

public class 예시 {
	외부api api = new 외부api();
	try {
		api.work();
		//예외처리가 비슷하다.
	}catch(외부api예외1 e1) {
		logger.log(e1.getMessage());
	}catch(외부api예외2 e2) {
		logger.log(e1.getMessage());
	}catch(외부api예외3 e3) {
		logger.log(e1.getMessage());
	}
	
}
/* ********************************* */
class 예시 {
	래퍼클래스 api = new 래퍼클래스();
	try {
		api.work();
	}catch(변환된예외 e1) {
		logger.log(e1.getMessage());
	}
	
}
class 래퍼클래스{
	외부api api;
	
	void work() {
		try {
			api.work();
			//예외처리가 비슷하다.
		}catch(외부api예외1 e1) {
			throw new 변환된예외(e1);
		}catch(외부api예외2 e2) {
			throw new 변환된예외(e2);
		}catch(외부api예외3 e3) {
			throw new 변환된예외(e3);
		}
	}
}

실제로 외부 API 사용할 때는 감싸기 기법이 최선이다.

외부 API 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.

나중에 다른 라이브러리로 갈아타도 비용이 적다.

또한 감싸기 클래스에서 외부 API 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.

 

마지막 장점으로 감싸기 기법을 사용하면 특정 업체가 API 설계한 방식에 발목 잡히지 않는다.

프로그램이 사용하기 편리한 API 정의하면 그만이다.

 

흔히 예외 클래스가 하나만 있어도 충분한 코드가 많다. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그렇다.

null 반환하지 마라

오류를 반환하는 흔한 습관 하나다.

null 반환한다는 것은 일거리가 늘 뿐만 아니라 호출자에게 책임을 전가하는 방법이다. 문제는어디서 인가 null체크를 번이라도 빼먹어서 NullPointerException 발생한다면 도대체 어디서 NullPointerException 찾아 처리해야 할까?

null 반환할 바에 예외를 던지거나 특수 사례 객체를 반환 단다.

//특수사례객체, 예외 던지기 대신 사용할 수 있는 방법
List find() {
	if(찾는게 없다...) {
		return Collections.EMPTY_LIST;
	}
}

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식

이점은 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

클래스나 객체가 예외적인 상황을 캡슐화해서 처리하니까

null 전달하지 마라

null 반환하는 것보다 나쁜 것이 메서드에 null 인자로 넘기는 것이다.

정상적인 인수로 null 기대하는 API 아니라면 메서드로 null 전달하는 코드는 최대한 피한다.

null 체크하기보다 애초에 null이 안 넘어오는 게 정상적인 것이다

결론

깨끗한 코드는 일기도 좋아야 하지만 안정성도 높아야 한다.

오류처리를 프로그램 논리와 분리해 독자적으로 처리해야 튼튼하고 깨끗한 코드가 된다.

오류처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해져 코드 유지보수성도 크게 높아진다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18
6장 객체와 자료 구조  (1) 2022.10.06
5장 형식 맞추기  (0) 2022.10.05
4장 주석 - 나쁜 주석  (1) 2022.10.01

+ Recent posts