[책 내용 정리] 읽기 좋은 코드가 좋은 코드다

June·2021년 7월 12일
0

책 요약 및 정리

목록 보기
2/6

1. 표면적 수준에서의 개선

특정한 단어 고르기

'이름에 정보를 담아내는' 방법 중 하나는 매우 구체적인 단어를 선택하여 '무의미한' 단어를 피하는 것이다.

def GetPage(url):
 ...

위에서 get은 지나치게 보편적이다. 이 메소드는 로컬 캐시, 데이터베이스, 아니면 인터넷 중 어디에서 페이지를 가져오는 것인가? 만약 인터넷에서 가져오는 것이라면 FetchPage() 혹은 DownloadPage()가 더 의미있다.

tmp나 retval 같은 보편적인 이름 피하기

var euclidean_norm = function (v) {
  var retval = 0.0;
  for (var i = 0;  i < v.length; i += 1)
    retval += v[i] * v[i];
  return Math.sqrt(retval);
};

위의 예에서 변수는 v를 제곱한 값을 모두 더한 값을 담고 있따. 따라서 이 상황에서 더 좋은 이름은 sum_squares다. 이런 이름은 변수의 목적을 직접적으로 나타낸다.

retval이라는 이름은 정보를 제대로 담고 있지 않다. 대신 변수값을 설명하는 이름을 사용하라.

tmp라는 이름은 대상이 짧게 임시적으로만 존재하고, 임시적 존재 자체가 변수의 가장 중요한 용도일 때에 한해서 사용해야 한다.

루프 반복자

for (int i = 0; i < clubs.size(); i++)
    for (int j = 0; j < clubs[i].members.size(); j++)
        for (int k = 0; k < users.size(); k++)
            if (clubs[i].members[k] == users[j])
                cout << "user[" << j << "] is in club[" << i << "]" << endl;

(i, j,k) 같은 인덱스 대신 (club_i, members_i, users_i) 혹은 간결하게 (ci, mi, ui) 같은 이름을 사용하는 것이 좋은 선택이다.

추상적인 이름보다 구체적인 이름을 선호하라

추가적인 정보를 이름에 추가하기

String id; // Example : "af84ef845cd8"

사용자가 ID의 내용을 기억해야 한다면, 변수명을 hex_id로 하는 편이 더 나을 것이다.

단위를 포함하는 값들
변수가 시간의 양이나 바이트의 수와 같은 측정치를 담고 있다면, 변수명에 단위를 포함시키는게 도움이 된다.

var start = (new Date()).getTime(); // 페이지의 맨 위
...
var elapsed = (new Date()).getTime() - start ; // 페이지의 맨 아래
document.writeln("Load Time was : " + elapsed + " seconds");

이 코드는 특별한 오류를 발생시키지는 않지만 getTime()이 초가 아니라 밀리초를 반환해서 잘못된 결과가 나온다.

var start_ms = (new Date()).getTime(); // 페이지의 맨 위
...
var elapsed_ms = (new Date()).getTime() - start_ms ; // 페이지의 맨 아래
document.writeln("Load Time was : " + elapsed_ms + " seconds");

다른 중요한 속성 포함하기

모든 변수에 추가적인 정보를 담을 필요는 없다. 누군가 변수를 잘못 이해했을 때 문제를 발생시킬 때만 중요한 의미가 있다.

이름은 얼마나 길어야 하는가?

좁은 범위에서는 짧은 이름이 괜찮다

if (debug) {
    map<string, int> m;
    LookUpNameNumbers(&m);
    Print(m);
}

여기서 m은 아무런 정보를 담고 있지 않지만, 필요한 모든 정보를 코드에서 쉽게 확인할 수 있으므로 문제될게 없다.

약어와 축약형
BackEndManager 대신 BEManager라는 이름을 사용하는 사람이 있다. 하지만 팀에 새로 합류하는 사람이 생기면 문제가 될 수 있다.

불필요한 단어 제거하기
경우에 따라서는 아무런 정보를 손실하지 않으면서 이름에 포함된 단어를 제거할 수도 있다. 예를 들어 ConvertToString() 대신 ToString()을 써도 실질적인 정보는 사라지지 않는다.

이름 포맷팅으로 의미를 전달하라

c++에서 클래스 멤버 변수의 끝을 '_'를 붙이면 다른 일반 변수와 구분할 수 있다.

3. 오해할 수 없는 이름들

예: Filter()

results = Database.all_objects.filter("year <= 2011")

results는 어떤 데이터를 담고 있는가?

  • year <= 2011인 객체들인가?
  • year <= 2011이 아닌 객체들인가?

filter의 의미가 모호하다. 대상을 '고르는' 것인지 아니면 '제거하는' 것인지 불분명하다. 대상을 '고르는' 기능을 원한다면 select()가, 아니면 exclue()가 더 낫다.

예: Clip(text, length)

# 텍스트의 끝을 오려낸 다음 '...'을 붙인다
def Clip(text, length):

여기에서 Clip()은 두 가지로 해석될 수 있다.

  • 문단의 끝에서부터 거꾸로 length만큼 제거한다
  • 문단을 처음부터 최대 length만큼 잘라낸다

함수명을 Truncate()로 하고, 파라미터 이름도 max_length로 하면 더 뚜렷해진다.

사실 max_length도

  • 바이트의 수
  • 문자의 수
  • 단어의 수

등 다양하므로 amx_chars가 더 명확하다

경계를 포함하는 한계값을 다룰 때는 in과 max를 사용하라

CART_TOO_BIG_LIMIT은 그 수까지인지 해당 수를 포함하면서 그 수까지인지 모호하다.

MAX_ITEMS_IN_CART = 10
if shopping_cart.run_items() > MAX_ITEMS_IN_CART:
    Error("Too many items in cart.")

경계를 포함하는 범위에는 first와 last를 사용하라

print inter_range(start=2, stop=4)
# 이 코드는 [2,3][2,3,4] 중에서 뭘 출력하는가?

경계의 양 끝 점이 포함된다는 의미에서 경계를 포함하는 범위에는 first/last가 좋은 선택이다

set.PrintKeys(first="Bart", last="Maggie")

경계를 포함하고/배제하는 범위에는 begin과 end를 사용하라

포함/배제가 동시에 일어나면 begin/end를 사용하는 것이 전형적인 프로그래밍 관행이다.

불리언 변수에 이름 붙이기

다음은 위험한 예이다.

bool read_password = true;
  • 우리는 패스워드를 읽을 필요가 있다.
  • 패스워드가 이미 읽혔다.

'need_password' 또는 'user_is_authenticated'가 더 명확하다.
일반적으로 is, has, can, should 같은 단어를 더하면 더 명확해진다.

예를 들어 SpaceLeft()와 같은 함수는 숫자값을 반환할 것처럼 보인다. 만약 불리언 값을 반환한다면 HasSpaceLeft()가 더 적합할 것이다.

끝으로 이름에서는 의미를 부정하는 용어는 피하는 것이 좋다.

bool disable_ssl = false;

위 대신

bool use_ssl = true;

이것이 더 간결하다.

사용자의 기대에 부응하기

사용자가 어떤 이름의 의미를 특정한 방식으로 이해해서 실제로 다른 의미가 있음에도 오해를 초래할 때가 있다. 이런 경우에는 '굴복하고' 그것이 일반적인 의미를 갖도록 하는게 좋다.

예: get*()
프로그래머들은 대게 get으로 시작되는 이름의 메소드는 '가벼운 접근자'로서 단순히 내부 멤버를 반환한다고 관행적으로 생각한다.

public class StatsticsController {
    public double getMean() {
        // 모든 샘플을 반복한 다음 total / num_samples를 반환
    }
    ...
}

이 경우 getMean()은 모든 데이터를 순차적으로 탐색하는데, 이 계산은 시간이 오래걸릴 것이다. 이 사실을 모르는 프로그램는 간단한 것으로 간주하고 getMean()을 호출할 수도 있다.
따라서 computeMean()으로 고쳐서 시간이 제법 걸리는 연산이라는 것을 나타내는 것이 좋다.

3. 미학

일관성과 간결성을 위해서 줄 바꿈을 재정렬하기

위의 코드는 비슷한 코드는 비슷하게 보여야한다를 위배한다.

위의 코드는 일관성을 가지지만, 수직 방향으로 너무 많은 빈칸을 사용하였다.

주석을 맨위로 올리고 모든 파라미터를 한줄에 놓으니 훨씬 깔끔하고 통일되어있다.

메소드를 활용하여 불규칙성을 정리하라

미학적으로 코드를 개선했지만 의도하지 않았던 장점도 있다.

  • 중복된 코드를 없애서 코드를 더 간결하게 한다.
  • 이름이나 에러 문자열 같은 테스트의 중요 부분들이 한 눈에 보이게 모아졌다. 수정 전에는 database_connection이나 error과 같은 토큰들이 섞인 채 흩어져 있었기 때문에 코드를 한 눈에 파악하기 어려웠다.
  • 새로운 테스트 추가가 훨씬 쉬워졌다.

도움이 된다면 코드의 열을 맞춰라

의미 있는 순서를 선택하고 일관성 있게 사용하라

  • 변수의 순서를 HTML 폼에 있는 <input> 필드의 순서대로 나열하라
  • 가장 중요한 것에서 가장 덜 중요한 순
  • 알파벳 순

선언문을 블록으로 구성하라

코드를 문단으로 쪼개라

5. 주석에 담아야 하는 대상

코드에서 빠르게 유추할 수 있는 내용은 주석으로 달지 마라

설명 자체를 위한 설명을 달지 말라

// 주어진 이름과 깊이를 이용해서 서브트리[h1]에 있는 노드를 찾는다
Node* FindNodeInSubtree(Node* subtree, string name, int depth);

이것은 무가치한 주석이다.

// 주어진 'name'으로 노드를 찾거나 null을 반환
// 만약 depth <= 0 dlaus 'subtree'만 검색
// 만약 depth == N이면 N 레벨과 그 아래만 검색
Node* FindNodeInSubtree(Node* subtree, string name, int depth);

이것이 훨씬 낫다.

나쁜 이름에 주석을 달지 마라 - 대신 이름을 고쳐라

// 반환되는 항목의 수나 전체 바이트 수와 같이
// Request가 정하는 대로 Reply에 일정한 한계를 적용한다
void CleanReply(Request request, Reply reply);

이 주석인 clean의 의미를 설명하려한다. 이렇게 하는 대신 한계를 적용한다는 부분을 애초에 함수명에 포함하자.

// reply가 count/byte 등과 같이 request 가 정하는 한계조건을 만족시키도록 한다
void EnforceLimitsFromRequest(Request request, Reply reply);

생각을 기록하라

감독의 설명을 포함하라

// 놀랍게도, 이 클래스에서 이진트리는 해시테이블보다 40%정도 빠르다
// 해시를 계산하는 비용이 좌/우 비교를 능가한다

이 주석은 코드를 읽는 사람에게 코드를 최적화하느라 시간을 허비하지 않게 도와준다

코드에 있는 결함을 설명하라

//TODO: 더 빠른 알고리즘을 사용하라

혹은

// TODO(더스틴): JPEG 말고 다른 이미지 포맷도 처리할 수 있어야 한다
  • TODO: 아직하지 않은 일
  • FIXME : 오동작을 일으킨다고 알려진 코드
  • HACK: 아름답지 않은 해결책
  • XXX: 위험! 여기에 큰 문제가 있다
  • TextMate: ESC

상수에 대한 설명

NUM_THREATS # 이 상수값이 2 * num_processors보다 크거나 같으면 된다

혹은 상수의 특정한 값이 아무런 의미를 갖지 않을때도 알려주면 유용하다

// 합리적인 한계를 설정하라 - 그렇게 많이 읽을 수 있는 사람은 어차피 없다
const int MAX_RSS_SUCSRIPTIONS = 1000;
image_quality = 0.72 // 사용자들은 0.72가 크기/해상도 대비 최선이라고 생각한다

명확하고 간결한 주석 달기

주석을 간결하게 하라

모호한 대명사는 피해라

엉터리 문장을 다듬어라

함수의 동작을 명확하게 설명하라

코너케이스를 설명해주는 입/출력 예를 사용하라

코드의 의도를 명시하라

이름을 가진 함수 파라미터 주석

7. 읽기 쉽게 흐름제어 만들기

조건문에서 인수의 순서

if (length >= 10)

if (10 <= length)

위의 조건식이 아래의 조건식보다 더 읽기 쉽다.

while (bytes_received < bytes_expected)

while (bytes_expected > bytes_received)

이 경우 위의 것이 더 낫다.

  • 왼쪽: 값이 더 유동적인 '질문을 받는' 표현
  • 오른쪽: 더 고정적인 값으로, 비교대상으로 사용되는 표현

if/else 블록의 순서

if (a == b) {
    // 첫 번째 경우
} else {
    // 두 번째 경우
}

다른 방법은

if (a != b) {
    // 두 번째 경우
} else {
    // 첫 번째 경우
}
  • 부정이 아닌 긍정을 다루어라. 즉 if(!debug)가 아니라 if(debug)를 선호하자

  • 간단한 것을 먼저 처리하라. 이렇게 하면 같은 화면에 if와 else 구문을 나타낼 수도 있다. 두 개의 주문을 동시에 보는게 더 좋다

  • 더 흥미롭고, 확실한 것을 먼저 다루어라

(삼항 연산자로 알려진)?:를 이용하는 조건문 표현

기본적으로 if/else를 이용하라. ?:를 이용하는 삼항 연산은 매우 간단할 때만 사용해야 한다.

do/while 루프를 피하라

do/while은 콛드를 두 번 읽기 때문에 부자연스럽다.

do/while은 while루프로 작성될 수 있다. while 안에 조건문을 넣으면 된다.

함수 중간에서 반환하기

함수 중간에서 반환하는 것은 완전히 허용되어야 한다.

중첩을 최소화하기

함수 중간에서 반환하여 중첩을 제거하라
특정한 조건을 만나면 함수를 반환하기 위해서 삽입된 중첩은 '실패한 경우들'을 최대한 빠르게 처리하고 함수에서 반환하여 제거할 수 있다.

루프 내부에 있는 중첩 제거하기

8. 거대한 표현을 잘게 쪼개기

설명 변수

커다란 표현을 쪼개는 가장 쉬운 방법은 작은 하위표현을 담을 추가 변수를 만드는 것이다.

if line.split(":")[0].strip() == "root":

위의 코드보다 아래가 낫다

username = line.split(":")[0].strip()
if username == "root":

요약 변수

if (request.user.id == documnet.owner_id) {
    // 사용자가 이 문서를 수정할 수 있다
}
...
if (request.user.id != document.owner_id) {
    // 문서는 읽기전용이다
}

처음 if문에는 변수가 5개나 들어가있다.

final boolean user_owns_document = (request.user.id == document.owner_id)

if (user_owns_document) {
    // 사용자가 이 문서를 수정할 수 있다
}
...
if (user_owns_document) {
    // 문서는 읽기전용이다
}

드모르간의 법칙 사용하기

드모르간의 법칙은 쉽게 말해 "not을 분배하고 and/or를 바꿔라"이다.

if (!(file_exists && !is_protected)) Error("파일을 읽을 수 없습니다");

위의 코드보다 아래의 코드가 낫다

if (!file_exists || is_protected) Error("파일을 읽을 수 없습니다);

쇼트 서킷 논리 오용 말기

Short-circuit logic은 if (a ||b)에서 a가 참이면 b를 평가하지 않는 것이다.

assert((!(bucket == FindBucket(key))) || !bucket->IsOccupied());

위의 코드보다 아래가 훨씬 낫다

bucket = FindBucket(key)
if (bucket != NULL) assert (!bucket->IsOccupied());

그렇다고 쇼트 서킷을 사용하지 말라는 것은 아니다.

if (object && object->method()) ..

위의 경우는 깔끔하게 잘 사용한 경우다.

복잡한 논리와 씨름하기

위의 코드보다 아래의 코드가 더 낫다

거대한 구문 나누기

반복되는 것들을 변수로 빼니 훨씬 쉽게 알아볼 수 있다. 그리고 오타도 방지한다.

9. 변수와 가독성

변수 제거하기

불필요한 임수 변수들

now = datetime.datetime.now()
root_message.last_view_time = now

now 변수가 꼭 필요한가? 그렇지 않다.

  • 복잡한 표현을 잘게 나누지 않는다
  • 명확성에 도움되지 않는다
  • 한번만 사용되어 중복된 코드를 압축하지 않는다.
root_message.last_view_time = datetime.datetime.now()

중간 결과 삭제하기

흐름 제어 변수 제거하기

boolean done = fals;e

while (/* 조건 */ && done) {
    ...
    if (...) {
        done = true;
        continue;
    }
}

done과 같은 변수를 우리는 '흐름 제어 변수'라고 부른다. 이러한 흐름 제어 변수는 프로그램의 구조를 잘 설계하면 제거할 수 있다.

while (/* 조건 */) {
    ...
    if (...) {
        break;
    }
}

변수의 범위를 좁혀라

전역 변수를 피하라는 조언은 들어봤을 것이다. 전역 변수는 어디에서 어떻게 사용되는지; 확인이 어렵고, 전역 변수의 이름과 지역 변수의 이름이 중복되어 namespace가 더러워질 수 있다.

변수가 적용되는 범위를 최대한 좁게 만들어라

class LargeClass {
    string str_;
    
    void Method1() {
        str_ = ...;
        Method2();
    }
    
    void Method2() {
        //Uses str_
    }
    // str_을 이용하지 않는 다른 메소드들...
};

클래스 멤버 변수는 어떤 의미에서 해당 클래스 내에 존재하는 미니 전역 변수다. 특히 커다란 클래스는 모든 멤버 변수를 일일이 기억하거나 어느 메소드가 값을 변경하는지 알기 어렵다. 이 경우에는 str_을 지역 변수로 '강등'시키는 편이 좋다

class LargeClass {
    void Method1() {
        String str = ...;
        Method2(str);
    }
    void Method2(String str) {
        // str를 이용
    }
    // 이제 다른 메소드느 str을 못 본다.
}

많은 메소드를 정적 static으로 만들어서 클래스 멤버 변수 접근을 제한하라.

또는 커다란 클래스를 여러 작은 클래스로 나누는 방법도 있다.

자바스크립트 전역 범위
자바스크립트에서는 키워드 var를 생략하면 전역변수가 된다. 그래서 항ㅅ아 var 키워드를 붙이자.

정의를 아래로 옮기기

위 보다는 아래 방식이 좋은데, 그 이유는 많은 변수를 가지고 있는 긴 함수일 때 코드를 읽는 사람에게 지금 당장 사용되지 않는 변수를 염두에 두게 강제하는 것은 좋지 않기 때문이다.

10. 상관없는 하위문제 추출하기

일반적인 목적을 가진 코드를 많이 만들어라

ReadFileToString()과 format_pretty()는 상관없는 하위문제를 다루는 대표적인 함수다. 이들은 매우 기본적이고 폭넓게 적용할 수 있는 일을 수행하므로 다른 프로젝트에서도 적용할 수 있다. 코드베이스는 종종 이와 같은 코드를 담아두는 (예를 들어 util/ 같은) 디렉터리를 따로 두고 있으므로 코드를 쉽게 공유할 수 있다.

일반적인 목적의 코드를 프로젝트의 특정 코드에서 분리하라

11. 한 번에 하나씩

12. 생각을 코드로 만들기

논리를 명확하게 설명하기

$is_admin = is_admin_request();
if ($document) {
  if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
    return not_authorized();
  }
} else {
  if (!$is_admin) {
    return not_authorized();
  }
}

위 코드는 상당한 논리가 있다. 논리를 쉬운 말로 묘사해보자.

사용이 허가되는 방법은 두 경우다.

13. 코드 분량 줄이기

코드베이스를 작게 유지하기

  • 일반적인 '유틸리티'를 많이 생성하여 중복된 코드를 제거하라.
  • 사용하지 않는 코드 혹은 필요 없는 기능을 제거하라
  • 프로젝트가 서로 분절된 하위프로젝트로 구성되게 하라
  • 코드베이스의 '무게'를 항상 의식하여 가볍고 날렵하게 유지시켜라
  • 제품에 꼭 필요하지 않는 기능을 제거하고, 과도한 작업을 피한다
  • 요구사항을 다시 생각해서, 가장 단순한 형태의 문제를 찾아본다
  • 주기적으로 라이브러리 전체 API를 훑어봄으로써 표준 라이브러리에 친숙해진다.

14. 테스트와 가독성

다른 프로그래머가 수정하거나 새로운 테스트를 더하는 걸 쉽게 느낄 수 있게 테스트 코드는 읽기 쉬어야 한다.

이 테스트는 어떤 점이 잘못되었을까?

이 함수는 'docs'를 내림차순으로 정렬하고 점수가 0보다 작은 문서를 제거한다.

이 테스트를 더 읽게 쉽게 만들기

덜 중요한 세부 사항은 사용자가 볼 필요 없게 숨겨서 더 중요한 내용이 눈에 잘 띄게 해야 한다.

코드의 있는 대부분 내용은 url, score, docs[]을 사용하는데 이는 상위 수준에서 보면 테스트가 실제로 수행하는 일과 상관없으며, 다만 C++ 객체를 초기화하는 것이다.

그래서 아래와 헬퍼 함수를 만들 수 있다.

하지만 url 파라미터가 여전히 거슬린다.
헬퍼 함수가 더 많은 일을 하게 하자.

읽기 편한 메시지 만들기

향상된 버전의 assert()를 사용하기
대부분의 언어와 라이브러리는 더 정교한 버전이 assert()를 제공한다. 따라서 단순히 assert보다는 AssertEqual()가은 것들이 훨씬 낫다.

좋은 테스트 입력값의 선택

가능하면 가장 간단한 입력으로 코드를 완전히 검사할 수 있어야 한다

CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

여기서 -99998.7은 그냥 "시끄러운 값"이다. 이는 단지 '임의의 음수'를 의미하니 그냥 -1로 적어도 상관없다. 만약 매우 큰 음수를 의미한다면 -1e100처럼 표현하는게 의미가 더 뚜렷하다.

필요한 작업을 수행하는 범위에서 가장 명확하고 간단한 테스트 값을 선택하라

테스트 함수에 이름 붙이기

테스트 함수를 위해 좋은 이름을 붙이는 것이 별로 안중요하게 보일 수 있지만, Test1(), Test2()와 같이 전혀 의미가 없는 이름은 안되ㅓㄴ다.

테스트를 상세하게 묘ㅕ사할 수 있는 이름을 사용하는게 좋다.

  • 테스트되는 클래스
  • 테스트되는 함수
  • 테스트 되는 상황이나 버그
void Test_SortedAndFillterDocs_BasicSortings() {
  ...
}

테스트코드는 실제 코드베이스에서 호출되는 함수가 아니니, "함수명을 너무 길게 하면 안된다" 라는 규칙을 따르지 않아도 된다.

테스트에 친숙한 개발

지금 작성하는 코드를 나중에 테스트하기 쉽도록 개발하는 것이 좋은 코드를 작성하는데 도움이 된다.

코드를 가장 철저하게 분리시키는 방법이 테스트하기 쉽다.

초기화되어야 하는 전역 변수, 라이브러리, 읽혀야하는 구성 파일 등과 같은 '외부' 컴포넌트를 많이 포함하는 것도 테스트 코드 작성을 힘들게 한다.

15. '분/시간 카운터'를 설계하고 구현하기

0개의 댓글