읽기 좋은 코드가 좋은 코드다

쉬운 예제로 테크닉을 깨닫게 해주는 책

전체적인 느낌

‘읽기 좋은 코드가 좋은 코드다’라는 책은 출퇴근할 때 읽었던 책인데 정말 지하철 역 지나가는 줄 모르고 몰입해서 읽었던 기억이 나는 책이다. 문제를 깨닫는 순간부터 문제를 의식할 수 있듯이 문제 없어보이던 코드가 문제가 있는 코드로 보이는 것을 책을 읽는 내내 경험했기 때문인 것 같다. 어떻게 보면 이 책을 시작으로 좋은 코드에 관한 책을 탐독했던 계기가 된 것 같다. 이 뒤로 마틴 파울러 『리펙토링』, 로버트 마틴 『클린 코더』 등을 읽었다. 누군가가 클린 코드에 관하여 처음으로 시작하기 좋은 책을 물어본다면 이 책을 추천할 것 같다.


코드 예제의 경우 책의 예제에서 변경한 것도 있어 이해가 안되시면 전적으로 저의 잘못이고 책을 함께 봐주시기를 부탁드립니다.

혹 의견주실 것이 있으면 댓글 달아주시면 감사하겠습니다 :)

개인 이메일(communication@ptokos.com)


이 글은 blog 에 한 챕터씩 나누어 포스팅하였던 것을 취합한 것이다.

Ch 1 코드는 이해하기 쉬워야 한다

Ch 2 이름에 정보 담기

Ch 3 오해할 수 없는 이름들

Ch 4 미학

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

Ch 6 명확하고 간결한 주석 달기

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

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

Ch 9 변수와 가독성

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

Ch 11 한 번에 하나씩

Ch 12 생각을 코드로 만들기

Ch 13 코드 분량 줄이기

Ch 14 테스트와 가독성

Ch 15 ‘분/시간 카운터’를 설계하고 구현하기

Ch 1 코드는 이해하기 쉬워야 한다

코드는 다른 사람이 그것을 이해하는 데 들이는 시간을 최소화하는 방식으로 작성되어야 한다.

p.23

예전에는 멋있어 보이는 프레임워크, 프로젝트를 했다고하면 잘하는 개발자라고 생각했는데 현재는 잘하는 개발자를 떠올리면 변수명 기가 막히게 잘 짓는 개발자, 객체를 잘 사용해 코드량과 버그를 줄이는 사람을 떠올린다.

잘하는 개발자란 딱 저 인용구가 맞는 것 같다. 다른 사람이 이해하는데 최소한의 시간을 사용하게하는 사람 이 최고의 개발자인 것 같다.

변수명을 잘 짓고 다른 사람이 코드를 이해하기 쉽도록 코드를 작성하기 위해서는 부단히 노력해야하는 것이다. 발전하기 위해서는 간단한 것부터 의식하고 변수명, 중복 코드, 난잡한 코드 등을 고치는 것부터 시작해야한다. 당장 넘어갈 수는 있겠지만 방치하면 나의 코딩 실력이 그만큼으로 남기 때문이다. 넘어가고 몇 일이 지나면 고치기 싫어 넘어가기 마련이다. 그래서 눈에 보였을 때 빨리 수정해야 발전할 수 있다고 생각한다.

분량이 적으면 항상 더 좋은가?

분량이 적다고 해서 항상 더 좋은 것은 아니다!

p.24

분량 적은 코드

assert((!(user = Login(id, pw))) || !user->IsAdmin());

분량 늘어난 코드

user = Login(id, pw)
if (user != NULL) assert(!user->IsAdmin()); 

위 예제는 기존 책과 매우 유사하지만 살짝 변형하여 작성한 코드라 납득이 잘 안되시면 저의 잘못입니다..

Ch 2 이름에 정보 담기

특정한 단어 고르기

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

p.30

보편적 단어

function getImages() {}

getImages 는 이미지를 가져온다는 것을 알 수 있지만 디바이스 내의 이미지, 로컬 캐시, 데이터베이스, 인터넷 등 어디서 가져오는지 알 수 없다. 인터넷에서 가져오는 것이라면 fetchImages 가 더 의미있는 이름이 될 것이라고 저자는 설명한다.

class VideoPlayer {
    stop() {}
}

stop 메소드명은 그럭저럭 괜찮지만 정지된 상태에서 시작이 가능하다면 pause, resume 의 메소드명이 더 적절하다고 저자는 설명한다.

단위를 포함하는 값들

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

p.40

function timeout(delay) {}

저자는 timeout 함수 인자 delay 에 시간 단위가 없음을 지적하고 있다. ms 인지 s 인지 변수명을 봐서는 모른다는 것이다. 단위일 경우에는 아래와 같이 변수명을 작성하기를 권고하고있다.

function timeout(delay) {} // X 
function timeout(delaySecs) {} // O
function timeout(delayMs) {} // O

function fileSizeLimitCheck(size) // X
function fileSizeLimitCheck(sizeMb) // O
function fileSizeLimitCheck(sizeKb) // O

다른 중요한 속성 포함하기

변수의 의미를 제대로 이해하는 것이 중요하다면 그 의미를 드러내는 정보를 변수의 이름에 포함시켜야 한다.

p.41

// 평문인 비밀번호
const password = .. // X
const plainTextPassword = .. // O

// url encode 된 data
const data = .. // X
const dataUrlEnc = .. // O 

약어와 축약형

팀에 새로 합류한 사람이 이름이 의미하는 바를 이해할 수 있을까? 만약 그렇다면 그 이름은 괜찮은 것이다.

p.45

예를 들어 AdminBoardManageController 를 ABMController 로 사용한다면 같이 만들고 사용했던 팀원은 알겠지만 새로운 팀원은 해당 명명을 보고 멈칫하게 된다. 쓰다보면 적응이야 되겠지만 저자는 이렇게하면 안된다고 말하고 있다.

Ch 3 오해할 수 없는 이름들

Filter

대상을 ‘고르는’ 것인지 아니면 ‘제거하는’ 것인지 불분명하다. 대상을 ‘고르는’ 기능을 원한다면 select() 가, 대상을 ‘제거하는’ 기능을 원한다면 exclude() 가 더 낫다

p.50

경계 포함 여부에 따른 변수명

min, max

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

p.51

first, last

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

p.52

begin, end

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

p.53

경계가 필요한 경우 대체로 자연스럽게 min, max 혹은 start, end 로 정의하곤했는데 저자가 말하는대로 (min, max), (first, last), (begin, end) 를 상황에 맞게 써야겠다. 이러한 변수명이 코드 이해와 버그를 줄 일 수 있는 것 같다. 한편으로는 이러한 경계와 관련해서 불편함을 인식하지 못했다는게 아쉽다.

불리언 변수에 이름 붙이기

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

disable_ssl = false;

대신

use_ssl = true;

p.54

사용자의 기대에 부응하기

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

getMean() 은 과거 데이터를 순차적으로 짚어가면서 동적으로 중앙값을 계산한다.

이러한 메소드명을 computeMean() 처럼 고쳐서 시간이 제법 걸리는 연산이라는 사실을 명확하게 드러내는 것이 좋다.

가져온다는 개념이면 거의 대부분 get 으로 시작하게 명명하였는데 읽고 탄식하기도 있지만 한편으로는 이제 알게되었으니 기분 좋기도 했다. 그냥 읽고 끄덕끄덕 것과 다르게 깨닫는 느낌이면 은은하게 기분이 좋은 것 같다. 몸소 경험했던 것이 떠오르기 때문일 것이다.

Ch 4 미학

좋은 소스코드는 ‘눈을 편하게’ 해야 한다.

특히 다음과 같은 세 가지 원리가 이용된다.

  • 코드를 읽는 사람이 이미 친숙한, 일관성 있는 레이아웃을 사용하라.
  • 비슷한 코드는 서로 비슷해 보이게 만들어라.
  • 서로 연관된 코드는 하나의 블록으로 묶어라.

p.62

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

Server 클래스에서 댓글과 관련된 기능을 사용할 수 있도록 되어있다. 개선전 코드는 다른 사람이 읽었을 때 메소드 전체를 파악하기가 어렵다.

개선전

Class Sever {
    createComment()
    openDatabase()
    updateComment()
    closeDatabase()
    replyComment()
    viewComment()
    commentNotFound()
    commentOK()
}

개선후

Class Sever {
    // 핸들러
    viewComment()
    createComment()
    updateComment()
    replyComment()
    
    // 질의 / 응답 유틸리티
    commentOK()
    commentNotFound()
    
    // 데이터베이스 헬퍼
    openDatabase()
    closeDatabase()
}

이것이 처음 인용문에서 언급된 것처럼 서로 연관된 코드는 하나의 블록으로 묶어라 이다. 연관된 코드를 묶었을 때 코드를 보고 한 눈에 파악할 수 있기 때문이다. 한 눈에 파악할 수 있다는 것은 버그를 줄일 수 있다는 뜻인거 같기도 하다.

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

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

p.78

필자는 주석을 거의 사용하지 않는데 대부분 이러한 이유였다. 그렇지만 외부인의 입장에서는 큰 틀에서 빠르게 유추할 수 없을 것이기 때문에 주석에 관하여 생각을 다시하게 한다.

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

p.79

저자도 말하듯 좋은 코드에는 주석이 필요없다. 변수명 함수명을 잘 지으면 어떠한 역할과 기능을 하고 있는지 설명할 필요가 없기 때문이다. 변수명을 잘 짓기는 항상 어렵다..

주석으로 코드가 왜 훌륭하지 않은지도 설명할 수 있다.

// 이 클래스는 점점 엉망이 되어가고 있다. 어쩌면 'ResourceNode' 하위클래스를 만들어서 정리해야 할지도 모르겠다.

이 주석은 코드가 엉망이라는 사실을 밝히고 다음 사람에게 어떻게 수정해야 하는지 알려준다. 만약 이 주석이 없으면 많은 사람이 이 코드에 겁을 먹어 건드리지 않으려고 할 것이다.

p.81

주석은 잘 작성된 코드에만 작성해야한다고 생각을 했었는데 저자는 지금 상황에서 안 좋은 코드가 있을 수 있고 그것을 그냥 넘어가서는 안된다고 말하고있다. 생각을 해보면 오히려 잘못된 코드일수록 주석을 달아야하는데 숨기고 싶은 마음에 주석을 작성 안하는 것은 오히려 더 안 좋은 코드로 만드는 것인 것 같다. 약점은 약점으로 받아들일 수 있어야하는 것처럼 정직하게 주석을 달아야할 것이다. 그 코드를 작성한 내가 가장 잘 알 것이기 때문이다.

표시 (p.82)

표시 보통의 의미
TODO: 아직 하지 않은 일
FIXME: 오동작을 일으킨다고 알려진 코드
HACK: 아름답지 않은 해결책
XXX: 위험! 여기 큰 문제가 있다
TextMate ESC

TODO 만 알고있었는데 다른 표시도 유용할 것 같다.

def GenerateUserReport():
  # 이 사용자를 위한 lock 을 얻는다.
  ...
  # 데이터베이스에서 사용자의 정보를 읽는다.
  ...
  # 정보를 파일에 작성한다.
  ...
  # 사용자를 위한 lock 을 되돌려 넣는다.

이러한 주석을 함수가 수행하는 기능의 글머리 요약 역할을 수행하므로, 코드를 읽는 사람은 자세한 내용을 읽기 전에 주석을 보고 요점을 파악할 수 있다.

p.88

위와 같은 코드가 필요하다면 필자는 들여쓰기로 기능별로 표현하려고하지만 주석을 달 생각은 하지 못했다. 저자가 예시를 든 상황 같은 경우는 주석이 매우 적절해보인다. 주석이 없다면 좋은 변수명과 함수명이라고해도 시간이 더 걸렸을 것이고 이해가 부족했을 수 있을 것 같다. 이 책이 너무 좋은 이유는 예시가 너무 와닿게 되어있다는 것이다. 적절한 예시를 얻기 위해 어떻게보면 좋지 않은 코드도 수도 없이 봐왔다는 것을 유추해볼 수 있다. 항상 좋은 것만 봐서는 진짜 좋은 것이 뭔지 모르듯 안 좋은 상황에서 좋은 상황을 바꿀 수 있는 개발자가 되고 싶다.

앞으로 주석 달기를 주저하게 된다면 머릿속에 떠오르는 생각을, 심지어 다듬어지지 않은 생각이라고 해도 일단 쓰기 시작하라.

p.89

주석을 안 달게되는 이유는 주저하게 되는 것이 큰데 좋은 주석을 작성하도록 노력해봐야겠다. 주석을 작성하기 위해서는 현재 기능이 무엇이고 어떠한 기능을 수행해서 어떤 변화와 어떤 값을 반환하는지 또 추후 어떤게 추가되어야하고 어떤게 분리가 되어야하는지 등 고려해야하기 때문이다. 저자도 지적하듯 설명을 위한 설명은 필요없지만 적절한 주석은 코드의 관리성을 높이는 것 같다.

Ch 6 명확하고 간결한 주석 달기

엉터리 문장을 다듬어라

주석을 명확하게 하는 작업과 간결하게 하는 작업은 대부분 한번에 이루어진다.

# 이 URL 을 전에 이미 방문했는지에 따라서 다른 우선순위를 부여한다

이 문장은 어느정도 괜찮지만, 다음 문장과 비교해보자.

# 전에 방문하지 않은 URL 에 높은 우선순위를 부여하라.

p.93

첫 번째 든 예시에서 뭔가 더 나은 문장이 생각나지 않았는데 아래의 개선된 문장을 보니까 여지없이 깔끔함 그 자체의 문장이다. 부족함이 더욱 느껴져서 다행이기도 하다.

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

// 입력된 'src'의 'chars'라는 접두사와 접미사를 제거한다.
String Strip(String src, String chars) {...}

// 예: Strip("abba/a/ba", "ab")은 "/a/"를 반환한다.
String Strip(String src, String chars) {...}

p.95

필자의 경우 이 인용구를 읽고 딱 떠오르 코드가 있어 수정하였다. 년월일자를 출력하기 위해서 moment 를 사용한다.

export function momentLL(value: string | Date) {
  return moment(value).locale('ko').format('LL');
}

문제는 나중에 보니 LL 포멧이 무엇을 출력하는지 기억이 나지않아 코드를 찾아서 상황을 보거나 official docs 를 살펴봐야했다. 그렇지만 주석을 달 생각을 하지 못했었다가 저 인용구를 보고 떠올라 수정하였다. 아래와 같이 수정하였다. 입출력 값 예제가 있으니 무엇을 출력하는지 한 눈에 알아볼 수 있게 되었다.

물론 아래 2개의 함수는 하나로 합칠 수 있다. 다른 부분엔 format 만 다르기 때문이다.

/*
ex 2022.08.20.
 */
export function momentL(value: string | Date) {
  return moment(value).locale('ko').format('L');
}

/*
ex 2022년 8월 20일
 */
export function momentLL(value: string | Date) {
  return moment(value).locale('ko').format('LL');
}

문장으로 설명을 잘하는 것도 중요하지만 프로그래머는 코드로 서로 이야기하듯 입출력 예시로 코드를 파악하는데 매우 큰 도움을 줄 수 있을 것이다. 잠깐 생각만해도 개발하다 레퍼런스를 보기 위해서 해당 함수를 들여다보면 입출력 예제가 있는 것을 적지 않게 볼 수 있다.

읽기 좋은 코드가 좋은 코드다는 정말 와닿게 예제를 너무 잘 만든 것 같다. 이번 책 리뷰가 끝나면 마틴 파울러의 리펙토링을 리뷰하려하는데 리펙토링 또한 깊이 고민안해도 한 눈에 무엇이 좋은 코드인지 알 수 있다. 저번 글에서도 적었지만 이러한 수준까지 올라오기 위해서 얼마나 많은 노력과 열정이 있었을지 감도 오지 않는다. 개발자로서 품고 있는 말이지만 클린코드를 작성하는 개발자가 되고싶다.

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

조건문에서 인수의 순서

다음 두 코드 중에서 어떤 코드가 더 읽기 쉬운지 생각해보라.

if (member.count() >= 5)

혹은

if (5 <= member.count()

while (loaded < total)

혹은

while (total > loaded)

대부분의 프로그래머는 첫 번째 코드가 더 읽기 쉽다고 느낄 것이다. (생략) 우리가 발견한 유용한 규칙은 다음과 같다.

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

이러한 가이드라인은 영어 어순은 일치한다. “당신이 적어도 1년에 10만 불을 번다면” 혹은 “당신이 적어도 18세라면” 이라고 말하는 것은 자연스럽다. 하지만 “만약 18년이 당신의 나이보다 작거나 같다면” 이라고 말하는 것은 부자연스럽다.

p.104 - 105

데이터가 변경되는 대상을 왼쪽에 적는 것이 더욱 가독성이 높다고 저자는 논리적으로 설명하고있다. 영어 어순으로 예를 드니까 확연하게 차이가 와닿는다. 개발하면서 이 원칙으로 생각을 하면서 개발했지만 이제 논리와 근거까지 추가된 것 같다. chai 의 assert.equal 의 인터페이스는 아래와 같다. 실제 값이 왼쪽에 기대 값 즉 변하지 않는 값이 오른쪽에 온다. 이처럼 변하는 값이 왼쪽에 오는 것은 흔하게 볼 수 있다.

equal<T>(actual: T, expected: T, message?: string): void;

if/else 블록의 순서

if(a == b) {

}
else {

}

다음과 같이 순서를 바꿀 수 있다.

if(a != b) {

}
else {

}

부정이 아닌 긍정을 다루어라.

if (!req.params.reverse) {
  ...
} else {
  list.reverse()
}

보다는

if (req.params.reverse) {
  list.reverse()
} else {
  ...
}

다음은 부정해야 더 단순하고 흥미로우면서 동시에 위험해지는 경우다.

if(!jwtToken) {
  // 401 반환
} else {

}

p106-108

결국 조건이란 논리인데 부정 또한 하나의 논리이기 때문에 논리를 최소한으로 사용해야 오해를 방지할 수 있다고 생각한다. 단순하고 직관적이여야한다는 것은 한 눈에 보기 좋아야한다는 것을 의미하기도 한다.

함수 중간에서 반환하기

어떤 프로그래머는 한 함수에서 반환하는 곳이 여러 곳이면 안 된다고 생각한다. 이는 말이 되지 않는다. 함수 중간에서 반환하는 것은 완전히 허용되어야 한다. 이는 종종 바람직할 때도 있다.

function login(id, pw) {
  const userById = findOne({ id: id});
  if (!userById) {
    // 아이디 없는 경우 처리
    return;
  }

  const userByPw = findOne({ id: id, pw: pw});
  if (!userByPw) {
      // 비밀번호 틀린 경우 처리
      return;
  }

  // 로그인 성공 처리
}

p.112

예외 처리를 앞에서 미리미리 return 하면 중첩이 줄어든다는 매우 큰 장점이 있다. 중첩이 될수록 가독성이 떨어진다고 생각하기 때문에 필자의 경우 함수 중간에서 반환하기를 매우 애용한다.

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

설명 변수

커다란 표현을 쪼개는 가장 쉬운 방법은 작은 하위 표현을 담을 ‘추가 변수(extra variable)’를 만드는 것이다. 추가 변수는 하위표현의 의미를 설명하므로 ‘설명 변수(explaining variable)’라고도 한다.

if (fileName.split('.')[1] === 'png') {}

다음은 설명 변수를 사용하는, 위와 동일한 코드의 예다.

const extend = fileName.split('.')[1] === 'png';
if (extend) {}

p.122

위 코드의 예제는 필자가 각색한 부분인데 해당 코드는 버그가 있는 코드이다. a.b.png 라는 파일명도 있을 수 있기 때문에 index 를 지정한 방법은 버그이다. 간단히 이해를 돕기위한 코드이기 때문에 위 코드처럼 작성하였다는 점을 알아주면 좋겠다. 실제로 확장자를 구하고 싶다면 마지막 인덱스를 참조해야한다.

요약 변수

if (req.user.id === document.ownerId) {}
if (req.user.id !== document.ownerId) {}

요약 변수를 더하면 더 명확하게 표현 될 수 있다.

const userOwnsDocument = req.user.id === document.ownerId; 
if (userOwnsDocument) {}
if (!userOwnsDocument) {}

p.123

코드가 대체로 짧으면 잘 작성한 코드라는 부분에 꽤 동의가 되지만 때에 따라서는 코드를 펼치기도 해야한다. 변수를 사용하지 않으면 중복된 코드가 펼쳐지기 마련이기 때문이다. 물론 변수를 사용할 때도 특정 block 에서만 사용된다면 고민해봐야한다.

Ch 9 변수와 가독성

중간 결과 삭제하기

var removeOne = function (array, valueToRemove) {
    var indexToRemove = null;
    for (var i = 0 ; i < array.length; i += 1) {
        if (array[i] === valueToRemove) {
            indexToRemove = i;
            break;
        }
    }
    
    if (indexToRemove !== null) {
        array.splice(indexToRemove, 1);
    }
}

변수 indexToRemove 는 단지 중간 결과를 저장할 뿐이다. 이러한 변수는 결과를 얻자마자 곧바로 처리하는 방식으로 제거할 수 있다.

p.135

var removeOne = function (array, valueToRemove) {
    for (var i = 0 ; i < array.length ; i += 1) {
        if (array[i] === valueToRemove) {
            array.splice(i, 1);
            return;
        }
    }
}

이게 답을 보면 눈에 확연히 들어오는데 처음 예제를 보면 어떠한 기능인지는 파악이 되나 딱 리펙토링이 눈에 들어오지는 않았다. 그렇지만 마치 넌센스 퀴즈의 답을 듣고나면 허무하듯 개선된 코드를 보니 너무나 당연한 것이라 허무하다. 한편으로는 왜 이러한 스킬이 없나 싶기는 하지만 다른 한편으로는 그래도 이걸 깨달았다는 것이 기분 좋기도 하다.

자바스크립트에서 프라이빗 변수 만들기

submitted = false;

var submitForm = function (formName) {
    if (submitted) {
        return;
    }
    
    ...
    submitted = true;
}

submitted 와 같은 전역 변수는 코드를 읽는 사람에게 고민을 안겨 줄 것이다. submitForm 만이 submitted 를 사용하는 유일한 함수처럼 보이지만, 확실히 알 수는 없다.

submitted 변수를 클로저 내부에 집어넣어 이러한 문제를 해결 할 수 있다.

p.140



var submitForm = (function () {
    var submitted = false;
    
    return function (formName) {
        if (submitted) {
            return;
        }

        ...
        submitted = true;
    };
})()

이 코딍 에제를 보고는 closure 를 사용할 생각을 하지 못 했다. 그렇지만 closure 를 사용하여 변수를 숨기는 것 매우 좋은 기법 같다. 캡슐화를 해서 데이터의 정교함을 더욱 높일 수 있겠다는 생각이 든다. 코딩을 할 때 closure 를 적용할 수 있는 것인지 의식하며 살펴봐야겠다.

파이썬과 자바스크립트에는 없는 중첩된 범위

example_value = None

if request:
    for value in request.values
        if value > 0:
            example_value = value
            break

if example_value:
    for logger in debug.loggers:
        logger.log("Example:", example_value) 

이 예제에서 example_value 를 완전히 제거할 수도 있다. example_value 는 ‘중간 결과 삭제하기’에서 보았던 것처럼 단지 중간 결과값을 저장할 뿐이다. 이러한 변수는 ‘작업을 최대한 일찍 끝마치는’ 방법으로 완전히 제거할 수 있다.

p.143

def LogExample(value):
    for logger in debug.loggers:
        logger.log("Example:", example_value)
            
if request:
    for value in request.values
        if value > 0:
            LogExample(value)
            break    

생각해보면 변수가 많아도 프로그램의 복잡도는 올라가는 것 같다. 해당 변수를 사용해야할지 혹은 변수를 수정할 때 이를 참조하고 있는 곳은 또 어디인지 혹은 지금 사용하려는 변수가 최신의 데이터를 가지고있는지 등 여러 상황을 살펴봐야한다. 이 뜻은 잘못 사용할 가능성이 도사리고 있다는 뜻이기도하다. 이전 챕터에서는 설명 변수, 요약 변수로 변수로 의미를 나타내는 것을 적었다. 모든 것이 그렇듯 적절함이 중요하다. 위 코드의 예제처럼 일회용이면서 변수를 제거하는 것이 가독성을 낮추지 않는다면 오히려 변수를 줄임으로 버그를 줄일 수 있을 것 같다.

값을 한 번만 할당하는 변수를 선호하라

변수들의 값이 변한다면 프로그램을 따라가는 일은 더욱 어려워진다. 변수 값을 일일이 기억하려면 추가적인 어려움이 야기되기 때문이다. 이러한 문제를 해결하기 위해서 조금 이상하게 들릴 수 있는 제안을 하고자 한다. 값을 한 번만 할당하는 변수를 선호하라는 것이다.

p.145

하나의 변수에 계속 값을 할당하며 사용하는 것보단 여러개의 const 를 사용하는 것이 가독성을 높이는데 더 좋은 것 같다.

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

별도 함수로 추출

자바스크립트 코드의 상위수준 목적은 주어진 점과 가장 가까운 장소를 찾는 것이다.

p.154

var findClosestlocation = function (lat, lng, array) {
    var closest;
    var closest_dist = Number.MAX_VALUE;
    
    for (var i = 0 ; i < array.length ; i += 1) {
        var lat_rad = radians(lat);
        var lng_rad = radians(lat);
        var lat2_rad = radians(array[i].latitude);
        var lng2_rad = radians(array[i].longitude);
        
        var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) + 
                            Math.cos(lat_rad) * Math.cos(lat2_rad) * 
                            Math.cos(lng2_rad - lng_rad));
        
        if (dist < closest_dist) {
            closest = array[i];
            closest_dist = dist;
        }
    }
    
    return closest;
}

루프의 내부에 있는 코드는 대부분 주요 목적과 직접 상관없는 하위문제를 다룬다. 구 위에 있는 두 개의 위도/경도 점 사이의 거리를 계산하는데, 이 내용의 분량이 꽤 많으니 spherical_distance() 라는 별도의 함수로 추출하는 편이 좋다.

p.155

var spherical_distance = function (lat1, lng1, lat2, lng2) {
    var lat_rad = radians(lat);
    var lng_rad = radians(lat);
    var lat2_rad = radians(array[i].latitude);
    var lng2_rad = radians(array[i].longitude);

    return Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
        Math.cos(lat_rad) * Math.cos(lat2_rad) *
        Math.cos(lng2_rad - lng_rad));
}

var findClosestlocation = function (lat, lng, array) {
    var closest;
    var closest_dist = Number.MAX_VALUE;

    for (var i = 0 ; i < array.length ; i += 1) {
        var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);

        if (dist < closest_dist) {
            closest = array[i];
            closest_dist = dist;
        }
    }

    return closest;
}

코드를 읽는 사람은 밀도 높은 기하 공식에 방해받지 않고 상위수준의 목적에 집중할 수 있으니 전반적으로 코드의 가독성이 높아졌다.

spherical_distance() 는 독립적인 테스트도 더 용이하다. spherical_distance() 는 나중에 재사용될 수 있는 종류의 함수다. 바로 이 때문에 이러한 함수가 ‘상관없는’ 하위문제가로 불리는 것이다. 이는 그 자체로 완결되었으며 애플리케이션이 자기 자신을 어떻게 사용하는지는 알 필요가 없다.

p.156

코드를 작성하다보면 낮은 수준의 코드를 작성할 때가 있다. 예를 들어 암호화 & 복호화, 인증, 수학 수식 등이 있을 것이다. 기능을 구현하기 위해서 처음에는 코드를 이해하며 작성하지만 시간이 지나서 자신이 보거나 혹 다른 사람이 보고서는 이해하기가 어렵다. 이럴 때 함수로 분리하면 input 과 output 만 보고 사용하면 된다. 또한 함수로 분리했을 때 unit test 작성에 유리하다. 만약 함수로 분리되지 않았다면 테스트를 더 큰 범위에서 밖에 하지 못한다. 예를 들어 로그인 기능을 함수로 분리해놓지 않으면 unit test 로 작성하지 못하고 http 를 통해 응답 값으로만 테스트가 가능하다.

함수를 분리한다는 것은 가독성과 재사용성, 테스트 용이성을 높일 수 있다.

지나치게 추출하기

대부분의 프로그래머는 충분히 적극적이지 않기 때문에 ‘적극적’이라는 표현을 일부러 사용했다. 하지만 너무 흥분해서 지나친 수준으로 나아가는 일도 벌어질 수 있다.

과하게 자잘한 함수를 사용하면 오히려 가독성을 해친다. 사용자가 신경 써야 하는 내용이 늘어나고, 실행 경로를 추적하려면 코드의 곳곳을 돌아다녀야 하기 때문이다.

p.165

무엇이든 과하면 안 좋듯 함수를 너무 많이 분리해도 가독성이 떨어지는 것 같다. 물론 가능하다면 함수를 분리 안하는 것보다는 낫겠지만..

Ch 11 한 번에 하나씩

한 번에 여러가지 일을 수행하는 코드는 이해하기 어렵다. 코드 블록 하나에서 새로운 객체를 초기화하고, 데이터를 청소하고, 입력을 분석하고, 비지니스 논리를 적용하는 일을 한꺼번에 수행하는 경우도 있다.

이번 장은 코드를 탈파편화(defragmenting) 하는 방법을 다룬다.

p.168

읽기 좋은 코드란 코드가 짧다고 읽기 좋은 코드라고 부를 수 없다. 한 눈에 코드가 어떤 역할을 하는지 알 수 있어야한다. 코드가 무엇을 하는지 알려면 한 블록 안에서 하나의 일을 해야한다. 이를 저자는 탈파편화라고 부르고 있다.

작업은 작을 수 있다.

블로그 사용자가 댓글에 ‘추천’과 ‘반대’ 의사표시를 할 수 있는 투표 도구가 있다고 해보자. 어떤 댓글의 전체 점수는 모든 득표의 합으로 계산된다. ‘추천’은 +1점이고 ‘반대’는 -1점이다.

p.169

개선 전 코드

var vote_changed = function (old_vote, new_vote) {
    var score = get_sorce();
    
    if (new_vote !== old_vote) {
        if (new_vote === 'Up') {
            score += (old_vote === 'Down' ? 2 : 1);
        } else if (new_vote === 'Down') {
            score -= (old_vote === 'Up' ? 2 : 1);
        } else if (new_vote === '') {
            score += (old_vote === 'Up' ? -1 : 1);
        }
    }
}

개선 코드

var vote_value = function (vote) {
    if (vote === 'Up') {
        return +1;
    }
    
    if (vote === 'Down') {
        return -1;
    }
    
    return 0;
}

var vote_changed = function (old_vote, new_vote) {
    var score = get_sorce();
    
    score -= vote_value(old_vote); 
    score += vote_value(new_vote);
    
    set_score(score);
}

보다시피 새로운 코드가 정상적으로 작동하는지 확인하는 정신적 노동은 이전보다 훨씬 적다. 바로 이것이 ‘이해하기 쉬운’ 코드를 만드는 핵심이다.

p.171

개선 전 코드와 개선된 코드를 보면 한 눈에 알아볼 수 있다는 차이점이 있다. 한 눈에 알아볼 수 있으니 나중에 코드 수정이나 디버깅할 때도 매우 편리할 것이다. 코드를 잘 짠다는 것은 보기 좋게 만들어서 오해를 줄이는 것을 말하는 것 같다. 개선 전 코드도 원하대로 동작은 하겠지만 나중에 기능을 추가하거나 변수가 추가되면 잘 돌아가는지 장담하기는 어려울 것이다. 이 예제처럼 작업을 작게해서 명확히 코드를 작성하는 것은 체감은 안되겠지만 버그를 줄이는 작업 같다.

Ch 12 생각을 코드로 만들기

‘쉬운 말’로 자신의 생각을 지식이 부족한 사람에게 전달하는 기술은 매우 소중하다.

p.182

어려운 것을 ‘해결’할 수 있는 것과 어려운 것을 쉽게 ‘설명’할 수 있는 부분은 다른 부분이다. 좋은 코드는 어렵게 보이는 현상을 쉽게 푼 코드라고 생각한다. 쉽게라는 뜻은 이해가 쉬워야하기 때문에 읽기 좋은 코드를 의미할 것이다. 또한 읽기 좋다는 것은 좋은 변수, 함수명과 주석을 넘어 잘 구성된 객체를 포함할 수 있다. 필자는 쉽게 설명하기 위해서 의식적으로 주의하는 것은 ‘그것과 같이’, ‘그 코드는’ 등 이것, 저것을 사용하지 않으려 한다. 예를 들어 그 코드 -> 로그인에 사용되는 쿼리 처럼 전체적인 문장의 길이는 늘어나지만 대명사를 사용하지 않아 오해를 줄이려고 한다.

논리를 명확하게 설명하기

이전 코드

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

// 계속해서 페이지를 렌더링한다...

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

  1. 관리자다
  2. 만약 문서가 있다면 현재 문서를 소유하고 있다.

그렇지 않으면 허가되지 않는다.

이러한 묘사로 영감을 받은 새로운 코드는 다음과 같다.

p.183

개선 코드

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

// 계속해서 페이지를 렌더링한다...

Ch 13 코드 분량 줄이기

그 기능을 구현하려고 애쓰지 마라 - 그럴 필요가 없다

새로운 프로젝트를 시작하면 공연히 흥분해서 뭔가 멋진 기능을 구현하려고 궁리한다. 하지만 프로그래머는 대개 프로젝트에 정말로 필요한 기능이 얼마나 있는지 과대평가하는 경향이 있다. 한편 프로그래머는 어떤 기능을 구현하는 데 필요한 노력을 과소평가하는 경향도 있다.

p.192

인용구는 완전히 나의 모습이다. 새로운 프로젝트를 시작하면 확장성을 엄청나게 고려해서 코드를 작성하는데 이게 독이 될 때가 매우 많았다. 확장성을 고려한 고민의 결과가 정답에 가까웠다면 문제가 안 되었을텐데 그 코드 또한 수정해야하니 추후 코드 수정에 대한 사이드 이펙트만 더욱 커졌기 때문이다. 물론 확장성을 고려한 결과물이 좋았던 경험도 있다. 몇 년 뒤에 생각은 달라질 수 있겠지만 현재로써는 일단 기능을 만들고 코드를 다듬는 것이 좋은 방법 같다. 이유는 여러가지를 들 수 있겠으나 그 중 가장 큰 것은 눈에 보이는 결과물이 있어야한다는 것이다. 눈에 보이는 결과물이 있어야 개발할 때 더욱 신나고 몰입할 수 있었다. 당연히 이것은 필자의 개인적인 경험이다. 그러므로 그저 경험이지 결코 정답이 아니다.

여하튼 조그마한 기능 개발 -> 눈에 보이는 아웃풋 -> 뿌듯함 & 성취감 -> 리펙토링 -> 조그마한 기능 개발 ->… 이 방법이 개인적으로는 좋은 것 같다. 조그만한 기능들을 잘 만들었을 때 결합해서 더 큰 범위의 기능을 만들 수 있다. 이럴 때 더 정확한 기능 구현이 된다. 처음부터 큰 그림을 그리면 추상적이기 때문에 현실과 다른 경우가 발생하기 때문이다. 물론 아예 큰 그림을 생각하지 않는 것도 문제이다.

자기 주변에 있는 라이브러리에 친숙해져라

라이브러리가 할 수 있는 일을 알고 활용하는 것은 대단히 중요하다. 다음은 실제로 도움을 주는 조언이다. 매일 15분씩 자신의 표준 라이브러리에 있는 모든 함수/모듈/형들의 이름을 읽어라.

라이브러리 전체를 암기하라는 게 아니다. 그냥 그 안에 무엇이 있는지 감을 잡아놓고, 나중에 새로운 코드를 작성할 때 “잠깐만, 이건 전에 API에서 보았던 것과 뭔가 비슷한데..” 라고 생각할 수 있기를 바라는 것이다.

p.196-197

필자는 인용구 연장선 상에서 비슷한 생각을 가지고 있다. 그것은 책 끝까지 읽기이다. 끝까지 읽었을 때 고차원?!적인 정보가 나올 확률이 높기 때문이다. 초반에는 이해가 되지 않아 도움이 안 되는 것 같아 덮고 싶은 마음이 들지만 그럼에도 끝까지 읽으면 이걸 왜 쓰는지 감은 온다. 또한 지금 나에게 필요한 정보를 소개하기도 한다. 이런 경험들을 하다보니 억지로라도 책을 끝까지 읽으려한다. 또한 특정 오픈소스를 사용할 때 인터페이스는 보는 습관이 있다. 요즘은 typescript 를 많이 사용하다보니 인자 type, 리턴 type 과 설명과 예제도 잘 되어있기 때문에 언제나 도움이 된다. 인용구에서 저자가 언급했듯 인터페이스를 보다보면 필요한 기능이 이미 구현되어있기도 하다. 예를 들어 이미지 슬라이딩을 구현하려고 우선 생각을 하니 FlatList 에서 사용자 Gesture 를 가지고 현재 x 좌표를 계산하는 코드가 필요하겠다고 생각했다. 그렇지만 FlatList 인터페이스를 보니 pagingEnabled 가 있었고 pagingEnabled={true} 를 사용하니 그냥 구현이 끝났다. 이를 몰랐으면 아마.. 꽤 고생을 했거나 외부 라이브러리를 사용했을 것이다. Alt text

가급적이면 적은 분량의 코드로 작성하는 방법을 배웠다. 새로 작성하는 코드를 모두 테스트하고, 문서화하고, 유지보수해야 한다. 더욱이 코드베이스에 더 많은 코드가 있으면 더 ‘무거워’져서 새로운 개발이 더 어렵게 된다.

p.196

Ch 14 테스트와 가독성

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

테스트 코드가 크고 두렵게 느껴지면 다음과 같은 일이 일어난다.

  • 코드를 수정하는 일이 두려워진다. 아니, 우리는 이 코드에 손대고 싶지 않아. 테스트 케이스를 모두 변경하는 일은 너무나 끔찍하다고!
  • 새로운 코드를 작성하면 그에 따르는 새로운 테스트를 작성하지 않는다. 시간이 흐름이 지나면서 더 낮은 비율의 코드가 테스트되며, 따라서 코드에 대한 확신이 줄어들 수 밖에 없다.

p.204

TDD(test-driven development) 의 단점 중 하나는 나중에 기능을 수정할 때 연관되어있는 테스트 케이스를 수정하는 사이드 이펙트와 변경한 테스트에서 놓치는 부분이 발생할 확률이 높다는 것이다. 변경할 때는 처음 테스트 케이스를 만들 때 보다 이해도가 떨어질 것이고 검증 또한 부실할 가능성이 높기 때문이다. 테스트 케이스 또한 코드이기 때문에 보기 좋게 되어있지 않다면 수정하기 싫은 코드가 될 것이다. 그렇다면 더더욱 나중에 코드를 수정하기 꺼려질 것이고 테스트 케이스는 오히려 검증을 해주지 못하고 불필요한 통과라는 값만 반환하게 될 것이다. 코드가 보기 좋지 않다면 새로운 테스트 케이스를 작성하기에 꺼려지는 것도 사실이다. 이럴 때 기존 코드를 수정해야하지만 여러 이유로 그러지 못하는 경우 많다. 테스트 케이스가 촘촘하지 못하면 결국 나중에 할 일이 더욱 많아지기 때문에 비효율적이 된다. 테스트 케이스 또한 코드로써 깔끔하게 잘 작성해야한다.

부끄럽지만 필자의 경험으로는 사용자 계정 관련 테스트 코드를 작성할 때 처음에는 아래와 같이 테스트 계정을 생성했다.

it('Sign Up Test', async () => {
   await (new UserModel({
       userId: uuidv4(),
       pw: uuidv4(),
       name: randomStr(4),
       ...
   })).save();
   
   assert.eqaul(..., ...)
});

논리상으로는 문제는 없지만 계정 생성을 함수로 빼야 훨씬 더 좋은 코드가 된다. 장점은 아래와 같다.

  1. 다른 테스트에서도 사용자 계정을 간편하게 만들 수 있다.
  2. 가입 명세가 달라졌을 때 한 함수만 수정하면 된다.
  3. 코드의 양이 함수 호출로 끝나기에 전체적인 코드 line 수가 줄어든다.

위 예제처럼 코드를 작성하여 다른 테스트에서도 가입 생성을 저 로직을 copy & paste 로 사용했다. 시간이 지나고 문제를 직면했다. 테스트 실패했을 때 test 로 만들어진 사용자 데이터가 사라지지 않는 것이였다. 물론 의도에 따라서 안 사라지게 할 수 있겠지만 필자는 모든 테스트 데이터는 사리지기를 원했다. assert 전에 사용자 계정을 사라지게할 수 있지만 그 또한 불필요한 코드라고 생각이 들었다. 그리하여 사용자 계정을 생성할 때 userId: uuidv4() -> userId: ` test-${uuidv4()} ` 로 변경하기로 하였다. 그리고 테스트 케이스 모두 종료 후 userId 가 test- 로 시작하는 데이터를 찾아 삭제하도록 하였다. 또 문제는 모든 사용자 계정을 생성하는 곳을 찾아가 수정을 해야했다. 이 때 잘못을 깨닫고 fakeCreateUser() 함수로 분리하여 사용하도록 하였다. 함수로 분리한 결과 나중에 pw 암호화이건 validation 이건 코드 수정이 최소화되었다. 사실 이론적으로 모르는 이야기는 아니였지만 실제 코드에서는 그러지 못했었다. 아는 것과 깨달은 것은 다른 것 같다.

일반적인 설계원리를 따르면 덜 중요한 세부 사항은 사용자가 볼 필요 없게 숨겨서 더 중요한 내용이 눈에 잘 띄게 해야 한다.

p.206

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

p.213

지금 작성하는 코드의 테스트 코드를 나중에 작성할 거라는 사실을 염두에 두면 재미있는 일이 벌어진다.

p.218

테스트 케이스도 당연히 코드이기 때문에 보기 좋아야한다. 일반적인 코드와 다른 것은 데이터까지 보기 좋아야한다는 것이다. 테스트 케이스에 필요한 데이터는 많은 데이터가 아니라 꼭 필요한 데이터 소수일 수 있다. 예를 들어 1~100 이 경우가 정상적인 데이터라면 경계값으로 데이터를 구성해야한다. [0, 1, 100, 101] 과 같이 말이다. 개인적으로 테스트 케이스도 보기 좋게 작성해야한다는 생각은 사실 크게 염두해두지 않았던 것 같다. 그래서 그런지 생각해보면 지금 작성 중인 테스트 케이스가 새삼 좋지 않게 보인다. 보이지 않던 것이 의식이 되면 보이는게 새삼 신기하다.

Ch 15 ‘분/시간 카운터’를 설계하고 구현하기

이 예를 통해서 여러분이 ‘프로그래머가 일반적으로 수행하는 자연스러운 사고의 흐름’을 밟도록 안내할 것이다. 제일 먼저 문제를 해결하려고 시도하고, 이어서 성능을 개선하고, 추가적인 기능을 더하는 흐름 말이다.

p.224

Ch 15 에서는 ‘분/시간 카운터’ 를 만들 때 조금씩 개선하면서 확장성, 성능을 높이는 것을 보여준다. 챕터 전체가 코드라 이를 옮기기는 어렵다.

좋은 코드란 눈으로 읽어 내려갈 때 필요한 절차들을 읽고 이해할 수 있는 코드라고 생각한다. 중간에 생뚱 맞은 코드가 있으면 갸우뚱하게 되기 때문이다. 또한 그것이 오해를 불러일으키기도한다. 다시 말해 좋은 코드란 일반적으로 수행하는 자연스러운 사고의 흐름으로 작성되어있는 것이라고 생각한다.

코드는 많이 사용하지만 성능은 훨씬 뛰어나고, 설계도 더 유연하다. 또한 각각의 클래스는 훨씬 읽기 편하다. 이는 언제나 긍정적인 변화다. 읽기 쉬운 100줄의 코드는 읽기 어려운 50줄의 코드에 비해서 훨씬 낫다.

p.241

일반적인 함수에서는 코드의 분량이 적을 수록 좋기는 할 것이다. 그렇지만 복잡한 상황에서는 객체를 잘 분리해서 서로 연결할 때 코드 라인 수는 많을 수 있지만 좋은 코드라고 볼 수 있다. 객체로 분리했을 때 얻을 수 있는 장점 다형성, 은닉성, 추상화 등을 얻을 수 있기 때문이다. 객체지향적으로 바라볼 때 어려운 문제도 쉽게 풀 수 있을 것이다.


Command
킥 오프
NCloud LB & SourcePipeline 구축하기
tech collection 서비스 성능 개선하기
Selenium 복권 구매 자동화 만들어보기
디자인 패턴
책 리뷰
블로그 챌린지