클린코드 1~3장

1. 깨끗한 코드

코드가 존재하리라

앞으로 코드를 자동으로 생성하는 시대가 다가올까? 저자는 절대 그럴 일이 없다고 말하고 있다. 코드는 요구사항을 상세히 표현하는 수단이다. 요구사항에 더욱 가까운 언어를 만들 수도 있고, 요구사항에서 정형 구조를 뽑아내는 도구도 만들 수 있다. 하지만 어느 순간에는 정밀한 표현이 필요하다. 그 필요성을 없앨 방법은 없기 때문에 코드도 반드시 존재할 것이다.

나쁜 코드로 치르는 대가

나쁜 코드는 개발 속도와 팀의 생산성을 크게 떨어트린다.

프로그래머도 마찬가지다. 나쁜 코드의 위험을 이해하지 못하는 관리자 말을 그대로 따르는 행동은 전문가답지 못하다.- p.7

깨끗한 코드란?

프로그래머마다 정의도 다양하겠지만, 일단 본문에서 언급된 깨끗한 코드의 특징들을 정리해보자.

  • 가독성이 뛰어난 코드
  • 한 가지 일을 하는 코드
  • 테스트 케이스가 존재하는 코드
  • 세세한 사항까지 꼼꼼하게 신경쓴 코드
  • 작게 추상화된 코드

보이스카우트 규칙

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라.

체크아웃할 때보다 좀 더 깨끗한 코드를 체크인한다면 코드는 절대 나빠지지 않는다.

2. 의미 있는 이름

의도를 분명히 밝혀라

변수나 함수, 클래스 이름은 다음과 같은 질문에 모두 답해야 한다.

  • 변수(혹은 함수나 클래스)의 존재 이유는?
  • 수행 기능은?
  • 사용 방법은?

따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 말이다.

그릇된 정보를 피하라

그릇된 단서는 코드 의미를 흐린다.

  • 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하면 안 된다.
    • hp 는 유닉스 플랫폼이나 유닉스 변종을 가리키는 이름이다. 직각삼각형의 빗변(hypotenuse)을 구현할 때는 hp 가 훌륭한 약어 같아 보여도 독자에게 그릇된 정보를 제공할 수 있다.
  • 실제 List가 아니라면 변수 이름을 List 로 명명하지 않는다.
    • 프로그래머에게 List는 특수한 의미이다. 값을 담는 컨테이너가 실제 List가 아니라면 프로그래머에게 그릇된 정보를 제공하는 것과 같다.
    • accountGroup, bunchOfAccounts, 아니면 단순히 Accounts라고 명명한다.
  • 서로 흡사한 이름을 사용하지 않도록 주의한다.
    • XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStorageOfStrings 라는 이름은 너무나 비슷하다.
  • 유사한 개념은 유사한 표기법을 사용한다.
    • 일관성이 떨어지는 표기법은 그릇된 정보다.

의미 있게 구분하라

컴파일러나 인터프리터만 통과하려는 생각으로 코드를 구현하는 프로그래머는 스스로 문제를 일으킨다.

  • 연속된 숫자를 덧붙이는 이름은 의도적인 이름과 정반대다.
    • ex. a1, a2, ..., aN
    • 이런 이름은 그릇된 정보를 제공하는 이름도 아니며, 아무런 정보를 제공하지 못하는 이름이다.
  • 불용어를 추가한 이름 역시 아무런 정보를 제공하지 못한다.
    • ProductProductInfo , ProductData 라는 클래스의 차이를 확연히 알 수 없다.
    • 불용어는 중복이다.
      • ex. Name , NameString

발음하기 쉬운 이름을 사용하라

  • 안 좋은 예시
    • genymdhms
    • modymdhms
    • pszqint
  • 좋은 예시
    • generationTimestamp
    • modificationTimestamp
    • recordId

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다.

이름 길이는 범위 크기에 비례해야 한다. 변수나 상수를 코드 여러 곳에서 사용한다면 검색하기 쉬운 이름이 바람직하다.

인코딩을 피하라

인코딩은 불필요한 정신적 부담이고, 발음하기도 어렵고 오타가 생기기도 쉽다.

  • 헝가리식 표기법

    컴퓨터 프로그래밍에서 변수 및 함수의 이름 인자 앞에 데이터 타입을 명시하는 코딩 규칙

    • 이전에는 컴파일러가 타입을 점검하지 않았으므로 프로그래머가 타입을 기억할 수 있는 단서가 필요했다.
    • 하지만 요즘에는 프로그래밍 언어가 훨씬 많은 타입을 지원하고, 컴파일러도 타입을 기억하고 강제한다.
    • 또한 클래스와 함수는 점차 작아지는 추세이다.
    • 이제는 헝가리식 표기법은 오히려 방해가 된다.
      • 변수, 함수, 클래스 이름이나 타입을 바꾸기 어렵고 읽기도 어려워지기 때문
  • 멤버 변수 접두어
    • 멤버 변수에 m_ 이라는 접두어를 붙일 필요가 없다.
    • 클래스와 함수는 접두어가 필요없을 정도로 작아야 하고, 멤버 변수를 눈에 띄는 색으로 표시해주는 IDE를 사용해야 한다.
  • 인터페이스 클래스와 구현 클래스
    • 때로는 인코딩이 필요한 경우도 있다.
    • 인터페이스 클래스 이름과 구현 클래스 이름 중 하나를 인코딩해야 한다면 구현 클래스 이름을 택하자.
      • 예: 인터페이스 클래스는 ShapeFactory, 구현 클래스는 ShapeFactoryImp (또는 CShapeFactory)

자신의 기억력을 자랑하지 마라

  • 독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.
  • 문자 하나만 사용하는 변수 이름은 문제가 있다.
    • 루프에서 반복 횟수를 세는 변수 i, j, k 는 괜찮다. 단, 루프 범위가 아주 작고 다른 이름과 충돌하지 않을 때만!
  • 똑똑한 프로그래머와 달리 전문가 프로그래머는 명료함이 최고라는 사실을 이해한다.
    • 전문가 프로그래머는 자신의 능력을 좋은 방향으로 사용해 남들이 이해하는 코드를 내놓는다.

클래스 이름

  • 클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
    • 👍 : Customer, WikiPage, Account, AddressParser
    • 👎 : Manager, Processor, Data, Info 등은 피하자. 동사 역시 사용하지 않는다.

메서드 이름

  • 동사나 동사구가 적합하다.
    • 👍 : postPayment, deletePage, save
  • 접근자, 변경자, 조건자는 값 앞에 get, set, is를 붙인다.
  • 생성자를 중복정의할 때는 정적 팩토리메서드를 사용한다.
  • 생성자 사용을 제한하려면 해당 생성자를 private으로 선언한다.

기발한 이름은 피하라

  • 특정 문화에서만 사용하는 농담은 피하는 편이 좋다. 의도를 분명하고 솔직하게 표현하라.

한 개념에 한 단어만 사용하라

  • 추상적인 개념 하나에 단어 하나를 선택한다.
    • 👎 : 클래스마다 같은 메서드를 fetch, retrieve, get으로 부르는 경우
    • 메서드 이름은 독자적이고 일관적이어야 한다.

말장난을 하지 마라

  • 한 단어를 두 가지 목적으로 사용하지 말 것.
    • 다른 개념에 같은 단어를 사용한다면 그것은 말장난에 불과하다.
  • 예: 지금까지 구현한 add 메서드는 모두 기존 값 두 개를 더하거나 이어서 새로운 값을 만든다. 여기서 집합에 값 하나를 추가하는 메서드를 추가하려고 한다면, 이 메서드를 add 라고 불러도 될까?
    • 이 메서드는 기존 add 메서드와 맥락이 다르다. 그러므로 insertappend 라는 이름이 적당하다.

프로그래머는 코드를 최대한 이해하기 쉽게 짜야 한다. 집중적인 탐구가 필요한 코드가 아니라 대충 훑어봐도 이해할 코드 작성이 목표다.

해법 영역에서 가져온 이름을 사용하라

  • 코드를 읽을 사람도 프로그래머라는 사실을 명심하라. 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용해도 괜찮다.
  • 모든 이름을 문제 영역(domain)에서 가져오는 정책은 현명하지 못하다.

문제 영역에서 가져온 이름을 사용하라

  • 적절한 ‘프로그래머 용어'가 없다면 문제 영역에서 이름을 가져온다.
    • 코드를 보수하는 프로그래머가 분야 전문가에게 의미를 물어서 파악할 수 있다.
  • 우수한 프로그래머와 설계자는 해법 영역과 문제 영역을 구분할 줄 알아야 한다. 문제 영역 개념과 관련이 깊은 코드라면 문제 영역에서 이름을 가져와야 한다.

의미 있는 맥락을 추가하라

  • 스스로 의미가 분명한 이름이 있긴 하나 대다수 이름은 그렇지 못하다.
    • 그래서 클래스, 함수, 이름 공간에 넣어서 맥락을 부여한다.
    • 모든 방법이 실패하면 접두어를 붙인다.
    • 예: firstName, lastName, street, ... , city, state, zipcode 라는 변수가 있다.
      • 변수를 훑어보면 주소라는 사실을 금방 알 수 있다. 하지만 어떤 메서드가 state라는 변수 하나만 사용한다면 state 가 주소 일부라는 사실을 금방 알아채기 어렵다.
      • addr 라는 접두어를 추가해 addrFirstName, addrLastName, addrState라고 쓰면 맥락이 좀 더 분명해진다.
      • Address 라는 클래스를 생성하면 더 좋다.
        • 컴파일러에게도 변수가 좀 더 큰 개념에 속한다는 사실이 분명해진다.

불필요한 맥락을 없애라

  • 고급 휘발유 충전소(Gas Station Deluxe)라는 애플리케이션을 짠다고 가정해보자. 모든 클래스 이름을 GCD 로 시작하겠다는 생각은 바람직하지 못하다.
  • 일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다.
    • 이름에 불필요한 맥락을 추가하지 않도록 주의한다.
    • accountAddress와 customerAddress는 Address 클래스 인스턴스로는 좋은 이름이지만 클래스 이름으로는 적합하지 못하다. Address는 클래스 이름으로 적합하다.

3. 함수

작게 만들어라

  • 함수를 만드는 첫째 규칙은 ‘작게'다.
  • if문/else문/while문 등에 들어가는 블록은 한 줄이어야 한다. 대개 거기서 함수를 호출한다.
    • 바깥을 감싸는 함수가 작아질 뿐 아니라 호출하는 함수 이름을 적절하게 지으면 코드를 이해하기도 쉽다.
  • 중첩 구조가 생길 만큼 함수가 커져서는 안 된다.
    • 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.

한 가지만 해라

함수는 한 가지만 해야 한다. 여기서 그 ‘한 가지'란 무엇인가?

  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행하는 것
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것과 같다.

함수 당 추상화 수준은 하나로

함수가 확실히 ‘한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

  • 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
    • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아지는 걸 내려가기 규칙이라 부른다.
      • 즉, 일련의 TO 문단을 읽듯이 프로그램이 읽혀야 한다.

Switch 문

  • switch 문은 작게 만들기 어렵다. 본질적으로 switch 문은 N가지를 처리한다.
  • switch 문을 완전히 피할 방법은 없지만 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.
    • 다형성 이용

서술적인 이름을 사용하라

  • 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
  • 길고 서술적인 이름이 짧고 어려운 이름, 길고 서술적인 주석보다 좋다.
  • 함수 이름을 정할 때는 여러 단어가 십게 읽히는 명명법을 사용한다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
  • 이름을 붙일 때는 일관성이 있어야 한다.
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
    • 👍 : includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage

함수 인수

  • 함수에서 이상적인 인수 개수는 0개이다. 3개는 가능한 피하는 편이 좋고, 4개 이상은 특별한 이유가 있어도 사용하면 안 된다.
  • 인수는 개념을 이해하기 어렵게 만든다.
  • 인수의 개수가 많아지면 여러 인수 조합을 구성해서 테스트를 하기가 복잡해진다.
  • 많이 쓰는 단항 형식
    • 함수에 인수 1개를 넘기는 경우
      • 인수에 질문을 던지는 경우
        • boolean fileExists("MyFile")
      • 인수를 뭔가로 변환해 결과로 반환하는 경우
        • InputStream fileOpen("MyFile")
    • 이벤트는 자주 사용하진 않지만 유용한 단항 함수 형식이다.
      • 이벤트 함수는 이벤트라는 사실이 코드에 명확히 드러나야 한다. 즉, 이름과 문맥을 주의해서 선택해야 한다.
  • 플래그 인수
    • 플래그 인수를 사용하는 것은 함수가 한꺼번에 여러 가지를 처리한다는 것과 같다.
  • 이항 함수
    • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
    • 이항 함수가 적절한 경우도 있다.
      • Point p = new Point(0, 0)
      • assertEquals(expected, actual) 은 당연하기 여겨지는 이항 함수이지만 인수의 순서를 인위적으로 기억해야 한다는 문제가 있다.
        • 이항 함수를 사용하면 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 한다.
  • 삼항 함수
    • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 어렵다.
  • 인수 객체
    • 인수가 2-3개가 필요하다면 일부를 독자적인 클래스 변수로 선언할 수 있는지 확인해보자.

      Circle makeCircle(double x, double y, double radius);
      Circle makeCircle(Point center, double radius);
  • 인수 목록
    • 인수 개수가 가변적인 함수가 필요할 때도 있다.
    • 가변 인수 전부를 동등하게 취급하면 List형 인수 하나로 취급할 수 있다.
      • 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있다.
      • 단, 이를 넘어서는 인수를 사용하면 문제가 있다.
  • 동사와 키워드
    • 함수의 의도나 인수의 순서, 의도를 제대로 표현하려면 좋은 함수 이름이 필요하다.
    • 단항 함수는 함수와 인수가 동사/명사 쌍을 이루어야 한다.
      • 👍 : writeField(name)
    • 함수 이름에 키워드를 추가할 수도 있다.
      • 함수 이름에 인수 이름을 넣는 방식
      • 👍 : assertExpectedEqualsActual(expected, actual)

부수 효과를 일으키지 마라

  • 부수 효과는 시간적인 결합을 초래하고, 이는 혼란을 일으킨다.
  • 시간적인 결합이 필요하다면 함수 이름에 분명히 명시한다.
  • 출력 인수
    • 객체 지향 언어에서는 출력 인수를 사용할 필요가 없다. 출력 인수로 사용하라고 설계한 변수가 this 기 때문이다.
    • 일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
  • 예: 이름이 attribute 인 속성을 찾아 값을 value 로 설정한 후 성공하면 true, 실패하면 false를 반환하는 함수
    • 👎 : set(String attribute, String value)
    • 👍 : attributeExists(String attribute) 라는 함수를 만들어 속성을 찾는 부분을 따로 분리

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 if 문에서 명령을 표현식으로 사용하기 쉽기 때문에 명령/조회 분리 규칙을 미묘하게 위반한다.
  • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
  • Try/Catch 블록 뽑아내기
    • try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다.
    • 따라서 별도 함수로 뽑아내는 편이 좋다.
  • 오류 처리도 한 가지 작업이다.
    • 함수는 ‘한 가지' 작업만 해야 하는데, 오류 처리도 ‘한 가지' 작업에 속한다.
    • 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
  • Error.java 의존성 자석
    • 오류 코드를 반환한다는 것은 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻이다.
    • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다.
      • 따라서 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라

  • 중복되는 부분이 있으면 코드 길이가 늘어날 뿐 아니라 수정해야 하는 곳도 늘어난다.
    • 어떤 부분을 빠트리면 오류가 발생할 확률도 높다.

구조적 프로그래밍

  • 어떤 프로그래머는 에츠허르 테이크스트라의 구조적 프로그래밍 원칙을 따른다.

    모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다.
    즉, 함수는 return 문이 하나여야 한다. 루프 안에서 break나 continue를 사용해선 안 되며, goto는 절대로 안 된다.

  • 그러나 함수가 작다면 위 규칙은 별 이익을 제공하지 않는다.
  • 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 위 규칙보다 의도를 표현하기 쉬워질 수 있다.

함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 글짓기와 비슷하다.
  • 처음에는 길고 복잡하다. 하지만 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다.
  • 그 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.
  • 처음부터 탁 짜내지 않는다. 이것이 가능한 사람은 없다!

좋은 웹페이지 즐겨찾기