Skip to main content

이정우

1. 📌 핵심 개념 정리

✅ 요약하기

  1. 오류 코드보다 예외를 사용하라
    • 논리가 오류 처리 코드와 뒤섞이지 않아 호출자 코드가 더욱 깔끔해진다.

  1. Try-Catch-finally 문부터 작성하라.
    • try 블록은 트랜잭션과 비슷하다. try블록과 별개로 catch 블록은 프로그램 상태를 일관성 있게 유지해야 하기 때문.
    • try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워짐

  1. 미확인 예외를 사용하라

    • 자바 초장기에는 메서드 선언시 메서드가 반환할 예외를 모두 열거하고, 메서드 사용 방식이 선언과 일칠하지 않으면 컴파일조차 제한했다.
    • 현재는 안정적 SW 제작 요소로 확인된 예외가 반드시 필요하지는 않다는 사실이 분명해졌다.
    • C#, C++, 파이썬, 루비 등은 확인된 예외를 지원하지 않지만 안정적인 소프트웨어를 구현하기에 무리가 없다.
    • 확인된 오류가 치르는 비용에 상응하는 이익을 제공하는지 따져봐야한다.
    • 확인된 예외도 물론 유용하다. 중요한 라이브러리 작성시 모든 예외를 잡아야 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크게 작용한다.
  2. 예외에 의미를 제공하라

    • 예외를 던질 때는 오류 발생 원인과 위치를 찾기 쉽도록 전후 상황을 충분히 덧붙인다.
    • 자바는 모든 예외에 호출 스택으 제공하지만 실패한 코드의 의도 파악을 위해선 부족하다.
    • 오류 메시지에 실패한 연산 이름과 실패 유형 등의 정보를 담아 오류와 함께 던져야한다.
    • 애플리케이션이 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.
  3. 호출자를 고려해 예외 클래스를 정의하라

    • 오류 분류 방법

      • 발생 위치
        • 발생 컴포넌트
      • 발생 유형
        • 디바이스 실패
        • 네트워크 실패
        • 프로그래밍 오류
    • 애플리케이션 오류 정의시 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

    • 개선 전

    ACMEPort port = new ACMEPort(12);
    
    try {
      port.open();
    } catch (DEviceResponseExveption 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 {
      ...
    }
    
    • 위의 코드는 예외에 대응하는 방식이 예외 유형과 무관하게 거의 동일하다.

    • 개선 후

     LocalPort port = new LocalPort(12);
     try {
       port.open();
     } catch (PortDeviceFailure e) {
       reportError(e);
       logger.log(e.getMessage(), e);
     } finally {
       ...
     }
    
    public class LocalPort {
      pirvate 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 (GMXRrror e) {
          throw new PortDeviceFailure(e);
        }
      }
       ...
    } 
    
  • LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 감싸기(wrapper) 클래스일 뿐이다.

  • port 디바이스 실패를 표현하는 예외 유형 하나를 정의

  • 외부 API를 사용할 때는 감싸기 기법이 최선이다.

    • 외부 라이브러리와 프로그램 사이의 의존성 대폭 감소
    • 이후 타 라이브러리 이전비용이 적다.
    • 테스트 코드를 통한 프로그램 테스트가 가능해 테스트가 쉽다.
    • 특정 업체의 API 설계 방식에 얽메이지 않는다.
  1. 정상 흐름을 정의하라
    • 앞 절의 지침을 따르다 보면 대부분이 깨끗하고 간결한 알고리즘으로 보이기 시작하지만, 오류감지가 프로그램 언저리로 밀려나게 된다.

    • 개선 전

    try {
      MealExpenses expenses = expenseReportDAO.getMeals(emplyee.getID());
      m_total += expenses.getTotal();
    } catch(MealExpensesNotFound e) {
      m_total += getMealPerDiem();
    }
    
    • 위 코든느 비용 처구 애플리케이션에서 총계를 게산하는 허술한 코드이다.

    • 식비를 비용으로 청구했다면 직원이 청구한 식비를 총계에 더한다.

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

    • 하지만, 예외가 논리를 따라가기 어렵게 만든다.

    • 개선 후

    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    
    • ExpenseReportDAO를 언제나 MealExpense 객체를 반환하도록 수정
    • 청구한 식비가 없다면 일일 기본 식비를 반환하는 MEalExpense 객체를 반환한다.
    • 이를 특수 사례 패턴이라 부른다.
      • 클래스를 만들거나 객체를 조작해 특수 사례를 ㄹ처리하는 방식이다.
      • 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

  1. null을 반환하지 마라.
  • null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘기다.

  • 메서드에서 null을 반환하고픈 유혹이 든다면 그 대신에 예외를 던지거나 특수 사례 객체를 반환한다.

  • 개선 전

 List<Employee> employees = getEmployees();
 if (employees != null) {
   for(Employee e : employees) {
     totalPay += e.getPay();
   }
 }
  • 위 코드에서 getEmployees는 null도 반환한다.

  • 개선 후

    public List<Employee> getEmployees() {
      if (.. 직원이 없다면 .. )
        return Collections.emptyList();
    }
  • 이런 수정은 코드를 깔끔하게 만들고 NullPointerException 발생 가능성도 줄인다.
  • getEmployees를 변경해 빈 리스트를 반환하면 코드가 깔끔해진다.

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

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

  • 개선 전

 public class MetricsCalculator {
   public double xProjection(Point p1, Point p2) {
     return (p2.x - p1.x) * 1.5;
   }
   ...
 }    
  • 인수로 null을 전달하면 NullPointerException 발생

  • 개선 후

 // 1. 새로운 예외 유형을 만들어 던지는 방법
 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;
   }
 }

 // 2. 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;
   }
 }    
  1. 결론
    • 깨끗한 코드란 읽기도 좋아야 하지만, 안정성도 높아야 한다.
    • 가독성과 안정성은 상충하는 목표가 아니다.
    • 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 꺠끗한 코드 작성 가능.
    • 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

2. 🤔 이해가 어려운 부분

🔍 질문하기

  1. 의도를개념 드러내변수명원칙의 이름
    • 어려웠던 부분
      • 변수명을 짧게 하면 간결하지만 의미 전달이 부족하고, 길게 하면 가독성이 떨어질 수도 있다.
      • 책에서는 "의도를 드러내라"라고 했지만, 어디까지 길게 써야 하는지 기준이 모호하다.
    • 궁금한 
      • 변수명이 너무 길어지는 것을 방지하면서도 의미를 명확히 하는 방법이 있을까?
      • 실무에서 일반적으로 따르는 변수명 작성 규칙이 있을까?

  1. 함수(메서드) 분리 기준
    • 어려웠던 부분
      • "함수는 한 가지 일만 해야 한다"는 원칙이 있지만, 어떤 기준으로 분리해야 하는지 애매하다.
      • 너무 잘게 나누면 오히려 코드가 더 복잡해질 수도 있을 것 같다.
    • 궁금한 점
      • 좋은 함수 분리 기준을 정하는 방법이 있을까?
      • 특정 길이(예: 10줄 이하) 같은 정량적인 기준이 존재할까?

  1. 주석 대신 코드로 의도를 표현하는 방법
    • 어려웠던 부분
      • "주석을 줄이고, 코드 자체로 의미를 표현하라"는 원칙을 강조했지만, 모든 경우에 적용하기 어려워 보인다.
      • 예외 처리나 복잡한 로직을 설명할 때도 주석 없이 이해할 수 있는 코드가 가능할까?
    • 궁금한 점
      • 주석 없이도 충분히 이해할 수 있는 코드를 작성하는 실질적인 방법이 있을까?
      • 코드의 가독성을 유지하면서도 주석을 최소화할 수 있는 팁이 있을까?

3. 📚 참고 사항

📢 논의하기

  1. 관련 자료 공유

  1. 논의하고 싶은 주제
    • 주제
      메서드는 몇 줄까지가 적당할까?
    • 설명
      • Clean Code에서는 짧을수록 좋다고 하지만 너무 잘게 쪼개면 오히려 가독성이 떨어질 수도 있다.
      • 실무에서는 어느 정도가 적절한 기준인지 논의해 보면 좋을 것 같다.