Clean Code - (6) : 오류처리

­이승환·2021년 11월 18일
0

Clean Code

목록 보기
6/7

Overview


상당수의 코드 기반은 전적으로 오류처리 코드에 좌우된다

  • 흩어진 오류처리코드 때문에 실제 코드가 하는 일 파악에 도움이 안 됨
  • 오류처리코드로 인해 프로그램 논리를 이해하기 어려워지면 안됨

따라서 아래와 같은 고려사항들을 파악할 필요가 있음

  1. 오류코드보다 예외를 처리하자
  2. Try-Catch-Finally 문부터 작성하자
  3. 미확인 ^unchecked 예외를 사용하자
  4. 예외에 의미를 제공하자
  5. 호출자를 고려해 예외 클래스를 작성하자
  6. 정상 흐름을 정의하자
  7. null 을 전달하거나 반환하지 말자

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

오류 코드를 사용하는 대부분의 방법은 다음과 같다.

public class DeviceController {
	...
	public void sendShutDown() {
		DeviceHandle handle = getHandle(DEV1);
		// 디바이스 상태를 점검
		if (handle != DeviceHandle.INVALID) {
			// 레코드 필드에 디바이스 상태를 저장
			retrieveDeviceRecord(handle);
			// 디바이스가 일시정지 상태가 아니라면 종료
			if (record.getStatus() != DEVICE_SUSPENDED) {
				pauseDevice(handle);
				clearDeviceWorkQueue(handle);
				closeDevice(handle);
			} else {
				logger.log("Device suspended. Unable to shut down");
			}
		} else {
			logger.log("Invalid handle for: " + DEV1.toString());
		}
	}
	...
}

위 형태의 단점은 아래와 같다
1. 호출한 즉시 오류를 확인해야하기 때문에 호출자 코드가 복잡하다
2. 오류 확인을 잊어버리기 쉽고, 오류 케이스 추가시 매번 코드를 확인해야한다

따라서 오류 발생시 예외를 던지는? 것이 훨씬 낫다.

public class DeviceController {
	...
	public void sendShutDown() {
		try {
			tryToShutDown();
		} catch (DeviceShutDownError e) {
			logger.log(e);
		}
	}

	private void tryToShutDown() throws DeviceShutDownError {
		DeviceHandle handle = getHandle(DEV1);
		DeviceRecord record = retrieveDeviceRecord(handle);
		pauseDevice(handle); 
		clearDeviceWorkQueue(handle); 
		closeDevice(handle);
	}

	private DeviceHandle getHandle(DeviceID id) {
		...
		throw new DeviceShutDownError("Invalid handle for: " + id.toString());
		...
	}
	...
}

위와 같이 caller 함수에서 catch로 예외를 받는? 역할을 맡고, callee 함수에서 예외를 throw 하는 형태로 가는 것이 좋다.

Try-Catch-Finally 부터 작성하자

try-catch-finally 문에서 try 스코프에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch, finally 순으로 진행된다.

  • try 블록에서 무슨일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지하자

TDD 방식으로 메소드 구현

  1. 단위 테스트를 만든다. (실패하는 케이스 형태로 먼저 개발한다)
// JUnit5 + assert를 활용하자
 @Test(expected = StorageException.class)
 public void retrieveSectionShouldThrowOnInvalidFileName() {
     sectionStore.retrieveSection("invalid - file");
 }
  1. 단위테스트에 맞춰 코드를 구현한다.
 public List<RecordedGrip> retrieveSection(String sectionName) {
 	// TODO - ...
     return new ArrayList<RecordedGrip>();
 }
  1. 로직을 작성한다.
 public List<RecordedGrip> retrieveSection(String sectionName) {
     try {
         FileInputStream stream = new FileInputStream(sectionName);
     } catch (Exception e) {
         throw new StorageException("retrieval error", e);
     }
     return new ArrayList<RecordedGrip>();
 }
  1. 리팩터링을 시도한다.

^Unchecked 예외를 사용하자

미확인 예외란?

checked 예외는 컴파일 단계에서 확인되며 반드시 처리해야 하는 예외이다.

IOException
SQLException

Unchecked 예외는 런타임에서 확인되며 명시적인 처리를 강제하지 않는 예외들이다.

NullPointerException
IllegalArgumentException
IndexOutOfBoundException
SystemException

미확인 예외의 단점

  • 메서드 선언시에 반환할 예외를 모두 열거해야 함
  • 확인된 예외는 예상되는 모든 예외를 사전에 처리할 수 있지만, 일반적인 어플리케이션 의존성이라는 비용이 이익보다 더 크다

예외에 의미를 제공하자

오류가 발생한 원인과 위치를 찾기 쉽도록 호출스택만으로 부족한 정보를 충분히 덧붙이자

  • 오류메시지에 정보를 담는다
  • 실패한 연산이름, 실패 유형을 언급하자
  • 멤버변수등을 포함해도 좋다

호출자를 고려해 예외 클래스를 작성하자

  1. 아래는 외부 라이브러리 호출하고 모든 예외를 try-catch-finally에서 처리하고 있다
 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");
 } finally {
     ...
 }
  1. 호출 라이브러리 API를 감싸 한 가지 예외 유형을 반환하는 방식으로 단순화 시키자
// caller
 LocalPort port = new LocalPort(12);
 try {
     port.open();
 } catch (PortDeviceFailure e) {
     reportError(e);
     logger.log(e.getMessage(), e);
 } finally {
     ...
 }
// callee
 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);
         }
     }
     ...
 }

위와 같이 실행하면 장점은 아래와 같이 생각해 볼 수 있다.

  • 에러 처리가 간결
  • 외부 라이브러리와 프로그램 사이의 의존성이 크게 줄어든다 (callee 만 수정할 수 있음(
  • 프로그램 테스트가 훨씬 쉬워짐
  • 외부 API 설계 방식에 의존하지 않아도 됨

정상 흐름을 정의하자

클래스나 객체가 예외적인 상황을 캡슐화해 처리하여 클라이언트 코드가 예외적인 상황 처리할 필요가 없도록 할 수 있다.

// caller
 try {
     MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
     m_total += expenses.getTotal();
 } catch(MealExpencesNotFound e) {
     m_total += getMealPerDiem();
 }
// callee
 public class PerDiemMealExpenses implements MealExpenses {
     public int getTotal() {
         // 기본값으로 일일 기본 식비를 반환한다.
         // (예외가 아닌)
         // throw new (...)
     }
 }

null 을 전달하거나 반환하지 말자

null 반환하는 습관은 좋지 않다. 이유는 아래와 같다.

  • 호출자가 null을 체크할 의무가 생긴다
  • NullPointerException 발생의 위험이 커진다

차라리 예외를 던지거나 특수 사례 객체를 반환하는 것이 훨씬 좋다.

profile
Mechanical & Computer Science

0개의 댓글