기술서 읽고 정리

The Art of Readable Code를 읽고

Jonchann 2023. 4. 13. 17:18

2020년 9월에 회사 내부 블로그(일본어)로만 적었던 글인데 오랫동안 게으름 피우며 방치했다가 드디어 이 블로그에도 기록을 남겨두려고 번역했다.

읽은 계기

상사와 1on1을 하면서 프로그래밍에 대한 기초지식을 다지려면 The Art of Readable Code같은 책을 읽어야 한다는 조언을 받았으므로 읽기 시작했다. 특히 이 책이 도움이 되는 건 엔지니어로 입사한 직후(1년 이내)라고 하니 읽어야겠다 싶었다.

내용 정리

크게 나누자면 '이해하기 쉬우면서 가독성도 높은 코딩법'와 '테스트 코드의 중요성'이라고 할 수 있다.

남이 읽어도 이해할 수 있는 코드를 구현하자

1. 이해하기 쉬운 코드란 어떤 것일까

이 책에서는 좋은 코드를 단시간에 읽을 수 있는 짧은 코드가 아니라 이해하는데 시간이 오래 걸리지 않는 코드라 정의하고 있다.

예를 들어, 6개월 후의 내가 봐도 금방 이해할 수 있는 코드인지 자문자답하면서 코드를 구현하는 것이 중요하다.

사실 이 사실은 이 책을 읽기 전 전 상사와 선배에게 들은 내용이고 특히 전 상사는 코드 리뷰에서 아래와 같은 코멘트를 적었었는데(예시는 내가 덧붙인 것) 이제서 읽어보니 이 책에서 말하는 것과 그것이 같은 내용이었다.

  • 인간이 코드를 읽는 순서(위에서 아래)와 프로세스가 실행되는 순서를 일치시키는 것이 좋다

    # 예를 들어, 어떠한 클래스를 정의한다고 했을 때 가장 메인(public) 메서드를 가장 위에 적고 그 안에 순차적으로 처리를 알기 쉽게 적은 후
    # 불리는 보조(private) 메서드를 아래에 적으면 책을 읽듯이 위에서부터 아래로 자연스럽게 읽을 수 있다.
    class SomeDoer:
      def do(self) -> None:
          self._prepare_data()
          self._preprocess_data()
          self._do_main_process()
          self._save_data()
    
      def _prepare_data(self) -> None:
          # prepare some data
    
      ...
  • 굳이 변수로 정의하지 않아도 될 것 같지만 정의하는 것이 알기 쉽다면 굳이 이름을 붙여서 줄 수를 늘리는 것이 좋다(설명변수)

# 예를 들어, 아래와 같이 원라인으로 다 적어버릴 수도 있지만 value가 어떤 형태인지 감이 안온다
def parse_json_data(data: str) -> list[str]:
    return [v for _, value in json.loads(data) for v in value.values()]

# 코드가 길어지긴 하지만 아래와 같이 제대로 변수명을 적어주면 이해하기 쉽다
def parse_dynamodb_value(data: str) -> list[str]:
    data_dict = json.loads(data)
    parsed_values = [
        value
        for column, value_with_type_dict in data_dict
        for value in value_with_type_dict.values()
    ]
    return parsed_values

2. 명명

예전에 읽으려 했던 SICP 1장에서 여러번 강조되던 것이 있다. 이 책의 내용과 다른 점은, SICP는 되도록 범용적이며 추상적으로 변수를 명명하는 것이 좋다고 하였고 이 책은 되도록 범용적인 이름은 피하라고 한다는 점이다.

이러한 차이가 생긴 것에는 아래와 같은 이유가 있을 것이다:

  • 변수 타입을 전달하기 위함
    • 값을 출력하지 않아도 코드를 읽는 것 만으로 읽을 수 있어야 한다
    • e.g. date_list, date_set
  • 단위를 전달하기 위함
    • 예를 들어, 단순한 size여서는 무엇의 어떤 사이즈인지 알 수 없다
    • e.g. delay_secs, size_mb, max_kbps
  • 되도록 명확한 편이 그 역할을 알기 쉽다
    • e.g. 메서드 이름이 get_mean()라면 이제부터 계산도 해서 값을 반환한다는건지 이미 계산한 값을 단순하게 반환만 하는 것인지 알 수 없기 때문에 compute_mean() 등의 이름을 사용하는 것이 좋다

또한 지금의 상사는 이에 대해 SICP는 교과서적인 책이기도 해서 최대한 범용적이고 추상적으로 명명하라는 것이지만 이 책은 현장에서 활용할 수 있는 정보를 엮었기 때문이라고 알려주었다.

실제로 위에서 든 예시와 같이 데이터 타입을 변수명에 붙이는 것이 좋겠다고 코드 리뷰에서도 자주 코멘트를 받았었다.

그렇다고 너무 구체적이어서는 공통화할 수 없는 코드(재사용할 수 없는 코드)가 되기 때문에 적당히 두가지 측면을 고려해서 명명하는 것이 좋겠다.

3. 아름다운 코드

아름다우면서 가독성도 좋은 코드를 구현하기 위해 아래 내용에 주의할 필요가 있다:

  • 비슷한 코드는 비슷한 것을 알게끔 구현할 것
  • 관련있는 코드는 덩어리지게 구현할 것
    • 때때로 메서드와 메서드 사이만 개행한 코드를 보는데 하나의 메서드에 하나의 처리만 있게끔 하지 않았을 때 참 알기 어렵다

Python유저로써 이 책에서 말하는 아름다운 코드를 구현하는 방법 중 아래 내용에는 동의할 수 없었다(언어에 따라 적절히 사용하면 될 것):

details    = response.get('details')
location  = response.get('location')
phone    = response.get('phone') 

4. 코멘트

코멘트를 적는 것이 좋은 때는 아래와 같다:

  • 감독 코멘터리: 여러 비슷한 기능을 하는 처리 중 특정 처리를 굳이 고른 이유에 대해 적는 등
  • 코드릐 결함을 설명
    • TODO:나중에 대응할 사항
    • FIXME:이미 버그의 존재를 인식하고 있음을 알림
    • HACK:권하지 않는 방법임을 알림
    • XXX:위험!
  • 하이퍼파라메터의 수치에 대해
    • e.g. # 사람이 이렇게 읽을리 없기 때문(1000)
  • 함정에 빠질 것 같은 곳을 알림
    • e.g. # 메일 송신시 외부 서비스를 참조(1분 안에 타임아웃 해버림)
    • e.g. # 실행 시간이 O(태그 수 * 태그 깊이의 평균)이므로 인덴트를 깊게 들어가지 않도록 주의
  • 복잡한 로직을 요약할 때
  • 개발자의 의도를 공유할 때

그렇다면 좋은 코멘트는 어떻게 써야 할까:

  • 애매한 대명사를 사용하지 않기
  • 간결하게 적기
  • 동작에 대해 정확하게 표현하기
  • 실제 예시를 들면서 적기
  • 정보밀도가 높은 표현을 사용하기
    • Avenue를Ave.로 표기하는 등

5. 단순한 로직을 짜기

로직을 단순화 시키기 위해서는 반복문을 알기 쉽고 간결하게 할 필요가 있다. 이 점에 대해서도 코드 리뷰를 받을 때 코멘트를 받은 적이 있다:

  • 범위를 최소한으로 하기
  • 긍정 조건부터 적기(케바케)
  • 처리가 짧은 쪽에 조건문을 적어서 인덴트를 얕게 하기 ← 여기까지가 코멘트 내용과 동일
  • 비교대상은 왼쪽에 적기

다음으로 하나의 메서드에는 한가지 기능만을 위하는 것이 좋다:

  • 여러 기능을 하나의 메서드에서 처리하게 되었다면 분할하자
    • 다만, 메서드 하나에 처리 한 줄씩 분할하는 등 그 정도가 심하면 가독성이 나빠지므로 조심하자
  • 메서드 간 혹은 클래스 간의 의존관계를 최대한 없애자
  • 범용 코드를 구현해서 재활용하자
    • SICP에서도 강조하던 내용
  • built-in 라이브러리를 잘 알아보고 적극적으로 사용하자(케바케)
  • 로직을 구현할 때 먼저 언어화 해놓자
    • 가능한 한 스크립트를 분할해서 작은 단위로 리뷰해달라고 하자
    • ↑최근 실감하는 것인데 태스크도 그렇지만 PR를 최소 단위(필요에 의해 역할 혹은 기능별로도)로 분할해서 리뷰해달라고 하는 편이 서로가 고려하지 못한 점이 적어 에러 발생율도 적어지니 좋다
  • 본인이 구현한 코드와 완성이 되기까지 들인 시간이 아깝게 느껴질 수 있으나 필요없는 코드는 그 순간 없애는 습관을 들이는 것이 좋다
    • 남기면 코드만 더러워질 뿐

좋은 코드를 구현하자

가독성이 좋은 코드를 구현하는 것도 중요하지만 기능 하나를 구현하고 바로 테스트 코드도 세트로 구현하는 것이 개선하기 쉽고 관리하기 쉬운 환경을 만드는 지름길이다.
좋지 못한 테스트 코드는 새로운 코드를 추가할 때마다 테스트코드를 고치는 데 공수를 더 들이게 되니 배보다 배꼽이 커진다. 그러다보면 자연스레 나쁜 코드를 그대로 방치하게 된다.

이 책에서 소개하는 좋은 테스트 코드는 아래 특징을 가진다:

  • 테스트 자체에 불필요한 정보는 확실하게 숨기자
  • 처리 내용을 바꾸는 최소한의 인수 외에는 넘기지 않도록 하자
    • 그렇지 않으면 테스트할 때 테스트와 관계없는 정보가 필요해지니 귀찮아진다
  • 단순히 assert로 비교할 땐 에러의 상세내용이 출력되지 않기 때문에 테스트를 위한 라이브러리 등을 적극적으로 사용하자
    • Python이라면 pytest, Kotlin이라면 kotest가 있다
  • 테스트코드에 넘기는 값은 최대한 알기 쉽고 간결하게 하자
    • e.g. 음수인지 확인하기 위해 굳이 -1999985.4를 넘길 필요는 없다
  • 테스트 케이스에 이름을 붙이자
    • e.g. TestSortAndFilterDocs()가 아니라、TestSortAndFilterDocsBasicSorting() 혹은 TestSortAndFilterDocsNegativeValues()가 좋다

즉, 딱 봐서 무엇을 테스트하며 그를 위해 어떤 것이 필요하고 어떻게 되어야 정상적인 상황인지를 알기쉬운 테스트 코드를 만들어야 한다는 것이다.

이 책에서 소개하는 다음에 읽으면 좋은 책

더 많은 책이 있었지만 게 중에 신경쓰이는 것만 골라봤다.(일본어 번역본을 읽은지라 일본어책도 섞여있다)

  • Refactoring: Improving the design of existing code
  • The Pragmatic Programmer
    • 이미 상사에게 추천받았다
  • Design Patterns: Elements of Reusable Object-Oriented Software
  • 珠玉のプログラミング 本質を見抜いたアルゴリズムとデータ構造
  • ハイパフォーマンスWebサイトー高速サイトを実現する14のルール