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

public class DeviceController {
    
    public void sendShutDown(){
        DeviceHandle = getHandle(DEV1);
        if(handle != DeviceHandle.INVALID){
            ...
        }
        else if
        ...
    }
}

이같은 방법으로 오류를 검출하고 그에 대한 동작을 부여하면 코드가 복잡해진다

차라리 오류 발생 시 예외를 던지는 것이 더 좋다 >> 호출자 코드가 더 깔끔해지기 때문

 

 

public class DeviceController{
    ...
    public void sendShutDown(){
        try{
            tryToShutDown();
        }
        catch(DeviceShutDownForError e){
            logger.log(e);
        }
    }
    
    private void tryToShutDown() throws DevieSHutDownError{
        ...
    }
}

개선한 코드

디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리했기 때문에 가독성 증가

 

 

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

try catch 문에서 catch 문은 try 블록에서 무슨 일이 생겨도 프로그램 상태를 일관성 있게 유지해야 한다

 

    public List<Object> errorCheck(String name){
        return new ArrayList<Object>();
    }

    public List<Object> errorCheck(String name){

        try{
            ...
        }
        catch(Exception e){
            throw new Exception(e);
        }
        
        return new ArrayList<Object>();
    }

위의 errorCheck 메서드에서는 ArrayList 를 바로 리턴해준다

만약 에러가 발생하면 예외를 던지지 못하므로 단위 테스트는 실패하게 된다

 

아래에서는 단위테스트를 실패할 때 에러를 던지도록 catch 문을 구현했다

코드가 예외를 던지므로 테스트 자체는 성공한다

 

try catch 문으로 범위를 정의했으므로 TDD를 사용해 필요한 논리를 추가가 가능

범위 내에서 트랜잭션의 본질을 유지하기 좋아진다

 

 

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

메서드가 반환할 예외를 하나하나 열거하며 예외 처리를 하는 방법은 좋지 않다

 

 

예외에 의미를 제공하라

예외를 던지는 경우에는 전후 상황을 충분히 덧붙여라

오류 발생 원인, 위치를 찾기 쉬워지기 때문

 

오류 메세지에 정보를 담아 예외와 함께 throw

실패한 연산 이름과 실패 유형도 언급

logging 기능 사용한다면 catch 블록에서 오류 기록

 

 

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

Exception 클래스를 만드는데 가장 중요한 것은 '오류를 잡아내는 방식' 이다

라이브러리 API 디자인에 종속적이지 않게 설계할 수 있다

 

    ACMEPort port = new ACMEPort(12);
    
    try{
        port.open();
    }
    catch(DeviceResponseException e){
        reportPortError(e);
        logger.log("unlock Exception", e);
    }
    catch(GMXError e){
        reportPortError(e);
        logger.log("Device response exception");
    }
    finally{
        ...
    }

오류 검출에 대한 중복이 심하지만, 예외에 대응하는 방식이 예외 유형과 무관하게 동일하다

 

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

따라서 이와 같이

호출하는 라이브러리 API 를 감싸면서 예외 유형 하나를 반환하도록 고칠 수 있다

 

여기서 LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 wrapper 클래스일 뿐이다

 

실제 외부 API를 사용할 때에는 감싸기 기법이 좋다

외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다

 

 

 

정상 흐름을 정의하라

try{
        MealExpense expenses = expenseReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
        
    }
    catch(MealExpensesNotFound e){
        m_total += getmealPerDiem();
    }

위에서 식비를 비용으로 청구했다면, 직원이 청구한 식비를 총계에 더한다

식비를 비용으로 청구하지 않았다면, 일일 기본 식비를 총계에 더한다

 

>> 예외가 논리를 따라가기 어렵게 만든다

특수한 상황 처리할 필요가 없다

 

    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();

예외 처리를 하지 않고 위 코드로만 동작하도록 ExpenseReportDAO 를 수정

 

public class PerDiemMealExpenses implements MealExpenses{
    public int getTotal(){
        // 기본값으로 일일 식비를 반환한다
    }
}

getTotal() 메서드는 언제나 MealExpense 객체를 반환한다

청구한 식비가 없다면 일일 기본 식비를 반환한다

>> 청구된 식비 여부에 따른 예외 처리가 필요 없다

 

이를 특수사례 패턴이라고 한다

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식

 

 

null 을 반환하지 마라

 

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

 

 

한줄 건너 하나씩 null 을 체크하는 좋지 않은 코드의 예

 

null 을 반환하는 코드는 호출자에게 문제를 떠넘기게 된다

하나라도 null 확인을 빼먹는다면 애플리케이션이 통제를 벗어날 수 있다

 

위 코드에선 두 번째 행에 null 확인이 빠졌다

if(persistentStore == null) 조건이 true 라면 실행 시 NullPointException 이 발생

>> 발생 된 Exception 을 처리할 코드가 없다

 

메서드에서 null 을 반환하고싶은 유혹이 든다면, 대신 예외를 던지거나 특수 사례 객체를 반환하라

 

 

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

 

위에서 getEmployees 는 null 도 반환한다

하지만 반드시 null 을 반환할 필요가 없으니까

 

    List<Employee> employees = getEmployees();
    for(Exployee e : employee){
        totalPay += e.getPay();
    }
    
    
    public List<Employee> getEmployees(){
        if( noEmployee ){
            return Collection.emptyList();
        }
    }

이렇게 고칠 수 있겠다

 

getEmployees() 는 언제나 리스트를 반환한다

Employee 가 없을 경우에는 미리 정의된 읽기 전용 리스트를 반환한다

 

이렇게 하면 예외 처리 필요 없이 객체를 무조건 반환 받는 방식으로 코드 작성이 가능하다

 

 

 

null 을 전달하지 마라

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2){
        return (p2.x - p1.x) * 1.5;
    }

    ...
}

이같은 메소드에

 

calculator.xProjection(null, new Point(12, 13));

이렇게 null 을 던지면 NullPointException 발생

 

if(p1 == null || p2 == null) throw InvalidArgumentException("Exception message");

이렇게 if 문을 사용할 수 있지만, if 문은 InvalidArgumentException 을 잡아내는 처리기가 별도로 필요하다

 

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

    ...
}

assert 문을 사용하는 방법 역시 존재하나, 누군가가 null 을 전달하면 여전히 실행 오류가 발생

 

>> 애초에 null 이 안넘어오도록 코드 짜라

 

 

 

'끄적 > Clean Code' 카테고리의 다른 글

Clean Code -8 클래스  (0) 2023.08.14
Clean Code -7 단위테스트  (0) 2023.08.14
Clean Code -5 객체와 자료구조  (0) 2023.08.10
Clean Code -4 형식 맞추기  (0) 2023.08.10
Clean Code -3 주석  (0) 2023.08.10

+ Recent posts