좋은 코드를 작성하기 위한 생각들

신입땐 명세를 만족하는 코드를 빠르게 작성하는게 최고인 줄만 알았다.

하지만 5년차쯤 되었을 때 이 생각에 문제가 있다는 것을 깨달았다.

복잡한 요구사항을 만족하는 코드도 보기 좋게 작성할 수 있다.

이 글에서 코드를 어떻게 작성해야 하는지에 대한 여러 생각들과 자료들을 정리해 본다.

불필요한 것들을 빼자

좋은 코드는 딱 필요한 만큼의 텍스트로만 작성되어 있다.

한 파일 안에 함수만 있다고 우습게 볼 것이 아닌데, 그렇게 생각하는 개발자를 본 적이 있다.

오히려 함수 하나면 되는데 클래스 이상을 동원하는 개발자가 이상한 것이다.

함수 하나면 순수함수가 되는 경향이 있고. 테스트도 쉬워진다.

이와 관련해서는 Kyle Simpson의 The Economy of Keystrokes를 보면 좋은데.

키 스트로크는 돈과 같아서 가성비를 따져야 하고.

사용한다면 가독성과 기능을 위해서만 사용해야 한다는 내용이다. 꼭 보길 바란다.

완성된 것들을 조합하자

리눅스의 쉘은 수 많은 완성된 명령 행 도구를 조합 사용하여 목적을 달성하게 되어 있다.

특정 파일들을 백업하는 경우 tar 명령어로 대상 파일들을 묶은 다음 ftp 명령어를 통해 업로드 한다.

압축파일이 생성되어 있지 않다면? tar명령어의 문제일 것이다.

압축파일은 있는데 업로드가 되지 않았다면? ftp를 포함한 네트워크의 문제일 것이다.

각 도구들의 역할이 명확하기 때문에 현상만으로 대략적인 문제를 파악할 수 있다.

이는 단위테스트가 추구하는 바 와도 일맥상통한다.

만약 모든 기능을 한번에 수행하는 함수를 작성했다면 함수 전체를 라인단위로 디버깅해야 하는데.

이런 코드가 많아질수록 유지보수하기 어렵게 된다. 아래 예제를 보자.

/**
 * path의 노드 중 pattern에 일치하는 노드들을 삭제함
 */
function deleteNodes(path, pattern) {
  for (const dirent of fs.opendirSync(path)) {
    if (dirent.name.match(pattern)) {
      fs.unlinkSync(dirent)
    }
  }
}

위의 코드는 아래처럼 리펙토링 할 수 있다.

/**
 * path의 하위 노드들을 조회함
 */
function getNodes(path) {
  return fs.opendirSync(path)
}

/**
 * pattern일치 노드를 제거함
 */
function deleteIfMatched(node, pattern) {
  if (node.name.match(pattern)) {
    fs.unlinkSync(node)
  }
}

/**
 * path의 노드 중 pattern에 일치하는 노드들을 삭제함
 */
function deleteNodes(path, pattern) {
  for (const node of getNodes(path)) {
    deleteIfMatched(node, pattern)
  }
}

위와 같은 방식의 리펙토링을 하면 다음과 같은 장점이 있다.

  1. 가독성

    • 리펙토링 이전: 표현식과 문이 섞여 코드가 한 눈에 들어오지 않는다.
    • 리펙토링 이후: 복잡한 식들이 적절한 이름의 메서드로 표현되어 마치 책을 읽는 것 처럼 코드가 읽힌다.
  2. 테스트 용이성

    • 리펙토링 이전: deleteNodes를 테스트하려면 한번에 여러 API Mock을 만들어야 하는 부담이 있다.
    • 리펙토링 이후: getNodes는 NodeJS의 API를 단순히 이용할 뿐이므로. 테스트하지 않아도 된다. 하나의 메서드에 섞여있던 테스트 포인트가 deleteIfMatched, deleteNodes로 나뉘었다. 그리고 각 메서드를 테스트하기 위한 Mock제공이 상대적으로 덜 부담스럽다.
  3. 확장성

    • 리펙토링 이전: copyNodes를 추가해야 한다면 리펙토링 이전은 주요 로직을 복사하고 요구사항에 맞게 수정해야 한다. 이 경우 전체 코드량이 2배가 되므로 테스트 커버리지가 감소한다.
    • 리펙토링 이후: 비슷한 레벨의 copyIfMatched 함수와 copyNodes만 추가 구현하면 된다. 테스트 커버리지를 상대적으로 적게 감소시키며 코드를 확장할 수 있게 된다.

... 작성중입니다 ...