CleanCode 7장 오류처리

김희윤·2021년 3월 7일
0

cleancode

목록 보기
7/13

1. 오류코드보다 예외를 사용해라

기존의 오류코드는 아래와 같았습니다.

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());
		}
	}
	...
}

보는 것처럼 호출자 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다. 아래와 같이 괴면 간단해진다. 논리와 오류코드가 섞이지 않아서다.

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());
		...
	}
	...
}

2. Try-Catch-Finally 문부터 작성해라

try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단 되고 catch로 넘어간다.

  • try 블록에서 무슨일이 생기든지 catch 블록은 프로그램 상태를 일관성있게 유지
  • try-catch-finally 문으로 코드를 짜면 호출자가 기대하는 상태를 정의하기 쉬워짐

TDD(Test Driven Development) 방식으로 메소드를 구현
1. 단위 테스트를 만든다

@Test(expected = StorageException.class)
 public void retrieveSectionShouldThrowOnInvalidFileName() {
     sectionStore.retrieveSection("invalid - file");
 }
  1. 단위 테스트에 맞춰 코드를 구현한다.
    (예외를 발생시키지 않아 단위 테스트에서 실패)
public List<RecordedGrip> retrieveSection(String sectionName) {
     // 실제로 구현할 때까지 비어 있는 더미를 반환한다.
     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. 리펙터링이 가능해졌다.
    (결과의 변경없이 코드의 구조를 재조정)
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>();
 }

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

  • checked 예외

    컴파일 단계에서 확인되며 반드시 처리해야 하는 예외
    (IOException, SQLException etc...)

  • unchecked 예외

    실행 단계에서 확인되며 확시적인 처리를 강제하지 않는 예외
    (NullPointerException, IllegalArgumentException, IndexOuntOfBoundException, SystemException)

  • checked 예외는 비용이 들기에, 그에 상응하는 이익이 있는지 따져야 함.
    (OCP를 위반함, 메소드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면, 그 사이 메소드들 모두에 예외를 정의해야하기 때문)

4.예외에 의미를 제공하라

  • 오류가 발생한 원인과 위치를 찾기 쉽도록 스택만으로는 부족한 정보를 충분히 덧붙여야 함
    -> 오류 메시지에 정보를 담음, 실패한 연산 이름, 실패 유형 언급

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

// 1
 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 {
     ...
 }
 
 // 2
 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);
         }
     }
     ...
 }

1번은 외부 라이브러리를 호출하고 모든 예외를 호출자가 잡아낸다.

그에 반해 2번은 호출 라이브러리 API를 감싸 한가지 예외 유형을 반환하는 방식으로 단순화

6. null을 반환하지 마라

null을 반환하는 습관은 좋지 않다

  • 호출자에게 null을 체크할 의무를 부여함.
  • NullPointerException의 발생 위험이 있음
  • null 확인이 너무 많아짐

// 1
List<Employee> employees = getEmployees();
if(employees != null) {
	for(Employee e : employees) {
		totalPay += e.getPay();
	}
}

// 2
List<Employee> employees = getEmployees();
for(Employee e : employees) {
	totalPay += e.getPay();
}

public List<Employee> getEmployees() {
	if (..직원이 없다면..)
		return Collections.emptyList();
}

1번처럼 null을 반환하지 말고 2번처럼 차라리 빈 list를 반환하게 하거나 assert 문을 사용하는 것도 방법이다.

profile
블록체인, IOT, 클라우드에 관심이 많은 개발자 지망생

0개의 댓글

관련 채용 정보