01장. 깨끗한 코드
코드가 존재하리라
코드는 요구사항을 표현하는 언어이다.
요구사항에 더욱 가까운 언어를 만들 수 있으며, 요구사항에서 정형 구조를 뽑아낼 수 있다.
코드의 도움 없이 요구사항을 상세하게 표현하기는 불가능하다. : 코드는 정밀한 표현이다.
고도로 추상화된 언어나 특정 응용 분야ㅐ 언어로 기술하는 명세도 마찬가지로 코드이다.
프로그래밍 언어에서 추상화 수준은 점차 높아질 것 이다.
나쁜 코드
우리 모두는 좋은 코드가 중요하다는 사실을 안다.
- 버그가 남아있고, 프로그램이 죽는 횟수가 늘어나는 나쁜 코드
- 출시에 바빠 코드를 마음대로 짜고, 기능을 추가할 수 록 엉망이 되는 나쁜 코드
나중에 손보겠다고 한 코드는 돌아간다는 사실에 안도감을 느끼며 위로하게 되면서 고치지 않게 된다.
고로, 시작부터 클린하게 잘 짜야한다.
나쁜 코드로 치르는 대가
나쁜 코드는 개발속도를 크게 떨어뜨린다.
나쁜 코드가 쌓일수록 팀 생산성은 떨어진다. -> 매번 얽히고설킨 코드를 해독해서 얽히고설킨 코드를 더하게 된다.
원대한 재설계의 꿈
깨끗한 코드를 만드는 노력이 비요ㅕㅇ을 절감하는 방법일 뿐만 아니라 전문가로서 살아남는 길이다.
- 기존 시스템 기능을 제공하는 새 시스템 + 새로운 변경 -> 10년 후 다시 새 시스템 설계
태도
좋은 코드를 사수하는 일은 바로 프로그래머의 책임이다.
- 요구사항의 변경, 일정 촉박, 관리자 고객 마케팅에 대한 불평
- 나쁜 코드로 전락 -> 프로그래머의 책임
원초적 난제
기한을 맞출려면? 나쁜 코드를 양산할 수 밖에 없다. XX
-> 언제나 코드를 최대한 깨끗하게 유지하는 습관
깨끗한 코드라는 예술
깨끗한 코드 = 코드 감각 & 절제와 규율
코드감각이 있는 프로그래머는 나쁜 모듈을 보면 좋은 모듈로 개선할 방안을 떠올린다.
깨끗한 코드란
바야네 스트롭스트룹
- 우아하고 효율적인 코드
- 보기에 즐거운 코드
- 나쁜코드는 나쁜코드를 유혹한다.
- 세세한 사항까지 꼼꼼하게 처리하는 코드
- 메모리 누수, 경쟁 상태, 일관성 없는 명명법
- 한 가지에 집중한다.
그래디 부치
- 가독성을 강조 (잘 쓴 문장처럼 읽혀야 한다는 시각)
- 해결할 문제의 긴장을 명확히 드러내야 한다. 클라이맥스에는 명백한 해법을 제시하여 긴장과 문제를 풀어야 한다.
- 코드는 추측이 아니라 사실에 기반해야 한다. 반드시 필요한 내용만 담아야 한다.
큰 데이브 토마스
- 깨끗한 코드란 다른 사람이 고치기 쉽다.
- 읽기 쉬운 코드와 고치기 쉬운 코드는 다르다.
- 아무리 코드가 우아해도, 아무리 가독성이 높아도, 테스트 케이스가 없으면 깨끗하지 않다.
- 큰 코드보다 작은 코드에 가치를 둔다. -> 작을수록 좋다.
- 코드가 문학적이어야 한다. -> 인간이 읽기 좋은 코드를 작성하라
마이클 페더스
- 깨끗한 코드는 주의 깊게 작성한 코드다.
- 시간을 들여 깔끔하고 단정하게 세세한 사항까지 꼼꼼하게 신경 쓴 코드
론 제프리스
- 모든 테스트를 통과한다.
- 중복이 없다.
- 시스템 내 모든 설계 아이디어를 표현한다.
- 클래스, 메서드, 함수 등을 최대한 줄인다.
- 한 객체가 여러 기능을 수행한다면 여러 객체로 나눈다.
- 메서드가 여러 기능을 수행한다면 메서드 추출을 통해 기능을 명확히 기술하는 메서드 하나와 기능을 실제로 수행하는 메서드 여러 개로 나눈다.
- 집합에서 특정 항목을 찾을 땐 추상 메서드나 추상 클래스를 만들어 실제 구현을 감싼다.
- 실제 구현은 언제든지 바꿔도 괜찮아진다.
- 중복을 피하라, 한 기능만 수행하라, 제대로 표현하라, 작게 추상화 하라*
워드 커닝햄
- 읽으면서 짐작한 그대로 돌아가는 코드가 깨끗한 코드다.
- 코드는 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드다.
- 언어를 단순하게 보이도록 만드는 책임은 프로그래머에게 있다.
우리들 생각 & 우리는 저자다.
오븢게트 멘토 진영이 생각하는 깨끗한 코드를 설명한다. -> 깨끗한 변수 이름, 깨끗한 함수, 깨끗한 클래스
코드를 잘 댄 우리가 저자이며 저자에게는 독자와 잘 소통할 책임이 있다.
새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다. -> 읽기 쉬운 코드를 만들어 내자.
보이스카우트 규칙
캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라
체크아웃할 때보다 좀 더 깨끗한 코드를 체크인 하자.
한꺼번에 많은 시간과 노력을 투자해 코드를 정리할 필요가 없다. 변수 이름 하나를 개선하거나, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하고, 복잡한 if문 하나를 정리하면 충분하다.
시간이 지날수록 코드가 좋아지는 프로젝트에서 작업한다고 생각하자
객체 지향 설계의 다섯가지 원칙
- SRP (단일 책임의 원칙)
- 클래스에는 한 가지, 단 한가지 변경 이유만 존재해야 한다.
- OCP (개방 폐쇠의 원칙)
- 클래스는 확장에 열려 있어야 하며 변경에 닫혀 있어야 한다.
- LSP (리스코프의 치환 법칙)
- 상속받은 클래스는 기초 클래스를 대처할 수 있어야 한다. (하위 클래스가 상위 클래스를 대처할 수 있어야 한다.)
- ISP (인터페이스 분리 원칙)
- 클라이언트에 밀접하게 작게 쪼개진 인터페이스를 유지한다.
- DIP (의존 역전의 법칙)
- 추상화에 의존해야하며, 구체화에 의존하면 안된다.
2장. 의미 있는 이름
의도를 분명히 밝혀라
변수나 함수, 클래스 이름은 의도가 분명한 이름이여야 한다.
- 변수(함수, 클래스)의 존재 이유
- 변수(함수, 클래스)의 수행 기능
- 변수(함수, 클래스)의 사용 방법
주석이 필요하다면 의도를 분명히 드러내지 못한 것이다.
//AS-IS
int d; // 경과 시간(단위: 날짜)
//TO-BE
int daysSinceCreateion;
코드의 단순성이 아닌 코드의 함축성을 고려해야 한다. 즉, 코드 맥락이 코드 자체에 명시적으로 드러나야 한다.
코드 맥락 정보를 제공하기 위해 개념에 이름을 붙인다.
// 예시 : 지뢰찾기 게임
//AS-IS
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<>();
for(int[] x : theList)
if(x[0] == 4)
list1.add(x);
return list1;
}
//TO-BE 1. 개념에 이름 붙이기
// theList : 게임판 / 배열 0번째 : 칸 상태 / 값 4 : 깃발이 꽂힌 상태
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for(int[] cell : gameBoard)
if(cell[STATUS_VALUE] = FLAGGED)
flaggedCells.add)(cell);
return falggedCells;
}
// 2. 칸을 간단한 클래스로
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for(Cell cell : gameBoard)
if(cell.isFlagged())
flaggedCells.add)(cell);
return falggedCells;
}
이름만 고치는 것으로도 이해하기 쉬워진다.
그릇된 정보를 피하라
프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다.
그릇된 단서는 코드 의미를 흐린다.
나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하면 안 된다.
- 직각삼각형의 빗변(hypotenuse) -> hp라는 변수는 독자에게 그릇된 정보를 제공한다.
여러 계정을 그룹으로 묶는다고 가정할 때, 실제 List가 아니면 accountList라 명명하지 않는다.
- accountGroup, bunchOfAccounts, Accounts라고 명명한다.
서로 흡사한 이름을 사용하지 않도록 한다.
- XYZControllerForEffcientHandlingOfStrings <-> XYZControllerForEfficientStorageOfStrings
이름만 보고 객체를 선택할 수 있게, 유사한 개념은 유사한 표기법을 사용해 일관성을 준다.
- L <-> 1 / O <-> 0
의미 있게 구분하라
이름이 달라야 한다면 의미도 달라져야 한다.
연속된 숫자를 덧붙인 이름은 아무런 정보를 제공하지 못한다.
//AS-IS
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}
//TO-BE
public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++) {
destination[i] = source[i];
}
}
불용어를 추가한 이름은 아무런 정보도 제공하지 않는다. 불용어는 중복이기 때문이다.
Product : ProductInfo, ProductData // 의미가 불분명하다.
Name : NameString
Customer : CustomerObject // 차이를 알 수 없다.
읽는 사람이 차이를 알 수 있도록 이름을 지어라
moneyAmount : money
customerInfo : customer
accountData : account
theMessage : message
발음하기 쉬운 이름을 사용하라
프로그래밍은 사회 활동이다. 발음하기 쉬운 단어를 사용할 때 대화가 편해진다.
// AS-IS
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
}
// TO-BE
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
}
검색하기 쉬운 이름을 사용하라
뮨자 하나를 사용하는 이름과 상수는 검색이 어렵다.
검색의 관점에서 긴 이름이 짧은 이름보다 좋다.
간단한 메서드의 로컬 변수는 한 문자를 사용해도 좋다.
이름 길이는 범위 크기에 비례해야 한다.
상수의 의미를 나타내는 변수로 바꾸자.
// AS-IS
for(int j = 0; j > 34; j++) {
s += (t[j] * 4) / 5;
}
// TO-BE
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 += realTaskWeeks;
}
인코딩을 피하라
헝가리식 표기법
이전에 컴파일러가 타입을 점검하지 않아 타입을 기억할 단서로 헝가리식 표기법을 사용했다.
그러나 지금은 변수 이름에 인코딩할 필요가 없다. 자바의 객체는 강한 타입이며, IDE는 오류를 감지해준다.
// AS-IS
PhoneNumber phoneString; // 타입이 바뀌어도 이름은 바뀌지 않는다.
// TO-BE
PhoneNumber phoneNumber;
멤버 변수 접두어
클래스와 함수는 접두어가 필요없을 정도로 작아야 마땅하다. 접두어를 무시하고 이름을 해독해야 한다. 멤버 변수를 눈에 띄게 보여주는 IDE를 사용하자.
// AS-IS
public class Part {
private String m_dsc; // 멤버 변수 접두어 m_
}
// TO-BE
public class Part {
private String description;
}
인터페이스 클래스와 구현 클래스
인코딩이 필요한 경우도 있다.
- 인터페이스 클래스 : ShapeFactory
- 구현 클래스 : ShapeFactoryImpl
자신의 기억력을 자랑하지 마라
문자 하나만 사용하는 변수 이름은 문제가 있다.
루프에서 반복 횟수를 세는 변수(i,j,k)는 괜찮지만 이 경우에도 루프 범위가 아주 작고 다른 이름과 충돌하지 않을 때만 괜찮다.
전문가 프로그래머는 명료함이 최고라는 사실을 이해한다. 즉, 남들이 이해하는 코드를 내놓는다.
클래스 이름
클래스이름과 객체 이름은 명사나 명사구가 적합하다.
Customer, WikiPage, Account,AddressParser 등이 좋은 예이다.
하지만 Manger, Processor, Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않는다.
메서드 이름
메서드 이름은 동사나 동사구가 적합하다.
postPayment, deletePage, save 등이 좋은 예이다.
- 접근자 : get
- 변경자 : set
- 조건자 : is
생성자를 중복정의할 때는 정적 팩토리 메서드를 사용한다. 메서드 이름은 인수를 설명하는 이름을 사용한다.
// AS-IS
Complex fulcrumPoint = new Complex(23.0);
// TO-BE
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
기발한 이름은 피하라
명료한 이름을 선택하라 (HolyHandGrenade -> DeleteImtes)
구어체나 속어의 이름보단 의도를 분명하게 하는 이름을 선택하라 (whack -> kill)
한 개념에 한 단어를 사용하라
메서드 이름은 독자적이고 일관적이야 한다.
추상적인 개념 하나에 단어 하나를 선택하라.
똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.
클래스 이름도 마찬가지이다. controller, manage, driver를 섞어 쓰면 혼란스럽다.
IDE는 문맥에 맞는 단서를 제공해주어 메서드 목록을 보여준다. 하지만 함수 이름과 매개변수만 보여줄 뿐 주석은 보여주지 않는다. 그러니 주석을 보지 않고도 프로그래머가 올바른 메서드를 선택할 수 있는 이름을 가져야 한다.
말장난을 하지 마라
한 단어를 두 가지 목적으로 사용하지 마라.
같은 맥락일 경우에만 일관성을 고려하라.
// 기존
add(); // 기존 값 두개를 더하거나 이어서 새로운 값 만듬
// 집합에 값 하나를 추가 (다른 맥락)
insert(); append();
프로그래머는 코드를 최대한 이해하기 쉽게 짜야 한다. 대충 훑어봐도 이해할 코드 작성이 목표이다.
해법 영역 & 문제영역에서 가져온 이름을 사용하라
해법 영역
프로그래머라면 당연히 알고 있을 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용하라. 코드를 읽을 사람도 프로그래머이니깐!
모든 이름을 문제 영역 즉, 도메인에서 가져오는 정책은 현명하지 못하다.
문제 영역
적절한 프로그래머 용어가 없다면 도메인 전문가에게 의미를 물어 파악할 수 있도록 문제 영역에서 이름을 가져오자.
의미있는 맥락을 추가하라
의미가 분명한 이름이 있게 하자. 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다.
// 변수 하나만 사용할 시 주소 일부라는 사실을 모른다.
String firstName, lastName, street, houseNumber, city, state, zipcode
// 접두어를 사용하면 맥락이 좀 더 분명해진다.
String addrFirstName, addrLastName addrState, .. ;
// 클래스를 생성한다. 변수가 큰 개념이라는 사실이 컴파일러에게도 분명해진다.
class Address {
String firstName;
String lastName;
String state;
...
}
맥락을 개선하면 함수를 쪼개기가 쉬워지며 알고리즘도 좀 더 명확해 진다.
// AS-IS : 함수 이름은 맥락 일부만 제공하고 알고리즘이 나머지 맥락을 제공한다.
// 함수를 끝까지 읽어야 변수 사용 의미를 알 수 있다.
// 독자가 맥락을 유추해야 한다.
private void printGuessSatistics(char candiate, int count) {
String number;
String verb;
String pluralModifier;
if(count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
}
...
String guessMessage = String.format(
"There %s %s %s%s",verb, number, candidate, pluralModifier
);
print(guessMessage);
}
// TO-BE : 클래스를 생성한다.
// 세 변수가 클래스에 속함으로써 맥락이 분명해진다.
public 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)
thereAreNoLetter();
...
}
}
불필요한 맥락을 없애라
의미가 분명하다면 일반적으로 짧은 이름이 긴 이름보다 좋다. 불필요한 맥락을 추가하지 말자.
- 고급 휘발유 충전소(Gas Station Deluxe) 애플리케이션을 짠다고 모든 클래스 이름을 GSD로 시작하는 것은 좋지 못하다.
- accountAddress, customerAddress : 인스턴스 이름 O , 클래스 이름 X (Address가 클래스 이름으로 적합)
- 포트 주소, MAC 주소, 웹 주소 -> PostalAddress, MAC, URI
결론
우리들 대다수는 자신이 짠 클래스 이름과 메서드 이름을 모두 암기하지 못한다.
암기는 요즘 나오는 도구에게 맡기고, 우리는 문장이나 문단처럼 읽히는 코드 아니면 적어도 표나 자료 구조처럼 읽히는 코드를 짜는 데만 집중해야 마땅하다.
다른 사람이 짠 코드를 손본다면 리팩터링 도구를 사용해 문제 해결 목적으로 이름을 개선하자.
'Back-End > Book' 카테고리의 다른 글
[Clean Code] 5장. 형식 맞추기 (0) | 2022.09.25 |
---|---|
[Clean Code] 3~4장. 함수 & 주석 (2) | 2022.09.17 |
[객체지향의 사실과 오해] 07. 함께 모으기 & 부록 A. 집합과 분해 (0) | 2022.06.12 |
[객체지향의 사실과 오해] 05. 책임과 메시지 (0) | 2022.06.05 |
[객체지향의 사실과 오해] 01. 협력하는 객체들의 공동체 (0) | 2022.05.26 |
댓글