본문 바로가기

개발자 강화/개발 독서

[Clean Code 2판] 1-6: Formatting

안녕하세요, 여러분!

메리크리스마스입니다!! 🎄


Part1. Code

Chapter5. Formatting

코드를 열었을 때 "이거 누가 짰어?"라는 반응이 나오면 안된다.
전문가가 작업했다는 느낌이 들어야 한다.
Code Formatting이 엉망이면, 프로젝트 전체가 엉망일거라고 예상하게 된다.

 

규칙은 단순하게 정할 것, 그것을 일관되게 지킬 것, 그리고 도구를 사용해 자동화할 것.

 

팀은 하나의 Formatting Rule을 정하고, 모든 구성원이 이를 따라야 한다.

 


The Purpose of Formatting

포맷팅의 목적

 

코드를 짤 때 가장 중요한 게 뭘까?

"Getting it working"이 중요한거 아냐? 돌아가면 되는거 아니야?

 

Formatting = Communication

코드 포맷팅은 '의사소통'에 관한 것이며, professional한 개발자의 최우선 업무이다.

 

단지 예쁘게 보이게 하려는 것이 아니다.

"이 코드가 뭘 하는지 다른 사람이 빨리 파악할 수 있게"하는 것이다.

 

오늘 작성한 기능은 내일 바뀐다.

다음 스프린트에 수정되고, 다음 배포에 또 바뀐다.

하지만, 코드를 작성하는 방식은 오래 남는다.

 

Your style and discipline survive, even though your code does not.

내가 작성한 그 코드가 없어지더라도, 코딩 스타일은 남는다!!

 

개발자는 코드를 작성하는 시간보다, 읽는 시간이 훨씬 길다.

읽기 좋은 코드가 중요하고, Formatting이 그 핵심 수단이다.

 

쉽게 하는 변명: "아니 원래 그랬다니까요??"

후임자에게 "원래 그랬음"이라는 핑계를 만들어주지 말자... (협업자들도 함께 힘들어진다...)


그래서, Formatting을 어떻게 해야한다는 걸까?

Vertical Formatting과 Horizontal Formatting, 크게 두 가지가 있다.

하나씩 살펴보자.

 

Vertical Formatting

수직 포매팅

 

차트 해석하는 법! (y축은 로그 스케일임)

- 위쪽 선 끝: 가장 긴 파일의 line 수

- 중간 박스의 길이: 파일의 약 1/3이 이 범위에 있음

- 중간선: 평균 파일 길이

- 아래쪽 선 끝: 가장 짧은 파일의 line 수

 

- Junit, FitNesse: 대부분 200줄 미만, 최대 500줄

- Tomcat, Ant: 수천 줄짜리 파일도 있고, 절반이 200줄 이상의 파일

 

그런데, FitNess는 총 5만 줄 규모의 시스템인데도 각 파일 대부분이 200줄 이하이다.

(정말 부럽다..............)

 

작은 파일은 큰 파일보다 이해하고 유지보수하기 쉽다.

한 파일의 이상적인 line 수는 200줄 이하이며, 상한선은 500줄이다.

 

파일이 500줄 이상이라면, 이 파일이 한 가지 일만 하고 있는지 고민해보자.

 


1. Vertical Opennes between Concepts

수직적 여백(빈줄)

 

빈 줄 = 문단 나누기

글을 쓸 때 문단을 나누듯, 코드도 빈 줄로 개념을 구분한다.

 

예를 들어, claude가 생산해 준 예시 코드를 보자

public class User {
  private String name;
  private int age;

  public User(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public boolean isAdult() {
    return age >= 18;
  }
}

 

여기에서 여백을 다 지운다면 어떻게 될까?

 

public class User {
  private String name;
  private int age;
  public User(String name, int age) {
    this.name = name;
    this.age = age;
  }
  public String getName() {
    return name;
  }
  public boolean isAdult() {
    return age >= 18;

 

흐릿...하게 보면, 코드가 그냥 한 뭉텅이로 보인다.

각 빈 줄은 새롭게 분리되는 개념을 식별하는 시각적 단서가 된다.

 

빈 줄을 넣어야 하는 곳은 다음과 같다.

package 선언
                    ← 빈 줄
import 문들
                    ← 빈 줄
클래스 선언 {
  필드들
                    ← 빈 줄
  메서드1() { }
                    ← 빈 줄
  메서드2() { }
}

 

package 선언, import, 각 함수들을 분리하는 빈 줄이 있다.

 


2. Vertical Density

수직적 밀집

 

앞에서 말했듯 빈 줄로 개념을 분리해야 한다면,

관련된 코드는 붙여서 연관성을 보여줘야 한다.

 

아래 claude가 만들어준 예시를 보자

 public class User {
  /**
   * 사용자의 이름
   */
  private String name;

  /**
   * 사용자의 나이
   */
  private int age;

  public void printInfo() {
    System.out.println(name + ", " + age);
  }
}

 

뭔가 한 눈에 들어오지 않는다.

산만한 주석... 두 변수와 한 메서드를 가진 클래스라는 것을 알기까지 시간이 필요하다.

 

다시 정리해보자

 

public class User {
  private String name;
  private int age;

  public void printInfo() {
    System.out.println(name + ", " + age);
  }
}

 

이 코드는 한 눈에 들어온다.

머리나 눈을 많이 움직이지 않고도, 두 변수와 한 메서드를 가진 class라는 것을 알 수 있다.

 


3. Vertical Distance

수직 거리

 

관련된 코드는 같은 파일에, 가까이 두라.

 

한 함수에서 다음 함수로 이동하며

파일을 위에서 아래로 스크롤하며

변수나 함수의 정의를 찾아 상속 체인을 거슬러올라가며

코드의 작동을 파악하려다 혼란의 미로 속에서 길을 잃은 적이 있는가?

~작가의 울분 섞인 경험담~

 

이 규칙이 별도의 파일에 속하는 개념에는 적용되지 않지만,

굳이 나눌 필요 없는 걸 나누지 마라.

 

// 아래는 claude의 예시임
// Parent.java
public class Parent {
  protected int value;  // 자식이 쓸 수 있음
}

// Child.java (다른 파일)
public class Child extends Parent {
  public void doSomething() {
    value = 10;  // 어? value가 어디있지?
  }
}

 

protected 변수의 경우,

'value'를 이해하기 위해서는 Child.java -> Parent.java로 이동해야 한다.

protected 변수는 파일 사이를 '뛰어다니게' 만드므로 피해야 한다고 말한다.

 


4. Variables Declarations

변수 선언

 

변수는 쓰는 곳 바로 근처에 선언하라.

인스턴스 변수는 한 곳에 모아라.

 

1) 지역 변수 - 함수 상단에

우리는 이전 장에서 최대한 함수를 작게 쪼개는 방법을 배웠다!

그렇다면 함수가 짧을 테니, 맨 위에서 선언해도 사용처와 거리가 멀지 않다.

 

만약 함수가 길어서 변수 선언부와 멀어진다면, 근본적인 해결법은 일단 함수를 작게 쪼개는 것이다.

//claude의 예시
public void printUserInfo() {
  String name = user.getName();
  int age = user.getAge();
  
  System.out.println(name + ", " + age);
}

 

2) 루프 변수 - 루프 안에서 선언

// claude의 예시
// 좋은 예
for (int i = 0; i < 10; i++) {
  // i를 여기서만 씀
}

// 나쁜 예
int i;
// ... 다른 코드 ...
for (i = 0; i < 10; i++) {
  // i가 어디서 온 거지?
}

 

3) 인스턴스 변수 - 한 곳에 모으기

지역 변수는 특정 코드에서만 쓰니까 그 근처에 둬야 한다.

// claude의 예시
public class User {
  private String name;  // 어떤 메서드 근처에 둬야 하지?

  public String getName() { return name; }
  public void setName(String n) { name = n; }
  public void print() { System.out.println(name); }
}

하지만, 인스턴스 변수는 여러 메서드가 공유한다.

특정 메서드 근처에 둘 수 없다.

그래서 한 곳에 모아둬야 한다.

 

근데, 한 곳에 모아둔다고 하면... 상단에 모아둬야 하나? 하단에 모아 둬야 하나?

 

C++ 스타일(하단)

// claude의 예시
class User {
public:
  void method1();
  void method2();

private:
  string name;  // private은 아래에
  int age;
};

 

public이 위, private이 아래 (scissors rule)

 

Java/C# 스타일(상단)

// claude의 예시
public class User {
  private String name;  // 맨 위에
  private int age;

  public void method1() { }
  public void method2() { }
}

 

작가 왈:

private 변수를 상단에 두는 C++이 더 합리적일 수 있으나,

Java/C#의 관례가 언어들에 만연해 있음

논리와 상식에 호소해 자신만의 규칙을 만들어 혼란을 야기하는 것보다 따르는 것이 더 낫다.

(작가의 꾸준한 주장은, 개인 스타일 주장하기 전에 팀 스타일을 먼저 따르는 것)

 

 

이런 것을 다 차치하고, 중요한 건 위치보다 일관성이라고 한다.

어디로 가야 변수가 있는지 모두가 합의해서 코드를 작성해야 함.

 

 


5. Dependent Functions

종속 함수

 

호출하는 함수가 위, 호출하는 함수가 아래.

 

이 관례를 지속적으로 따르면, 코드의 독자들은 '함수 정의가 사용 직후에 나올 것'이라고 믿게 된다.

 

// claude의 예시
public void process() {
  validate();
  execute();
  cleanup();
}

private void validate() {
  // 검증 작업
}

private void execute() {
  // 실행 작업
}

private void cleanup() {
  // 정리 작업
}

 

호출 순서대로 정의됨.

 

claude에게 추가 설명을 부탁했는데, 신문 기사에 비유해줬다.

[헤드라인] process()가 뭘 하는지 한눈에
     ↓
[본문1] validate()의 세부 내용
     ↓
[본문2] execute()의 세부 내용
     ↓
[본문3] cleanup()의 세부 내용

 

개요를 먼저 보고, 궁금하면 아래로 내려가면서 상세 내용을 보는 신문 읽기와 비슷하다고

claude가 부연설명 해줬는데, 꽤 그럴싸했음

 

public void main() {
  step1();
}

private void step1() {
  step1_1();
  step1_2();
}

private void step1_1() {
  // ...
}

private void step1_2() {
  // ...
}

중첩 호출도 마찬가지로, 

호출 -> 정의 -> 호출 -> 정의 순서대로 흘러가면 된다고 함.

 

그리고, 저자가 본문에서 보여준 또 다른 예시가 있다.

// 좋은 예
public void makeResponse() {
  String pageName = getPageNameOrDefault("FrontPage");
  // ...
}

private String getPageNameOrDefault(String defaultName) {
  // defaultName을 사용
}

 

상수를 적절한 위치에 두는 것이 중요하다는 예시를 보여준 것인데,

이 구조에서는 FrontPage라는 default 값을 상위 함수에서 내려준다.

 

하지만, 이걸 하위 함수(getPageNameOrDefault) 내부에서 선언하는 방법도 있을 것이다.

// 나쁜 예
public void makeResponse() {
  String pageName = getPageNameOrDefault();
  // ...
}

private String getPageNameOrDefault() {
  String defaultName = "FrontPage";  // 상수가 숨어있음
  // ...
}

 

하지만, FrontPage는 기본값을 설정하는 중요한 함수이다.

하위 함수에서 선언하면 '값을 숨기는 행위'가 되어버림.

중요한 비즈니스 상수는 눈에 잘 띄는 곳(메인 로직)에 있어야 한다고 함.

 


6. Conceptual Affinity

개념적 친화성

 

비슷한 일을 하는 코드는 가까이 둬라!

(서로 호출하지 않더라도!)

 

종속 함수: A가 B를 호출한다 -> 가까이 둬라

개념적 친화성: A와 B가 비슷한 일을 한다 -> 가까이 둬라

 

public void assertTrue(boolean condition) {
  if (!condition) fail();
}

public void assertFalse(boolean condition) {
  if (condition) fail();
}

public void assertEquals(Object a, Object b) {
  if (!a.equals(b)) fail();
}

public void assertNotNull(Object obj) {
  if (obj == null) fail();
}

 

이 함수들은 서로 호출하지 않는다. 하지만 전부 'assert' 계열이다.

공통된 명명 체계를 공유하고, 동일한 기본 작업의 변형을 수행하므로, 강한 Concept Affinity를 가진다.

 


유후! 드디어 수직 끝나고 수평!

평평하다 평평한

 

Horizonal Formatting

수평 포맷팅

위에서 살펴봤던 7개의 레포를 다시 분석해본다.

 

20~60자: 전체의 40%

10자 미만: 약 30%

80자 이상: 급격히 감소

 

개발자들은 한 줄에 적은 글자수가 들어가는 걸 선호한다!

(당연함. 코드 읽다가 가로 스크롤해야 하면 빡침)

 


1. Horizontal Openess and Density

수평적 여백과 밀집

 

공백으로 관계를 표현하라.

관련 있으면 붙이고, 분리하고 싶으면 띄워라.

 

1) 할당 연산자 - 띄워라

// 좋은 예
int lineSize = line.length();
totalChars += lineSize;

// 나쁜 예
int lineSize=line.length();
totalChars+=lineSize;

 

할당문은 왼쪽(변수)과 오른쪽(값)이 다른 요소다.

공백을 양쪽에 둬서 '이건 둘이 다른 값임!'을 보여준다.

 

2) 함수와 괄호 - 붙여라

// 좋은 예
recordWidestLine(lineSize);
System.out.println(name);

// 나쁜 예
recordWidestLine (lineSize);
System.out.println (name);

 

함수와 인자는 하나의 덩어리다.

띄우면 분리된 것처럼 보인다.

 

3) 인자들 사이 - 띄워라

// 좋은 예
addLine(lineSize, lineCount);

// 나쁜 예
addLine(lineSize,lineCount);

 

인자들은 각각 다른 값이다.

쉼표 뒤 공백으로 '이건 별개야'라고 보여준다.

 

4) 연산자 우선순위 - 공백으로 표현

// 좋은 예
return (-b + Math.sqrt(determinant)) / (2*a);
return b*b - 4*a*c;

// 나쁜 예
return (-b + Math.sqrt(determinant)) / (2 * a);
return b * b - 4 * a * c;

 

높은 우선순위는 붙이고, 낮은 우선순위는 띄워서 차이를 둔다.

(근데 ctrl+s하면 공백 사라지는 경우도 있음..ㅋㅋㅠ)

 


2. Horizontal Alignment

수평 정렬

 

수평 정렬은 변수명이나 값을 세로로 맞춰서 정렬하는 것이다.

private   Socket          socket;
private   InputStream     input;
private   OutputStream    output;
private   Request         request;

 

작가가 어셈블리 언어 프로그래머였을 때 이런 정렬을 되게 열심히 했다고 한다.

깔쌈해 보이긴 한다...

근데, 타입은 건너뛰고 자꾸 변수명만 읽게 되는 문제가 있다.

차라리 정렬 안했을 때 타입이랑 변수명을 같이 읽게 된다.

 

만약 정렬이 필요할 정도로 코드가 길고 지저분하게 느껴진다면,

정렬로 예쁘게 만들 게 아니라, 클래스를 분할해야 함.

 


 

3. Indentation

들여쓰기

 

들여쓰기는 계층 구조를 눈으로 보여준다.

 

claude가 생성해준 예제는 아래와 같다.

public class User {
private String name;
public User(String name) {
this.name = name;
}
public void sayHello() {
if (name != null) {
System.out.println("Hello, " + name);
}
}
}

 

메모장에 복붙 잘못한 코드처럼 생겼네...

아무튼, 뭐가 어디 안에 들어있는지도 모르겠음

 

public class User {                    // 레벨 0: 클래스
  private String name;                 // 레벨 1: 필드

  public void process() {              // 레벨 1: 메서드
    if (isValid()) {                   // 레벨 2: 조건문
      for (int i = 0; i < 10; i++) {   // 레벨 3: 반복문
        doSomething();                 // 레벨 4: 실행
      }
    }
  }
}

 

들여쓰기를 보기만 해도,

클래스 경계, 메서드 갯수, 중첩 깊이를 알 수 있음.

 

4. Breaking Indentation

들여쓰기 파괴

 

public int getCount() { return count; }

이런 진짜 간단한 건 한줄로 써도 되는데, 웬만하면 펼쳐서 쓰는 걸 권장한다.

 

// 하지 마시오
public CommentWidget(ParentWidget parent, String text) {super(parent, text);}
public String render() throws Exception {return "";}

// 하시오
public CommentWidget(ParentWidget parent, String text) {
  super(parent, text);
}

public String render() throws Exception {
  return "";
}

 

 


Team Rules

팀 규칙

 

내 취향보다 팀 규칙이 우선!

 

작가는 FitNesse 프로젝터를 시작할 때 팀원들이 모여서 10분 동안 규칙을 정했다고 함.

- 중괄호 위치

- 들여쓰기 크기

- 클래스/변수/메서드 네이밍

 

하지만, 작가가 선호하는 규칙은 아니었다고 한다.

그러나, 팀이 정한 규칙이었기 때문에 팀원으로서 그것을 따랐다고 한다.

 

개발자들은 자신만의 '쪼'가 있다.

그걸 주장하고 싶을 수 있다. 어떻게 보면 그게 본인의 엔지니어적 자존심이니까.

하지만, 팀으로서 일할 때는 공통 협의점을 같이 따르는 게 맞다. (plz...)

 

후임자(내 코드의 독자)가 코드를 읽을 때,

이 소스 파일에서 본 Formatting이 다른 파일에서도 같을 것이라고 '신뢰할 수 있어야'한다.

 

가장 최악의 상황은

각기 다른 개인 스타일의 뒤죽박죽 코드를 작성하고,

거기에 또 나만의 스타일을 얹어서 코드에 복잡성을 늘리는 것이다.


이 내용을 읽으니,

종근님과 예전에 1:1로 밥을 먹으면서 나눴던 대화가 기억났다.

 

나는 당시 레거시에 정말 많이 지쳐있었다.

그냥 이걸 다 버리고 내가 코드를 새로 짜서 엎어버리고 싶었다.

솔직하게 말하자면, 초반에 코드 일부는 사실 그렇게 하기도 했다.

 

그런데, 그게 점점 시간이 지날수록 뭔가 깨닫기 시작했다.

 

레거시를 읽는 기간이 늘어날수록,

정말 많은 개발자들을 커밋 기록에서 만났다.

 

A 개발자...B 개발자...C 개발자...D 개발자...

다 자신의 '쪼'대로 다른 코드 구조를 써놨었다.

 

10명이 넘는 개발자의 Formatting style을 다 이해해야 코드를 제대로 읽을 수 있었다.

그게 정말 오랜 시간이 걸렸다. 그리고 너무 고통스럽고 힘들었다.

 

그 과정에서 내가 든 생각은...

"내가 지금 엎고 새로 짜면, 이 개발자들이랑 다를 게 뭐지?"

내가 똑같은 짓을 반복하고 있다는 사실을 깨달았다.

 

후임자에게 나는

"더 나은 구조를 제시한 멋진 개발자"가 아닌,

"그저 또다른 구조를 만들어 레거시 패턴을 늘린 E 개발자"에 불과하단 사실도 깨달았다.

그게 너무 부끄럽고 속상했다.

 

그래서, 종근님께 이렇게 말했다.

 

"예전에는 있는 걸 다 버리고 새로 짜는 게 옳은 행동이라고 여겼는데,

개발자마다 다른 패턴을 읽으며, 내가 하고 있는 행동도 그들과 다르지 않다는 걸 느꼈다.

지금은 있는 코드를 무작정 버리기 보다 더 나은 방향으로 개선하는 방향으로 레거시 리팩토링 작업을 하고 있다."

 

종근님은 그때

"내가 본 수연님의 성장 중 가장 큰 성장인 것 같다. 지금 완전 소름돋았다"라고 말씀해주셨다.

그때는 당황해서 잘 대답을 못했지만, 그렇게 말씀해 주셔서 정말 감사했다.

(갑자기 진실 고백;;)


 

마침 어제 코딩 작업할 때, 내가 저 내용을 깨달았던 지점의 코드를 다시 만나서 작업했다.

다시 생각해도 그 때 애매하게 리팩토링된 이 레거시 컴포넌트를 버리고 새로 짜는 선택을 하지 않아서 다행이라고 생각한다.

 

이번에 서버드리븐 ui 작업을 하면서 이 컴포넌트를 결국 버려야 할지도 모르는 상황이 되긴 했다.

하지만, 만약 이 컴포넌트를 애진작 버리고 새로운 컴포넌트를 애매하게 만들었다면,

서버드리븐 작업을 하면서 레거시 컴포넌트, 새로운 컴포넌트 이 두 개를 모두 핸들링하는 방안을 고민했어야 했을 것이다.

그럼 설계에 더 난항을 겪었을 것이다.

 

아무튼, Formatting 챕터를 읽으며 내가 깨달았던 내용들이 떠올라서 좋았다.

그리고, 내가 조금씩 성장하고 있어서 다행이라는 생각도 들었다.


이후에 작가가 제시하는 정석 Formatting 예시가 나오는데,

이건 각자 책에서 직접 확인해보는 게 좋을 것 같다.

 

나는 이 책을 읽으면서 내가 잘 이해할 수 있도록 claude에게 새로운 예시를 생산하도록 요청하며 정리하기도 한다.

따라서, 정확한 본문 내용은 항상 책에서 확인하는 것을 권장한다.

 

책을 읽을수록 우리 회사의 모든 개발자가 이 책을 한 번씩 꼭 읽어봤으면 좋겠다는 생각이 든다.


1차 작성

작성 시작: 2025.12.24. 23:50

작성 종료: 2025.12.25. 01:01

 

2차 작성

작성 시작: 2025.12.25. 14:05

작성 종료: 2025.12.25. 15:55

 

메리 크리스마스! 🎄

728x90