클린 코드
4 주석
나쁜 코드에 주석을 달지 마라. 새로 짜라.
- 브라이언 W. 커니핸, P.J. 플라우거
우리는 코드로 의도를 표현하지 못해, 그러니까 실패를 만회하기 위해 주석을 사용한다. ... 진심이다. 주석은 언제나 실패를 의미한다.
내가 이렇듯 주석을 무시하는 이유가 무엇이냐고? 거짓말을 하니까. 항상도 아니고 고의도 아니지만 너무 자주 거짓말을 하니까.
주석을 엄격하게 관리 ... 하지만 나라면 코드를 깔끔하게 정리하고 표현력을 강화하는 방향으로, 그래서 애초에 주석이 필요 없는 방향으로 에너지를 쏟겠다.
진실은 한곳에만 존재한다. 바로 코드다.
주석은 나쁜 코드를 보완하지 못한다
- 코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다.
코드로 의도를 표현하라!
- 확실히 코드만으로 의도를 설명하기 어려운 경우가 존재한다.
java
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && employee.age > 65)
다음 코드는 어떤가?
java
if (employee.isEligibleForFullBenefits())
5 형식 맞추기
- 프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다. 코드 형싱글 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다. 필요하다면 규칙을 자동으로 적용하는 도구를 활용한다.
형식을 맞추는 목적
- 코드 형식은 의사소통의 일환이다. 의사소통은 전문 개발자의 일차적인 의무다.
- 어쩌면 '돌아가는 코드'가 전문 개발자의 일차적인 의무라 여길지도 모르겠다. 하지만 이 책을 읽으면서 생각이 바뀌었기 바란다.
- 원래 코드는 사라질지라도 개발자의 스타일과 규율은 사라지지 않는다.
적절한 행 길이를 유지하라
- JUnit, FitNesse, Time and Money는 상대적으로 파일 크기가 작다. 500줄을 넘어가는 파일이 없으며 대다수가 200줄 미만이다.
- 500줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다는 사실이다.
신문 기사처럼 작성하라
- 최상단에 기사를 몇 마디로 요약하는 표제가 나온다. 독자는 표제를 보고서 기사를 읽을지 말지 결정한다.
- 첫 문단은 전체 기사 내용을 요약한다. 쭉 읽으며 내려가면 세세한 사실이 조금씩 드러난다.
- 코드도 마찬가지다. 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경 써서 짓는다. 그리고 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다.
개념은 빈 행으로 분리하라
- 일련의 행 묶음은 완결된 생각 하나를 표현한다.
세로 밀집도
- 줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다. 즉, 서로 밀접한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다.
목록 5-3
typescript
class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
private m_className: string = "";
/**
* 리포터 리스너의 속성
*/
private m_properties: Property[] = [];
public addProperty(property: Property) {
this.m_properties.push(property);
}
}
- 목록 5-4가 훨씬 더 읽기 쉽다. 코드가 '한눈'에 들어온다.
목록 5-4
typescript
class ReporterConfig {
private m_className: string = "";
private m_properties: Property[] = [];
public addProperty(property: Property) {
this.m_properties.push(property);
}
}
변수 선언
- 변수는 사용하는 위치에 최대한 가까이 선언한다.
인스턴스 변수
- 반면, 인스턴스 변수는 클래스 맨 처음에 선언한다.
- 위치는 아직도 논쟁이 분분하다. C++에서는 모든 인스턴스 변수를 클래스 마지막에 선언한다는 소위 가위 규칙을 적용한다.
- 나로서는 어느쪽이든 이의가 없다. 잘 알려진 위치에 인스턴스 변수를 모은다는 사실이 중요하다.
개념적 유사성
- 어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다.
세로 순서
- 일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 다시 말해, 호출되는 함수를 호출하는 함수보다 나중에 배치한다. 그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.
- 신문 기사와 마찬가지로 가장 중요한 개념을 가장 먼저 표현한다.
가로 형식 맞추기
- 100자나 120자에 달해도 나쁘지 않다.
가로 공백과 밀집도
typescript
private measureLine(line: string): void {
this.lineCount++;
const lineSize = line.length;
this.totalChars += lineSize;
this.lineWidthHistogram.addLine(lineSize, this.lineCount);
this.recordWidestLine(line);
}
- 할당 연산자를 강조하려고 앞뒤에 공백을 줬다.
- 함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않았다. 함수와 인수는 서로 밀접하기 때문이다.
가로 정렬
typescript
public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
...
}
- 가로 정렬이 별로 유용하지 못하다는 사실을 깨달았다.
- 정렬이 필요할 정도로 목록이 길다면 문제는 목록 길이지 정렬 부족이 아니다. 클래스를 쪼개야 한다는 의미다.
들여쓰기 무시하기
- 때로는 간단한 if 문, while 문, 짧은 함수에서 들여쓰기 규칙을 무시하고픈 유혹이 생긴다. 이런 유혹에 빠질 때마다 나는 항상 원점으로 돌아가 들여쓰기를 넣는다.
팀 규칙
- 팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 소프트웨어가 일관적인 스타일을 보인다. 개개인이 따로국밥처럼 맘대로 짜대는 코드는 피해야 한다.
밥 아저씨의 형식 규칙
- 내가 사용하는 규칙은 아주 간단하다. 코드 자체가 최고의 구현 표준 문서가 되는 예다.
6 객체와 자료 구조
자료 추상화
목록 6-1
typescript
class Point {
x: number;
y: number;
}
목록 6-2
typescript
interface Point {
getX(): number;
getY(): number;
setCartesian(x: number, y: number): void;
getR(): number;
getTheta(): number;
setPolar(rho: number, theta: number): void;
}
- 목록 6-2는 점이 직교좌표계를 사용하는지 극좌표계를 사용하는지 알 길이 없다. 둘 다 아닐지도 모른다!
- 그럼에도 불구하고 인터페이스는 자료구조를 명백하게 표현한다.
- 사실 목록 6-2는 자료 구조 이상을 표현한다. 클래스 메서드가 접근 정책을 강제한다. 좌표를 읽을 때는 각 값을 개별적으로 읽어야 한다. 하지만 좌표를 설정할 때는 두 값을 한꺼번에 설정해야 한다.
- 목록 6-1은 구현을 노출한다. 변수를 private로 선언하더라도 각 값마다 조회 함수와 설정 함수를 제공한다면 구현을 외부로 노출하는 셈이다.
- 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.
목록 6-3
typescript
interface Vehicle {
getFuelBunkCapacityInGallons(): number;
getGallonsOfGasoline(): number;
}
목록 6-3
typescript
interface Vehicle {
getPercentFuelRemaining(): number;
}
- 목록 6-4가 좋다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다.
자료/객체 비대칭
- 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
- 자료 구조는 자료를 그대로 공개하며 함수는 제공하지 않는다.
목록 6-5
typescript
class Square {
topLeft: Point;
side: number;
}
class Rectangle {
topLeft: Point;
height: number;
width: number;
}
class Circle {
center: Point;
radius: number;
}
class Geometry {
private readonly PI = 3.141592653589793;
public area(shape: Object): number {
if (shape instanceof Square) {
let s: Square = shape as Square;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
let r: Rectangle = shape as Rectangle;
return r.height * r.width;
} else if (shape instanceof Circle) {
let c: Circle = shape as Circle;
return this.PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
- 목록 6-5는 절차적인 도형 클래스다. Geometry 클래스는 세 가지 도형 클래스를 다룬다. 각 도형 클래스는 간단한 자료 구조다. 즉, 아무 메서드도 제공하지 않는다. 도형이 동작하는 방식은 Geometry 클래스에 의해 구현한다.
- 만약 Geometry 클래스에 둘레 길이를 구하는 perimemter() 함수를 추가하고 싶다면? 도형 클래스는 아무 영향도 받지 않는다!
- 반대로 새 도형을 추가하고 싶다면? Geometry 클래스에 속한 함수를 모두 고쳐야 한다.
목록 6-6
java
interface Shape {
topLeft: Point;
}
class Square implements Shape {
topLeft: Point;
side: number;
area() {
return this.side * this.side;
}
}
class Rectangle implements Shape {
topLeft: Point;
height: number;
width: number;
area() {
return this.height * this.width;
}
}
- 목록 6-6는 객체 지향적인 도형 클래스다. 여기서 area()는 다형 메서드다. Geometry 클래스는 필요없다. 그러므로 새 도형을 추가해도 기존 함수에 아무 영향을 미치지 않는다. 반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.
- 앞서도 말했듯이, 목록 6-5와 목록 6-6은 상호 보완적인 특질이 있다. 사실상 반대다! 그래서 객체와 자료 구조는 근본적으로 양분된다.
(자료 구조를 사용하는) 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
- 때로는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.
디미터 법칙
- 디미터 법칙은 잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
- 다음 코드는 (다른 법칙은 제쳐두고서라도) 디미터 법칙을 어기는 듯이 보인다.
typescript
const outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath() as string;
기차 충돌
- 흔히 위와 같은 코드를 기차 충돌이라 부른다.
- 일반적으로 조잡하다 여겨지는 방식이므로 피하는 편이 좋다.
위 코드는 다음과 같이 나누는 편이 좋다.
typescript
const options = ctxt.getOptions() as Options;
const scratchDir = options.getScratchDir() as File;
const outputDir = scratchDir.getAbsolutePath() as string;
- 방금 살펴본 코드 예제는 둘 다 디미터 법칙을 위반할까?
ctxt
,Options
,ScratchDir
이 객체인지 아니면 자료 구조인지에 달렸다. - 자료 구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다.
- 코드를 다음과 같이 구현했다면 디미터 법칙을 거론할 필요가 없어진다.
typescript
const outputDir = ctxt.options.scratchDir.getAbsolutePath as string;
- 자료 구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다면, 문제는 훨씬 간단하리라. 하지만 단순한 자료 구조에도 조회 함수와 설정 함수를 정의하라
잡종 구조
- 잡종 구조는 자료 구조와 객체를 혼합한 구조다.
- 양쪽 세상에서 단점만 모아놓은 구조다.
구조체 감추기
- 만약
ctxt
,Options
,ScratchDir
이 진짜 객체라면? 그렇다면 앞서 코드 예제처럼 줄줄이 사탕으로 엮어서는 안 된다. 객체라면 내부 구조를 감춰야 하니까. 그렇다면 임시 디렉터리의 절대 경로는 어떻게 얻어야 좋을까?
typescript
ctxt.getAbsolutePathOfScratchDirectoryOption();
ctxt.getScratchDirectoryOption().getAbsolutePath();
- 어느 방법도 썩 내키지 않는다.
ctxt
가 객체라면 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안 된다.
typescript
const outFile: string = outDir + "/" + className.replace(/\./g, "/") + ".class";
const fout: FileOutputStream = new FileOutputStream(outFile);
const bos: BufferedOutputStream = new BufferedOutputStream(fout);
- 위 코드는 같은 모듈에서 (한참 아래로 내려가서) 가져온 코드다.
- 어찌 됐거나, 임시 디렉터리의 절대 경로를 얻으려는 이유가 임시 파일을 생성하기 위한 목적이라는 사실이 드러난다.
- 그렇다면
ctxt
객체에 임시 파일을 생성하라고 시키면 어떨까?
typescript
const bos: BufferedOutputStream = ctxt.createScratchFileStream(classFileName);
- 객체에게 맡기기에 적당한 임무로 보인다!
ctxt
는 내부 구조를 드러내지 않으며, 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다. 따라서 디미터 법칙을 위반하지 않는다.
자료 전달 객체
- 자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 때로는 자료 전달 객체(data transfer object, DTO)라고 부른다.
- DTO는 굉장히 유용한 구조체다. 특히 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용하다.
결론
- 객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
- 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬운 반면, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
- (어떤) 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
- 우수한 소프트웨어 개발자는 편견없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.