본문 바로가기
Study/Java

[Clean-Code] Chapter. 7-10

by novxerim 2023. 6. 22.

Clean-Code_week3_yerimi11

오류 처리

[Try-Catch-Finally 문부터 작성하라]
try-catch 구조로 범위를 정의한 후 TDD를 사용해 필요한 나머지 논리를 추가한다.

[미확인(unchecked) 예외를 사용하라]
지금은 안정적인 소프트웨어를 제작하는 요소로 확인된(checked) 예외가 반드시 필요하지는 않다는 사실이 분명해졌다.
C#와 C++, 파이썬, 루비는 확인된 예외를 지원하지 않는다. 그럼에도 불구하고 해당 언어들은 안정적인 소프트웨어를 구현하기에 무리가 없다.
그러므로 확인된 오류가 치르는 비용에 상응하는 이익을 제공하는지 따져봐야 한다.

  • 확인된 예외는 OCP(Open Closed Principle)를 위반한다.
    메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다.
    즉, 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.
    모듈과 관련된 코드가 전혀 바뀌지 않았더라도 (선언부가 바뀌었으므로) 모률을 다시 빌드 한 다음 배포해야 한다.

  • 확인된 오류를 던진다면 함수는 선언부에 throws 절을 추가해야 한다.
    그러면 변경한 함수를 호출하는 함수 모두가 1) catch 블록에서 새로운 예외를 처리하거나 2) 선언부에 throw 절을 추가해야 한다.
    결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다.
    throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.

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

[예외에 의미를 제공하라]
예외를 던질 때는 전후 상황을 충분히 덧붙인다.
오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다.
애플리케이션이 로깅 기능을 시용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

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

  • 오류를 형편없이 분류한 사례

    ACME Port 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 클래스가 던지는 예외를 잡아 변환하는 감싸기 wrapper 클래스이다.

    public class LocalPort { 
    private ACMEPort innerPort;
    
    public LocalPort(int portNumber) { 
      innerPort ne씨 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);
      } 
    }
    ...
    }

    외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.
    나중에 다른 라이브러리로 갈아타도 비용이 적다.
    감싸기 클래스에서 외부 API를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.

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

MealExpenses expenses =expenseReportDAO.getMeals(employee.getID()); 
m_total += expenses.getTotal();

ExpenseReportDAO를 고쳐 언제나 MealExpense 객체를 반환한다. 청구한 식비가 없다면 일일 기본 식비를 반환하는 MealExpense 객체를 반환한다.

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

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

[null을 반환하지 마라]
한 줄 건너 하나씩 null을 확인하는 코드
image
null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다. 누구 하나라도 null 확인을 빼먹는다면 애플리케이션이 통제 불능에 빠질지도 모른다.
위 코드에서 둘째 행에 null 확인이 빠졌다. 만약 per- sistentStore가 null이라면 실행 시 NullPointerException이 발생한다.
애플리케이션 저 아래서 날린 NullPointer Exception는 처리하기 어렵다.

실상은 null 확인 이 너무 많아 문제다.
메서드에서 null을 반환하고 싶다면, 그대신 예외를 던지거나 특수 사례 객체를 반환한다.
사용하려는 외부 API가 null을 반환 한다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려한다.

[null을 전달하지 마라]
정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.
누군가 인수로 null을 전달하면 당연히 NullPointerException이 발생한다.

1) 이럴 때는 새로운 예외 유형을 만드는 방법이 있다.
image
위 코드는 NullPointerException보다는 조금 낫지만, InvalidArgumentException을 잡아내는 처리기가 필요하다.
2) assert 문을 사용하는 방법도 있다.
image
문서화가 잘 되어 코드 읽기는 편하지만 문제를 해결하지는 못한다. 누군가 null 을 전달하면 여전히 실행 오류가 발생한다.

대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다. 그렇다면 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적 이다.
즉, 인수로 null이 넘어오면 코드에 문제가 있다는 말이다. 이런 정책을 따르면 그만큼 부주의한 실수를 저지를 확률도 작아진다.

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


경계

[외부 코드 사용하기]
제네릭스(Generics)을 사용하면 코드 가독성이 크게 높아진다.

Map<String, Sensor> sensors new HashMap<Sensor>(); 
...
Sensor s = sensors.get(sensorId);

자바5가 제네릭스를 지원하면서 Map 인터페이스가 변했고, 실제로 제네릭스 사용을 금지하는 시스템도 있다.

다음은 Map을 좀 더 깔끔하게 사용한 코드다.

public class Sensors {
  private Map sensors = new HashMap();

  public Sensor getById(String id) { 
     return (Sensor) sensors.get(id);
  }
  // 이하 생략 
}

경계 인터페이스인 Map을 Sensors안으로 숨겨서, Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다.
Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문에 제네릭스를 사용하든 하지 않든 더 이상 문제가 안 된다.

Map클래스를 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다.
Map을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 말라는 말이다.
Map과같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.

[경계 살피고 익히기]
외부에서 가져온 패키지를 시용하려고 할 때, 보통 하루나 이틀 (아니면 더 오랫동안) 문서를 읽으며 사용법을 결정한다. 그런 다음 우리쪽 코드를 작성해 라이브러리가 예상대로 동작하는지 확인한다.
그러나 외부 코드를 호출하는 대신, 곧바로 우리쪽 간단한 테스트 케이스를 작성해 외부 코드를 익히는 학습 테스트가 있다.
학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다.

[log4j]
로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용한다.

[아직 존재하지 않는 코드를 사용하기]
경계와 관련한 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계가 있다.
예시) 다른 팀이 아직 API를 설계하지 않아 우리 팀은 구현을 나중으로 미룬 상태에서 이쪽 코드를 진행하고자 자체적으로 인터페이스를 정의한다.
Transmitter라는 간단한 클래스를 만든 후 transmit라는 메서드를 추가했다.
image

  • 우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제 한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다.
  • ADAPTER 패턴으로 API 사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.
    이와 같은 설계는 테스트도 아주 편하다. 적절한 FakeTransmitter 클래스를 시용하면 CommunciationsController 클래스를 테스트할 수 있다.

[깨끗한 경계]
변경이 대표적인 예이다.
경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.
통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다. 자칫하면 오히려 외부 코드에 휘둘리고 만다.
외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.
어느 방법이든 코드가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.


단위 테스트

[TDD 법칙 세가지]
· 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
· 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
· 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

위 세 가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다.
이렇게 일하면 매일 수십 개, 매달 수백 개, 매년 수천 개에 달하는 테스트 케이스가 나와서, 사실상 실제 코드를 전부 테스트하는 테스트 케이스가 나온다.
하지만 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.

[깨끗한 테스트 코드 유지하기]
지저분한 테스트 코드는 테스트를 하는 것 보다 더 못하다.
문제는 실제 코드가 진화하면 테스트 코드도 변해야 하는데, 테스트 코드가 지저분할 수록 변경하기 어려워진다.
테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸린다.
테스트코드는 실제 코드 못지 않게 깨끗하게 짜야 한다.
테스트는 유연성, 유지보수성, 재사용성을 제공한다.
테스트 커버리지가 높을수록 공포는 줄어든다.

[깨끗한 테스트 코드 만들기]
가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다.
테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

예시) addpage와 assertSubString을 부르느라 중복되는 코드가 매우 많다. 자질구레한 사항이 너무 많아 테스트코드의 표현력이 떨어진다.
image
image

BUILD-OPERATE-CHECK 패턴이 위와 같은 테스트 구조에 적합하다.

1) 테스트 자료를 만든다. 2) 테스트 자료를 조작하며, 3) 조작한 결과가 올바른지 확인한다.
잡다하고 세세한 코드를 거의 다 없앴다는 사실에 주목한다.

[도메인에 특화된 테스트 언어]
9-2는 도메인에 특화된 언어(DSL)로 테스트 코드를 구현하는 기법을 보여준다.
시스템 조작 API를 사용하는 대신 API 위에다 함수와 유틸리티를 구현한 후, 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.
이렇게 구현한 함수와 유틸리티는 테스트코드에서 사용하는 특수 API가 된다.

[이중 표준]
테스트 API 코드에 적용하는 표준은 단순하고, 간결하고, 표현력이 풍부해야하지만, 실제 코드만큼 효율적일 필요는 없다.
image
목록 9-3을 읽으면 코드에서 점검하는 상태 이름과 상태 값을 확인하느라 눈길이 이리저리 흩어진다. heaterState라는 상태를 보고서는 왼쪽으로 눈길을 돌려 asseltTrue를 읽는다.
테스트 코드를 읽기가 어렵다. 9-4로 변환해 코드 가독성을 높인다.
image
tic 함수는 wayTooCold라는 함수를 만들어 숨겼다. 대문자는 '켜짐'이고 소문자는 '꺼짐'을 뜻한다.
일단 의미만 안다면 눈길이 문자열을 따라 움직이며 결과를 재빨리 판단한다. 테스트 코드를 이해하기 쉬워진다.

[테스트 당 assert 하나]
JUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있다.
가혹한 규칙이라 여길지도 모르지만, 목록 9-5를 보면, 확실히 장점이 있다. assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.
image

테스트를 두 개로 쪼개 각자가 assert를 수행할 수도 있다. (given-when-then 관례 사용, 테스트 코드를 읽기가 쉬워진다.)
image
image

TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다.
given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.
아니면 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣어도 된다.

‘단일 assert 문’ 규칙은 훌륭한 지침이다.
하지만 때로는 주저 없이 함수 하나에 여러 assert 문을 넣기도 한다. 단지 assert문 개수는 최대한 줄여야 좋다는 생각이다.

[테스트 당 개념 하나]
“테스트 함수마다 한 개념만 테스트하라”는 규칙이 더 낫겠다.
image
목록 9-8은 각 절에 assert 문이 여럿이라는 사실이 문제가 아니라, 한 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제다.
그러므로 가장 좋은 규칙은 “개념 당 asseert문 수를 최소로 줄여라”와 “테스트 함수 하나는 개념 하나만 테스트하라”이다.

[F.I.R.S.T]
깨끗한 테스트의 5가지 규칙

1) F: 빠르게 (Fast)
2) I: 독립적으로 (Independent)
각 테스트는 서로 의존하면 안된다. 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다.
3) R: 반복가능하게 (Repeatable)
실제 환경, QA 환경, 버스를 타고 집으로 가는 길에 사용하는 (네트워크에 연결되지 않은) 노트북 환경에서도 실행할 수 있어야 한다.
4) S: 자가검증하는 (Self-Validating)
테스트는 부울(bool) 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안된다.
5) T: 적시에 (Timely)
테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다.

테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문에 실제 코드보다 더 중요할지도 모른다.
그러므로 테스트 코드는 지속적으로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자. 테스트 API를 구현해 도메인 특화 언어(Domain Specific Language, DSL)를 만들자.
그러면 그만큼 테스트 코드를 짜기가 쉬워진다. 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. 테스트 코드를 깨끗하게 유지하자.


클래스

[클래스는 작아야 한다]
단지 클래스를 설계할 때도, 함수와 마찬가지로, ‘작게’가 기본 규칙이다.
클래스 이름은 해당 클래스 책임을 기술해야 한다. 실제로 작명은 클래스 크기를 줄이는 첫 번째 관문이다.
또한 클래스 설명은 만일(if), 그리고(and), -(하)며(or), 하지만(but)을 사용하지 않고서 25단어 내외로 가능해야 한다.
'-하며'는 SuperDashboard에 책임이 너무 많다는 증거다.

[단일 책임 원칙]
단일 책임 원칙(Single Responsibility Principle, SRP)은 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다.
클래스는 책임, 즉 변경할 이유가 하나여야 한다. 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.
큰 클래스 몇 개가 아니라, 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며. 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

[응집도 (Cohesion)]
클래스는 인스턴스 변수 수가 작아야 한다. 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다.
모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.
일반적으로 이처럼 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다. 그렇지만 우리는 응집도가 높은 클래스를 선호한다.
응집도가 높다는 말은, 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미기 때문이다.

‘함수를 작게, 매개변수 목록을 짧게’라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.
응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두 세개로 쪼개준다.

'Study > Java' 카테고리의 다른 글

[Clean-Code] Chapter. 14  (1) 2023.07.11
[Clean-Code] Chapter. 11-13  (0) 2023.06.23
[Clean-Code] Chapter. 4-6  (0) 2023.06.21
[Clean-Code] Chapter. 1-3  (0) 2023.06.20
[JAVA] template method패턴  (0) 2023.06.19

댓글