객제지향, TDD, 클린코드/클린코드

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

ummchicken 2023. 3. 8. 22:08

출처: Clean Code 클린 코드 애자일 소프트웨어 장인 정신 / 로버트 C. 마틴 저

 

(얘 나랑 좀 닮음;;; 내 친구도 인정함)

 

 

클린코드에 대해 인상깊은 문장을 봐서 공부해봤다.

 

 

예외처리 서두에 

'깨끗하고 튼튼한 코드에 한걸음 더 다가가는 단계로 

우아하고 고상하게 오류를 처리하는 기법...(후략)' 문구가 인상깊다.

 

 

📌 요약

  • 오류 처리도 한 가지 작업이다.
    • 함수는 '한 가지' 작업만 해야 한다. 오류 처리도 '한 가지' 작업에 속한다.
    • 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.

 

  • try/catch 블록은 원래가 추하다. 코드 구조에 혼란을 일으키며, 정상적인 동작과 오류 처리 동작을 뒤섞는다. try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.

 

public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

 

 

 


 

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

 

 

예를 들어 

if (deletePage(page) == E_OK)

이러한 코드가 있다면, 

여러 단계로 중첩되는 코드를 야기한다.

 

 

if(deletePage(page) == E_OK) {
    if(registry.deleteReference(page.name) == E_OK) {
        if(configKeys.deleteKey(Page.name.makeKey()) == E_OK) {
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteReference from registry failed");;
    }
} else {
    logger.log("delete failed");
    return E_ERROR;
}

이런 식으로 말이다.

 

 

 

 

반면 오류 코드 대신 예외를 사용하면, 

오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKey.deleteKey(page.name.makeKey());
} catch (Exception e) {
    logger.log(e.getMessage());
}

이런 식으로 말이다.

 

 

 

 

 

✔️ Try/Catch 블록 뽑아내기

try/catch 블록은 원래가 추하다.
코드 구조에 혼란을 일으키며, 정상적인 동작과 오류 처리 동작을 뒤섞는다.
try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.

 

public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKey.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
    loggger.log(e.getMessage());
}
  • delete 함수는 모든 오류를 처리한다. 그래서 코드를 이해하기 쉽다.
  • 실제로 페이지를 제거하는 함수는 deletePageAndAllReferences다.
  • deletePageAndAllReferences 함수는 예외를 처리하지 않는다.

 

 

 

이렇게 정상 동작과 오류 처리 동작을 분리하면, 

코드를 이해하고 수정하기 쉬워진다.

 

 

 

 

 

✔️ 오류 처리도 한 가지 작업이다.

함수는 '한 가지' 작업만 해야 한다. 오류 처리도 '한 가지' 작업에 속한다.
그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.

 

 

위 예제에서 보았듯이 함수에 키워드 try가 있다면, 

함수는 try 문으로 시작해 catch/finally 문으로 끝나야 한다.

 

 

 

 

 

✔️ Error.java 의존성 자석

오류 코드를 반환한다는 이야기는, 
클래스든 열거형 번수든, 어디선가 오류 코드를 정의한다는 뜻이다.

 

public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_EVENT;
}
  • enum 타입으로 error 코드를 정의했다.

 

 

위와 같은 클래스는 의존성 자석(magnet)이다.

다른 클래스에서 Error eum을 import해 사용해야 하므로, 

즉, Error enum이 변한다면,

Error enum을 사용하는 클래스를 전부 다시 컴파일 하고 다시 배치해야 한다.

그래서 Error 클래스 변경이 어려워진다.

따라서 새 오류 코드를 추가하는 대신, 기존 오류 코드를 재사용한다.

 

 

하지만 오류 코드 대신 예외를 사용하면, 

새 예외는 Exception 클래스에서 파생된다.

따라서 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

→ 이것은 OCP(Open Closed Principle) 개방-폐쇄 원칙을 보여주는 예다!

 

 

 

 

++ 이 부분이 뭔 말인지 이해가 안 가서 더 찾아보니, 

에러를 enum으로 한 곳에서 관리하지 말고, exception 예외처리로 하라는 뜻이라고 한다.

즉, 의존성 자석은 붙이기는 쉬운데 때어낼려면 고생해야 하는 것이라고 한다.

 

enum Error를 정의해버리면,

그 enum 한개를 코드 전체에서 전부 참조하게 되니까 좋지 않다는 것이다.

 

 

 

 

 

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

try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면, 
어느 시점에서든 실행이 중단된 후 catch 블록으로 넘어살 수 있다.

 

 

💡 어떤 면에서 try 블록은 트랜잭션과 비슷하다.
try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램상태를 일관성 있게 유지해야 한다.
그러므로 예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 게 낫다.
그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.

 

 

 

예외를 좁혀가는 예제를 살펴보자.

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

    return new ArrayList<RecordedGrip>;
}

이 코드에서 리팩터링을 해보자.

 

 

 

catch 블록에서 예외 유형을 좁혀 FileNotFoundException을 잡아내봅시다.

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

    return new ArrayList<RecordedGrip>;
}

 

 

 

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

 

 

 


 

💡 결론

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

 

 

 

 

 

 

 

내 생각...

객체지향을 이해해야 클린 코드를 학습할 수 있다고 생각했는데, 

클린코드를 먼저 하니까 객체지향이 비로소 이해가 가기 시작한다..? 

나만 이런가ㅋ;;

 

 

암튼 재밌다ㅎㅎ