# 형식 맞추기

TIP

예시 코드는 java-racingcar-precourse (opens new window)에서 확인할 수 있습니다.

프로그래머라면 형식을 깔끔하게 맞춰 코드를 작성해야 한다. 코드 형식을 맞취기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다.

# 형식을 맞추는 목적

코드 형식은 의사소통의 일환이다. 오늘 구현한 기능이 다음 버전에서 바꿜 확률은 아주 높다. 오늘 구현한 코드의 가독성은 앞으로 바꿜 코드의 품질에 지대한 영향을 미친다.

# 적절한 행 길이를 유지하라

일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.

# 신문 기사처럼 작성하라

독자는 위에서 아래로 기사을 읽는다. 소스 파일도 신문 기사와 비슷하게 작성한다. 이름은 간단하면서 설명이 가능하게 짓는다. 아래로 내려갈수록 의도를 세세하게 묘사한다. 마지막에는 가장 저차원 함수와 세부 내역이 나온다.

# 개념은 빈 행으로 분리한다

package racingcar.car;

import static java.util.stream.Collectors.*;

import java.util.List;

public class Cars {
    private static final String NEW_LINE = "\n";
    private static final String WINNER_NAMES_DELIMITER = ", ";
    private static final int DEFAULT_MAX_POSITION = 0;

    private final List<Car> cars;

    public Cars(List<String> names, MovingPolicy movingPolicy) {
        this.cars = names.stream()
            .map(name -> new Car(name, movingPolicy))
            .collect(toList());
    }

    public void move() {
        cars.forEach(Car::move);
    }

    public String getExecutionResult() {
        return cars.stream()
            .map(Car::getStateMessage)
            .collect(joining(NEW_LINE));
    }

    public String getWinnersNames() {
        int maxPosition = getMaxPosition();
        return cars.stream()
            .filter(car -> car.isSamePosition(maxPosition))
            .map(Car::getName)
            .collect(joining(WINNER_NAMES_DELIMITER));
    }

    private int getMaxPosition() {
        return cars.stream()
            .map(Car::getPosition)
            .reduce(DEFAULT_MAX_POSITION, Integer::max);
    }
} 

위 코드를 보면 패키지 선언부, import 문, 각 메서드 사이에 빈 행이 들어간다. 빈 행은 새로운 개념을 시작한다는 시각적 단서다. 만약 이러한 빈 행을 생략하면 코드의 가독성은 현저하게 떨어진다.

package racingcar.car;
import static java.util.stream.Collectors.*;
import java.util.List;
public class Cars {
    private static final String NEW_LINE = "\n";
    private static final String WINNER_NAMES_DELIMITER = ", ";
    private static final int DEFAULT_MAX_POSITION = 0;
    private final List<Car> cars;
    public Cars(List<String> names, MovingPolicy movingPolicy) {
        this.cars = names.stream()
            .map(name -> new Car(name, movingPolicy))
            .collect(toList());}
    public void move() {
        cars.forEach(Car::move);}
    public String getExecutionResult() {
        return cars.stream()
            .map(Car::getStateMessage)
            .collect(joining(NEW_LINE));}
    public String getWinnersNames() {
        int maxPosition = getMaxPosition();
        return cars.stream()
            .filter(car -> car.isSamePosition(maxPosition))
            .map(Car::getName)
            .collect(joining(WINNER_NAMES_DELIMITER));}
    private int getMaxPosition() {
        return cars.stream()
            .map(Car::getPosition)
            .reduce(DEFAULT_MAX_POSITION, Integer::max);}
} 

# 세로 밀집도

세로 밀집도는 연관성을 의미한다. 서로 밀집한 코드 행은 세로로 가까이 놓여야 한다. 아래 예시는 의미없는 주석으로 두 인스턴스 변수를 떨어뜨려 놓는다.

public class Car {
    private static final int DEFAULT_POSITION = 0;
    ...

    /*
     * Car 클래스 이름
     */
    private final String name;
    
    /*
     * Car의 이동 정책
     */
    private final MovingPolicy movingPolicy;
    
    /*
     * Car의 위치
     */
    private int position = DEFAULT_POSITION;
    ...
}

아래 코드가 훨씬 더 읽기 쉽다. 코드가 한 눈에 들어온다. 같은 코드지만 더 많은 범위를 읽어야 한다.

public class Car {
    private static final int DEFAULT_POSITION = 0;
    ...

    private final String name;
    private final MovingPolicy movingPolicy;
    private int position = DEFAULT_POSITION;
    ...
}

# 수직 거리

서로 밀접한 개념은 세로로 가까이 둬야 한다. 같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다. 여기서 연관성이란 한 개념을 이해하는 데 다른 개념이 중요한 정도다. 연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다.

변수(StringBuilder stringBuilder)는 사용하는 위치에 최대한 가까이 선언한다. 또한 반복을 제어하는 변수(int i)는 반복문 내부에 선언해야 한다.

public class Car {
    ...

    protected String getStateMessage() {
        StringBuilder stringBuilder = new StringBuilder(String.format(NAME_MESSAGE, name));
        for (int i = ZERO_INDEX; i < position; i++) {
            stringBuilder.append(MOVING_STICK);
        }
        return stringBuilder.toString();
    }
}

인스턴스 변수는 클래스 맨 처음에 선언한다. 변수 간에 세로로 거리를 두지 않는다. 잘 설계한 클래스는 대다수의 클래스 메서드가 인스턴스 변수를 사용하기 때문이다.

public class GameMachine {
    private static final int ZERO_INDEX = 0;

    private final MovingPolicy movingPolicy;
    private final InputView inputView;
    private final OutputView outputView;
    ...
}

한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다.

public class Cars {
    ...

    public String getWinnersNames() {
        int maxPosition = getMaxPosition();
        return cars.stream()
            .filter(car -> car.isSamePosition(maxPosition))
            .map(Car::getName)
            .collect(joining(WINNER_NAMES_DELIMITER));
    }

    private int getMaxPosition() {
        return cars.stream()
            .map(Car::getPosition)
            .reduce(DEFAULT_MAX_POSITION, Integer::max);
    }
}

어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다. 아래는 JUnit5 org.junit.jupiter.api.Assertions이다.

public class Assertions {
    ...
	public static void assertTrue(boolean condition) {
		AssertTrue.assertTrue(condition);
	}

	public static void assertTrue(boolean condition, Supplier<String> messageSupplier) {
		AssertTrue.assertTrue(condition, messageSupplier);
	}

	public static void assertTrue(BooleanSupplier booleanSupplier) {
		AssertTrue.assertTrue(booleanSupplier);
	}

	public static void assertTrue(BooleanSupplier booleanSupplier, String message) {
		AssertTrue.assertTrue(booleanSupplier, message);
	}
    ...
}

# 세로 순서

일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 호출되는 함수를 호출하는 함수보다 나중에 배치한다. 이렇게 배치할 경우 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.

# 가로 형식 맞추기

짧은 행이 바람직하다. 책에서는 120자 정도로 행 길이를 제한할 것을 추천한다.

# 가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

public void run() {
    Cars cars = getCars();
    int tryCount = getTryCount();
    System.out.println();

    repeat(cars, tryCount);
}

위 메서드를 살퍄보면 할당 연산자를 강조하기 위해 앞뒤에 공백을 넣었다. =를 기준으로 왼쪽 요소와 오른쪽 요소가 분명히 나뉜다.

반면, 메서드 이름과 이어지는 괄호 사이에는 공백을 넣지 않는다. 함수와 인자는 서로 밀접하기 때문이다.

메서드를 호출하는 코드에서 괄호 안 인수는 공백으로 분리한다. 쉼표를 강조해 인수가 별개라는 사실을 보여준다.

public static void main(String[] args) {
    GameMachine gameMachine = new GameMachine(new RandomMovingPolicy(), new InputView(), new OutputView());
    gameMachine.run();
}

승수 사이에는 공백이 없다. 곱셈은 우선순위가 가장 높기 때문이다. 항 사이에는 공백이 들어간다. 덧셈과 뺄셈은 운선순위가 곱셈보다 낮기 때문이다.

a*a - 4*a*c

WARNING

코드 형식을 자동으로 맞춰주는 도구는 대다수가 연산자 우선순위를 고려하지 못한다.

# 가로 정렬

선언문과 할당문을 별도로 정렬하지 않는다.

public class GameMachine {
    private final MovingPolicy movingPolicy;
    private final InputView inputView;
    private final OutputView outputView;

    public GameMachine(MovingPolicy movingPolicy, InputView inputView, OutputView outputView) {
        this.movingPolicy = movingPolicy;
        this.inputView = inputView;
        this.outputView = outputView;
    }
    ...
}

# 들여 쓰기

소스 파일은 윤곽도와 계층이 비슷하다. 범위로 이뤄진 계층을 표현하기 위해 코드를 들여쓴다. 들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례한다. 클래스 내 메서드는 클래스보다 한 수준 들여쓴다. 메서드 코드는 메서드 선언보다 한 수준 들여쓴다. 블록 코드는 블록을 포함하는 코드보다 한 수준 들여쓴다.

# 들여 쓰기 전

public class RandomMovingPolicy implements MovingPolicy {
    private static final int MIN_NUMBER_RANGE = 0;
    private static final int MAX_NUMBER_RANGE = 9;
    private static final int MOVE_CONDITION = 4;
    @Override public boolean isMoving() { if (Randoms.pickNumberInRange(MIN_NUMBER_RANGE, MAX_NUMBER_RANGE) >= MOVE_CONDITION) { return true; } return false; }
}

# 들여 쓰기 후

public class RandomMovingPolicy implements MovingPolicy {
    private static final int MIN_NUMBER_RANGE = 0;
    private static final int MAX_NUMBER_RANGE = 9;
    private static final int MOVE_CONDITION = 4;

    @Override
    public boolean isMoving() {
        if (Randoms.pickNumberInRange(MIN_NUMBER_RANGE, MAX_NUMBER_RANGE) >= MOVE_CONDITION) {
            return true;
        }
        return false;
    }
}

위 두 코드는 동일하다. 들여 쓰기는 파일의 구조가 한 눈에 들어온다.

간혹 간단한 if, while, 메서드는 들여쓰기 규칙을 무시하고픈 유혹이 생긴다. 이것 또한 가독성을 망치는 지름길이다.

# 팀 규칙

팀은 한 가지 규칙에 함의해야 한다. 모든 팀원은 그 규칙을 따라야 한다. 그래야 소프트웨어가 일괄적인 스타일을 유지할 수 있다.

좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다는 사실을 기억하기 바란다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 줘야 한다. 온갖 스타일을 뒤섞어 소스 코드를 필요 이상으로 복잡하게 만드는 실수는 피해야 한다.

# 정리

책을 접하기 이전에는 단순히 읽기 좋게 혹은 짧게 작성하는 것을 선호하였다. 다양한 예시를 기반으로 들여쓰기와 배치 순서 등을 보며 개선해야 할 점들을 확인할 수 있었다.

무엇보다 가장 중요한 것은 동일한 코드 스타일을 유지하는 것이다. 여러 사람이 함께 작업을 진행하기 때문에 모두 같은 코드 스타일을 유지하도록 노력해야 한다. 추후 협업을 진행할 때 책에서 강조한 내용을 기반으로 빠른 변경에 대응할 수 있는 일관된 코드를 작성할 수 있도록 노력해야 겠다.

# References

로버트 C.마틴, 『클린코드 애자일 소프트웨어 장신 정신』, 박재호 이해형 옮김, 인사이트(2013),p95-116

#clean code
last updated: 9/16/2022, 8:54:51 PM