본문 바로가기
memo

[clean_code] 07장.오류처리

by 바까 2022. 7. 5.
반응형

개인적으로 공부하려고 메모해둔것 때문에 정리가안돼있고 깔끔하지않음


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

 

//7-1.DeviceController.java

public class DeviceController{
...
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        //디바이스 상태를 점검한다.
        if (handle != DeviceHandle.INVALID) 
        {
            //레코드 필드에 디바이스 상태를 저장한다.
            retireveDeviceRecord(handle);
            //디바이스가 일시정지 상태가 아니라면 종료한다.
            if (record.getStatus() != DEVICE_SUSPENDED) 
            {
                    pauseDevice(handle);
                    clearDeviceWorkQueue(handle);
                    closeDevice(handle);
            }
            else 
            {
                logger.log("Device suspened. Unable to shut down");
            }
        }
        else 
        {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
    ...
}


//7-2.DeviceController.java (예외사용)

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 = retireveDeviceRecord(handle);

        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DeviceHandle getHandle(DeviceID id) {
        throw new DeviceShutDownError("Invalid handle for : " + id.toString());
        ...
    }
    ...
}

 

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

 

//try 블록은 트랜잭션과 비슷하다.

//파일이 없으면 예외를 던지는지 알아보는 단위 테스트

@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}


//단위테스트에 맞춰 다음코드를 구현
//그런데 코드가 예외를 던지지 않으므로 단위테스트는 실패.

public List<RecordedGrip> retrieveSection(String sectionName) {
    //실제로 구현할 때까지 비어 있는 더미를 반환한다.
    return new ArrayList<RecordedGrip>();
}


//잘못된 파일 접근을 시도하게 구현을 변경

public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInfutStream stream = new FileInputStream(sectionName)
    } catch (Exception e){
        throw new StorageException("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}


//코드가 예외를 던지므로 테스트 성공
//이 시점에서 리팩터링이 가능하다.
//catch 블록에서 예외 유형을 좁혀 실제로 FileInfutStream 생성자가 던지는 fileNotFoundException을 잡아낸다.

public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInfutStream stream = new FileInputStream(sectionName);
        stream.close();
    } catch (FileNotFoundException e){
        throw new StorageException("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}

 

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


//오류를 형편없이 분류한 사례
//외부 라이브러리를 호출하는 try-catch-finally 문을 포함한 코드로
//외부 라이브러리가 던질 예외를 모두 잡아낸다.

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


// 대다수 상황에서 오류를 처리하는 방식은
// 오류를 일으킨 원인과 무관하게 비교적 일정하다.
// 1) 오류를 기록한다.
// 2) 프로그램을 계속 수행해도 좋은지 확인한다.

// 위의 코드는 예외에 대응하는 방식이 예외 유형과 무관하게 거의 동일
// 그래서 코드를 간결하게 고치기가 아주 쉬움
// 호출하는 라이브러리 api를 감싸면서 예외 유형 하나를 반환

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


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

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 PortDeviceFailure(e);
    } catch (GMXError e) {
        throw new PortDeviceFailure(e);
    }
    ...
}


// 감싸기 클래스의 장점
// 외부 api를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.
// 나중에 다른 라이브러리로 갈아타도 비용이 적다.
// 또한 감싸기 클래스에서 외부 api를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.
// 특정 업체가 api를 설계한 방식에 발목 잡히지 않는다.

06.정상 흐름을 정의하라

//다음은 비용 청구 애플리케이션에서 총계를 계산하는 허술한 코드

try { 
    MealExpenses expenses = expenseReportDAO.getMeals(emplyee.getId());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e){
    m_total += getMealPerDiem();
}


//특수한 상황을 처리할 필요가 없다면?

MealExpenses expenses = expenseReportDAO.getMeals(emplyee.getId());
m_total += expenses.getTotal();

//이 코드가 가능할까?
ExpenseReportDAO를 고쳐 언제나 MealExpense 객체를 반환한다.
청구한 식비가 없다면 일일 기본 식비를 반환하는 MealExpense 객체를 반환한다.

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


// 이를 특수사례패턴이라고 부른다.
클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다.
그러면 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

07.null을 반환하지마라

//

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


//null을 반환하는 코드는 일거리를 늘릴뿐만 아니라 호출자에게 문제를 떠넘긴다.
//메서드에서 null을 반환하고프다면 그 대신 예외를 던지거나 특수사례객체를 바호나한다.
//사용하려는 외부 api가 null을 반환한다면 감싸기 메서드를 구현해 예외를 더닞거나 특수 사례 객체를 반환하는 방식을 고려한다.

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



//위에서 getEmployees는 null도 반환한다.
//하지만 반드시 null을 반환할 필요가 있을까?
//getEmployees를 변경해 빈 리스트를 반환한다면 코드가 훨씬 깔끔해진다.

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


//자바에는 Colloections.emptyList()가 있어 미리 정의된 읽기 전용 리스트를 반환한다.

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


//코드도 깔끔해질 뿐더러 NullPointerException이 발생할 가능성도 줄어든다.

08.null을 전달하지마라

정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

//두 지점 사이의 거리를 계산하는 간단한 메서드

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


//누군가 인수로 null을 전달한다면?

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


//NullPointerException발생
//->새로운 예외 유형을 만들어 던지는 방법으로 해결

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



//하지만 InvalidArgumentException 예외를 처리기가 필요
//->assert 문을 사용하는 방법

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


/*
결론
대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다.
애초에 null을 넘기지 못하도록 금지하는 정책이 가장 합리적
*/


생각이 없는 나는..평소 null반환과 전달을 난무하여 사용했는데

많이 찔리는 챕터였다^^..왜 그렇게 null처리가 힘들었는지

내가 멍청해서였다는걸 깨달았당

Copyright 2022. GA-YOUN. All rights reserved

반응형

'memo' 카테고리의 다른 글

[clean_code] 08장.경계  (0) 2022.07.15
[clean_code] 06장.객체와 자료구조  (0) 2022.07.05
sql 문제 모음  (0) 2022.05.11
[엑셀]주간 업무일지 파일 공유  (0) 2021.10.01
Visual Studio Code 설치하기(한국어 변경)  (0) 2020.03.12

댓글