깨끗한 코드, 이름, 함수, 주석
- 1.1 나쁜 코드의 대가
- 1.2 원대한 재설계의 꿈
- 1.3 깨끗한 코드란?
- 1.4 보이스카우트 규칙
- 1.5 의도를 분명히 밝혀라
- 1.6 그릇된 정보를 피하라
- 1.7 의미 있게 구분하라
- 1.8 발음하기 쉬운 이름을 사용하라
- 1.9 검색하기 쉬운 이름을 사용하라
- 1.10 인코딩을 피하라
- 1.11 클래스 이름과 메서드 이름
- 1.12 한 개념에 한 단어를 사용하라
- 1.13 작게 만들어라
- 1.14 한 가지만 해라
- 1.15 함수 당 추상화 수준은 하나로
- 1.16 Switch 문
- 1.17 서술적인 이름을 사용하라
- 1.18 함수 인수
- 1.19 부수 효과를 일으키지 마라
- 1.20 명령과 조회를 분리하라
- 1.21 오류 코드보다 예외를 사용하라
- 1.22 주석은 나쁜 코드를 보완하지 못한다
- 1.23 좋은 주석
- 1.24 나쁜 주석
나쁜 코드는 너희 회사를 망하게 할 수 있어. 로버트 마틴이 이 책에서 하고 싶은 말은 결국 이 한마디에서 시작하지.
누구나 급한 일정에 쫓겨서 "나중에 정리하지 뭐" 하면서 대충 짠 경험이 있잖아. 근데 르블랑의 법칙이라는 게 있거든 — "나중은 결코 오지 않는다(Later equals never)". 나쁜 코드가 쌓이면 생산성이 떨어져. 처음엔 빠르게 기능을 찍어내던 팀이, 시간이 지나면서 코드 한 줄 고치는 데 며칠이 걸리게 되는 거야. 저자는 이걸 "생산성 0을 향한 추락"이라고 표현해. 코드를 고치면 다른 데서 터지고, 그걸 고치면 또 다른 데서 터지는 악순환이지. 관리자들이 이 생산성 하락을 보고 뭘 하냐면 — 사람을 더 뽑아. 근데 새로 온 사람들은 기존 코드를 이해 못 하고, 기존 팀원들은 새 사람 가르치느라 더 생산성이 떨어져. 이게 나쁜 코드의 대가야.
생산성이 바닥을 치면 개발자들이 "처음부터 다시 짜자"고 반란을 일으켜. 관리자도 더 이상 방법이 없으니 허락하고, 드림팀을 꾸려서 재설계 프로젝트가 시작돼. 근데 여기서 비극이 벌어지거든. 새 시스템이 기존 시스템의 모든 기능을 따라잡으려면 오래 걸리는데, 그 사이에 기존 시스템에도 계속 기능이 추가돼. 새 시스템이 기존 시스템을 겨우 따라잡을 때쯤이면, 새 시스템 코드도 이미 엉망이 되어 있어. 왜? "빨리 따라잡아야 한다"는 압박에 또 나쁜 코드를 짰으니까. 저자가 실제로 이런 재설계가 10년 걸린 사례를 봤다고 해. 결론은 간단해 — 재설계는 답이 아니야. 처음부터 깨끗하게 짜는 게 답이지.
그리고 저자는 여기서 불편한 진실을 말해 — "나쁜 코드의 책임은 프로그래머에게 있다". 일정이 촉박해서, 관리자가 압박해서, 요구사항이 바뀌어서 등등 핑계를 대지만, 결국 코드를 짜는 건 프로그래머이고, 코드 품질을 지키는 것도 프로그래머의 책임이라는 거야. 의사가 수술 전에 손을 씻으려는데 환자가 "시간 없으니까 그냥 해달라"고 하면 안 씻을 거냐는 비유가 나와. 프로그래머도 마찬가지 — 관리자가 빨리 하라고 해도 코드 품질은 양보하면 안 돼.
그래서 깨끗한 코드가 뭔데? 저자가 유명한 프로그래머들한테 물어봐. 비야네 스트롭스트룹(C++ 창시자) — 깨끗한 코드는 보기에 즐거운 코드야. 나쁜 코드는 나쁜 코드를 유혹하거든. 옆에 있는 코드가 엉망이면 "나도 대충 짜지 뭐" 하게 돼. 이걸 "깨진 유리창 이론"이라고 불러. 그래디 부치(UML 창시자) — 깨끗한 코드는 잘 쓴 산문처럼 읽혀. 데이브 토마스(OTI 창립자) — 깨끗한 코드는 작성자가 아닌 다른 사람이 읽기 쉽고 고치기 쉬워. 테스트가 있어야 하고, 의미 있는 이름이 붙어야 해. 마이클 페더스 — 깨끗한 코드는 누군가 주의 깊게 짰다는 느낌이 들어. 고치려고 봤더니 고칠 게 없는 코드. 론 제프리스 — 중복을 줄이고, 한 가지를 잘 표현하고, 작게 추상화해. 워드 커닝햄(위키 창시자) — 코드를 읽으면서 "짐작했던 대로"라는 반응이 나오면 깨끗한 코드야. 이 답변들을 종합하면 공통점이 보여: 읽기 쉽고, 의도가 명확하고, 중복이 없고, 테스트가 있고, 작아.
미국 보이스카우트에 이런 규칙이 있어 — "캠프장을 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라". 코드에도 같은 원칙을 적용하자는 거야. 체크아웃할 때보다 체크인할 때 더 깨끗하게. 변수 이름 하나 바꾸고, 긴 함수 하나 쪼개고, 중복 하나 제거하고. 매번 조금씩이면 돼. 이게 누적되면 코드가 점점 좋아져. 저자는 이걸 "지속적인 개선"이라고도 불러. 한 번에 대대적인 리팩터링을 하는 게 아니라, 매일 조금씩 개선하는 습관. 이게 프로페셔널리즘이라고.
이 철학을 실천하는 첫 번째 도구가 바로 이름이야. 변수 이름 하나 잘 짓는 게 주석 열 줄보다 나아. 프로그래머가 하루 종일 하는 일 중 하나가 이름 짓기인데, 대부분 이걸 대충 하거든. 이름을 지을 때 가장 중요한 건 "왜 존재하는지, 무슨 일을 하는지, 어떻게 사용하는지"가 이름만 보고 드러나야 한다는 거야. int d; 이런 거 하지 마. 경과 시간을 나타내는 거면 int elapsedTimeInDays;라고 써. 이름이 길어지는 게 두려운 거보다, 의미를 알 수 없는 짧은 이름이 훨씬 위험하거든. 저자가 예시로 드는 코드가 인상적인데, theList라는 이름을 gameBoard로 바꾸면 지뢰찾기 게임판이라는 맥락이 바로 보여. 코드 자체는 한 글자도 안 바꿨는데, 이름만 바꿨을 뿐인데 읽기가 확 달라져.
"그릇된 정보(disinformation)"란 코드를 읽는 사람을 잘못된 방향으로 이끄는 이름이야. 대표적인 예 — 실제로는 List가 아닌데 변수 이름에 accountList라고 쓰는 것. 진짜 List 자료구조인 줄 알고 접근했다가 낭패 봐. 그냥 accounts나 accountGroup이 나아. 서로 비슷한 이름도 문제야. XYZControllerForEfficientHandlingOfStrings와 XYZControllerForEfficientStorageOfStrings — 이 둘의 차이를 한눈에 구분할 수 있겠어? 못 해.
컴파일러를 통과하기 위해 이름을 바꾸는 건 최악이야. a1, a2 같은 연속된 숫자 덧붙이기, ProductInfo와 ProductData 같은 구분 불가능한 이름들. "불용어(noise word)"를 넣어서 구분하는 것도 나빠. nameString에서 String이 무슨 의미가 있어? name이 숫자일 리가 없잖아. 정보를 추가하지 않는 단어는 빼.
genymdhms(generate date, year, month, day, hour, minute, second의 약자)라는 변수명을 실제로 쓴 사례가 나와. 회의 시간에 이걸 어떻게 말하냐? "젠 와이 엠 디 에이치 엠 에스"? generationTimestamp라고 쓰면 돼. 사람이 입으로 말할 수 있어야 협업이 되는 거야. 코드 리뷰할 때 "야, 그 젠얌드흠스 변수 있잖아..." 이러면 웃기지도 않거든.
한 글자 변수명이나 숫자 상수는 코드에서 검색이 안 돼. e를 검색해봐 — 영어 텍스트에서 가장 많이 쓰이는 글자야. 모든 파일 모든 줄에서 잡혀. 반면 MAX_CLASSES_PER_STUDENT는 검색하면 딱 나오지. 저자의 기준 — "이름 길이는 범위(scope) 크기에 비례해야 한다". 짧은 루프 안에서 i는 괜찮아. 근데 여러 곳에서 쓰이는 변수라면 검색 가능한 긴 이름을 써야 해.
옛날에는 변수 이름에 타입 정보를 인코딩했어. 헝가리안 표기법이 대표적이지 — strName, iCount, bFlag 같은 것. 컴파일러가 타입 체크를 안 해주던 시절에는 유용했지만, 이제는 IDE가 타입을 다 보여주니까 필요 없어. 멤버 변수 접두어도 마찬가지야. m_name 같은 거. 클래스가 작으면 멤버 변수가 눈에 보이고, 클래스가 크면 클래스를 쪼개야지 접두어를 붙일 게 아니야. 인터페이스와 구현 클래스 이름은 어떻게 할까? IShapeFactory보다는 인터페이스를 ShapeFactory로, 구현체를 ShapeFactoryImp나 CShapeFactory로 하는 편이 나아. 인터페이스에 I를 붙이면 사용하는 쪽에서 불필요하게 구현 세부사항을 알게 되니까.
클래스 이름은 명사나 명사구여야 해. Customer, WikiPage, Account, AddressParser 같은 것. Manager, Processor, Data, Info 같은 단어는 피해 — 너무 범용적이라 아무 의미도 전달하지 않거든. 메서드 이름은 동사나 동사구여야 하고. postPayment(), deletePage(), save() 같은 것. 생성자를 오버로딩할 때는 정적 팩토리 메서드가 좋아. Complex.fromRealNumber(23.0)이 new Complex(23.0)보다 의도가 명확하지.
같은 개념인데 클래스마다 다른 단어를 쓰면 혼란스러워. 어떤 클래스에서는 fetch, 다른 데서는 retrieve, 또 다른 데서는 get. "한 개념에 한 단어"를 써. 반대로, 같은 단어를 두 가지 다른 목적에 쓰는 것도 나빠. 기존 값 두 개를 더해서 새 값을 만드는 add 메서드가 있는데, 컬렉션에 원소 하나를 추가하는 것도 add라고 부르면? 의미가 다른데 이름이 같으니까 혼란이 와. 후자는 insert나 append가 맞아. 그리고 해법 영역에서 가져온 이름도 괜찮아. 프로그래머끼리 쓰는 용어 — JobQueue, AccountVisitor 같은 거. 디자인 패턴 이름을 쓰면 다른 프로그래머가 바로 의도를 파악하거든.
이름을 잘 지었으면 이제 함수 차례야. 함수의 첫째 규칙은 "작게"야. 둘째 규칙은 "더 작게". 저자가 켄트 벡의 집을 방문했을 때 본 코드는 함수가 전부 2~4줄이었다고 해. 각 함수가 너무 명백해서 읽는 데 노력이 거의 안 들었다는 거야. if 문이나 while 문 안에 들어가는 블록은 한 줄이어야 하고, 대개 거기서 함수를 호출해. 그러면 바깥 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 잘 지으면 코드를 이해하기도 쉬워지거든. 중첩 구조가 생길 만큼 함수가 커서는 안 돼. 들여쓰기 수준은 1단이나 2단을 넘기면 안 돼.
"함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다." 근데 "한 가지"가 뭔지 판단하는 게 어렵잖아. 저자의 기준은 이래 — 함수가 하는 일을 의미 있는 다른 이름으로 추출할 수 있다면, 그 함수는 여러 가지를 하고 있는 거야. 한 가지만 하는 함수는 섹션으로 나누기 어려워. 선언, 초기화, 검증 같은 여러 섹션으로 나눌 수 있다면 한 가지 이상을 하고 있는 거지.
함수가 한 가지만 하려면, 함수 내 모든 문장의 "추상화 수준"이 동일해야 해. getHtml() 같은 높은 추상화 수준의 호출과 .append("\n") 같은 낮은 추상화 수준의 코드가 한 함수에 섞이면 읽는 사람이 헷갈려. 저자는 "내려가기 규칙(The Stepdown Rule)"을 제안해. 코드를 위에서 아래로 읽으면 추상화 수준이 한 단계씩 낮아지는 거야. 마치 이야기를 하듯이 — "이것을 하려면, 먼저 이것을 하고, 그 다음 이것을 하고..." 이런 식으로.
switch 문은 본질적으로 N가지를 처리해. 한 가지만 하는 게 불가능하지. 저자는 switch 문을 완전히 피하라는 게 아니라, "다형성(polymorphism)"으로 숨기라고 해. 직원 유형에 따라 급여를 계산하는 switch 문이 calculatePay(), isPayday(), deliverPay() 등 여러 함수에서 반복되면? 직원 유형이 추가될 때마다 모든 switch 문을 찾아서 고쳐야 하거든. 해결책은 switch 문을 추상 팩토리에 한 번만 쓰고, 이후에는 다형성으로 처리하는 거야.
"길고 서술적인 이름이 짧고 어려운 이름보다 좋아". 이름을 붙이는 데 시간을 들여. 여러 이름을 시도해보고 코드를 읽어봐. 모듈 내에서 이름의 일관성도 중요해. includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage — 같은 문구와 동사를 쓰면 읽는 사람이 다음에 올 이름을 짐작할 수 있거든.
함수 인수는 적을수록 좋아. 이상적인 수는 0개(무항)이고, 그 다음이 1개(단항), 2개(이항). 3개(삼항) 이상은 가능하면 피해야 해. 플래그 인수는 끔찍해. render(true) — 이거 뭐야? 함수가 플래그 값에 따라 다른 일을 한다는 뜻이고, 그건 한 가지만 하는 게 아니라는 뜻이거든. renderForSuite()와 renderForSingleTest()로 나눠. 이항 함수는 단항보다 이해하기 어렵고, assertEquals(expected, actual)은 expected가 먼저인지 actual이 먼저인지 매번 헷갈려. 인수가 많아지면 "인수 객체"로 묶어. makeCircle(double x, double y, double radius) 보다는 makeCircle(Point center, double radius)가 나아.
부수 효과는 "거짓말"이야. 함수가 한 가지를 한다고 해놓고, 몰래 다른 것도 하거든. checkPassword(String userName, String password) 함수가 비밀번호를 확인하면서 몰래 세션을 초기화한다면? 이름만 보고 호출한 프로그래머는 세션이 날아간 걸 몰라. 이걸 "시간적 결합(temporal coupling)"이라고 하는데, 특정 상황에서만 호출할 수 있는 함수가 되어버리지.
함수는 뭔가를 "수행하거나(명령)", 뭔가에 "답하거나(조회)" — 둘 중 하나만 해야 해. public boolean set(String attribute, String value) — 이 함수는 속성을 설정하면서 성공 여부를 반환해. 그래서 if (set("username", "bob")) 같은 코드가 나오는데, 읽는 사람은 "username이 bob으로 설정되어 있는지 확인"하는 건지 "bob으로 설정하는" 건지 헷갈려. 명령과 조회를 분리해야 해.
명령 함수에서 오류 코드를 반환하면 호출자가 바로 오류를 처리해야 해서 코드가 중첩돼. 예외를 쓰면 오류 처리 코드가 원래 코드에서 분리돼서 깔끔해져. 오류 처리도 "한 가지"야. 오류를 처리하는 함수는 오류만 처리해야 해. 오류 코드를 반환하면 의존성 문제도 생기거든. Error enum을 쓰는 모든 클래스가 Error enum에 의존하게 되고, 새 오류를 추가할 때마다 다 재컴파일/재배포해야 해. 예외 클래스를 쓰면 **OCP(개방-폐쇄 원칙)**를 지킬 수 있어.
이름을 잘 짓고 함수를 잘 쪼갰으면, 주석이 필요한 경우가 확 줄어들어. 주석은 기껏해야 필요악이야. 코드로 의도를 표현할 능력이 부족하니까 주석을 쓰는 거고, 주석이 필요하다는 건 코드가 실패했다는 증거라는 게 저자의 도발적인 핵심이야. 주석의 가장 큰 문제는 "코드와 함께 유지보수되지 않는다"는 점이거든. 코드는 변하는데 주석은 안 바뀌면, 주석이 거짓말을 하게 돼. 시간이 지나면 주석이 코드와 멀어지고, 결국 주석을 믿을 수 없게 돼. 저자는 "진실은 한곳에만 존재해야 한다"고 말해. 그 한곳이 코드야. 주석이 아니라.
그래도 어쩔 수 없이 주석이 필요한 경우가 있어. 법적인 주석 — 저작권 정보, 라이선스 표시 같은 것. 법적으로 넣어야 하니까 어쩔 수 없지. 정보를 제공하는 주석 — 정규표현식 패턴이 뭘 의미하는지 설명하는 주석처럼, 코드만으로는 전달이 어려운 정보를 보충하는 것. 다만 이것도 함수 이름이나 클래스 구조를 잘 잡으면 대부분 필요 없어져. 의도를 설명하는 주석 — 구현 방법이 아니라 "왜 이렇게 했는지"를 설명하는 거야. 코드가 뭘 하는지는 보이지만, 왜 이렇게 설계했는지는 코드만으로 안 보일 때가 있거든. 경고 주석, TODO 주석, 중요성을 강조하는 주석, 공개 API의 Javadoc 같은 것도 가치가 있어. 다만 TODO를 달고 안 하면 그냥 쓰레기가 되니까 주기적으로 검토해서 처리하거나 없애야 해.
대부분의 주석은 나쁜 주석이야. 주절거리는 주석 — 이해가 안 되는 주석. 주석을 읽고도 다른 모듈까지 뒤져야 의미를 알 수 있다면, 주석의 존재 이유가 없어. 같은 이야기를 중복하는 주석 — 코드가 하는 일을 그대로 옮겨놓은 주석. 코드보다 읽기가 더 어렵고, 오히려 코드를 안 읽게 만들어. 의무적인 주석 — 모든 함수에 Javadoc을 달아야 한다는 규칙. /** @param name 이름 */ 이런 주석이 무슨 정보를 추가하겠어? 없느니만 못하지. 이력을 기록하는 주석은 Git이 있으니까 삭제해. 닫는 괄호에 다는 주석 — } // while 같은 것도, 함수가 길어서 표시해야 한다면 함수를 줄여야지 주석을 달 게 아니야.
그리고 "주석으로 처리한 코드" — 이게 진짜 나빠. 주석 처리된 코드를 아무도 삭제하지 않거든. "이유가 있겠지" 하고 남겨두니까 쓰레기가 쌓여. 소스 코드 관리 시스템이 기억해주니까 과감히 삭제해. 전역 정보를 담은 주석도 나빠 — 포트 번호 기본값을 주석으로 적어놨는데 실제 설정은 다른 파일에 있다면? 설정이 바뀌어도 주석은 안 바뀌거든. 역사적 배경이나 관련 없는 기술 상세를 장황하게 쓰는 것도 마찬가지야. RFC 문서 전체를 주석에 옮겨놓으면 누가 읽겠어.
정리
1장 읽고 기억할 거 세 가지:
- 나쁜 코드의 대가는 생산성 추락이고, 재설계는 답이 아니다. 보이스카우트 규칙 — 매일 조금씩 개선하는 습관이 프로의 자세다.
- 이름에 의도를 담고, 함수는 작게 한 가지만. 인수는 적을수록 좋고, 부수 효과는 거짓말이다. 오류 코드보다 예외를 써라.
- 주석이 필요하다면 코드가 실패한 것이다. 주석을 달기 전에 코드를 개선할 방법을 먼저 찾아라. 주석으로 처리한 코드는 즉시 삭제해라.