도서 리뷰 시리즈 - 읽기 좋은 코드가 좋은 코드다
출처
『읽기 좋은 코드가 좋은 코드다』 더스틴 보즈웰, 트레버 파우커 공저 / 임백준 역 | 한빛미디어 | 2012년 04월
한 줄 리뷰
기능 구현 코딩에 익숙해졌다면 그다음에는 이 도서를 읽고 좋은 코드를 코딩해야 합니다.
1 코드는 이해하기 쉬워야 한다
코드는 다른 사람이 그것을 이해하는 데 들이는 시간을 최소화하는 방식으로 작성되어야 한다.
코드를 완전히 이해한다는 것은
- 코드를 자유롭게 수정가능
- 버그를 짚는 것을 가능
- 수정된 내용이 작성한 다른 부분의 코드와 어떻게 상호작용하는지 알 수 있어야 함
을 의미한다.
2 이름에 정보 담기
변수, 함수, 혹은 클래스 등의 이름을 결정할 때는 항상 같은 원리가 적용된다. 이름은 일종의 설명문으로 간주해야 한다.
이름에 정보를 담아내는 방법
- 보편적인 단어를 피하고 구체적인 단어를 선택한다.
- get => fetch, download
- find => search, extract, locate
- start => launch, create, open, begin
- 변수의 목적이나 담고 있는 값을 설명
- 변수명을 명확히 하면 버그를 예방/추적하기 용이하다.
- 시간의 양이나 바이트의 수 같이 측정치를 포함한다면, 단위를 포함시킨다.
// Wrong
const current = new Date().getTime()
console.log(`Current Seconds is ${current}`)
// Right
const currentMs = new Date().getTime()
console.log(`Current Seconds is ${currentMs}`)
- 위험한 요소 또는 나중에 놀랄만한 내용이 있다면 표현
- 패스워드가 암호화가 안되있다면 : password => plainTextPassword
- URL Encoded 데이터라면 : data => dataURLEncoded
이름은 얼마나 길어야 하는 가?
- 좁은 범위에서는 짧은 이름이 괜찮다
- 좁은 범위에서만 사용되는 변수의 이름에 많은 정보를 담을 필요가 없다.
- 변수의 타입, 초기값 등 모든 정보가 쉽게 한눈에 보이므로 짧은 이름을 사용해도 좋다.
- 약어와 축약형
- 특정 프로젝트에 국한된 의미를 가진 약어 사용은 좋은 생각이 아니다.
- 새로 합류한 사람에게 비밀스럽고 위협적인 모습이다.
- 팀에 새로합류한 사람이 이름이 의미하는 바를 이해할 수 있을 까 기준
- 불필요한 단어제거하기
- 정보의 손실하지 않으면서 이름에 포함된 단어를 제거할 수 있다.
- convertToString() => ToString()
이름 포매팅으로 의미를 전달하라
문법적 차이가 드러나게 서로 다른 개체의 이름에 각자 다른 포맷팅 방식을 적용하는 방식은 코드를 더 읽게 쉽게 해준다.
- 클래스 : PascalCase
- 변수, 함수, 메소드 : camelCase
- 상수 : CONSTANT_NAME
3 오해할 수 없는 이름들
본인이 지은 이름을 다른 사람들이 다른 의미로 해석할 수 있을까라는 질문을 던져보며 철저하게 확인해야 한다.
한계를 설정하는 이름을 가장 명확하게 만드는 방법은 제헌 받는 대상의 이름 앞에 max/min을 붙이는 것이다.
경계의 양끝점을 포함된다는 의미에서 경계를 포함하는 범위에는 first/last가 좋은 선택이다.
불리언 이름은 일반적으로 is/has/can/should와 같은 단어를 더하면 불리언 값을 의미한다.
4 미학
미학적으로 보기 좋은 코드가 사용하기 더 편리하다는 사실은 명백하다. 잘 생각해보면 소비되는 시간이 코드를 바라보는 데 많이 소요된다. 코드를 훑어보는 데 걸리는 시간이 적을 수록, 사람들은 코드를 더 쉽게 사용할 수 있다.
- 일관성과 간결성을 위해서 줄바꿈을 재정렬하기
- 메소드를 활용해 불규칙성을 정렬한다.
- 의미 있는 순서를 선택하고 일관성 있게 사용하라
- 가장 중요한 것을 시작해서 가장 덜 중요한 순서로 정렬
- 알파벳 순서 정렬
- 선언문을 블록으로 구성
- 우리의 뇌는 자연스럽게 그룹과 계층 구조를 따라서 동작한다. 이런 방식으로 조직하면 코드를 읽는 데 도움이 된다.
- 논리 영역에 따라서 필드를 선언한다.
- 코드를 문단으로 쪼개라
- 비슷한 생각을 묶어서, 성격이 다른 생각과 구분한다.
- 문단은 시각적 디딤돌 역할을 수행한다.
5 주석에 담아야 하는 대상
주석으로 설명하지 말아야 할 것
- 설명 자체를 위한 설명은 달지 마라
- 나쁜 이름에 주석을 달지 마라
- 대신 이름을 고쳐라
주석은 생각을 기록하는 것이다.
- 자신의 생각을 기록하는 것만으로 좋은 주석이 탄생할 수 있다.
- 코드에 있는 결함을 설명하라
7 읽기 쉽게 흐름제어 만들기
조건, 루프, 흐름을 통제하는 선언문은 코드를 복잡하게 만드는 원인이다. 코드를 읽을 때 다시 되돌아가서 코드를 읽지 않아도 되게끔 만들어야 한다.
- 조건문에서 인수의 순서
- 왼쪽 : 질문을 받는 표현
- 오른쪽 : 비교대상
// 가독성 좋음
if (length >= 10) {}
// 가독성 낮음
if (10 <= length) {}
- 이러한 가이드 라인은 영어 어순과 일치한다.
- if/else 순서
- 부정이 아닌 긍정을 다루어라, 즉
if(!isTrue)
가 아닌if(isTrue)
- 부정이 아닌 긍정을 다루어라, 즉
- 중첩을 최소화하기
- 코드의 중첩이 심할 수록 코드를 읽는 사람의 마음속에 존재하는 정신적 스택에 추가적인 조건이 입력된다.
- 함수 중간에서 반환하기로 중첩을 제거하라
- 루프 내부에 있는 중첩 제거하라
8 거대한 표현을 잘게 쪼개기
우리는 한번에 서너개 일만 생각할 수 있다고 한다. 즉, 코드의 표현이 커지면 이해하기 더 어렵다.
설명 변수
커다란 표현을 쪼개는 가장 쉬운 방법은 작은 하위 표현을 담을 추가 변수를 만드는 것이다. 하위표현의 의미를 설명하므로 설명 변수라고도 한다.
// Not Cool
if (line.split(':')[0]) === "root") {}
// Cool
const username = line.split(':')[0]
if (username === "root") {}
드모르간의 법칙 사용하기
동일한 불리언 표현은 다음과 같이 두가지 방법으로 작성할 수 있다.
!(a || b || c) === !a && !b && !c
!(a && b && c) === !a || !b || !c
// Not Cool
if (!(fileExists && !isProtected)) { throw "아이고 파일을 읽을 수 없습니다."; }
// Cool
if (!fileExists || isProtected) { throw "아이고 파일을 읽을 수 없습니다."; }
거대한 구문 나누기
개별적인 표현은 그렇게 크지 않지만, 모두 한 곳에 있어서 코드를 읽는 사람의 머리를 강타하는 거대한 구문을 형성한다.
다행히도 표현하는 많은 부분이 동일하다. 따라서 동일한 부분을 요약 변수로 추출해서 함수의 앞부분에 놓아둘 수 있다.
// Not Cool
const updateHighlight = messageNum => {
if ($(`#vote_value${messageNum}`).html() === "Up") {
$(`#thumbs_up${messageNum}`).addClass("highlighted");
$(`#thumbs_down${messageNum}`).removeClass("highlighted");
} else if ($(`#vote_value${messageNum}`).html() === "Down") {
$(`#thumbs_up${messageNum}`).removeClass("highlighted");
$(`#thumbs_down${messageNum}`).addClass("highlighted");
} else {
$(`#thumbs_up${messageNum}`).removeClass("highlighted");
$(`#thumbs_down${messageNum}`).removeClass("highlighted");
}
}
// Cool
const updateHighlight = messageNum => {
const thumbsUp = $(`#thumbs_up${messageNum}`)
const thumbsDown = $(`#thumbs_down${messageNum}`)
const voteValueHtml = $(`#vote_value${messageNum}`).value()
const ACTIVE_CLASS = "highlighted"
if (voteValueHtml === "Up") {
thumbsUp.addClass(ACTIVE_CLASS);
} else {
thumbsUp.removeClass(ACTIVE_CLASS);
}
if (voteValueHtml === "Down") {
thumbsDown.addClass(ACTIVE_CLASS);
} else {
thumbsDown.removeClass(ACTIVE_CLASS);
}
}
예: 복잡한 논리와 씨름하기
주어진 양쪽 경계값이 other의 범위에 속하는지 확인하는 함수이다.
const begin = 2;
const end = 4;
const overlapsWith = other => {
return (begin >= other.begin && begin <= other.end) ||
(end >= other.begin && end <= other.end);
};
생각해야 하거나 조건이 너무나 많으므로 버그가 발생할 확률이 매우 높다. 사실은 버그가 있다. 앞선 코드는 범위 [0, 2)
가 [2, 4)
와 겹친다고 말한다.
문제는 <= 혹은 <로 begin/end 값을 비교할 때 매우 신중해야 한다는 점이다. 이 버그를 수정하면 다음과 같다.
const overlapsWith = other => {
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end);
};
이제는 정확한가? 사실은 또 다른 버그가 있다. 이 코드는 begin/end가 other를 완전히 포함하는 경우를 무시한다. 이를 제대로 해결한 수정 코드는 다음과 같다.
const overlapsWith = other => {
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end) ||
(begin <= other.begin && end >= other.end);
};
이 코드는 이제 걷잡을 수 없이 복잡해졌다. 다른 사람이 이 코드를 읽고 정확한지 판단할 수 있으리라고 기대할 수 는 없다. 그럼 이 거대한 표현을 어떻게 쪼갤 수 있을까?
더 우아한 접근방법 발견하기
더 우아한 해결책을 찾으려면 창의력이 필요하다. 그럼 어떻게 하는가? 한 가지 해결책은 똑같은 문제를 반대되는
방법으로 해결할 수 있는지 확인하는 것이다.
여기서 overlapsWith의 반대는 겹치지 않는 것
이다. 두 개의 범위가 서로 겹치지 않는 것을 확인하는 방법에는 두 가지 가능성만 존재하므로 훨씬 더 풀기 쉬운 문제로 다가온다.
- 다른 범위가 이 범위 시작보다 전에 끝난다.
- 다른 범위가 이 범위가 끝난 후에 시작된다.
이를 코드로 만드는 방법은 쉽다.
const overlapsWith = other => {
if (other.end <= begin) return false; // 시작보다 전에 끝난다.
if (other.begin >= end) return false; // 끝난 후에 시작한다.
return true;
};
코드의 각 줄은 전보다 훨씬 더 간단하다. 한 번의 비교만 포함할 뿐이다. 이렇게 하면 코드를 읽는 사람이 <= 연산자를 정확하게 사용했는 지 쉽게 확인할 수 있다.
10 상관없는 하위문제 추출하기
엔지니어링은 커다란 문제를 작은 문제들로 쪼갠 다음, 각각의 문제에 대한 해결책을 구하고, 다시 하나의 해결책으로 맞추는 일련의 작업을 한다. 이러한 원리를 코드에 적용하면 코드가 더 튼튼해지며 가독성도 좋아진다.
이 장에서 말하는 조언은 큰 흐름과 관계가 적은 하위문제를 적극적으로 발견해서 추출하라는 것이다. 이 말이 의미하는 바는 다음과 같다.
- 주어진 함수가 코드 블록을 보고, 스스로에게 질문하라 상위수준에서 본 이 코드의 목적은 무엇인가?
- 코드의 모든 줄에 질문을 던져라 이 코드는 직접적으로 목적을 위해서 존재하는 가? 혹은 목적을 위해서 필요하긴 하지만 목적 자체와 직접적으로 상관없는 하위문제를 해결하는가?
- 만약 상당히 원래의 목적과 직접적으로 관련되지 않은 하위문제를 해결하는 코드 분량이 많으면, 이를 추출해서 별도의 함수로 만든다.
소개를 위한 예: findClosestLocation()
다음 자바스크립트 코드의 상위수준 목적은 주어진 점과 가장 가까운 장소를 찾는 것이다.
const findClosestLocation = (lat, lng, array) => {
let closest
let closestDist = Number.MAX_VALUE
for (let i = 0, len = array.length; i < len; i++) {
const latRad = radians(lat)
const lngRad = radians(lng)
const lat2Rad = radians(array[i].latitude)
const lng2Rad = radians(array[i].longitude)
// 코사인의 특별법칙 공식을 사용한다.
const dist = Math.acos(
Math.sin(latRad) * Math.sin(lat2Rad) +
Math.cos(latRad) * Math.cos(lat2Rad) *
Math.cos(lng2Rad - lngRad)
)
if (dist < closestDist) {
closest = array[i]
closestDist = dist
}
}
return closest
}
루프의 내부에 있는 코드는 대부분 주요 목적과 직접 상관없는 하위문제를 다룬다.
const sphericalDistance = (lat1, lng1, lat2, lng2) => {
const latRad = radians(lat1)
const lngRad = radians(lng1)
const lat2Rad = radians(lat2)
const lng2Rad = radians(lng2)
return Math.acos(
Math.sin(latRad) * Math.sin(lat2Rad) +
Math.cos(latRad) * Math.cos(lat2Rad) *
Math.cos(lng2Rad - lngRad)
)
}
이제 원래 코드는 이렇게 변한다.
const findClosestLocation = (lat, lng, array) => {
let closest
let closestDist = Number.MAX_VALUE
for (let i = 0, len = array.length; i < len; i++) {
const latRad = radians(lat)
const lngRad = radians(lng)
const lat2Rad = radians(array[i].latitude)
const lng2Rad = radians(array[i].longitude)
// 코사인의 특별법칙 공식을 사용한다.
const dist = sphericalDistance(lat, lng, array[i].latitude, array[i].longitude)
if (dist < closestDist) {
closest = array[i]
closestDist = dist
}
}
return closest
}
코드를 읽는 사람도 밀도 높은 기하 공식에 방해받지 않고 상위수준의 목적에 집중할 수 있으니 전반적으로 코드의 가독성이 높아졌다.
기존의 인터페이스를 단순화하기
라이브러리가 깔끔한 인터페이스를 제공하면 누구나 좋아한다. 하지만 자신이 사용하는 인터페이스가 깔끔하지 않다면, 깔끔한 덮개(Wrapper)로 보완할 수 있다.
예를 들어 자바스크립트가 브라우저 쿠키를 다루는 방식은 전혀 이상적이지 않다. 개념적으로 보면 쿠키는 이름/값 짝으로 이루어진다. 브라우저가 제공하는 인터페이스는 다음과 같은 문법으로 된 하나의 document.cookie
를 사용한다.
name1=value1; name2=value2; ...
필요한 쿠키를 찾으려면 이 거대한 문자열의 구문분석을 직접 수행해야 한다. 다음은 max_results
라는 이름을 가진 쿠키의 값을 읽는 코드이다.
let maxResults
const cookies = document.cookie.split(';')
for (let i = 0, len = cookies.length; i < len; i++) {
const cookie = cookies[i].replace(/^[ ]+/, '')
if (cookie.indexOf('max_results') === 0) {
maxResults = Number(cookie.substring(12, cookie.length))
}
}
정말 지저분한 코드다. 다음과 같이 사용할 수 있는 get_cookie()
함수를 만들어야 할 것 처럼 보인다.
const maxResults = Number(getCookie('max_results'))
여기서 이상적이지 않은 인터페이스를 그냥 받아들일 이유가 없다는 교훈을 얻을 수 있다. 이런 인터페이스가 있으면 언제나 이를 둘러싸는 함수를 작성하여 지저분한 내부를 감출 수 있다.
11 한 번에 하나씩
한 번에 여러 가지 일을 수행하는 코드는 이해하기 어렵다. 코드 블록 하나에서 새로운 객체를 초기화하고, 데이터를 청소하고, 입력을 분석하고, 비즈니스 논리를 적용하는 일을 한꺼번에 수행하는 경우도 있다. 그러한 코드가 모두 한자리에 뒤섞이면 각각의 작업이 별도로 시작되었다가 완료되는 경우보다 이해하기 어렵다.
다양한 코드 조각이 뒤섞인 채 일을 수행하는 모습을 한 가지 작업만 수행하게 재정비하는 것을 탈파편화라고 한다.
코드가 한번에 한 가지 일만 수행하게 하는 절차는 다음과 같다.
- 코드가 수행하는 모든 작업을 나열한다.
- 이러한 작업을 분리하여 서로 다른 함수로 혹은 적어도 논리적으로 구분되는 영역에 높을 수 있는 코드로 만든다.
14 테스트와 가독성
테스트 코드가 읽기 쉬워야 한다는 점은 테스트와 상관없는 실제 코드와 마찬가지로 중요하다. 다른 프로그래머는 종종 테스트 코드를 실제 코드가 어떻게 동작하며 어떻게 사용되어야 하는지에 관한 비공식적인 문서라고 생각한다. 따라서 테스트 코드가 읽기 쉬우면, 사용자는 실제 코드가 어떻게 동작하는 지 그만큼 더 쉽게 이해할 수 있다.
일반적인 설계원리를 따르면 덜 중요한 세부 사항은 사용자가 볼 필요 없게 숨겨서 더 중요한 내용이 눈에 잘 띄게 해야 한다.