📕 목차
1. 의도를 분명히 밝혀라
2. 그릇된 정보를 피하라
3. 의미 있게 구분하라
4. 발음하기 쉬운 이름을 사용하라
5. 검색하기 쉬운 이름을 사용하라
6. 인코딩을 피하라
• 헝가리식 표기법
• 멤버 변수 접두어
• 인터페이스 클래스와 구현 클래스
7. 자신의 기억력을 자랑하지 마라
8. 클래스 이름
9. 메서드 이름
10. 기발한 이름은 피하라
11. 한 개념에 한 단어를 사용하라
12. 말장난을 하지 마라
13. 해법 영역에서 가져온 이름을 사용하라
14. 문제 영역에서 가져온 이름을 사용하라
15. 의미 있는 맥락을 추가하라
16. 불필요한 맥락을 없애라
1. 의도를 분명히 밝혀라
좋은 이름을 지으려면 시간이 걸리지만, 좋은 이름으로 절약하는 시간이 훨씬 많다.
변수나 함수 그리고 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 한다.
- 변수(혹은 함수나 클래스)의 존재 이유는?
- 수행 기능은?
- 사용 방법은?
int d; // 경과 시간(단위: 날짜)
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
💡 따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 말이다.
- d라는 변수는 아무 의미도 드러나지 않는다.
- 결과 시간이나 날짜라는 느낌이 들기 위해서는 측정하려는 값과 단위를 표현하는 이름이 필요하다.
아래 코드는 어떠한가?
복잡한 문장이 없음에도 코드가 하는 일을 짐작하기가 어렵다.
public class Main {
List<int[]> theList = new ArrayList<>();
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<>();
for (int[] x : theList) {
if (x[0] == 4) {
list1.add(x);
}
}
return list1;
}
}
💡 문제는 코드의 단순성이 아니라 코드의 함축성이다.
- theList에 무엇이 들어있는가?
- theList에서 0번째 값이 어째서 중요한가?
- 값 4는 무엇을 의미하는가?
- 함수가 반환하는 리스트 list1을 어떻게 사용하는가?
코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 하지만 정보 제공은 충분히 가능했었다.
위 코드는 지뢰찾기의 일부라고 가정해보자. 그럼 theList는 gameBoard로 바뀌어야 한다.
public class Main {
List<int[]> gameBoard = new ArrayList<>();
static int STATUS_VALUE = 0;
static int FLAGGED = 4;
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
}
- 코드의 단순성은 변하지 않았다.
- 연산자 수와 상수 개수 또한 변하지 않았다.
✒️ 제네릭과 열거 타입 적용해보기
항상 이론으로만 공부하던 내용을 직접 적용해보았다.
class Cell {
private enum Status {
VALUE(0), FLAGGED(4);
private final int status;
Status(int status) { this.status = status; }
public int getStatus() { return status; }
}
private int[] cell = new int[5];
public boolean isFlagged() {
return cell[Status.VALUE.getStatus()] == Status.FLAGGED.getStatus();
}
}
class Foo<T extends Cell> {
List<T> gameBoard = new ArrayList<>();
public List<T> getFlaggedCells() {
List<T> flaggedCells = new ArrayList<>();
for (T cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
}
- Cell 클래스를 분리하는 내용은 책에서 나왔다.
- 여기서 상수 값을 처리하기 위한 Status 열거 타입을 추가했다.
- 상수값을 바로 노출시키지 않고, 메서드를 통해 플래그가 설정된 셀만을 추출하여 반환하는 isFlagged 추가
- Foo 클래스의 경우엔 클래스 수준의 제네릭을 사용하였다.
- 한정적 타입 토큰을 이용하여 Cell 하위 클래스만을 다루도록 제한하였다.
2. 그릇된 정보를 피하라
1️⃣ 나름대로 널리 쓰이는 의미가 있는 단어를 함부로 사용하지 마라
- 약어를 사용할 때 주의해라
- hp, aix, sco는 유닉스 플랫폼이나 유닉스 변종을 가리키는 이름이다.
- 직각삼각형의 빗변(hypotenuse)의 약어로 hp가 훌륭해 보이겠지만 그렇지 않다.
- 실제 List가 아니라면, 변수명에 List를 붙이지 않는다.
- 여러 계정을 그룹으로 묶는다고 해서 accountList라 명명하지는 않는다.
- accountGroup, bunchOfAccounts, 아니면 단순히 Accounts라고 해라
2️⃣ 흡사한 이름을 사용하지 마라
- 한 모듈에서 XYZControllerForEfficientHandlingOfStrings라는 이름을 사용하고, 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings라는 이름을 사용한다면?
- 두 단어는 너무나도 비슷하다.
3️⃣ 유사한 개념은 유사한 표기법을 사용하라
class GameCharacter {
...
public void move() {...}
public void attack() {...}
}
class GameController {
public void controlCharacter(GameCharacter character) {...};
}
// 보다 일관성있는 네이밍
class GameController {
// ...
public void moveCharacter(GameCharacter character) { character.move(); }
public void attackCharacter(GameCharacter character) { character.attack(); }
}
- 일관성이 떨어지는 표기법은 그릇된 정보다.
- 유사한 개념을 다루는 코드 요소(변수, 메서드, 클래스 등)들이 서로 유사한 표기법을 공유하는 것이 좋다.
4️⃣ 소문자 L과 대문자 O를 주의하라
- 소문자 L과 대문자 I와 숫자 1, 그리고 대문자 O와 숫자 0 은 서로 구분이 굉장히 힘들다.
3. 의미 있게 구분하라
컴파일러를 통과할지라도 연속된 숫자를 덧붙이거나 불용어(noise word)를 추가하지 마라
📌 연속된 숫자의 문자
public static void copyChars(char[] a1, char[] a2) {
for (int i = 0; i < a1.length; i++)
a2[i] = a1[i];
}
- 연속적인 숫자를 덧붙인 이름(a2, a2, ..., aN)은 그릇된 정보는 커녕 아무런 정보도 제공하지 못한다.
- 위의 코드는 src와 dst로 사용했다면 훨씬 읽기 쉬워진다.
📌 불용어(noise world)
💡 읽는 사람이 차이를 알도록 지어라
불용어란 문장 내에서 빈번하게 발생하여 의미를 부여하기 어려운 단어들이다.
- 클래스 이름에 Info, Data와 같은 불용어를 붙이지 말자. 개념을 구분하지 않은 채 이름만 달리한 경우다.
- 중복 또한 있어선 안 된다.
- NameString이 Name보다 나은 점이 뭔가?
- Customer와 CustomerObject의 차이는 뭔가?
- getActiveAccount(), getActiveAccounts(), getActiveAccountInfo() 중 무엇이 고객 급여 이력을 반환하는 메서드일까?
- moneyAccount는 money, tomerInfor는 customer와 accountData는 account, theMessage는 message와 구분할 수 없다.
4. 발음하기 쉬운 이름을 사용하라
발음하기 어려운 이름은 토론하기도 어렵다.
(책의 저자가 아는 회사 중엔) genymdhms(generate date, year, month, day, hour, minute, second)라는 변수명을 사용했는데, 뭐라고 읽어야 할 지도 모르겠다.
회사 직원들은 "젠 와이 엠 디 에이취 엠 에스"라고 발음했다고 한다.
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
}
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
}
둘 중 어떤 변수명을 놓고 이야기하는 것이 보다 지적으로 보일까?
5. 검색하기 쉬운 이름을 사용하라
💡 이름의 길이는 사용 범위 크기에 비례해야 한다.
- 숫자 7이나 e라는 문자를 찾는 것은 굉장히 힘들다. 대부분의 프로그램이나 문장에 들어갈 가능성이 있다.
- 긴 이름이 짧은 이름보다 좋다. 검색하기 쉬운 이름이 상수보다 좋다.
- 변수나 상수를 코드 여러곳에서 사용한다면 검색하기 쉬운 이름이 바람직하다.
아래 두 코드를 비교해보라.
for (int j=0; j<34; j++)
s += (t[j]*4)/5;
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 = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
- sum이 별로 유용한 변수는 아니지만, 최소한 검색이 가능하다.
- 이름을 의미있게 지으면 함수가 길어지는 것은 맞지만, WORK_DAYS_PER_WEEK를 찾기 훨씬 쉬워졌다.
6. 인코딩을 피하라
인코딩이란 변수에 부가 정보를 덧붙여 표기하는 것을 말한다.
📌 헝가리식 표기법
예전에는 컴파일러가 타입을 점검하지 않아서 변수명에 단서가 필요했다.
하지만 이제는 변수 이름에 타입을 적지 마라. (그럴 필요가 없다.)
오히려 헝가리식 표기법이나 기타 인코딩 방식이 방해가 된다.
📌 멤버 변수 접두어
클래스와 함수는 접두어가 필요없을 정도로 작아야 한다.
"m_dsc"라는 이름보다 "description"이라는 이름이 훨씬 유용하다.
📌 인터페이스 클래스와 구현 클래스
때로는 인코딩이 필요한 경우가 있다. 인터페이스 클래스와 구체 클래스가 그렇다.
Interface class | Concrete class | |
X | IShapeFactory | ShapeFactory |
O | ShapeFactory | ShapeFactoryImpl |
O | ShapeFactory | CShapeFactory |
- 되도록 접두어 I는 붙이지 마라. 주의를 흐트리거나 과도한 정보를 제공한다.
7. 자신의 기억력을 자랑하지 마라
💡 전문가 프로그래머는 자신의 능력을 좋은 방향으로 사용해 남들이 이해하는 코드를 내놓는다.
독자가 머리속으로 한 번 더 생각해 변환해야 할만한 변수명을 쓰지마라.
- 문자 하나만 사용하는 변수 이름은 문제가 있다.
- 루프에서 반복 횟수를 세는 변수 i,j,k는 범위가 아주 작고 다른 이름과 충돌하지 않을 때만 괜찮다. (루프에서 반복 횟수 변수는 전통적으로 한 글자를 사용하기 때문)
- 똑똑한 프로그래머와 달리 전문가 프로그래머는 명료함이 최고라는 사실을 이해한다.
- 소문자 r이라는 변수가 호스트와 프로토콜을 제외한 소문자 URL이라는 사실을 언제나 기억한다면 확실히 똑똑하긴 하나 전문가는 아니다.
8. 클래스 이름
- 명사나 명사구를 사용해라. (Customer, WikiPage, Account, AddressParser 등)
- Manager, Processor, Data, Info 같은 단어는 피해라
- 동사는 사용하지 않는다.
9. 메서드 이름
- 동사나 동사구가 적합하다. (postPayment, deletePage, save 등)
- javabean 표준에 따라 접근자-get, 변경자-set, 조건자-is를 붙여라
- 생성자를 오버로딩할 때는 정적 팩터리 메서드를 사용하라. (이때 생성자는 private로 선언하는 게 좋다.)
- 메서드는 인수를 설명하는 이름을 사용한다.
// 생성자 방식
Complex fulcrumPoint = new Complex(23.0);
// 정적 팩터리 메서드 방식
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
10. 기발한 이름은 피하라
특정 문화에서만 사용되는 재미있는 이름보다 의도를 분명히 표현하는 이름을 사용해라
- HolyHandGrenade → DeleteItems
- whack → kill
- eatMyShort → abort
11. 한 개념에 한 단어를 사용하라
추상적인 개념 하나에 단어 하나를 선택하고 이를 고수해라.
똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.
최신 IDE는 객체가 제공하는 메서드 목록을 보여주지만, 주석을 뒤져보지 않고도 프로그래머가 올바른 메서드를 선택할 수 있는 이름을 선택해야 한다.
그러기 위해서는 메서드 이름은 독자적이고 일관적이어야 한다.
12. 말장난을 하지 마라
💡 의미를 해독할 책임이 독자에게 있는 논문 모델이 아닌, 저자에게 있는 잡지 모델이 바람직하다.
public static String add(String msg, String msgToAppend);
public List<Element> add(Element element);
- 하나는 기존의 값에 두 개를 더하여 새로운 값을 만들고, 하나는 리스트에 값 하나를 추가하는 메서드다
- 새 메서드는 기존 add와 맥락이 다르므로 insert나 append라는 이름으로 부르는 게 적당하다.
13. 해법 영역에서 가져온 이름을 사용하라
개발자라면 당연히 알고 있을 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용해도 괜찮다.
모든 이름을 문제 영역(domain)에서 가져오는 정책이 오히려 현명하지 못하다.
기술 개념에는 기술 이름이 가장 적합한 선택이다.
14. 문제 영역에서 가져온 이름을 사용하라
적절한 '프로그래밍 용어(#13)'가 없다면, 문제 영역(domain)에서 이름을 가져와라
분야 전문가에게 의미를 물어 기능을 파악할 수 있다.
잘 선택해야 한다. 해법 영역과 문제 영역에서 가져오는 순서가 정해진 것이 아니다.
문제 영역 개념과 관련이 깊은 코드(ex. model)라면 문제 영역에서 가져오고,
기술 영역 개념과 관련이 깊은 코드(ex. 새로운 알고리즘)라면 해법 영역에서 가져와야 한다.
15. 의미 있는 맥락을 추가하라
- 클래스, 함수, namespace 등으로 감싸서 맥락(Context)을 표현하라
- firstName, lastName, street, houseNumber, city, state, zipcode가 의미하는 바는 알지만 state라는 변수 하나만 사용한다면 주소 일부라는 사실을 바로 알기 힘들다.
- Address라는 클래스로 묶는 방법이 좋다. (컴파일러에게도 분명해진다.)
- 그래도 불문명하다면 접두어를 사용하자.
- addrFirstName, addrLastName, addrState라 쓰면 맥락이 좀 더 분명해진다.
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
);
System.out.println(guessMessage);
}
- 변수에 좀 더 의미있는 맥락을 부여해야 할까?
- 함수는 맥락 일부를 제공하며, 알고리즘이 나머지 맥락을 제공한다.
- 함수를 끝까지 읽어야만 변수 세계가 '통계 추측' 메시지에 사용된다는 사실이 드러난다.
- 독자가 맥락을 유추해야만 하며, 메서드만 훑어서는 세 변수 의미가 불분명하다.
class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
"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";
}
}
- 함수를 작은 클래스로 쪼개고자 Class로 묶고 세 변수를 포함시켰다.
- 즉, 세 변수는 맥락이 분명해졌다.
- 맥락이 분명해졌기에 함수를 쪼개기가 쉬워지므로 알고리즘도 명확해졌다.
16. 불필요한 맥락을 없애라
일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다.
이름에 불필요한 맥락을 추가하지 않도록 주의해라.
예를 들어, Gas Station Deluxe 애플리케이션을 작성한다고해서 모든 클래스 이름 앞에 GSD를 붙이는 것은 비효율적이고, 현명하지 못한 방법이다.