[책요약] 읽기 좋은 코드가 좋은 코드다 2장 이름에 정보 담기
2020-04-23
2. 이름에 정보 담기
- 변수, 함수, 클래스 등의 이름을 결정할 때는 항상 같은 원리가 적용된다.
- 이름을 일종의 설명문으로 간주해야 한다.
- 충분한 공간은 아니지만, 좋은 이름을 선택하면 생각보다 많은 정보를 전달할 수 있다.
이름에 정보를 담아내라
- 프로그램에서 사용하는 대다수 이름이 tmp처럼 모호하다.
- size 혹은 get처럼 그럴듯하게 보이는 이름조차 많은 정보를 담아내지 못한다.
- 정보를 잘 표현하는 좋은 이름을 선택하는 방법
- 특정한 단어 고르기
- 보편적인 이름 피하기(혹은 언제 그런 이름을 사용해야 하는지 깨닫기)
- 추상적인 이름 대신 구체적인 이름 사용하기
- 접두사 혹은 접미사로 이름에 추가적인 정보 덧붙이기
- 이름이 얼마나 길어져도 좋은지 결정하기
- 추가적인 정보를 담을 수 있게 이름 구성하기
2.1 특정한 단어 고르기
- 매우 구체적인 단어를 선택하여 ‘무의미한’ 단어를 피하는 것이다.
- 예를 들어 ‘get’은 지나치게 보편적이다.
def GetPage(url):
...
get
은 별다른 의미를 전달하지 않는다.- 로컬캐시, db, 인터넷 중 어디에서 페이지를 가져오는 것인가?
- 만약에 인터넷에서 가져오는 것이라면
FetchPage()
혹은DownloadPage()
가 더 의미 있는 이름이 될 것이다.
- 다음은 BinaryTree 클래스의 예이다.
class BinaryTree {
int Size();
...
};
- Size() 메소드는 무엇을 반환할까? 트리의 높이? 노드의 개수? 트리의 메모리 사용량?
- 우리가 의도한 정보를 전달하지 못하는 문제가 있다.
- Height(), NumNodes(), MemoryBytes() 등이 더 의미 있는 이름일 것이다.
- 다음과 같은 Thread 클래스가 있다고 해보자
class Thread {
void Stop();
...
};
- Stop() 이라는 메소드명은 그런대로 괜찮지만, 정확히 무엇을 수행하는지에 따라서 더 의미있는 이름을 사용할 수 있다.
- 다시는 되돌릴 수 없는 최종 동작을 수행한다면 Kill()이 더 확실한 의미를 전달할 것이다.
- 만약 Resume()을 호출해서 다시 돌이킬 수 있는 동작이라면 Pause()가 더 좋을 것이다.
더 ‘화려한’ 단어 고르기
- 유의어 색인집을 찾아보거나 동료에게 더 나은 이름을 묻는 일을 주저하면 안된다.
- 영어는 매우 풍부한 언어이며, 선택할 수 있는 단어는 무궁무진한다.
- 다음 표는 어떤 단어와 그 단어보다 상황에 더 적합할 수 있는 ‘화려한’ 단어를 예로 나열한 것이다.
단어 | 대안 |
---|---|
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 |
- 하지만 화려한 단어가 꼭 좋은 것은 아니다.
- PHP는 문자열은 explode()하는 함수가 있다.
- 이는 상당히 화려한 이름으로, 무언가를 조각으로 잘게 부서뜨리는 느낌을 잘 전달하지만, split()과 무엇이 다른가?
- 두 함수는 서로 다르지만, 이름만으로는 무엇이 다른지 구별할 수 없다.
재치있는 이름보다 명확하고 간결한 이름이 더 좋다.
2.2 tmp나 retval 같은 보편적인 이름 피하기
- tmp, retval, foo 같은 이름
- ‘내 머리로는 이름을 생각해낼 수 없어요’라고 고백하면서 책임을 회피하는 증거에 불과하다
- 이렇게 무의미한 이름이 아니라, 개체의 값이나 목적을 명확하게 설명하는 이름을 골라야 한다.
- 다음은 retval을 이용하는 자바스크립트 코드이다.
var euchildean_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);
};
- 반환되는 값의 이름을 생각하기 어려울 때면 그냥 retval라는 이름을 이용하고 싶은 유혹이 만만치 않다.
- 하지만 retval 이라는 이름은 ‘저는 사실 반환되는 값이랍니다.’ 라는 정보 이외에는 아무것도 담지 않는다.
- 더 좋은 이름은 변수의 목적이나 담고 있는 값을 설명해주어야 한다.
- 위의 예제에서는 변수는 v를 제곱한 값을 모두 더한 값을 담고 있다.
- 이 상황에서 더 좋은 이름은 sum_squares다
- 이런 이름은 변수의 목적을 직접적으로 나타내므로 나중에 버그를 잡는 데 도움이 될 수도 있다.
- 예를 들어 실수로 루프의 내부가 다음과 같이 작성되었다고 하자
retval += v[i];
- 만약 변수명이 sum_squares였다면 다음 처럼 버그는 더 명백하게 드러날 것이다.
sum_squares += v[i]; // 더해야 하는 '제곱'은 어디에 있다는 말인가? 버그다!
retval이라는 이름은 정보를 제대로 담고 있지 않다. 대신 변수값을 설명하는 이름을 사용하라.
- 하지만 오히려 보편적인 이름이 필요한 의미를 전달해주는 경우도 있다. 그러한 예를 살펴보자.
tmp
- 두 변수를 서로 교환하는 전형적인 알고리즘을 생각해보자.
if (right < left) {
tmp = right;
right = left;
left = tml;
}
- 이 경우는 tmp라는 이름이 완벽하다
- 변수의 목적 자체가 코드 몇줄에서만 사용하는 임시저장소 역할로 제한되어 있다.
- 이때 tmp라는 이름은 코드를 읽는 사람에게 변수가 임시저장소 이외에 다른 용도가 없다는 사실을 잘 전달한다.
- 하지만 다음 코드에서 사용된 tmp는 게으름의 산물에 불과하다
String tmp = user.name();
tmp += ' ' + user.phone_number();
tmp += ' ' + user.email();
...
template.set('user_info', tmp);
- 이 변수도 삶의 주기가 짧긴 하지만, 변수의 주된 목적은 임시저장소의 역할로 국한되지 않는다.
- 이럴 땐 user_info 같은 이름이 더 적절하다.
- 다음은 이름에 tmp라는 단어가 필요하긴 하지만, 이름 전체가 아니라 일부분이 되어야 하는 경우이다
tmp_file = tempfile.NamedTeporaryFile();
...
SaveData(tmp_file, ...);
- 대상이 파일 객체이므로 이름을 tmp가 아니라 tmp_file로 하였다.
- 만약 단순히 tmp로 했으면, 파일인지, 파일 이름인지, 파일에 기록되는 데이터 자체를 의미하는 지 알 수 없다.
tmp라는 이름은 대상이 짧게 임시적으로만 존재하고, 임시적 존재 자체가 변수의 가장 중요한 용도일 때에 한해서 사용해야 한다.
루프반복자
- i, j, iter, it 같은 이름은 흔히 인덱스나 루프반복자로 사용된다.
- 이런 이름은 보편적이지만 ‘나는 반복자랍니다’ 라는 의미를 충분히 전달한다.
- 하지만 이러한 이름을 다른 목적으로 사용하면 혼동을 초래 할 것이다. 그렇게 하지 말아야 한다.
- i, j, k보다 루프 반복자에 더 좋은 이름이 있다.
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])
....
- if 구문에서 잘못된 인덱스를 사용하고 있다.
- 이런 종류의 버그는 따로 떼어놓고 보면 잘못된게 없어 보여서 좀처럼 찾기 힘들다.
- 이럴 때는 더 명확한 의미를 드러내는 이름을 사용하면 도움이 된다.
- club_i, members_i, users_i
2.3 추상적인 이름보다 구체적인 이름을 선호하라
- 변수나 함수 혹은 다른 요소에 이름을 붙일 때, 추상적인 방식이 아니라 구체적인 방식으로 묘사하라
- 예를 들어 서버가 어느 tcp/ip 포트를 사용할 수 있는지 검사하는 ServerCanStart()라는 내부 메소드가 있다고 하자.
- 이때 ServerCanStart()라는 이름은 다소 추상적이다.
- 이보다 더 구체적인 이름은 CanListenOnPort()이다.
- 이 이름은 해당 메소드가 수행하는 일을 직접적으로 설명한다.
예: –run_locally
- 우리가 작성한 프로그램에서 –run_locally라는 명령행 플래그 옵셥을 사용한 적이 있다
- 이 플래그가 선택되면 프로그램은 디버깅 정보를 출력한다. 대신 동작 속도는 다소 느려진다.
- –run_locally라는 이름이 어떻게 탄생되었는지 어느 정도 이해는 되지만 이 이름에는 몇가지 문제점이 있다.
- 팀에 새로 합류한 사람은 무엇을 위한 플래그인지 알 수 없다. 프로그램을 로컬 컴퓨터에서 실행할 때는 (대충 짐작으로) 플래그를 사용하겠지만, 왜 필요한지는 알지 못한다.
- 때로는 프로그램을 원격컴퓨터에서 실행할 때도 디버깅 정보를 출력할 수 있다. 원격으로 실행되는 프로그램에 –run_locally 라는 플래그를 전달하는 우스운 광경으로 인하여 혼란이 초래된다.
- 로컬 컴퓨터에서 성능검사를 수행할 때 로깅 기능으로 인한 성능저하를 원하지 않는 경우도 있다. 이때는 –run_locally라는 플래그를 사용하지 않을 것이다.
- 문제는 –run_locally라는 이름이 실제 내용보다 주로 사용된느 환경을 나타내는 방식으로 지어졌다는 점이다.
- 이보다는 –extra_logging이라는 이름이 더 직접적이고 명확하다.
- 하지만 –run_locally 플래그가 추가적인 로깅 이외에 다른 일도 해야 한다면 어떻게 할까?
- 예를 들어 로컬 데이터베이스를 설정하고 사용하는 일을 수행한다고 하자.
- 이 경우 –run_locally라는 이름이 추가적인 로깅과 로컬 데이터베이스 설정이라는 두 가지 동작을 모두 포괄할 수 있으므로 매력적으로 보인다.
- 하지만 이름을 선택하는 이유가 모호하고 간접적이므로 좋은 이름이라 볼 수 없다.
- –use_local_database 라는 두 번째 플래그를 만드는 것이 더 좋은 방법이다.
2.4 추가적인 정보를 이름에 추가하기
- 앞에서 언급한 바와 같이 변수의 이름은 작은 설명문이다.
- 충분한 공간은 아니지만 이름안에 끼워 넣은 추가 정보는 변수가 눈에 보일 때마다 전달된다.
- 따라서 사용자가 반드시 알아야 하는 변수와 관련한 중요한 정보를 추가적인 ‘단어’로 만들어서 이름에 붙이는 것이 좋다.
- 예를 들어 16진수 문자열을 담고 있는 변수가 있다고 해보자
string id; // example: 'af84ef845cd8'
- 사용자가 id의 내용을 기억해야 된다면 변수명을 hex_id로 하는 편이 더 나을 것이다.
단위를 포함하는 값들
- 변수가 시간의 양이나 바이트의 수와 같은 측정치를 담고 있다면, 변수명에 단위를 포함시키는 게 도움이 된다.
- 다음은 웹페이지를 로딩하는 시간을 측정하는 자바스크립트 코드이다.
var start = (new Date()).getTime(); // 페이지의 맨 위
...
var elapsed = (new Date()).getTime() - start; // 페이지의 맨 아래
console.log('load time was: ' + elapsed + ' seconds');
- 이 코드는 특별한 오류를 발생시키지는 않지만 getTime()이 초가 아니라 밀리초를 반환하기 때문에 잘못된 결과를 출력한다.
- 변수에
_ms
를 추가하면 모든 문제가 명확해진다.
함수 | 인수 단위를 포함하게 재작성 |
---|---|
Start(int delay) | delay -> delay_secs |
CreateCache(int size) | size -> size_mb |
ThrottleDownload(float limit) | limit -> max_kbps |
Rotate(float angle) | angle -> degrees_cw |
다른 중요한 속성 포함하기
- 이름에 추가적인 정보를 붙이는 기술은 단위를 포함하는 값에 국한되지만은 않는다.
- 어떤 변수에 위험한 요소 혹은 나중에 놀랄만한 내용이 있다면 언제든지 이 방법을 사용할 필요가 있다.
- 예를 들어 대부분의 보안 취약점은 프로그램이 전달받는 일부 데이터가 아직 불안전하다는 사실을 제대로 인식하지 못할 때 발생한다.
- 이러한 부분은 untrustedUrl이나 unsafeMessageBody와 같은 변수명을 사용하여 보완하는 게 좋다.
- 안전하지 않은 입력을 안전하게 만드는 함수를 호출한 다음에, 동일한 데이터의 내용을 trustedUrl이나 safeMessageBody와 같은 변수명에 담는다.
상황 | 변수명 | 더 나은 이름 |
---|---|---|
패스워드가 ‘plaintext’에 담겨있고, 추가적인 처리를 하기 전에 반드시 암호화 되어야 한다. | password | plaintext_password |
사용자에게 보여지는 설명문이 화면에 나타나기 전에 이스케이프 처리가 되어야 한다. | comment | unexcaped_comment |
html의 바이트가 UTF-8로 변환되었다. | html | html_utf8 |
입력데이터가 ‘url encoded’되었다. | data | data_urlenc |
- 그렇다고 모든 변수에
unescaped_
혹은_utf8
같은 추가 적인 정보를 담을 필요는 없다. - 누군가 변수를 잘못 이해했을 때, 심각한 결과를 낳을 가능성이 있을 때만 중요한 의미가 있다.
2.4 이름은 얼마나 길어야 하는가?
- 좋은 이름을 선택할 때, 이름이 지나치게 길면 안된다는 제한이 암묵적으로 존재한다.
- 다음과 같은 이름을 사용하려는 사람은 아무도 없을 것이다.
- newNavigationControllerWrappingViewControllerForDataSourceOfClass
- 이름이 길면 길수록 기억하기도 어렵고, 화면도 많이 차지한다.
- 반면 오직 단어 하나 문자 하나로 된 이름을 사용할지도 모른다.
- 변수명을 d, days, day_since_last_update 중에 어떤 걸로 할지 어떻게 결정하는가?
- 정확한 용도에 따라 그때그때 판단해야 한다.
좁은 범위에서는 짧은 이름이 괜찮다.
- 좁은 범위에서는 변수의 타입이 무엇인지, 초기값이 무엇인지 등의 정보가 쉽게 한눈에 보이므로 짧은 이름을 사용해도 상관없다.
if (debug) {
map<string, int> m;
LookUpNamesNumbers(&m);
Print(m);
}
- 여기서 m은 아무런 정보를 담고 있지 않지만, 필요한 모든 정보를 코드에서 쉽게 확인 할 수 있으므로 문제 될 게 없다.
- 하지만 큰 범위를 갖는다면, 의미를 분명하게 만들기 위한 정보를 충분히 포함해야 한다.
긴 이름 입력하기 - 더 이상 문제가 되지 않는다.
- 긴 이름을 사용하면 안 되는 이유는 많다.
- 하지만 ‘긴 이름은 입력하기 어렵다’는 변명은 더 이상 성립하지 않는다.
- 프로그램이 문서 편집기에는 ‘단어 완성 기능’이 있다.
약어와 축약형
- 때때로 짧은 이름을 위해서 약어나 축약형을 사용한다.
- 예를 들어 BackEndManager 대신 BEManager라는 이름을 사용한다.
- 경험에 비추어보면 특정 프로젝트에 국한된 의미를 가진 약어 사용은 좋은 생각이 아니다.
- 이런 이름은 프로젝트에 새로 합류한 사람에게 비밀스럽고 위협적인 모습으로 다가온다.
- 우리의 규칙은 이렇다.
- 팀에 새로 합류한 사람이 이름이 의미하는 바를 이해할 수 있을까? 그렇다면 괜찮은 이름이다.
- 예를 들어 evaluation 대신 eval을 사용하고, document대신 doc를 사용하고, string 대신 str을 사용하는 경우는 꽤 흔하다.
- 그렇기 때문에 새로온 프로그래머도 FormatStr()이라는 함수를 보면 의미하는 바를 이해할 수 있을 것이다.
- 하지만 BEManager의 의미는 이해할 수 없다.
불필요한 단어 제거하기
- 경우에 따라서 아무런 정보를 손실하지 않으면서 이름에 포함된 단어를 제거할 수도 있다.
- 예를 들어 ConverToString()이라는 이름 대신 ToString()이라고 짧게 써도 실질적인 정보는 사라지지 않는다.
- 마찬가지로 DoServerLoop() 대신 ServerLoop()라고 해도 의미는 충분히 명확하다.
2.5 이름 포맷팅으로 의미를 전달하라
- 밑줄과 대시, 그리고 대문자를 잘 이용하면 이름에 더 많은 정보를 담을 수 있다.
- 다음은 구글 오픈소스 프로젝트에서 사용되는 포맷틴 관습을 따르는 c++코드이다.
static const int kMaxOpenFiles = 100;
class LogReader {
public:
void OpenFile(string local_file);
private:
int offset_;
DISALLOW_COPY_AND_ASSIGN(LogReader);
}
- 문법적 차이가 드러나게 서로 다른 개체의 이름에 각자 다른 포맷팅 방식을 적용하는 방법은 코드를 더 읽기 쉽게 해준다.
- 이 예에서 사용된 포맷팅 방식은 클래스명을 CamelCase라고 쓰고 변수명을 lower_separated라고 썼는데, 상당히 흔한 관습이다.
요약
- 특정한 단어를 사용하라
- 예를 들어 Get 대신 Fetch나 Download를 사용하는 것이 더 나을 수 있다.
- 꼭 그래야 하는 이유가 없다면 tmp나 retval과 같은 보편적인 이름의 사용을 피하라.
- 대상을 자세히 묘사하는 구체적인 이름을 이용하라
- ServerCanStart()는 CanListenOnPort()에 비해 의미가 모호하다
- 변수명에 중요한 세부 정보를 덧붙여라
- 예를 들어 밀리초의 값을 저장하는 변수 뒤에
_ms
를 붙이거나 이스케이핑을 수행하는 변수의 앞에raw_
를 붙이는 것이다.
- 예를 들어 밀리초의 값을 저장하는 변수 뒤에
- 사용 범위가 넓으면 긴 이름을 사용하라
- 여러 페이지에 걸쳐서 사용되는 변수의 이름을 하나 혹은 두 개의 짧은 문자로 구성해 의미를 아라보기 힘들게 짓지 말라.
- 다만 적은 분량(좁은 범위)에서 잠깐 사용되는 변수명은 짧은수록 더 좋다.
- 대문자나 밑줄 등을 의미 있는 방식으로 활용하라
- 예를 들어 클래스 멤버를 로컬 변수와 구분하기 위해서 뒤에 ‘_‘를 붙일 수 있다.
Comments