이정우
1. 📌 핵심 개념 정리
✅ 요약하기
- 오류 코드보다 예외를 사용하라
- 논리가 오류 처리 코드와 뒤섞이지 않아 호출자 코드가 더욱 깔끔해진다.
- Try-Catch-finally 문부터 작성하라.
- try 블록은 트랜잭션과 비슷하다. try블록과 별개로 catch 블록은 프로그램 상태를 일관성 있게 유지해야 하기 때문.
- try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워짐
-
미확인 예외를 사용하라
- 자바 초장기에는 메서드 선언시 메서드가 반환할 예외를 모두 열거하고, 메서드 사용 방식이 선언과 일칠하지 않으면 컴파일조차 제한했다.
- 현재는 안정적 SW 제작 요소로 확인된 예외가 반드시 필요하지는 않다는 사실이 분명해졌다.
- C#, C++, 파이썬, 루비 등은 확인된 예외를 지원하지 않지만 안정적인 소프트웨어를 구현하기에 무리가 없다.
- 확인된 오류가 치르는 비용에 상응하는 이익을 제공하는지 따져봐야한다.
- 확인된 예외도 물론 유용하다. 중요한 라이브러리 작성시 모든 예외를 잡아야 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크게 작용한다.
-
예외에 의미를 제공하라
- 예외를 던질 때는 오류 발생 원인과 위치를 찾기 쉽도록 전후 상황을 충분히 덧붙인다.
- 자바는 모든 예외에 호출 스택으 제공하지만 실패한 코드의 의도 파악을 위해선 부족하다.
- 오류 메시지에 실패한 연산 이름과 실패 유형 등의 정보를 담아 오류와 함께 던져야한다.
- 애플리케이션이 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.
-
호출자를 고려해 예외 클래스를 정의하라
-
오류 분류 방법
- 발생 위치
- 발생 컴포넌트
- 발생 유형
- 디바이스 실패
- 네트워크 실패
- 프로그래밍 오류
- 발생 위치
-
애플리케이션 오류 정의시 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.
-
개선 전
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 설계 방식에 얽메이지 않는다.
- 정상 흐름을 정의하라
-
앞 절의 지침을 따르다 보면 대부분이 깨끗하고 간결한 알고리즘으로 보이기 시작하지만, 오류감지가 프로그램 언저리로 밀려나게 된다.
-
개선 전
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 객체를 반환한다.
- 이를 특수 사례 패턴이라 부른다.
- 클래스를 만들거나 객체를 조작해 특수 사례를 ㄹ처리하는 방식이다.
- 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.
-
- 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를 변경해 빈 리스트를 반환하면 코드가 깔끔해진다.
- 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;
}
}
- 결론
- 깨끗한 코드란 읽기도 좋아야 하지만, 안정성도 높아야 한다.
- 가독성과 안정성은 상충하는 목표가 아니다.
- 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 꺠끗한 코드 작성 가능.
- 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
의도를개념드러내또는변수명원칙의 이름- 어려웠던 부분
변수명을 짧게 하면 간결하지만 의미 전달이 부족하고, 길게 하면 가독성이 떨어질 수도 있다.책에서는 "의도를 드러내라"라고 했지만, 어디까지 길게 써야 하는지 기준이 모호하다.
- 궁금한
변수명이 너무 길어지는 것을 방지하면서도 의미를 명확히 하는 방법이 있을까?실무에서 일반적으로 따르는 변수명 작성 규칙이 있을까?
- 어려웠던 부분
함수(메서드) 분리 기준어려웠던 부분"함수는 한 가지 일만 해야 한다"는 원칙이 있지만, 어떤 기준으로 분리해야 하는지 애매하다.너무 잘게 나누면 오히려 코드가 더 복잡해질 수도 있을 것 같다.
궁금한 점좋은 함수 분리 기준을 정하는 방법이 있을까?특정 길이(예: 10줄 이하) 같은 정량적인 기준이 존재할까?
주석 대신 코드로 의도를 표현하는 방법어려웠던 부분"주석을 줄이고, 코드 자체로 의미를 표현하라"는 원칙을 강조했지만, 모든 경우에 적용하기 어려워 보인다.예외 처리나 복잡한 로직을 설명할 때도 주석 없이 이해할 수 있는 코드가 가능할까?
궁금한 점주석 없이도 충분히 이해할 수 있는 코드를 작성하는 실질적인 방법이 있을까?코드의 가독성을 유지하면서도 주석을 최소화할 수 있는 팁이 있을까?
3. 📚 참고 사항
📢 논의하기
- 논의하고 싶은 주제
- 주제
메서드는 몇 줄까지가 적당할까? - 설명
Clean Code에서는 짧을수록 좋다고 하지만 너무 잘게 쪼개면 오히려 가독성이 떨어질 수도 있다.실무에서는 어느 정도가 적절한 기준인지 논의해 보면 좋을 것 같다.
- 주제