Clean Code
마틴 파울러나, 스캇 마이어스, 켄트 벡과 같은 네임드들의 책을 읽어봤지만, 로버트 C.마틴이 책을 제일 쉽고 간결하게 쓰는 것 같다. 밥 아저씨의 책 중에 제일 유명한 클린 코드를 읽고 요약해봤다.
네임드들이 말하는 깨끗한 코드
- 비야네 스트롭스트룹(C++ 창시자, The C++ Programming Language 저자)
- 보기에 즐거운 코드
- 효율적인 코드
- 나쁜 코드는 나쁜 코드를 이끈다.(깨진 창문 이론)
- 오류 처리가 철저한 코드
- 한 가지를 잘 하는 코드
- 그래디 부치(UML 개발자, Object Oriented Analysis and Design with Application 저자)
- 잘 쓴 문장처럼 읽혀야 하는 코드
- 명쾌한 추상화
- 데이브 토마스 형님(OTI 창립자, 이클립스 전략의 대부)
- 고치기 쉬운 코드
- 테스트 케이스가 있는 코드
- 작은 코드
- 인간이 읽기 좋은 코드
- 마이클 페더스(Working Effectively with Legacy Code 저자)
- 주의 깊게 작성한 코드
- 깔끔하고 단정한 코드
- 론 제프리스(Extreme Programming Installed, Extreme Programming Adventure in C# 저자)
- 중복을 피하라.
- 한 기능만 수행하라.
- 제대로 표현하라.
- 작게 추상화하라.
- 워드 커닝햄(Wiki 창시자, Fit 창시자, Extreme Programming 창시자, 객체지향의 정신적 지도자)
- 읽으면서 짐작한대로 동작하는 코드
- 문제를 풀기 위한 언어처럼 보이는 코드
- 코드는 요구사항을 표현하는 언어이다.
- 나중은 결코 오지 않는다. 깨끗한 코드를 위한 노력은 지금 당장 해야 한다.
- 코드 읽는 시간:코드 작성 시간 = 약 10:1
- 체크아웃할 때보다 나은 코드로 체크인 한다. 캠프장은 도착했을 때보다 떠날 때 더 깨끗해야한다는 보이스카웃 규칙을 떠올리자.
의미 있는 이름
- 의도를 분명히 밝혀라.
- 알 수 없이 줄인 이름
- 자료구조를 단순히 적은 이름
- 정의하지 않고 그대로 사용한 상수
- 나만 알 수 있는 이름
- 그릇된 정보를 피하라.
- 자료구조와는 다른 이름(Map인데 이름에 List를 넣는다거나)
- 레거시 의미와는 다른 의미로 사용
- 의미있게 구분하라.
- 단순히 컴파일러를 통과하기 위한 이름(a1, a2, a3…)을 사용하지 말라.
- Object, Data, Info, Manager, Processor와 같은 불분명한 이름을 덧붙여 사용하지 말라.
- 검색하기 쉬운 이름을 사용하라.
- 상수를 그대로 사용하지 말 것.
- 이름의 길이는 범위 크기에 비례할 것(간단한 메서드의 로컬 변수에만 줄인 이름을 사용)
- 인코딩한 이름을 사용하지 말라.
- Abstract와 Implementation을 구분하기 위해 인코딩을 사용한다면, Implementation에 인코딩을 사용하는 것이 낫다.(ClassImpl)
- 헝가리안 표기법의 시대는 갔다.
- 컴파일러가 타입 점검을 하기 때문에 프로그래머는 굳이 타입을 표기할 필요가 없다.
- 최근 IDE는 컴파일하기 전에 타입 점검을 지원한다.
- IDE에서 색상으로 구분해주기 때문에, 멤버 변수 접두어는 더이상 필요 없다.
- 클래스 이름
- 명사나 명사구를 사용한다.
- 메소드 이름
- 동사나 동사구를 사용한다.
- 접근자는 get, 변경자는 set, 조건자는 is를 붙인다.(javabean 표준)
- 생성자를 오버로딩할 때에는 Static Factory Method를 사용한다.
- 한 개념에 일관성 있는 한 단어를 사용하라. Manager, Controller, Driver등을 의미 없이 섞어 쓰지 말라.
- 동일하지 않은 개념을 동일한 이름으로 정의하지 말라. Add, Insert, Append는 각각 다른 의미이다.
- 의미가 불명확하다면 맥락을 추가하라. Name으로 의미 표현이 부족하면 LastName과 같이 맥락을 추가하라.
- 불필요한 맥락(엔진 이름, 작성자 이름 등)은 제거하라. 검색도 잘 안되고, IDE의 자동완성 기능이 의미 없게 된다.
함수
- 작아야 한다.
- if/else, while 등에 들어가는 블록은 1줄(함수 호출)이어야 한다.
- 함수의 들여쓰기(중첩 구조)는 2단을 넘어서면 안된다.
- 한가지만 해야 한다.
- 추상화 수준이 하나여야 한다.
- 추상화 수준이 여러개 섞여 있으면, 이해도 어렵고 분리도 어렵다.
- 의도한 동작 안에 초기화와 같이 외부로 드러나지 않는 부수적인 동작이 숨어있지 않도록 해야한다. 하나의 역할만 해야 한다.
- 코드를 읽어 나감에 따라 등장하는 함수의 추상화 수준은 점점 낮아져야 한다.
- switch 문은 SRP에 위배된다. Abstract Factory등과 같은 패턴을 사용해 최소화 하도록 한다.
- 서술적인 이름을 사용하라. 간결하면 좋지만 의미를 충분히 드러내기 위함이라면 길어도 좋다.
- 일관성 있는 이름을 사용하라.
- 함수 인수의 갯수는 적으면 적을 수록 좋다. 3개를 넘어가면 의미 파악도 어렵고, 테스트가 어려워진다.
- 함수와 인수 사에에 추상화 수준도 동일하게 맞춰야 한다.
- 1개의 인수를 가지는 함수에서, 함수 이름은 동사 인수는 명사의 쌍을 이루어야 한다. 함수의 의미를 명확하게 하기 위해서 동사구로 표현할 수 있지만, 기본적으로 동사+명사의 쌍이다.
- 1개의 인수를 가지는 함수는 명령인지 조회인지 이벤트인지 의미를 명확하게 파악해서 각각의 이름 형식을 일관되게 맞춰야 한다.
- 조회(명사+동사)
- boolean fileExists(string fileName)
- 명령(동사+명사)
- InputStream openFile(string fileName)
- 이벤트(on+명사+동사)
- onFileOpen(string fileName)
- 조회(명사+동사)
- 1개의 인수를 가지는 함수인데 명령, 조회, 이벤트를 나타내지 않는다면 리팩토링 해라.
- 함수의 의미가 명확해지지 않기 때문에 출력 인수는 최대한 피한다. 변환 함수에서는 출력 인수를 사용해 변환 결과를 얻지 말고, 반환 값 또는 객체 내 필드를 사용해 변환 결과를 얻어라.
- 플래그 인수는 존재 자체가 SRP 위반이다.
- 함수가 여러개의 인수를 가진다면, 인수를 묶어 객체로 만들 수 있는지 고려하라. 인수들은 자연스럽게 개념을 표현하게 된다.
- 명령 함수가 오류코드를 반환하면 중첩코드가 만들어진다. 예외 처리를 사용하라.
- 오류코드는 보통 enum으로 표현되고, 변경에 취약하다(재컴파일/재배치 발생). 예외 처리를 사용하면 이를 없앨 수 있다.
- try/catch 블록에 그대로 코드를 넣지 말고, 함수로 뽑아내라. 코드도 깔끔해지고 이해도 쉽다.
- 구조적 프로그래밍의 원칙이 절대적인 것은 아니다. return, break, continue, goto 등도 적절히 사용할 수 있다.
- 시스템은 구현의 대상이 아니라 풀어갈 이야기이다.
주석
- 코드로 의도를 표현하는데 실패했다는 것을 의미한다. 필요악이다.
- 좋은 주석
- 저작권 정보, 소유권 정보 등 법적인 내용을 표시하는 주석
- 지엽적인 설명이 아닌, 전체적인 의도를 설명하는 주석
- 중요성(경고 포함)을 강조하는 주석
- To-do 주석
- 나쁜 주석
- 간결하지 않고, 주절거리는 주석
- 같은 이야기 반복하는 주석
- 오해의 여지가 있는 명확하지 않은 주석
- 의무적으로 다는 주석
- 이력을 기록하는 주석. 요새는 소스 관리 시스템에서 다 해준다.
- 있으나 마나한 주석
- 위치를 표시하는 주석. -여기서부터 XX임
- 닫는 괄화에 다는 주석. 함수가 길다는 의미이며 함수를 줄이려는 노력이 먼저다.
- 저자를 표시하는 주석. 요새는 소스 관리 시스템에서 다 해준다.
- 주석으로 처리한 코드. 요새는 소스 관리 시스템에서 다 추적 가능하다.
- HTML로 표시한 주석. 극혐. HTML로 표시하는 역할은 그 역할을 하는 도구에 맡기는게 맞다.
- 전역 정보. 가까이에 있는 코드만 기술하라.
- 주석 자체가 다시 설명을 요구하는 애매한 주석
형식 맞추기
- 코드 형식은 의사소통의 일환이다.
- 파일 당 코드 라인의 수는 최대한 적게 유지하고 500라인을 넘지 않도록 한다.
- 코드의 가로 길이는 최대한 작게 유지하고 120자를 넘지 않도록 한다.
- 코드는 신문 기사를 작성하는 것과 같이 제목-추상적 내용-구체적 내용의 순서로 작성한다.
- 개념과 개념 사이는 빈 행으로 구분한다.
- 서로 밀접한 관련이 있는 코드들은 한 파일 내에 가까이 묶는다.
- 변수는 사용 위치에 최대한 가까운 곳에 선언한다.
- 루프를 제어하는 변수는 루프 문 내부에 선언한다.
- 인스턴스 변수들은 클래스의 맨 처음(Java) 또는 맨 나중(C++ - Scissors Rule)에 선언하며, 빈 행을 두지 않는다.
- 호출 하는 함수 뒤에 호출 되는 함수를 가까이 배치한다.
- 의미 있는 상수라면, 저차원 함수 내에 선언하지 말고, 클래스 또는 고차원 함수 내에서 선언하고 넘겨주는 방식을 사용한다.
- 연산자의 앞뒤에 공백을 넣는다.
- 함수 인수들 사이에 공백을 넣는다.
- 코드를 가로 정렬하면 연산자만 부각되기 때문에 의미가 흐려진다. 정렬하지 않는다.
- 함수를 한줄로 표현하고 싶은 유혹을 떨쳐버리고 들여쓰기를 해서 명확히 표현하자. 가독성에 좋다.
객체와 자료 구조
- 아무 생각 없이 get/set 메소드를 넣는다고 해서 캡슐화가 되는 것이 아니다. 캡슐화를 위해서는 추상화가 필요하다.
- 추상 인터페이스를 통해 사용자가 구현에 신경쓰지 않고 자료를 조작할 수 있어야 진정한 캡슐화가 되었다고 할 수 있다.
- 절차 지향적 코드는 새로운 데이터를 추가하기는 어려우나(수정 사항이 많음), 새로운 함수를 추가하기는 쉽다.
- 객체 지향적 코드는 새로운 데이터를 추가하기는 쉬우나, 새로운 함수를 추가하기는 어렵다(수정 사항이 많음).
- 클래스 C의 메소드 f가 있다면 f가 호출할 수 있는 객체는
- 클래스 C
- f가 생성한 객체
- f의 인수로 넘어온 객체
- 클래스 C의 인스턴스 변수
- 해당 객체의 메소드를 호출했을 때 또 다른 객체를 반환한다면, 반환된 객체의 메소드는 호출하면 안된다.(디미터의 법칙)
- 즉, C.f()만 사용해야 하며, C.f().f1().f2()와 같이 사용하면 안된다.(Train Wreck)
- 그러나 해당 객체가 반환하는 것이 자료구조(Data Transfer Object - 메소드 없이 공개된 데이터)라면 큰 상관 없다.
오류 처리
- 오류 코드 리턴 대신 예외를 사용하라.
- 예외가 발생할 코드의 경우 try-catch-finally 문으로 시작하라.
- 상위 예외처리 함수에서 모든 예외를 열거해서 처리하고 있다면, 하위 함수에서 예외가 추가될 때마다 예외 처리 함수를 수정해야 한다.(OCP 위반) 따라서 - 미확인 예외를 사용하도록 한다.
- 예외에 디버깅을 위한 정보를 전달하라.
- 외부 API가 발생시키는 예외를 처리할 때에는, DIP에 따라 외부 API를 감싼다. 외부 API에 대한 의존성이 역전된다.
- 예외 처리 때문에 정상 흐름이 잘 읽히지 않는다면, 예외 상황을 캡슐화 하는 클래스를 만들거나 객체를 조작해 예외 처리를 없앤다.(null 객체 등 - Special Case Pattern)
- null은 전달하지도, 반환하지도 말라.
경계
- 외부 코드를 사용할 때에는, 우리가 사용하고자 하는 대로 인터페이스(Adaptor)를 작성하고, 그 인터페이스에 맞춰 외부 코드를 작성하여 의존성을 역전시킨다.
- 외부 코드를 사용하여 테스트 케이스 작성하고 테스트 함으로써 외부 코드를 학습한다.
단위 테스트
- TDD의 3가지 법칙
- 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
- 테스트 코드의 중요도 = 구현 코드의 중요도
- 테스트 코드가 잘 되어 있으면 구현 코드 변경이 두렵지 않다.
- 테스트 코드에서는 가독성이 가장 중요하다. 단순, 간결하면서도 표현력이 풍부해야 한다.
- 테스트 코드에는 테스트와 관계 없는 코드를 넣지 않는다.
- 테스트 코드는 BUILD-OPERATE-CHECK의 절차를 따라 작성한다.
- 테스트 코드는, 필요하다면 API를 추상화(테스트를 위한 언어를 작성한다는 개념) 하여 작성한다.
- 테스트 함수 1개 당 1개의 개념만 테스트하자.
- 테스트 함수 1개 당 assert 문을 1개 두도록 하면, 1개의 개념만 테스트한다는 의미를 가질 수 있고, 읽고 이해하기 쉽다.
- FIRST 원칙
- 빠른(Fast)
- 테스트는 자주해야 하므로, 빠르게 수행되어야 한다.
- 독립적인(Independent)
- 테스트는 서로 독립적이어야 한다.
- 반복 가능한(Repeatable)
- 테스트는 반복 가능해야 한다.
- 셀프 검증(Self-Validating)
- 테스트는 성공/혹은 실패만 리턴해야 한다.
- 시점(Time)
- 테스트는 적시(구현 전)에 작성해야 한다.
- 빠른(Fast)
클래스
- 클래스 정의 순서
- 정적 공개 상수
- 정적 비 공개 상수
- 비 공개 인스턴스 변수
- 추상화된 공개 함수
- 추상화된 공개 함수에서 사용된 구체적인 비 공개 함수
- 클래스 정의 순서에 따르면 클래스가 주제 - 주요 행위 - 상세 행위로 표현되기 때문에 마치 신문 기사처럼 쉽게 읽힌다.
- 클래스는 작아야 한다.(책임의 수가 적어야 하고 25 단어 내로 설명 가능해야 한다.)
- 클래스 이름은 클래스의 책임을 표현해야 한다. 책임이 간결해야 이름도 간결해 진다.
- 모호한 클래스 이름(Processor, Manager, Super)은 클래스의 책임이 모호하다는 것을 의미한다.
- 단일 책임 원칙(Single Responsibility Principle)
- 클래스나 모듈은 단 하나의 책임(단 하나의 메소드가 아니다)을 갖는다.
- 클래스나 모듈을 변경할 이유는 단 하나뿐이어야 한다.
- 응집도(Cohesion)
- 클래스의 인스턴스 변수 수가 적어야 한다.
- 클래스의 메소드는 인스턴스 변수를 1개 이상 사용해야 하며, 더 많이 사용할수록 응집도가 높다.
- 몇몇 메소드만이 사용하는 인스턴스 변수가 보이면, 대부분 클래스를 분리해야할 경우이다.
- 응집도를 유지하면 작은 클래스가 여럿 나온다.
- 개방 폐쇄의 원칙(Open-Closed Principle)
- 확장에 개방적, 수정에 폐쇄적이어 한다.
- 추상 클래스와 구현 클래스를 분리하여 구현 변경에 따른 영향을 없앤다.
- 추상 인터페이스를 사용하면, 세부 구현이 분리되므로 클래스의 테스트 코드를 작성하기도 쉽다.
- 의존성 역전의 원칙(Dependency Inversion Principle)
- 자주 변경되고 우리가 제어하기 어려운 상세 구현에 의존하지 말고, 우리가 제시한 추상화된 인터페이스에 의존하도록 해야 한다.
시스템
- 시스템 생성과 시스템 사용을 분리해야 한다.
- 초기화 지연 기법은 생성과 사용을 뒤섞어 SRP원칙을 위배할 수도 있다.
- 생성과 사용을 분리시키는 방법
- Main 분리
- 생성과 관련된 동작은 Main에서 모두 하고, 어플리케이션에서는 생성된 객체를 사용만 한다.
- Abstract Factory 패턴
- 생성 시점은 어플리케이션에서 결정하지만, 실제 생성은 팩토리에서 하기 때문에 어플리케이션에서는 생성된 객체를 사용만 한다.
- 의존성 주입(Dependency Injection)
- 클래스의 외부에서 의존 클래스를 생성한 뒤에 주입(생성자의 파라미터, setter 메소드, 메소드 등을 이용)한다.
- Main 분리
- 처음부터 확장을 고려한 시스템을 만들 수 없고, 점진적, 반복적 개선이 필요하다. 이것이 애자일의 철학.
- 시스템에서 관심사(도메인 영역)를 잘 분리해 내야 한다.
창발성(하다보면 자연스럽게 발현됨)
- 켄트 백이 말하는 설계를 단순하게 만드는 법칙 4가지(중요도 순)
-
모든 테스트를 실행한다.
- 의존성 주입(DI), 의존성 역전의 원칙(DIP), Abstraction, Interface 등을 사용하여 결합도를 낮춰야 테스트가 쉽다.
- 테스트가 잘 되도록 코드를 작성하고, 실제로 모든 테스트를 한다면, 저절로 OOP가 달성된다.
- 테스트 케이스를 모두 돌릴 수 있으면, 리팩토링도 두렵지 않다. -
중복 코드를 없앤다.
- 단순한 중복이라도 모두 없앤다.
- Template Method 패턴(중복 Method를 상위 클래스에 정의하고 상위 클래스를 상속)을 사용할 수 있다. -
의도를 표현한다.
- 좋은 이름을 사용하라.
- 함수 및 클래스의 크기를 줄여라.
- 표준 명칭을 사용하라.(패턴을 적용했다면, 클래스 이름에 패턴 명을 넣어라)
- 단위 테스트를 모두 작성하라.
- 꼼꼼해야 한다.(장인 정신) -
클래스와 메소드 수를 최소화 한다.
- 1,2,3번을 다 하고나서 클래스와 메소드 수를 줄이라.
-
동시성
- 구조적 관점
- 컴포넌트의 동작 시점이 제한되지 않기 때문에, 모듈의 독립성을 높이고 결합도를 줄일 수 있다.
- 성능 관점
- 멀티 프로세싱이 가능한 환경이라면, 컴포넌트를 병렬로 동작시켜 처리 능력을 향상시킬 수 있다.
- 늘 성능이 향상 되는 것은 아니다.
- 동시성을 고려하면 설계가 크게 달라질 수 있다.
- 오히려 부하를 발생시킬 수 있다.
- 복잡하다.
- 동시성 관련한 문제는 재현하기 어렵고, 일회성 문제로 치부하기 쉽다.
- 동시성 관련 코드를 분리하라.(SRP)
- 공유되는 자료를 최대한 줄이고, 사본을 활욜하라.
- 쓰레드는 가능한 독립적으로 구현하고, 다른 쓰레드와 자료 공유를 최대한 줄인다.
- 라이브러리의 컬렉션이 쓰레드에 안전한지 확인하라.
냄새와 휴리스틱
- 주석
- 부적절한 정보
- 쓸모 없는 주석
- 중복된 주석
- 성의 없는 주석
- 주석 처리된 코드
- 환경
- 여러단계로 된 빌드
- 여러단계로 된 테스트
- 함수
- 너무 많은 인수
- 출력 인수
- 플래그 인수
- 죽은 함수
- 일반
- 한 소스 파일에 여러 언어를 사용
- 당연히 지원해야 하는 동작을 지원하지 않음(최소 놀람의 원칙 위배)
- 경계를 올바로 처리 하지 않음
- 안전절차를 무시
- 중복
- 다형성 이용
- 올바르지 않은 추상화 수준
- 기초 클래스가 파생 클래스에 의존
- 과도한 정보
- 작고 간결하게
- 죽은 코드
- 수직 분리
- 연관된 코드는 수직으로 가깝게 배치
- 일관성 부족(최소 놀람의 원칙 위배)
- 잡동사니
- 비어있는 기본 생성자 따위
- 인위적 결합
- 이유 없이 결합시키지 않는다.
- 기능 욕심
- 다른 클래스의 기능을 탐냄
- 플래그(선택자) 인수
- 모호한 의도
- 개행 안함, 헝가리안 표기법, 매직 넘버
- 잘못 지운 책임
- 부적절한 static 함수
- 재정의 가능성이 있는 함수인데 static으로 선언
- 서술적 변수의 부재
- 연속된 처리의 해독을 쉽게 하기 위해, 중간 요약을 하는 서술적 변수를 사용
- 이름과 기능이 불일치하는 함수
- 이해하지 못하고 사용한 알고리즘
- 물리적으로 드러나지 않은 논리적 의존성 : 논리적으로 의존한다면, 물리적인 인터페이스를 정의해 의존하라.
- if/else 또는 switch/case
- 다향성을 이용해 제거하라.
- 표준 표기법을 따르지 않음
- 명명된 상수로 정의되지 않은 매직 넘버
- 이유가 명확하지 않은 결정
- 구조적 강제성 없는 관례 : 따르지 않으면 그만인 관례보다는 구조적으로 강제되도록 한다.
- 캡슐화되지 않은 조건
- if (x && y && z) 하지 말고 if (a()) 처럼 캡슐화 하라.
- 부정 조건
- 숨겨진 시간적 결합 : 함수 인수를 드러내어 함수가 호출되는 순서(시간)를 명확히 한다.
- 일관성 없음
- 캡슐화되지 않은 경계 조건 : 경계 조건은 한 곳에서 정의하여 처리한다. 코드 여기저기에 index+1, index-1를 늘어놓지 않는다.
- 단계적이지 않은 함수의 추상화 수준
- 흩어져 있는 기본값 상수 또는 설정 관련 상수 : 최상위 단계에 위치시키고, 저차원 함수에서는 인수로 넘겨받아 처리한다.
- 오지랖 넓은 코드를 없애고, 부끄럼 많은 코드를 작성하자.
- 디미터의 법칙. 자신이 직접 사용하는 모듈만 호출할 것. 추이적 탐색하지 말것.
- 자바
- 긴 import 목록을 피하고 와일드 카드를 사용하라.
- 상수는 상속하지 말고 static import를 사용하라.
- 상수 대신 Enum을 활용하라. Enum은 method와 field를 지원한다.
- 이름
- 서술적인 이름을 사용하라.
- 적절한 추상화 수준에서 이름을 선택하라.
- 상세 구현을 드러내는 이름은 피한다.(추상화!)
- 가능하면 표준 명명법(디자인 패턴 명, toXX, isXX 등)을 사용하라
- 명확한 이름
- 긴 범위는 긴 이름을 사용할 것
- 인코딩을 하지 말고 풀어 쓸 것
- 이름으로 부수 효과를 설명하라.
- 테스트
- 충분한 테스트 케이스를 만들어라.
- 커버리지 도구를 사용하라.
- 사소한 테스트를 건너뛰지 마라.
- 무시한 테스트는 모호함을 뜻한다.
- 경계조건을 테스트하라.
- 버그 주변은 철저히 테스트 하라.
- 실패 패턴을 살펴라.
- 테스트 커버리지 패턴을 살펴라.
- 테스트는 빨라야 한다.