Skip to content

클린 코드

클린 코드
28.5714285714285729 %

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는 굉장히 유용한 구조체다. 특히 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용하다.

결론

  • 객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
  • 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬운 반면, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
  • (어떤) 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
  • 우수한 소프트웨어 개발자는 편견없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.