개발노트

Clean Code | 오류 처리 본문

Computer Science/Software Enginerring

Clean Code | 오류 처리

개발자? 2023. 5. 24. 22:47

목차

7장 오류 처리

  • 오류 코드보다 예외를 사용하라
  • Try-Catch-Finally 문부터 작성하라
  • 미확인(Unchecked) 예외를 사용하라
  • 예외에 의미를 제공하라
  • 호출자를 고려해 예외 클래스를 정의하라
  • 정상 흐름을 정의하라
  • null을 반환하지 마라
  • null을 전달하지 마라
  • 결론

 

Intro

깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 여기저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

따라서 깨끗한 코드에 한 걸음 다가가는 오류 처리 기법과 고려 사항에 대해 알아둘 필요가 있다.

 

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

오류가 발생하면 예외를 던지는 편이 낫다. 그러면 논리가 오류 처리 코드와 뒤섞이지 않기 때문에 호출자 코드가 더 깔끔해진다. 

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(handel);
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
}

if-else 문을 사용해서 오류를 처리하고 있다. 그러다보니 본질이 흐려진다.

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

try-catch 문을 사용해서 try 블록에 본질을 작성하고, catch 문에서 예외처리를 하였다.

본질 부분은 함수로 추출하여 1가지의 기능만 하도록 했다.

 

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

try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch 블록으로 넘어갈 수 있다. 이는 프로그램 상태를 일관성있게 유지해준다. 그러므로 예외가 발생하는 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다.

먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.

 

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

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

 

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

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

확인된 오류를 던진다면 함수는 선언부에 throws 절을 추가해야 한다. 그러면 변경한 함수를 호출하는 함수 모두가 1)catch 블록에서 새로운 예외를 처리하거나 2) 선언부에 throw 절을 추가해야 한다는 말이다. 결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다.

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

> checked 예외란?
checked 예외는 반드시 예외 처리를 해주어야 한다.
throws "checked 예외" 가 명시되어 있는 메서드를 호출하는 경우

 

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다. 자바는 모든 예외에 호출 스택을 제공한다. 하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다.

오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 애플리케이션이 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

 

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

애플리케이션에서 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

외부 API 를 사용할 때는 감싸기 기법(wrapper class 사용)이 최선이다.

  • 의존성이 크게 준다
  • 다른 라이브러리로 갈아탈 때 비용이 적다
  • 테스트가 쉽다
  • 특정 업체가 API를 설계한 방식에 얽매이지 않게 된다

 

null 을 리턴하지 마라

  • null을 리턴하고 싶은 생각이 들면 위의 Special Case object를 리턴하라.
  • 써드파티 라이브러리에서 null을 리턴할 가능성이 있는 메서드가 있다면 Exception을 던지거나 Special Case object를 리턴하는 매서드로 래핑하라.
  // BAD!!!!

  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);
        }
      }
    }
  }
  
  
  // 위 peristentStore가 null인 경우에 대한 예외처리가 안된 것을 눈치챘는가?
  // 만약 여기서 NullPointerException이 발생했다면 수십단계 위의 메소드에서 처리해줘야 하나?
  // 이 메소드의 문제점은 null 체크가 부족한게 아니라 null체크가 너무 많다는 것이다.

 

null 을 전달하지 마라

  • null을 리턴하는 것도 나쁘지만 null을 메서드로 넘기는 것은 더 나쁘다.
  • null을 메서드의 파라미터로 넣어야 하는 API를 사용하는 경우가 아니면 null을 메서드로 넘기지 마라.
  • 일반적으로 대다수의 프로그래밍 언어들은 파라미터로 들어온 null에 대해 적절한 방법을 제공하지 못한다.
  • 가장 이성적인 해법은 null을 파라미터로 받지 못하게 하는 것이다.
// Bad
// calculator.xProjection(null, new Point(12, 13));
// 위와 같이 부를 경우 NullPointerException 발생
public class MetricsCalculator {
  public double xProjection(Point p1, Point p2) {
    return (p2.x – p1.x) * 1.5;
  }
  ...
}

// Bad
// NullPointerException은 안나지만 윗단계에서 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;
  }
}

// Bad
// 좋은 명세이지만 첫번째 예시와 같이 NullPointerException 문제를 해결하지 못한다.
public class MetricsCalculator {
  public double xProjection(Point p1, Point p2) {
    assert p1 != null : "p1 should not be null";
    assert p2 != null : "p2 should not be null";
    
    return (p2.x – p1.x) * 1.5;
  }
}

 

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 이 둘은 상충하는 목표가 아니다. 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

 

이 내용은 로버트 C.마틴 의 『Clean Code』 를 보고 정리하였습니다.
반응형

'Computer Science > Software Enginerring' 카테고리의 다른 글

Clean Code | 경계  (0) 2023.05.31
Clean Code | 클래스  (2) 2023.05.24
Clean Code | 객체와 자료구조  (0) 2023.05.24
Clean Code | 주석  (0) 2023.05.24
Clean Code | 함수  (2) 2023.05.21
Comments