우리는 평균적으로 코드를 작성하는 시간보다 코드를 읽고 해석하는데 많은 시간을 소비한다. 하물며, 자기 자신이 짠 소스 코드도 많은 분석시간이 필요하다. 그렇기 때문에 우리는 미래의 나를 믿지 말고 미래의 나까지도 쉽게 이해 시킬 수 있는 깨끗한 코드를 작성해야 한다. 그렇다면 깨끗한 코드는 무엇일까? 바로 "읽기가 쉬운 코드" 이다.
소프트웨어에서 이름은 어디서나 쓰인다. 하지만 주의 깊게 이름을 붙이지 않는다.
우리는 변수에도 이름을 붙이고, 함수에도 이름을 붙이고, 인수와 클래스 그리고 패키지명에도 이름을 붙인다. 소스 파일을 저장하는 디렉토리에도 이름을 붙인다. 여기저기 도처에서 이름 사용한다. 이렇듯 많이 사용하므로 이름을 잘 지으면 여러모로 편하다. 그렇다면 어떻게 짓은 이름이 과연 잘 지은 이름일까?
첫 번째 규칙 : 의도를 분명히 밝혀라.
"의도가 분명한 이름을 지으라". 이름을 지으려면 많은 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다. 이름에서 정확한 의도가 파악이 된다면 코드를 읽는 사람은 좀 더 행복해지리라.
그리고 의도를 더 분명히 하고 싶을 때 클래스로 정의하는 것 또한 매우 좋다. 단순히 int kong = 100
이라는 정의하는 것 보다, Vegitable kong = new Vegitable(100)
; 클래스로 정의하는 것이 더 의미를 분명하게 전달할 수 있다.
변수나 함수 그리고 클래스 이름은 아래와 같은 질문에 모두 답해야 한다.
// 이름으로 의도가 파악되지 않는 이름
int d; // 경과 시간(단위: 날짜)
// 이름으로 의도가 올바르게 파악되는 이름
int elapsedTimeInDay;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
다음 예제 코드는 하는 일을 짐작하기 어렵다. 복잡한 문장도 아니고, 복잡한 구조(다형성)도 아니다. 문제는 코드의 단순성이 아니라 코드의 함축성에 있다. 코드 맥락이 코드 자체에 명시적으로 드러나 있지 않다. 위 코드의 저자는 독자가 다음과 같은 정보를 안다라는 가정에 작성했다.
public class MeaninglessName {
private static final int[][] theList = new int[5][5];
@PostConstruct
public void init(){
log.info("After call constructor function, this function Run");
for (int i = 0; i < theList.length; i++) {
for (int j = 0; j < theList[i].length; j++) {
theList[i][j] = ((int) (Math.random() * 5));
}
}
}
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<>();
for (int[] x : theList) {
if (x[0] == 4)
list1.add(x);
}
return list1;
}
}
“지뢰찾기” 게임을 만드는 코드라는 기반 지식이 있다고 가정해보자. 그러면 theList는 게임판이라는 사실을 알 수 있다. theList를 gameBoard로 바꿔보자.
그리고 배열에서 0번째 값은 칸 상태를 의미하고 4는 깃발이 꽂힌 상태를 가리킨다. 그러면 의미를 담고 있는 함축적인 이름으로 바꿔보자.
list1은 깃발이 꽂혀 있는 셀만 담고 있는 배열로 보여진다. 이 또한 의도를 잘 포현하고 있는 이름으로 바꿔보자.
@Component
@Slf4j
public class MeaningfulName {
private static final int[][] gameBoard = new int[5][5];
private static final int STATUS_VALUE = 0;
private static final int FLAGGED = 4;
@PostConstruct
public void afterConstructorCallInit(){
log.info("After Constructor call, this function call");
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard[i].length; j++) {
gameBoard[i][j] = ((int) (Math.random() * 5));
}
}
}
public List<Cell> getFlaggedCell(){
List<Cell> flaggedCells = new ArrayList<>();
for(Cell cell : gameBoard){
if(cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
if(cell.isFlagged())
flaggedCells.add(cell);
}
return flaggedCells;
}
}
코드의 단순함은 여전한데, 각 개념에 이름만 붙였는데도 불구하고 코드가 상당히 나아졌다.
두 번째 규칙: 그릇된 정보를 피하라.
프로그래머는 코드에 그릇된 단서를 남겨서는 안된다. 그릇된 단서는 코드 의미를 흐린다.
나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용해도 안된다. 예를 들어 "여러 계정을 묶을 때", 실제 List형이 아니라면, accountList라고 명명하면 안된다. 프로그래머에게 List라는 단어는 특수한 의미이기 때문이다.
String accountList = "송혜교, 전지현, 김태희, 고소영";
for(String account : accountList){
// 데이터 처리
...
}
그래서 올바른 명명은 "accountGroup", "bunchOfAccounts", "Accounts"정도가 아주 휼륭한 이름이지 않을가 싶다.
String accountGroup = "송혜교, 전지현, 김태희, 고소영";
// or String accounts = "송혜교, 전지현, 김태희, 고소영";
// or String bunchOfAccounts = "송혜교, 전지현, 김태희, 고소영";
ArrayList<String> accountList = (ArrayList<String>) Arrays.stream(accountGroup.split(",")).toList();
for(String account : accountList){
// 데이터 처리
...
}
서로 흡사한 이름을 사용하지 않도록 주의한다.
한 모듈에서 XYZControllerForEfficientHandlingOfString
라는 이름을 사용하고, 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfString
라는 이름을 사용한다면 차이를 알아 챘는가?
class XYZControllerForEfficientHandlingOfString {
}
...
class XYZControllerForEfficientStorageOfString {
}
유사한 개념은 유사한 표기법으로 사용한다. 이것도 정보다. 일관성이 떨어지는 표기법은 그릇된 정보이다. 에디터를 사용하여 코드를 작성한다면, 우리는 자동 완성 기능을 사용한다. 이름을 몇 자만 입력한 후 핫키 조합을 누르면 가능한 후보 목록이 뜬다. 후보 목록에 유사한 개념이 알파벳 순으로 나온다면 그리고 각 개념 차이가 명백히 드러난다면 코드 자동 완성 기능은 굉장히 유용해진다.
이름으로 그릇된 정보를 제공하지 말자. 끔찍한 예가 소문자 L이나 대문자O 변수다. 소문자 l은 1과 유사하고 대문자 O와 0(zero)는 매우 유사하다.
세 번째 규칙: 의미 있게 구분하라.
동일한 범위 안에서는 다른 두 개념에 같은 이름을 사용하지 못한다. 컴파일러를 통과할지라도 연속된 숫자를 덧붙이거나 불용어를 추가하는 방식은 적절하지 못하다. class라는 변수를 이미 사용하고 있어서 klass를 사용한다. 이러한 이름은 아무런 정보도 제공하지 못하기 때문에 코드를 읽는 사람에게 도움이 되지 못한다. 이름이 달라야 한다면 의미도 달라져야 한다.
예를 들어, Customer라는 클래스와 CustomerObject라는 클래스를 발견했다면 차이를 알 수 있는가? getActiveAccount( ); getActiveAccounts( ); getActiveAccountInfo( ); 이 세 개의 함수 차이를 알 수 있는가?
new Customer("Jinho");
new CustomerObject("Jinho");
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
읽는 사람이 차이를 알도록 이름을 지어라.
네 번째 규칙: 발음하기 쉬운 이름을 사용하라.
소스를 읽을 때, 읽기가 쉽지 않고 줄임말로 인해서 우스깡스러운 표현을 하게 된다. 우리들은 단어에 능숙하다. 우리 두뇌에서 상당 부분은 단어라는 개념만 전적으로 처리한다. 그리고 정의상으로 단어는 발음이 가능하다. 말을 처리하려고 발달한 두뇌를 활용하지 않는다면 안타까운 손해 때문이다. 그러므로 발음하기 쉬운 이름을 선택한다.
// 발음하기 어려운 명명 규칙을 사용한 소스
// 또한 약어로 인해 "그릇된 정보"를 줄 수 있다.
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 = "02"
}
다섯 번째 규칙: 검색하기 쉬운 이름을 사용하라.
문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다는 문제점이 있다. MAX_CLASSES_PER_STUDENT는 grep으로 찾기가 쉽지만, 숫자 7은 은근히 까다롭고 검색 결과가 많이 나오기 때문에 찾기가 힘들다.
이름 길이는 범위(Scope) 크기에 비례해야한다. 변수나 상수를 코드 여러 곳에서 사용한다면 검색하기 쉬운 이름이 바람직하다.
여섯 번째 규칙: 인코딩을 피하라
명명 규칙에 데이터 타입을 인코딩할 필요는 없다. 이름 안에 데이터타입이 인코딩되면 의미가 혼탁해 질 수 있다.
String phoneNumberString = "010-1234-1234";
// 미래에 String -> PhoneNumber로 타입이 변경되도, 변수명은 바뀌지 않는다!!!
PhoneNumber phoneNumberString;
때로는 인코딩이 필요할 때 가 있다. 인터페이스 클래스와 구현 클래스의 경우, 인터페이스 클래스는 접두어를 붙이지 않고, 구체 클래스에 접두어를 붙이는 것이 좀 더 보기 좋다. ShapeFactory
인터페이스 클래스와 구현체인 ShapeFactoryImp
가 보기 좋다.
클래스 이름
클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
메서드 이름
동사나 동사구가 적합하다. 접근자, 변경자, 조건자는 javabean 표준에 따라 값 앞에 set, get, is 접두사를 붙인다.
일곱 번째 규칙: 한 개념에 한 단어를 사용하라.
추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다. 예를 들어, 유사한 행동을 하는 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.
일관성 있는 어휘를 선택해서 이름을 붙이자.
// 일관성 없는 어휘 선택은 혼란을 줄 수 있음.
userService.getUserNameAndPasswrod();
boardService.fetchHotBoardList();
boardService.retrieveBookmakr();
// 일관성 있는 어휘를 선택하자.
userService.getUserNameAndPasswrod();
boardService.getHotBoardList();
boardService.getBookmakr();
과거에는 서비스 계층 별 어휘를 나눠서 결정한 케이스도 있다. Controller 계층에서는 fetch
로 어휘를 통일했고 Service 계층에서는 get
으로 어휘를 통일해서 계층 별로도 접두사로 사용되는 어휘를 구분하는 케이스도 있었다. Logging 정보를 좀 더 효과적으로 구분하고자 하여 효율적인 디버깅을 하고자 했던 것으로 기억이 든다.
여덟 번째 규칙: 의미 있는 맥락을 추가하라.
스스로 의미가 분명한 이름이 없지 않다. 하지만 대다수 이름은 그렇지 못하다. 그래서 클래스, 함수, 이름 공간에 의미를 넣어 맥락을 부여한다. 의미 전달이 어렵다고 판단될 경우, 접두어를 붙어서 의미를 전달한다.
// 변수를 모두 훑어봐야 주소와 관련된 데이터임을 알 수 있다.
String firstName;
String lastName;
String zipCode;
String city;
String state;
// 맥락을 좀 더 분명하게 하기 위해서 addr라는 접두어 붙이면
// 단번에 알 수 있다.
String addrFirstName;
String addrLastName;
String zipCode;
String addrCity;
String addrState;
아홉 번째 규칙: 불필요한 맥락을 없애라.
일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 주의한다.
NHN KCP의Data를 처리하는 Application을 만든다고 가정해보자. 그래서 우리는 모든 클래스의 명칭에 KCPData
라는 접두어를 붙이는 것은 매우 바람직하지 못하다. 접두어가 의미가 있다면 붙이는 것이 바람직하겠지만, 그렇지 않기 때문에 접두어를 붙이는 건 매우 협오적인 행동이다.
변수명은 짧으면 짧을 수록 좋다. 단, 의미가 잘 전달된다는 조건이다. 만약, 의미가 제대로 전달되지 않는 짧은 변수명이라면 과감하게 의미를 명확하게 전달할 수 있는 변수명으로 바꿔라. 변수명이 길어지더라도 말이다.
boolean isExisted;
boolean isExistTempDirectory
작게 만들어라!
함수를 만드는 첫째 규칙은 "작게!"다. 함수를 만드는 둘째 규칙은 "더 작게"다.
1.1 블록과 들여쓰기
if 문/else 문/ whie 문 등에 들어가는 블록은 한 줄이어야 한다.
한 가지만 해라!
함수는 한 가지를 해야한다. 그 한 가지를 잘 해야 한다. 그렇다면 그 '한 가지가' 무엇인지 알기가 어렵다. 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
1.2 함수 내 섹션
한 함수에서 섹션을 나눌 때, 자연스럽게 나눠진다면 한 함수에서 여러 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.
함수 당 추상화 수준은 하나로!
추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다.
1.3 위에서 아래로 코드 읽기: 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계씩 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아진다.
예를 들어, "DB Table의 데이터를 읽는다." → "DB의 Connection 맺고 객체를 반환한다." → "Connection 객체를 이용해서 SQL문을 실행시켜 데이터를 가져온다."