[Review] Clean Code 내용정리 - 3

Cover image

Clean Code 내용 정리 - 3

7장. 오류 처리

깨끗한 코드와 오류 처리는 연관성이 존재한다.

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

오류가 발생 시 예외를 던지는 방법이 논리와 오류 처리 코드가 뒤섞이지 않아 코드가 더 깔끔해진다.

Try-Catch-Finally 문부터 작성하기

예외가 발생할 코드를 짤 경우에는 try-catch-finally 문으로 시작하는 것이 좋다.

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

미확인(unchecked) 예외를 사용하기

확인된 예외는 몇 가지 장점을 제공하지만, 반드시 필요하지는 않다.

  • 확인된 오류가 치르는 비용에 대해서 잘 생각해보아야 한다.
  • 확인된 예외는 OCP(Open Closed Principle)를 위반한다.
  • Ex) 확인된 예외를 던졌으나, catch가 세 단계 위에 있다면 모든 선언부에 예외가 필요하다. 대규모 시스템에서는 힘들다.

예외에 의미를 제공하기

  • 예외를 던질 때 전후 상황을 충분히 덧붙일 때, 오류가 발생한 원인과 위치를 찾을 수 있다.
  • 오류 메시지에 정보(실패한 연산 이름, 실패 유형)등을 함께 던진다.

    • 로깅 기능을 통해 충분한 정보를 제공하자.

호출자를 고려해 예외 클래스를 정의하기

오류를 잡아내는 다양한 방법이 존재한다.

  • 외부 API를 사용할 때는 감싸기 기법을 사용하는 것은 좋은 방법이다.
  • LocalPort port = new LocalPort(12);
    try {
      port.open();
    } catch (PortDeviceFailure e) {
      reportError(e);
      logger.log(e.getMessage(), e);
    } finally {
      ...
    }
  • 예외 클래스가 하나만 있어도 되는 경우가 많다.
  • 더 나아가, 한 예외는 잡아내고 다른 예외는 무시해도 되는 경우에는 여러 예외 케이스를 사용하는 것도 방법이다.

정상 흐름을 정의하기

특수 사례 패턴을 사용하기

  • 클래스나 객체로 예외적인 상황을 캡슐화하여 처리할 수 있음

null을 반환하지 말기

null에 대한 과한 확인은 문제가 될 수 있다. 즉, 예외나 특수 사례 패턴이 더 좋은 경우가 많다.

null을 전달하지 말기

인수에 null이 넘어가지 않는 정책이 있다면, 많은 실수를 줄일 수 있다.

결론.

  • 깨끗한 코드는 읽기도 좋아야 하지만, 안정성도 높아야 한다.
  • 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드 작성이 가능

8장. 경계

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.

외부 코드 사용하기

인터페이스 제공자와 사용자 사이에는 이해관계가 들어간다. 제공자는 최대한 적용성을 늘리기를 원하지만, 사용자는 자신의 요구에 집중하는 인터페이스를 희망한다.

즉, Map과 같은 경계 인터페이스를 사용할 때는 이용하는 클래스나 계열 밖으로 노출되지 않도록 주의해야 한다.(캡슐화도 한 방법)

경계를 살피고 익히기

  • 외부 코드를 사용하면 적은 시간에 많은 기능을 넣을 수 있는 장점이 존재.
  • 그러나, 테스트를 해서 진행하는 방법이 바람직하다.

    • 이러한 간단한 테스트 케이스를 사용해 익히는 방법을 학습 테스트라고 부른다.

log4j 익히기

로깅을 직접 구현하기보다는 아파치의 log4j 패키지를 사용하자.

간단한 예시 코드는 다음과 같다.

public class LogTest {
  private Logger logger;

  @Before
  public void initialize() {
    logger = Logger.getLogger("logger");
    logger.removeAllApenders();
    Logger.getRootLogger().removeAllAppenders();
  }

  @Test
  public void basicLogger() {
    BasicConfigurator.configure();
    logger.info("basicLogger");
  }

  @Test
  public void addAppenderWithStream() {
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n"),
      ConsoleAppender.SYSTEM_OUT));
    logger.info("addAppenderWithStream");
  }

  @Test
  public void addAppenderWithoutSteam() {
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n")));
    logger.info("addAppenderWithoutStream");
  }
}

학습 테스트는 공짜 이상

  • 학습 테스트에 드는 비용은 없으며, 필요한 지식만 확보하는 손쉬운 방법
  • 투자하는 노력보다 얻는 성과가 더 크다
  • 패키지가 새 버전이 나오면 학습 테스트를 돌려 예상대로 나오는지 체크한다.

아직 존재하지 않는 코드를 사용하기

  • 아는 코드와 모르는 코드(미완성 코드)를 분리하기
  • 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제할 수 있다는 장점을 얻을 수 있음.
  • 이러한 설계는 테스트에서도 편하다.

깨끗한 경계

경계에서는 다양한 문제가 발생할 수 있다.

따라서.

  • 통제 못하는 코드를 사용하는 경우에는 비용이 적게 구성해야 한다.
  • 경계에 위치한 코드는 깔끔하게 분리한다.
  • 외부 패키지에 의존하는 대신, 통제 가능한 우리 코드에 의존한다
  • 외부 패키지 호출 코드를 가능한 줄여서 경계를 관리한다

9장. 단위 테스트

제대로 된 테스트가 필요하다.

TDD 법칙 세가지

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

위 규칙을 잘 지키면, 테스트 코드와 실제 코드가 같이 나온다.

깨끗한 테스트 코드 유지하기

코드가 망가지기 시작하면 망가진다. 따라서, 테스트 코드는 실제 코드 못지않게 중요하게 짜야한다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

  • 단위 테스트는 코드에 유연성, 유지보수성, 재사용성을 제공하는 기둥이 된다.
  • 테스트 케이스가 있다면 많은 공포를 해결할 수 있다.
  • 테스트 코드가 지저분해질수록 실제 코드도 지저분해진다.

깨끗한 테스트 코드

  • 가독성 : 명료성, 단순성, 풍부한 표현력

도메인에 특화된 테스트 언어

  • 도메인에 특화된 언어(DSL)도 좋은 방법이다.

이중 표준

  • 단순, 간결, 표현력이 풍부, 그러나 꼭 효율적인 필요는 없다.

테스트 당 assert 하나

  • Assert 문은 하나가 좋지만, 때로는 여러 개를 써도 된다. (단, 최대한 줄여야 한다.)

Tip) Assert문.

정해진 조건에 맞지 않을 때 프로그램을 중단. ex) assert(표현식)

  • 테스트 함수마다 한 개념만 테스트

F.I.R.S.T

  • Fast(빠르게) : 테스트는 빠르게 돌아야 한다.
  • Independent(독립적으로) : 각 테스트는 서로 의존하면 안 된다.
  • Repeatable(반복 가능하게) : 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • Self-Validating(자가 검증하는) : 테스트는 부울(bool) 값으로 결과를 내야 한다.
  • Timely(적시에) : 테스트는 적시에 작성해야 한다.

결론

  • 테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화해야 한다.
  • 도메인 특화 언어(DSL, Domain Specific Language)를 구성하면 테스트 코드가 짜기 쉬워진다.

10장. 클래스

클래스 체계

추상화 단계는 순차적으로 내려간다.

  • public, private, 비공개 인스턴스 변수 순으로 진행된다.

캡슐화

꼭 유지해야 하는 것은 아니나, 캡슐화를 푸는 방법은 최후의 수단입니다.

클래스는 작아야 한다.

하나의 클래스는 적은 책임을 얻어야 한다.

즉, 클래스 설명은 if, and, or, but 등의 단어를 제외하고 25 단어 내외로 가능해야 한다.

단일 책임 원칙(SRP, Single Responsibility Principle)

  • 클래스나 모듈을 변경할 이유가 하나뿐이어야 한다는 원칙.
  • 객체 지향 설계에서 더우 중요한 개념이다.

응집도

  • 클래스는 인스턴스 변수 수가 적어야 한다.
  • 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
  • '함수를 작게, 매개변수 목록을 짧게'라는 전략으로 진행하고, 응집도가 높아지도록 새로운 클래스로 분리한다.

응집도를 유지하면 작은 클래스 여럿으로 구성된다.

  • 클래스가 응집력을 잃으면 쪼개야 한다.
  • 3가지 룰을 사용한다.

    1. 리팩터링한 프로그램은 좀 더 길고 서술적인 변수 이름을 사용
    2. 리팩터링한 프로그램은 코드에 주석을 추가하는 수단으로 함수 선언과 클래스 선언을 활용
    3. 가독성을 높이기 위해 공백을 추가하고 형식을 맞춤

변경하기 쉬운 클래스

대표적으로 수정하기 어려운 코드가 SQL 클래스이다. 잠재적으로 수정되는 여지를 남기는 것이 좋다.

변경으로부터 격리

  • 구체적인 클래스와 추상 클래스의 사용이 중요하다. 즉, 인터페이스와 추상 클래스가 중요하다.
  • 시스템의 결합도를 낮추면 유연성과 재사용성이 높아지고 각 요소를 이해하기 쉬워진다.
  • 이는 클래스 설계 원칙(DIP, Dependency Inversion Principle)을 따르는 클래스를 지킬 수 있다.

    • DIP : 상세한 구현이 아니라 추상화에 의존한다는 원칙