Clean Code (2)

intro

어제 mac os를 sierra 로 업데이트 했다. desktop용으로 siri도 생기고, 한글 쓸 때 capslock으로 변경이 되고, 작업중 윈도우를 작게 변경해서(?) 뭐 이런 기능들이 새로 생긴 것 같다.

그리고 Mail app에서 인증서가 신원을 확인할 수 없는 서버라며 승인이 안되는 문제도 있었다…. (인터넷 찾아보니 인증서를 항상 신뢰 하도록 체크만 해주면 되는 문제였다… ㅎ)

자, 아무튼 이번에는 이름 선정에 대한 내용이다. (모든 소스코드는 Clean Code 책 내에 포함된 소스코드 입니다. 전 아무런 저작권이 없답니다… ^^)

그리고 스크롤 압박이 심하니 주의하시기 바랍니다.

의도를 분명하게 하라

말은 쉽다. 의도가 무엇인지 분명해야 보는 사람으로 하여금 스무~스 하게 이해하고 지나갈 수 있다. 근데 이게 말처럼 쉽지가 않다.

좋은 이름을 지으면 작명하는데 시간이 오래 걸리지만, 그로 인해 앞으로 사용할 때 시간을 절약할 수 있다. (올커니!)

이런 예를 한번 보자.

1
int d;	// 경과 시간(단위: 날짜)

d 의 의도를 알겠는가? 안다면 너님이 지으신 변수 일 것이다. 이런 작명은 땡이다. 아주. 완전히. 땡! 아래와 같이 짓는게 무릎을 탁! 치게 만든다.

1
2
3
4
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

자, 주석 따위 없어도 요놈이 무엇을 하는지 감이 온다. 잘 지었다.

이제 위와 같은 잘못된 작명으로 인한 예제를 한번 보자.

1
2
3
4
5
6
7
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}

혼란스럽다. x의 첫 번째 요소의 값이 4이면 ArrayList로 저장해서 반환하는건데 뭣땜에 첫 번째 요소가 4여야하고, 반환하는지 모르겠다.

위 메소드를 예쁜 작명으로 아래와 같이 바꿔보자.

1
2
3
4
5
6
7
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}

위의 소스코드는 같은 일을 한다. (로직도 같다) 하지만 아래 예제는 무슨 일을 하는지 짐작이 가지 않는가? (안간다면 반복해서 보시면 된다)

내가 두 번째 소스코드를 보고 느낀 느낌은 이렇다.

  1. 메소드 이름에서 flag가 설정된 cell을 반환할 거 같다.
  2. flaggedCells라는 ArrayList는 falg가 설정된 cell들이 들어 있을 것 같다.
  3. gameBoard라는 변수는 왠지 게임 판때기를 뜻할 것 같다.
  4. for-loop을 돌면서 gameBoard에서 뽑아낸 cell 이라는 배열은 cell의 상태값을 저장하는거 같다.
  5. cell의 상태가 FALGGED (상수겠지?) 라면 flaggedCells에 저장해서 리턴할 것 같다.

분명 같은 행위를 하는 메소드인데, 변수 이름만 바꿨을 뿐인데, 굉장히 많은 정보를 알게 되었다. (개신기!)

다음은 위의 소스코드를 클래스를 통해 바꾼 예제도 있다. 한번 감상해 보시라.

1
2
3
4
5
6
7
public List<Cell> getFlaggedCells() {
flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlaged())
flaggedCells.add(cell);
return flaggedCells;
}

그릇된 정보를 피하라

나름대로 널리 쓰이는 의미 있는 단어들은 가능하면 피하는게 좋겠다.

예를 들면, 직각삼각형의 빗변이 영어로 hypotenuse라고 변수명으로 int hp 해버리면 hp를 보고 컴퓨터 회사를 떠올릴 지도 모른다. (아님 말고 ㅎ)

또한 이건 내가 자주 쓰던 패턴이었는데 책에선 피하라고 적혀 있었다.

여러 값들을 하나로 묶을 때 List라는 단어를 생각해서 int AccountList 와 같은 형태로 변수를 짓지 않는게 좋단다.

List는 개발자에게 의미있게 널리 쓰이는 단어이기 때문이다.

또한 서로 흡사한 이름은 사용하지 말라고 당부하고 있다. 예를 들면

XYZControllerForEfficientHandlingOfString```
1
2
3
4
5
6
7
8
9
10
11
12
13

이라는 변수와 조금 떨어진 곳에서

```String XYZControllerForEfficientStorageOfString``` 이렇게 쓰면 겁나 헷갈리기 때문이다. (내가 예전에 이렇게 썼다가 머리에 쥐날 뻔 했다 ㅎ)

그리고 소문자 L과 대문자 O (숫자 0 아님에 주의) 사용에 주의해야 한다. l은 1처럼 보이고, O는 0 처럼 보인다.

```java
int a = 1;
if (O == 1)
a = Ol;
else
l = 01;

보기만 해도 속 터진다.

의미 있게 구문하라

동일한 클래스 내에서, 비슷한 개념의 두 객체에 대해, 어떻게 이름을 지어야 잘 했다고 소문이 날 까 고민을 해본 경험이 있을 것이다. (class가 있으니 klass라고 짓자. 뭐야 이건!)

에라 모르겠다, 숫자만 바꾸자.

1
2
3
4
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++)
a2[i] = a1[i];
}

그래. 메소드 이름을 보니 character를 copy 한다는 건 알겠다. 근데 어떤 놈을 어떤 놈에게 copy하는 걸까?

이렇게 바꿔보자.

1
2
3
4
public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++)
destination[i] = source[i];
}

좀 더 명확해졌다. (무릎 탁!)

불용어 라는 단어가 등장하는데 영어로는 noise word 되시겠다. 느낌이 오는가? 쓰면 될까 안될까?

이런 느낌이다. Object ProductInfoObject ProductData 둘 중 차이점을 알겠는가? Object zorkObject theZork의 차이점을 알겠는가?

읽는 사람이 차이를 알도록 이름을 짓는게 중요하다.

발음하기 쉬운 이름을 사용하라

말 그대로 발음하기 쉬운 단어들을 변수명 혹은 클래스, 메소드 기타 등등 이름으로 지으면 된다.

1
int bcr3cntepsgq;

이거 어떻게 읽을꺼니?

이런 예제도 한번 보자

1
2
3
4
5
6
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
/* ... */
}

신입 개발자: 제니므드흠스 변수는 대체 뭔가요?

1
2
3
4
5
6
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
/* ... */
}

두 번째 예제 코드를 이용하면 개발자들끼리 서로 지적인 대화가 가능해진다.

신입 개발자: 제너레이션타임이 최초 생성된 시간을 의미하겠군요. 아하! (무릎 탁!)

시니어 개발자: 그렇지!

검색하기 쉬운 이름을 사용하라

예를 들면 (설마 이러진 않겠지만) 변수 명을 int e 라고 선언했고, 중요한 변수라고 치면 이 변수는 검색으로 어떻게 찾을 것인가?

이러한 한 글짜나 짧은 변수명은 로컬 변수로 잠깐 사용하고 말 때만 쓰는게 좋다. for-loop에서 쓸 int i 라던가.

여기서 한가지 팁은 이름 길이는 범위 크기에 비례해야 한다. 라고 한다. 길이가 길면 찾기도 수월할테니 말이다.

아래 예제를 보자.

1
2
3
for (int j = 0; j < 34; j++) {
s += (t[j] * 4) / 5;
}

뭐야 이게 (짜증이 밀려온다.)

1
2
3
4
5
6
7
8
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
sum += realTaskWees;
}

두 소스코드를 비교하면 아래 소스코드가 훨씬 이해하기 좋다. (두 번째가 더 많은 선언이 보여서 그렇다면 기분탓?)

5를 검색 하는 것 보다는 WORK_DAYS_PER_WEEK 을 검색하는게 더 좋다.

인코딩을 피하라

여기서 인코딩은 변수명을 암호화 하지 말라는 의미 인 것 같다. (UTF-8을 euc-kr로 바꾼다거나 이런 거창한게 아닌듯)

그러면서 헝가리안 표기법이 나오는데, 요즘엔 언어도 많이 발달하고 IDE도 발달해서 타입이 뭔지 바로 알 수 있으니깐 안쓰는게 덜 복잡하다.

예제를 한번 보면

1
2
3
4
5
public class Part {
private String m_dsc; // 설명 문자열
void setName(String name) {m_dsc = name;
}
}

의미는 알겠으니 큰 상관은 없겠으나 m_ 이라는 접두사는 살짝 거슬리는건 기분탓?

1
2
3
4
5
6
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}

이게 조금 더 명확한 기분이다.

추가로, 인터페이스 이름을 지을 때에도 앞에 ‘I’를 접두사로 붙히곤 한다. 근데 굳이… 그럴 필요가 있을까? 느슨한 연결을 위해 인터페이스를 사용하는데 굳이 I를 붙혀서 ‘나 인터페이스지롱~’ 할 필요는 없을 것 같다. 는게 저자의 마인드.

차라리 구현 클래스에 접미사로 impl을 붙히는게 낫다고 한다.

자기 기억력을 자랑하지 마라

다른 사람의 코드를 읽는데 변수 이름이 이해가 안되서 뭔가 다른 이름으로 변환해야 한다면… 잘못 된 변수명이다. (당연하잖아?)

또한, 나는 url을 r 이라는 변수명으로 지어서 써야지~ 하고 프로그램 전체에 걸쳐 r을 이용해 10만라인짜리 소스코드를 뚝딱뚝딱 만들었다. (r 이 뭔지 안헷갈렸다면 당신은 굉장히 똑똑하다)

근데 이러면 다른 사람이 코드를 읽는데 r이 뭐하는 놈인지 자꾸 확인하면서 소스코드를 읽어야 할 것이다. (아 귀찮게 말이야)

똑똑한 개발자와 전문 개발자사이의 차이점은 명료함이 최고 라고 한다. (r 은 명료하다기 보단 걍 귀차니즘 같은 스멜)

클래스 이름

클래스 이름과 객체 이름은 명사 혹은 명사구를 이용한다. (동사는 메서드에게 양보하세요)

메서드 이름

메서드 이름은 동사 혹은 동사구가 적합하다. (명사는 클래스에게 양보하세요)

접근자(Accessor), 변경자(Mutator), 조건자(Predicate) 는 javabean 표준에 따라 앞에 get, set, is 를 붙인다.

1
2
3
String name = employee.getName();
customer.setName("Hun");
if (paycheck.isPosted())...

그리고 생성자(Constructor)를 중복 정의(Overload) 할 경우에는 정적 팩토리 메소드(static factory method)를 이용하라는 꿀팁도 있다.

1
Complex fulcrumPoint = new Complex(23.0);

보다는

1
Complex fulcrumPoint = Complex.FromRealNumber(23.0);

이 더 명확하다.

기발한 이름은 피하라

유머 센스가 넘쳐 흐르는 개발자라서 kill() 메소드를 whack() 이라고 표현할 수 있다.

하지만 일반적인 사람들은 whack()이 뭔지 모를 것이다.

의도를 분명하고 솔직하게 표현하는게 짱.

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택하고 이를 고수해야 한다. (고수가 아니라 이 고수다.)

예를 들어 데이터를 가져오는 메소드의 명칭을 어디선 fetch, 어디선 retrieve, 어디선 get 이래버리면 소스코드를 트래킹 할 때마다 짜증이 밀려올 것이다. (시간 소모는 덤)

말장난을 하지 마라

다른 개념에 같은 단어를 사용한다면, 이게 말장난이라고 언급하고 있다. (다른 개념인데 이름이 같으면 같은 애라고 착각을 일으킬 것이다. 적어도 나는 그래.)

좋은 코드는 집중적인 탐구가 필요한 코드가 아니라 대충 훑어봐도 이해할 코드 이다.

해법 영역에서 가져온 이름을 사용하라

문제 영역과 해법 영역을 분리해서 설명하고 있다.

문제 영역은 해당 기능의 구현이 필요한 곳. 즉, 문제가 발생한 곳을 의미하고,

해법 영역은 문제 영역에서 발생한 문제들을 해결하기 위한 해법들이 있는 영역을 의미한다.

그렇다면 해법 영역에서 가져온 이름이란 놈들의 예를 들어보면, 우리는 개발자니깐 전산 용어들을 많이 알고 있을 것이다.

jobQeue, stack, list 이런 애들이 해법 영역에서 가져온 이름이 된다.

문제 영역에서 가져온 이름을 사용하라

만약, 위에서 언급한 해법 영역에서 가져올 만한 이름이 없다면? 차선책으로 문제 영역에서 가져온 이름을 사용하면 된다.

모르는 단어가 나오면 해당 업무 담당자나 전문가에게 물어봐서 의미를 파악하면 된다. (이건 귀찮네)

그리고, 문제 영역 개념과 관련이 깊은 코드라면, 문제 영역에서 이름을 가져오는게 맞다.

의미 있는 맥락을 추가하라

아래와 같은 변수들이 있다.

1
2
3
4
5
6
7
String firstName;
String lastName;
String street;
int houseNumber;
String city;
String state;
int zipcode;

뭐하는 변수인지 감이 온다. 바로 주.소. 같지 아니한가?

근데 만약, String state 하나만 떼어놓고 본다면, 과연 주소라고 생각할 수 있을까?

이럴 경우 명확한 맥락을 위해 접두사를 아래와 같이 붙혀보자.

1
2
3
4
5
6
7
String addrFirstName;
String addrLastName;
String addrStreet;
int addrHouseNumber;
String addrCity;
String addrState;
int addrZipcode;

이제 String addrState 하나만 갖다 써도 아! 주소구나! 라고 알 수 있을 거다.

또 한가지 예를 보자. 이 경우는 메소드 명은 일부 맥락만을 제공하고, 코드를 모두 읽으면 알고리즘을 통해 맥락을 파악할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
print(guessMessage);
}

이해를 돕기 위해, (바로 내 이해) 영어 단어 몇개의 뜻을 아래에 적어둔다. (아 영어 제길)

1
2
candidate: 후보
plural: n. 복수, a. 복수형의

위 소스코드는 String guessMessage 이부분이랑 잘 엮어서 봐야 무슨 일을 하는 메소드인지 명확해진다. (잘 모르겠으면 알때까지 보자)

위 예제를 클래스로 만들어서 if 구문을 각각의 메소드로 분리했다. 좀더 보기 좋은 맥락을 위해!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;

public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.foramt(
"There %s %s %s%s",
verb, number, candidate, pluralModifier);
}

private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}

private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}

private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}

private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}

불필요한 맥락을 없애라

일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서 말이다. 즉, 이름에 불필요한 맥락을 추가하지 않도록 주의하자.

accountAddress와 customerAddress는 Address 클래스의 인스턴스로는 괜찮다. 하지만 클래스 이름으론 빵점이다 빵점!

결론

우리는 영어로 코드를 만든다. (물론 한글로 변수명, 클래스명을 만들어도 돌아는 간다.) 그럼 역시 영어를 잘 해야 한다.

영어 공부 하자.