개발자라면 누구나 들어보았을 Clean Code가 무엇인지 어떻게 작성해야하는지에 대한 책이다.
지은이의 말 中
이 책을 관통하는 핵심 아이디어는 코드는 이해하기 쉬워야 한다는 것이다.
특히, 자신의 코드를 다른 사람이 읽고 이해할 때 걸리는 시간을 최소로 만들어야한다.
1. 코드는 이해하기 쉬워야한다.
가독성의 기본 정리
코드는 다른 사람이 그것을 이해하는 데 들이는 시간을 최소화하는 방식으로 작성되어야 한다.
"이해를 위한 시간"이 바로 개발자가 최소화해야하는 값이다.
자신이 작성한 코드를 6개월 뒤에 봤을 때 낯설어 보이고, 이해가 되지 않는다면 위에서 언급한 다른사람에 자기자신 또한 포함될 수 있다.
분량이 적으면 항상 더 좋은가?
일반적으로, 분량이 더 적은 코드로 똑같은 문제를 해결할 수 있다면 더 낫다.
하지만 항상 더 좋은 것은 아니다.
때로는 분량이 적은 코드보다 많은 코드가 이해하기 더 쉬울 때도 있다. 주석처리 또한 "코드를 더하는" 행위이지만 코드를 더 빨리 이해하게 도와준다.
PART 1. 표면적 수준에서의 개선
표면적 수준은 좋은 이름을 짓고, 좋은 설명을 달고, 코드를 보기 좋게 정렬하는 것을 의미한다.
이는 작성하는 모든 코드에 영향을 줄 가능성이 있어 매우 중요하다. 이름이 훌륭하게 지어지고, 잘 작성된 설명문이 달리고, 알맞게 빈칸이 활용되면 훨씬 더 읽기 편할 것이다.
2. 이름에 정보 담기
흔히 사용하는 tmp와 같은 이름은 매우 모호하고 정보를 담아내지 못한다.
- 특정한 단어 고르기
- 보편적인 이름 피하기(혹은 언제 그런 이름을 사용해야하는지 깨닫기)
- 추상적인 이름 대신 구체적인 이름 사용하기
- 접두사 혹은 접미사로 이름에 추가적인 정보 덧붙이기
- 이름이 얼마나 길어져도 좋은지 결정하기
- 추가적인 정보를 담을 수 있게 이름 구성하기
특정한 단어 고르기
이름에 정보를 담아내는 방법 중 하나는 매우 구체적인 단어를 선택하여 "무의미한" 단어를 피하는 것.
class BinaryTree {
int size;
....
}
- size만 보고 무엇을 의미하는 변수인지 파악하기 어렵다.
- 즉, 의도한 정보를 전달하지 못한다.
- height, numNodes, memoryBytes 등이 더 의미있는 이름일 것이다.
단어 | 대안 |
send | deliver, dispatch, announce, distribute, route |
find | search, extract, locate, recover |
start | launch, create, begin, open |
make | create, set up, build, generate, compose, add, new |
재치 있는 이름보다 명확하고 간결한 이름이 더 좋다.
tmp나 retval 같은 보편적인 이름 피하기
변수의 목적이나 담고 있는 값을 설명해주어야 한다. 이런 이름은 변수의 목적을 직접적으로 나타내므로 버그를 잡는데 도움이 될 수도 있다.
for문을 돌면서 제곱의 값을 모두 더하는 코드가 있다고 생각해보자.
tmp += v[i] * v[i] // 원래 코드
tmp += v[i] // 잘못 작성된 코드
sum_squares += v[i] * v[i] // 정상 코드
sum_squares += v[i] // 잘못 작성된 코드
tmp 대신 sum_squares와 같은 변수의 목적을 변수명에 담게되면 버그를 잡는데에 훨씬 더 도움이 될 것이다.
하지만 만약 tmp가 단순히 임시 저장소 외에 다른 용도가 없다면 tmp라는 이름은 변수명으로써 완벽할 수도 있다.
if (right < left) {
tmp = right;
right = left;
left = tmp;
}
두 변수를 교환하는 전형적인 알고리즘에서 tmp라는 이름은 해당 변수가 임시 저장소 외에 다른 용도가 없음을 잘 전달하고 이는 다른 함수로 전달되거나 다시 초기화되거나 반복적으로 사용되는 변수가 아니므로 완벽하다.
tmp라는 이름은 대상이 짧게 임시적으로만 존재하고,
임시적 존재 자체가 변수의 가장 중요한 용도일 때에 한해서 사용해야한다.
보편적인 이름을 사용하려면, 꼭 그렇게 해야하는 이유가 있어야한다.
추상적인 이름보다 구체적인 이름을 선호하라
변수나 함수 혹은 다른 요소에 이름을 붙일 때, 추상적인 방식이 아니라 구체적인 방식으로 묘사하라.
추가적인 정보를 이름에 추가하기
변수의 이름은 작은 설명문이다. 이름 안에 끼워 넣은 추가 정보는 변수가 눈에 보일 때마다 전달된다.
예를 들어, 16진수 문자열을 담고 있는 id라는 변수가 있다면 이는 id보다는 hex_id로 작성되는 것이 더 나을 것이다.
변수가 시간의 양이나 바이트의 수와 같은 측정치를 담고 있다면, 변수명에 단위를 포함시키는 것이 도움이 된다.
함수 | 인수 단위를 포함하여 재작성 |
start(int delay) | delay -> delay_secs |
createCache(int size) | size -> size_mb |
throttleDownload(float limit) | limit -> max_kbps |
rotate(float angle) | angle -> degrees_cw |
이름에 추가적인 정보를 붙이는 것은 단위를 포함하는 값에 국한되지 않는다.
상황 | 변수명 | 더 나은 이름 |
password가 "plaintext"에 담겨있고, 추가적인 처리를 하기전에 반드시 암호화되어야 한다. |
password | plaintext_password |
사용자에게 보여지는 설명문(comment)이 화면에 나타나기 전에 escaping 처리가 되어야 한다. |
comment | unescaped_commet |
html의 바이트가 UTF-8로 변환되었다. | html | html_utf8 |
입력데이터가 "url encoded"되었다. | data | data_urlenc |
모든 변수에 이와 같은 추가적인 정보를 담는 것이 아니다. 변수의 의미를 잘못 이해했을 때 버그가 발생할 가능성이 있을 때만 이것이 의미가 있다. 변수의 의미를 제대로 이해하는 것이 중요하다면 그 의미를 드러내는 정보를 변수의 이름에 포함시켜야한다.
이름은 얼마나 길어야 하는가?
좁은 범위(scope)에서는 짧은 이름이 괜찮다.
if (debug) {
map<String, int> m;
LookUpNamesNumbers(&m);
print(m);
}
하지만 클래스의 멤버이거나 전역 변수일 때는 이야기가 달라진다. 사용 범위가 넓으면 긴 이름을 사용하는 것이 좋다.
경우에 따라서 아무런 정보를 손실하지 않으면서 이름에 포함된 단어를 제거할 수 있다.
예를 들어, ConvertToString()이라는 이름 대신 ToString()이라고 짧게 써도 실질적인 정보는 사라지지 않는다.
이름 포맷팅으로 의미를 전달하라
밑줄과 대시 그리고 대문자를 잘 이용하면 이름에 더 많은 정보를 담을 수 있다.
예를 들어, 클래스 멤버를 로컬 변수와 구분하기 위해서 뒤에 "_"을 붙일 수도 있다. 이렇게 하면 해당 변수가 로컬 변수인지 전역 변수인지 한눈에 파악하기 쉽다.
프로젝트나 언어에 따라서 포맷팅 관습은 여러가지가 존재할 수 있다. 이러한 관습을 실제로 사용할 지는 사용자와 그 팀의 결정이다.
3. 오해할 수 없는 이름들
본인이 지은 이름을 "다른 사람들이 다른 의미로 해석할 수 있을까?"라는 질문을 던져보며 철저하게 확인하자
filter => "고르는" 기능을 원한다면 select()를, "제거하는"기는을 원한다면 exclude()를
경계를 포함하는 한계값을 다룰 때는 min과 max를 사용하라
한계를 설정하는 이름을 가장 명확하게 만드는 방법은 제한받는 대상의 이름 앞에 max_나 min_을 붙이는 것이다.
- max_items
- max_counts
- max_chars
경계를 포함하는 범위에는 first와 last를 사용하라
이러한 배열이 존재할 때 index의 범위는 0 <= index <= 9로 포함할 수 있는데 이와 같이 경계를 포함하는 범위일 때는
first와 last를 사용하는 것이 좋다.(first = 0, last = 9)
min/max도 해당 문맥에서 "의미 있게 들린다면" 경계를 포함하는 범위에서 사용될 수 있다.
경계를 포함하고/배제하는 범위에는 begin과 end를 사용하라
범위의 한쪽 끝이 포함되지만 다른 한쪽 끝은 포함되지 않는 범위가 사용되곤한다.
오늘의 모든 시간을 나타내는 범위를 "5월30일 00:00 <= 시간 <= 5월30일 11:59"과 같이 나타내는 것 대신
"5월30일 00:00 <= 시간 < 5월 31일 00:00"과 같이 나태내는 것이다. 이럴 때는 begin과 end를 사용하는 것이 좋다.
Boolean 변수에 이름 붙이기
일반적으로 is, has, can, should 와 같은 단어를 더하면 Boolean값의 의미가 더 명확해진다. 또한 의미를 부정하는 용어를 피하는 것이 좋다.(disable 등과 같은)
- spaceleft -> has_spaceleft
사용자의 기대에 부응하기
사용자가 어떤 이름의 의미를 이미 특정한 방식으로 이해해서 실제로 다른 의미가 있음에도 오해를 초래할 때가 있다. 이런 경우에는 굴복하고 그것이 일반적인 의미를 갖도록 하는 것이 좋다.
ex) get~~(), size()
- 일반적으로 get이나 size와 같은 메서드명을 보고 프로그래머는 단순히 가벼운 접근자(getter), 크기를 반환하는 간단한 함수로 생각한다.
- 하지만 만약 get을 붙혀 작명한 메서드가 내부에서는 매우 복잡한 연산을 수행한다면?
- 사용자는 그것을 모른채 가볍게 여러 곳에서 가져와 사용한다면?
예약어(?), 관행처럼 프로그래머들이 생각할만한 이름은 사용하지 말거나, 사용한다면 그 내부도 관행처럼 사용되어야한다.
의마가 오해되지 않는 이름이 최선의 이름이다.
이름을 정하기 전에 항상 최악의 경우를 가정하고 이름의 의미가 잘못 이해되는 가능성을 고려해봐야 한다.
4. 미학
좋은 소스코드는 "눈을 편하게"해야 한다.
여기에 이용되는 3가지 원리가 있다.
- 코드를 읽는 사람이 이미 친숙한, 일관성 있는 레이아웃을 사용하라
- 비슷한 코드는 서로 비슷해 보이게 만들어라
- 서로 연관된 코드는 하나의 블록으로 묶어라
이러한 수정은 적용이 쉬우면서도 가독성을 상당히 향상시킨다.
일관성과 간결성을 위해서 줄 바꿈을 재정렬하기
다양한 네트워크 연결 속도에 따라서 프로그램 수행동작이 달라지는 방식을 측정하는 자바코드가 존재한다고 가정
생성자에서 parameter 4개를 받아들인다.(연결속도, 평균 대기시간, 대기시간의 흔들림, 패킷 손실)
줄 바꿈, 들여쓰기, 주석을 통해 코드를 일관성 있게 작성할 수 있다. 일관성 있는 패턴을 가지므로 훑어보기 용이하다.
하지만 수직 방향으로 너무 많은 빈칸을 사용하고 있고, 똑같은 주석도 3번씩 반복하고 있다.
주석을 맨 위로 올리고 모든 파라미터를 한 줄에 놓으므로써 더욱 간결하게 코드를 작성할 수 있다.
메소드를 활용하여 불규칙성을 정리하라
코드의 가독성이 줄 바꿈 정도로는 향상되지 않고, 같은 문자열이 반복되는 등의 문제가 발생한다면 이를 메소드로 만들어해결할 수 있다.
DatabaseConnection database_connection;
String error;
assert(ExpandFullName(database_connection, "Doug Adams", &error)
== "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, " Jake Brown ", &error)
== "Mr. Jacob Brown III");
assert(error == "");
줄이 너무 길어서 다음 줄까지 이어지고, 일관성 있는 패턴도 결여되었다.
CheckFullName("Doug Adams", "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown", "Mr. Jake Brown III", "");
void CheckFullNAme(String partial_name,
String expected_full_name,
String expected_error) {
String error;
String full_name = ExpandFullName(database_connection, partial_name, &error);
assert(error == expected_error);
assert(full_name == expected_full_name);
}
CheckFullName이라는 메소드를 만들고 공통부분을 추출한다면 훨씬 미학적으로 개선될 수 있다. 우리는 미학적으로 개선되는 것을 의도했지만 의도하지 않은 장점들이 존재한다.
- 중복된 코드를 없애서 코드를 더 간결하게 한다.
- 이름이나 에러 문자열 같은 테스트의 중요 부분이 한 눈에 보이게 모아졌다.
- 새로운 테스트 추가가 쉬워졌다.
코드를 "보기 예쁘게" 만드는 작업은 표면적인 개선 이상의 결과를 가져온다.
즉, 코드의 구조 자체를 개선시킨다.
도움이 된다면 코드의 열을 맞춰라
경우에 따라서 열 정렬로 코드를 더 읽기 쉽게 할 수도 있다.
의미 있는 순서를 선택하고 일관성 있게 사용하라
- 변수의 순서를 HTML 폼에 있는 <input> 필드의 순서대로 나열하라
- "가장 중요한 것"에서 시작해서 "가장 덜 중요한 것"까지 순서대로 나열하라
- 알파벳 순서대로 나열하라.
어떤 순서를 사용하든 코드 전반에 걸쳐서 일관된 방식으로 나열해야 한다.
선언문을 블록으로 구성하라
이렇게 작성하는 것보다는 논리적 영역에 따라서 여러 개의 그룹으로 나누면 더 좋을 것이다.
코드를 "문단"으로 쪼개라
바로 위의 내용과 비슷한 내용이다. 함수를 작성한다면 그 함수의 동작 과정에 대한 단계 별로 문단으로 나누고, 주석까지 추가한다면 사용자가 코드를 훑어보는 데 도움이 될 것이다.
void suggest_new_friends(user, email_password){
// 사용자 친구들의 이메일 주소를 읽는다.
--/ 코드 작성 /--
// 사용자의 이메일 계정으로부터 모든 이메일 주소 읽는다.
--/ 코드 작성/--
// 아직 친구가 아닌 사용자들을 찾는다.
--/ 코드 작성/--
// 추천 리스트를 화면에 출력한다.
--/ 코드 작성/--
}
개인적인 스타일 vs 일관성
일관성 있는 스타일은 "올바른" 스타일보다 더 중요하다.
'Book' 카테고리의 다른 글
HTTP 완벽 가이드 - 10장 : HTTP/2.0 (0) | 2021.11.08 |
---|---|
HTTP 완벽 가이드 - 4장. 커넥션 관리 (0) | 2021.11.05 |
모두의 네트워크 (0) | 2021.07.29 |
이펙티브 자바 3판(읽는 중) (0) | 2021.07.23 |
읽을 책 목록 (계속 추가 예정) (0) | 2021.05.27 |