Skip to main content

박수완

1. 📌 핵심 개념 정리

✅ 요약하기

  1. 오류 코드보다 예외를 사용해라

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

  • 개선 전

    // Bad
    public class DeviceController {
      ...
      public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        // Check the state of the device
        if (handle != DeviceHandle.INVALID) {
          // Save the device status to the record field
          retrieveDeviceRecord(handle);
          // If not suspended, shut down
          if (record.getStatus() != DEVICE_SUSPENDED) {
            pauseDevice(handle);
            clearDeviceWorkQueue(handle);
            closeDevice(handle);
          } else {
            logger.log("Device suspended. Unable to shut down");
          }
        } else {
          logger.log("Invalid handle for: " + DEV1.toString());
        }
      }
      ...
    }
    
  • 개선 후

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

코드가 확실히 깨끗해졌다. 단순히 보기만 좋아지지 않았다. 코드 품질도 나아졌다. 앞서 뒤섞였던 개념, 즉 디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리했기 때문이다.


  1. Try-Catch-Finally 문 부터 작성하라
    try 불록에서 무슨 일이 생기는지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다. 그러므로 예외가 발생할 코들르 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다. 그럼ㄴ try블록에서 무슨 일이 생기는지 호출자가 기대하는 상태를 정의하기 쉬워진다.

    // Step 3: Exception의 범위를 FileNotFoundException으로 줄여 정확히 어떤 Exception이 발생한지 체크하자.
    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-catch 구조로 범위를 정의했으므로 TDD를 사용해 필요한 나머지 논리를 추가한다. 나머지 논리는 FileInputStream울 생성하는 코드와 close 호출문 사이에 넣으먀 오류나 예외가 전혀 발생하지 않는다고 가정한다. 먼저 강제로 옝외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try 불록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.


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

    때로는 확인된 예외도 유용하다. 아주 중요한 라이브러리를 작성한다면 모든 예회를 잡아야 한다. 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다.


  1. 예외에 의미를 제공하라

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


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

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

    • 개선 전
     // Bad
     // catch문의 내용이 거의 같다.
    
     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 {
       ...
     }
    
    • 개선 후
    // Good
    // ACME 클래스를 LocalPort 클래스로 래핑해 new ACMEPort().open() 메소드에서 던질 수 있는 exception들을 간략화
    
    LocalPort port = new LocalPort(12);
    try {
     port.open();
    } catch (PortDeviceFailure e) {
     reportError(e);
     logger.log(e.getMessage(), e);
    } finally {
     ...
    }
    
    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 new PortDeviceFailure(e);
       } catch (GMXError e) {
         throw new PortDeviceFailure(e);
       }
     }
     ...
    }
    

    예외 클래스가 하나만 있어도 충분한 코드가 많다. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그렇다. 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우라면 여러 예외 클래스를 사용한다.


  1. 정상 흐름을 정의하라

    외부 API를 감싸 독자적인 예외를 던지고 코드 위에 처리기를 정의해 중단된 계산을 처리한다. 대개는 멋진 처리 방식이지만, 때로는 중단이 적합하지 않은 때도 있다.

    • 개선 후
     // Bad
    
     try {
       MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
       m_total += expenses.getTotal();
     } catch(MealExpensesNotFound e) {
       m_total += getMealPerDiem();
     }
    
  • 개선 후

    // Good
    
    // caller logic.
    ...
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    ...
    
    public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
      // return the per diem default
    }
    }
    
    

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


  1. null을 반환하지 마라

    오류 처리를 논하는 장이라면 우리가 흔히 저지르는 바람에 오류를 유발하는 행위도 언급해야 한다고 생각한다. 그 중 첫째가 null을 반환하는 습관이다.

    • 개선 전

       // 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체크가 너무 많다는 것이다.

    • 개선 후

      // Good
      List<Employee> employees = getEmployees();
      for(Employee e : employees) {
       totalPay += e.getPay();
      }
      
      public List<Employee> getEmployees() {
       if( .. there are no employees .. )
           return Collections.emptyList();
         }
      }
      

      이렇게 코드를 변경하면 코드도 깔끔해질뿐더러 NullPinterException이 발생할 가능성도 줄어든다.


  1. null을 전달하지 마라

    메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다. 정상적인 인수로 null을 기대하는 api가 아니라면 메서드로 ㅜull을 전달하는 코드는 최대한 피한다.

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

결론

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


2. 🤔 이해가 어려운 부분

🔍 질문하기

책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다.
    • 궁금한 점
      해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      .
    • 궁금한 점
      .

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      .
    • 궁금한 점
      .

3. 📚 참고 사항

📢 논의하기

관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.

  1. 관련 자료 공유
    • 추가 자료
      관련 블로그 글이나 공식 문서 링크를 제공합니다.

  1. 논의하고 싶은 주제
    • 주제
      논의하고 싶은 내용을 간략히 정리합니다.
    • 설명
      논의하고 싶은 이유를 작성합니다.