개발 일반

개발 일반잊을 때쯤 한번 다시 읽어봐야할 Clean code에 관하여 (오류 처리)

민초부 2021. 3. 23. 20:28
반응형

 

필자는 '깨끗한' 예외 처리가 클린 코드를 위한 요소 중 하나라고 말한다. 

 

그리고 이를 위한 '깨끗한' 오류 처리의 기준은 아래와 같다. 

 

  • 오류 코드를 반환하지 말고 예외 처리를 하라
    • 이는 예컨데 DeviceHandle.INVALID와 같은 오류를 값으로 return하고 받지 말고 try..catch로 묶어야 한다.
  • try.. catch를 사용하라 
    • try .. catch 문 자체가 트랜잭션과 같이 범위를 정의할 수 있어 구조적으로 깔끔해진다.
  • 미확인 예외를 사용하라
    • 이건 뭔가 반대로 생각했었던거임
    • 필자는 확인된 예외는 OCP(Open Closed Principle)을 위반한다고 한다. 
      OCP (개방폐쇄원칙) : 확장에는 열려있어야 하고 변경에는 닫혀있어야한다.
    • 이는 메서드에서 추가로 확인된 예외를 던졌는데 catch 블록이 3단계 위에 있다면 이 사이의 모든 선언부에 예외를 정의해야 한다는 것으로 즉 변경이 이루어졌을 때 최상위 단계까지 연쇄적인 수정을 해야한다는 것이다. 
    • 아주 핵심적인 라이브러리를 작성한다면 모든 예외를 잡아주는 것이 맞지만 일반적인 개발 코드라면 의존성이라는 비용이 예외를 확인하는 이익보다 크다고 보고 있다. 
  • 예외에 의미를 제공하라 
    • 호출 스택만으로는 예외처리한 코드의 의도를 파악하기는 쉽지 않다. 
    • 오류 메세지에 정보(실패한 연산 이름, 실패 유형 등)를 충분히 추가한다.
  • 호출자를 고려해 예외 클래스를 정의하라
    • 전형적인 어떤 오류를 미리 잡아낼지가 아닌 어떻게 오류를 잡아낼 것인가에 대한 내용이다.
    • 예시로 호출하는 외부 라이브러리가 있다면 이는 따로 클래스로 만들어 주는 것이 깔끔한 코드를 만들기 좋다.
      • 이렇게 하면 일단 라이브러리와 프로그램 사이에 의존성도 줄어들고
      • class안에서 세부적인 예외처리를 하고 이를 사용하는 프로그램의 코드에서는 간단하게만 예외처리를 받을 수 있게 작성할 수 있다. 

<감싸는 클래스 (Wrapper)없이 예외처리한 케이스>

ACMEPort port = new ACMEPort(12);

try{
	  port.open();
} catch (DeviceResponseException e) {
		reportPortError(e);
} catch (ATM1212UnlockedException e) {
		reportPortError(e);
} catch (GMXError e) {
		reportPortError(e);
} finally {
	...
}

 

 

<Wrapper Class를 통해 예외 처리>

   - 실제 프로그램에서 외부 라이브러리를 사용할 때는 이렇게 개발자가 임의로 만든 PortDeviceFailure를 받아주기만 하면 된다. 

LocalPort port = new LocalPort(12);
try {
  port.open();
} catch (PortDeviceFailure e) {
  reportError(e);
  logger.log(e.getMessage(), e);
} finally {
  ...
}

   - 그리고 세부적인 예외처리는 Wrapper class, 여기서는 LocalPort안에서 작성해준다. 

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

 

  • 정상흐름을 정의하라 
    • 이건 평소에도 많이쓰는 try .. catch를 if else처럼 사용하는 거 
  • Null을 반환하지 마라 
    • 이것도 내가 은근히 습관적으로 사용하는거 같은데 Null을 리턴하고 이를 if로 체크하고 그러지 말라는 거임 
    • 객체나 함수에 대해 Null을 체크를 해야한다면 이를 함수를 사용한 곳에서 체크하는 것이 아닌 함수 안에서 미리 처리하고 밖에서는 똑같이 사용할 수 있게 해야한다. 
// Before Code 
List<Employee> employees = getEmployees();
if (employees != null) {
	for(Employee e : employees) {
		totalPay += e.getPay();
	}
}


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

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

 

  • Null을 전달하지도 마라
    • null을 받는게 정상적인 상황이 아닌 이상을 Null을 받는다고 Null를 그대로 반환해선 안 된다.
    • 이런 경우 assert를 통해 의도적으로 예외를 발생시키는 것도 괜찮다고 한다.
반응형