본문으로 바로가기

[Clean Code] 7장 오류처리

category Prodo 독서 리뷰 2021. 3. 26. 17:23

뭔가 잘못될 가능성은 늘 존재한다.

뭔가 잘못되면 바로 잡을 책임은 바로 프로그래머에게 있다.

 

그렇다면 깨끗한 코드와 오류 처리가 관련이 있을까? 확실히 연관성이 있다.

 

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

여기서 좌우된다는 표현은 코드 기반이 오류만 처리한다는 의미가 아닌 여기저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기가 거의 불가능하다는 의미이다.

 

오류 처리는 중요하지만 이 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

 

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

예외 처리를 지원하지 않는 프로그래밍 언어가 오류 처리를 할 땐,

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

public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    // Check the state of the device
    if (handle != DeviceHandle.INVALID) {
        // Save the device status to the record field
        retrieveDeviceRecord(handle);
        // If not suspended, shut down
        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 void sendShutDown() {
  try {
    tryToShutDown();
  } catch (DeviceShutDownError e) {
    logger.log(e);
  }
}

디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리하여 코드 품질이 좋아졌다!

이제는 각 개념을 독립적으로 살펴보고 이해할 수 있다.

 

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

try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch 블록으로 넘어갈 수 있다. try 블록에서 무슨 일이 생기든 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.

그러므로 예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다.

 

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

예전에는 메서드를 선언할 때 메서드가 반환할 예외를 모두 열거했다.

그러나 비용이 상응하는 이익을 제공하지 못했고 확인된 예외는 OCP를 위반한다.

 

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

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

오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다.

 

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

오류를 분류하는 방법은 수없이 많고 그중 오류가 발생한 위치, 유형 등이 있다.

하지만 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

 

아래 방법은 외부 라이브러리가 던질 예외를 모두 잡아낸다.

그러나 코드 중복이 심하고 변경하기 어렵다.

 

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 {
	...
}

 

또 다른 방법은 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하는 방법이다.

LocalPort 클래스를 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 감싸기(Wrapper) 클래스이다.

Wrapper 클래스를 사용하면 의존성이 크게 줄어들고 나중에 다른 라이브러리로 갈아타도 비용이 적다.

 

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

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

 

6. null을 반환하지 마라

nulll을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.

public void registerItem(Item item) {
    if (item != null) {
        ItemRegistry registry = peristentStore.getItemRegistry();
        if (registry != null) {
            Item existing = registry.getItem(item.getID());
            if (existing.getBillingPeriod().hasRetailOwner()) {
                existing.register(item);
            }
        }
    }
}

 

7. null을 전달하지 마라

메서드에서 null을 반환하는 방식도 나쁘지만 전달하는 방식은 더 나쁘다.

// Parameter에 null값이 들어오면 NullPointerException 발생
public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {
        return (p2.x – p1.x) * 1.5;
    }
}

// InvalidArgumentException 사용: 위 방법보단 낫다.. 하지만 좋은 방법은 아님
public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {
        if (p1 == null || p2 == null) {
            throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
        }
        return (p2.x – p1.x) * 1.5;
    }
}

대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다.

애초에 null을 넘기지 않는 것이 가장 좋다.

'Prodo 독서 리뷰' 카테고리의 다른 글

[Clean Code] 9장 단위 테스트  (0) 2021.03.26
[Clean Code] 8장 경계  (0) 2021.03.26
[Clean Code] 6장 객체와 자료 구조  (0) 2021.03.26
[Clean Code] 5장 형식 맞추기  (0) 2021.03.26
[Clean Code] 4장 주석  (0) 2021.03.26