코드 형식은 겉모습의 문제가 아니라 의사소통의 문제다. 개발자의 일차 의무는 동작하는 코드가 아니라 다음 사람이 읽을 수 있는 코드를 남기는 것이고, 형식은 그 소통의 문법이다. 오늘 짠 코드의 세부 구현은 다음 릴리스에서 바뀌겠지만, 오늘 세운 형식과 규율은 그 뒤로도 오래 코드의 인상을 좌우한다. 그래서 형식은 “예쁘게”가 아니라 “예측 가능하게”를 목표로 한다.

형식은 읽는 순서를 만든다

잘 정리된 파일은 신문 기사처럼 읽힌다. 표제에서 큰 의도를 잡고, 위쪽 문단에서 개요를, 아래로 내려갈수록 세부를 확인한다. 소스 파일도 같다. 이름과 상단에서 무엇을 하는 모듈인지 드러나고, 아래로 갈수록 저수준 세부가 나온다. 너무 긴 파일은 한 번에 담아야 할 맥락이 많아 이 흐름을 무너뜨리므로, 대개 200줄 안팎, 길어도 500줄을 넘기지 않는 편이 읽기 좋다.

세로 배치의 핵심은 “연관된 것은 가깝게, 무관한 것은 멀게”다. 이 규율이 지켜지면 독자는 위아래로 시선을 크게 튀기지 않고도 흐름을 따라갈 수 있다.

빈 줄과 밀집도

빈 줄은 서로 다른 개념 사이의 경계다. 생각의 단위가 바뀌는 곳에 빈 줄을 넣으면, 독자는 그 공백을 “여기서 화제가 바뀐다”는 신호로 읽는다.

// before — 개념 경계가 없어 한 덩어리로 뭉쳐 읽힌다
public class ReportService {
    private final Clock clock;
    public ReportService(Clock clock) { this.clock = clock; }
    public Report build(List<Sale> sales) {
        BigDecimal total = sales.stream().map(Sale::amount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        return new Report(total, clock.now());
    }
}
// after — 필드 / 생성자 / 동작을 빈 줄로 나눈다
public class ReportService {
    private final Clock clock;
 
    public ReportService(Clock clock) {
        this.clock = clock;
    }
 
    public Report build(List<Sale> sales) {
        BigDecimal total = sales.stream()
            .map(Sale::amount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
 
        return new Report(total, clock.now());
    }
}

판단 기준: 빈 줄은 “개념이 바뀌는가”로 넣는다. 반대로 밀집도도 의미를 가진다. 서로 밀접하게 엮인 코드는 붙여 두어야 한 호흡으로 읽힌다. 함정: 습관적으로 모든 줄 사이에 빈 줄을 넣거나, 반대로 100줄을 빈 줄 없이 쌓으면 둘 다 경계 신호가 사라진다. 빈 줄은 아껴 써야 신호로 기능한다.

세로 거리 — 가까운 개념은 가까이

변수는 사용되는 위치와 가깝게 선언한다. 인스턴스 변수는 클래스 상단에 모으고, 지역 변수는 처음 쓰이는 줄 바로 위에 둔다. 서로 호출하는 함수는 세로로 가깝게 배치하되, 호출하는 함수를 호출되는 함수보다 위에 둔다. 그래야 개요에서 세부로 내려가는 신문 흐름이 유지된다.

// 호출자를 위에, 피호출자를 아래에 — 위에서 아래로 자연히 읽힌다
public class Authentication {
    public User login(String username, String password) {
        if (!isValidCredential(username, password)) {
            throw new AuthenticationException("자격 증명이 올바르지 않습니다.");
        }
        return userRepository.findByName(username);
    }
 
    private boolean isValidCredential(String username, String password) {
        // 검증 로직
        return true;
    }
}

개념적으로 유사한 코드(오버로드된 함수, 같은 일을 하는 변형들)는 서로 가까이 둔다. 이름과 시그니처가 비슷한데 파일 여기저기 흩어져 있으면 독자는 매번 찾아 헤매야 한다.

가로 형식 — 줄 길이와 공백

긴 줄은 한 번에 많은 정보를 요구하고, 가로 스크롤은 흐름을 끊는다. 한 줄은 대체로 화면 안에 들어오도록 나눈다. 특히 메서드 체인은 연산자 앞에서 끊으면 “무엇을 거쳐 무엇이 되는지”가 세로로 정렬돼 보인다.

String userDetails = userRepository.findByName("John Doe")
        .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다."))
        .getDetails();

공백은 관계를 드러내려고 쓴다. 대입 연산자 양옆의 공백은 좌변과 우변을 갈라 보이게 하고, 함수 이름과 여는 괄호를 붙이는 것은 이름과 인수가 한 단위임을 드러낸다. 함정: 여러 줄의 대입문을 세로로 맞추려고 공백을 잔뜩 넣는 정렬은 오히려 잘못된 강조를 만든다 — 독자의 눈이 값들만 훑고 타입과 이름을 건너뛰게 된다. 그런 정렬이 필요할 만큼 목록이 길다면, 정렬이 아니라 목록을 쪼개야 한다는 신호다.

들여쓰기는 코드의 구조(범위 계층)를 눈으로 보여주는 가장 기본적인 장치다. 한 줄짜리 if나 빈 while 본문이라도 들여쓰기와 중괄호를 생략해 구조를 감추지 않는다.

팀 규칙과 포매터

형식을 개인 취향에 맡기면 파일마다 다른 리듬이 생기고, 한 소스 파일 안에서도 스타일이 충돌한다. 그러면 독자는 코드를 읽기 전에 스타일부터 해석해야 한다. 중요한 것은 어떤 스타일이 절대적으로 옳은가가 아니라, 한 프로젝트가 하나의 스타일로 예측 가능하게 유지되는가다.

그래서 형식 논쟁은 기계에 맡기는 편이 낫다. 포매터(예: spotless, google-java-format, prettier)를 CI나 커밋 훅에 걸어 두면, 형식은 자동으로 통일되고 개발자는 이름과 구조 같은 진짜 어려운 문제에 집중할 수 있다. 코드 리뷰에서도 세미콜론과 들여쓰기 대신 설계에 대한 대화만 남는다.

정리

  • 형식은 의사소통이다. 목표는 “예쁘게”가 아니라 한 프로젝트 안에서 “예측 가능하게”다.
  • 파일은 신문 기사처럼 위에서 아래로, 개요에서 세부로 읽히게 배치한다.
  • 빈 줄은 개념의 경계, 밀집도는 연관성이다. 둘 다 아껴 써야 신호로 남는다.
  • 변수는 쓰이는 곳 가까이, 호출자는 피호출자 위에 둔다.
  • 정렬을 위한 과잉 공백은 잘못된 강조다. 정렬이 필요할 만큼 길면 목록을 쪼갠다.
  • 스타일 논쟁은 포매터에 맡기고, 사람은 이름과 구조에 집중한다.

다음장으로 6장