[Clean Code] Ⅴ. 형식 맞추기

Clean Code 3판을 읽고 정리한 글입니다

Ⅴ.형식 맞추기(Formatting)

목적

코드 형식은 중요하다

  • 너무 중요하지만 융통성 없이 맹목적으로 따르면 안된다
  • 코드 형식은 의사소통의 일환이며 개발자의 1차적 의무
  • 돌아가는 코드가 1차적인 의무가 아님
  • 오늘 구현한 코드의 가독성은 앞으로의 코드 품질에 큰 영향
  • 원래 코드는 사라져도 스타일과 규율은 사라지지 않고 유지보수에 계속 영향을 미친다

적절한 행 길이를 유지하라

책에서는 여러 프로젝트의 파일 크기를 조사한 결과를 보여준다
대표적인 예시 - FitNesse

  • 5만 라인이 넘는 시스템
  • 500줄이 넘어가는 파일 없으며, 대다수 200줄 미만
    일반적으로 큰 파일 보다 작은 파일이 이해가 쉽다

신문기사처럼 작성하라

좋은 신문 기사의 모습

  • 최상단에 기사를 몇 마디로 요약하는 표제
  • 독자는 위 아래로 기사를 읽으며 표제를 읽고 기사를 읽을지 결정
  • 첫 문단 : 전체 기사 내용 요약, 세부사항 숨기고 큰 그림 위주로 보여줌
  • 쭉 읽어 나가면? : 이름, 발언, 주장등 세부사항이 드러남

소스파일도 비슷하게 작성

  • 이름 : 간단하면서도 이름만 보고 올바른 모듈인지 판단되게 신경써서 짓는다
  • 소스 파일 첫 부분 : 고차원 개념과 알고리즘 설명
  • 아래로 내려갈 수록 의도가 세세하게 묘사됨
  • 마지막에 가장 저차원 함수와 세부내역

생각해볼 점

  • 신문은 다양한 기사로 이루어짐
  • 대다수 기사가 아주 짧으며 어떤 기사는 조금 길며 한 면을 채우는 기사는 매우 드물다
  • 신문이 세부내ㅔ역을 무작위로 섞은 긴 기사 하나만 싣는다면 아무도 읽지 않는다
  • 소스코드와 비교하면서 생각해보자

개념은 빈 행으로 분리

  • 거의 모든 코드의 읽는 순서 : 왼쪽→오른쪽, 위→아래
  • 각 줄 : 수식이나 절을 나타낸다
  • 여러 줄의 묶음 : 완결된 생각 하나
  • 생각 사이에는 빈 행을 넣어 분리해야 한다
  • 빈 행이 없으면 가독성이 떨어진다
    빈 행 없을 때
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package fitnesse.wikitext.widgets;
    import java.util.regex.*;
    public class BoldWidget extends ParentWidget {
    public static final String REGEXP = "'''.+?'''";
    private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
    Pattern.MULTILINE + Pattern.DOTALL);
    public BoldWidget(ParentWidget parent, String text) throws Exception {
    super(parent);
    Matcher match = pattern.matcher(text); match.find();
    addChildWidgets(match.group(1));}
    public String render() throws Exception {
    StringBuffer html = new StringBuffer("<b>");
    html.append(childHtml()).append("</b>");
    return html.toString();
    }
    }
    빈 행 있을 때
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package fitnesse.wikitext.widgets;

    import java.util.regex.*;

    public class BoldWidget extends ParentWidget {
    public static final String REGEXP = "'''.+?'''";
    private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
    Pattern.MULTILINE + Pattern.DOTALL
    );

    public BoldWidget(ParentWidget parent, String text) throws Exception {
    super(parent);
    Matcher match = pattern.matcher(text);
    match.find();
    addChildWidgets(match.group(1));
    }

    public String render() throws Exception {
    StringBuffer html = new StringBuffer("<b>");
    html.append(childHtml()).append("</b>");
    return html.toString();
    }
    }

세로 밀집도

  • 줄바꿈이 개념 분리라면? 세로 밀집도는 연관성을 의미
  • 밀접한 코드행은 세로로 가까이 놓여야 한다
  • 연관성이 있는 부분을 주석등의 추가로 멀리 떨어뜨리면 한 눈에 들어오지 않게 된다
연관성 부분에 주석이 들어가 변수끼리 멀어진 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
ublic class ReporterConfig {
/**
* The class name of the reporter listener
*/
private String m_className;

/**
* The properties of the reporter listener
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
주석 제거로 한 눈에 들어오게 된 클래스
1
2
3
4
5
6
7
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();

public void addProperty(Property property) {
m_properties.add(property);
}

수직 거리

이런 경험이 있는가?

  • 함수 연관 관계, 동작을 이해하려고 함수를 오가며 소스 파일을 위로 오가는데 못찾게 되는 경험?
  • 함수/변수가 정의된 코드를 찾으려 상속 관계를 줄줄히 올라간 경험?
  • 시스템을 이해하려는 목적과 달리 각 조각이 어디에 있는지 찾고 기억해야 함
  • 시간과 노력을 엄청나게 소모
  • 서로 밀접한 개념은 세로로 가까이 둬야 한다
  • 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다.
  • 타당한 근거가 없다면 밀접한 개념은 한 파일에 → protected 변수를 피해야 하는 이유 중 하나
  • 연관성 : 한 개념을 이해하는데 다른 개념이 중요한 정도
  • 연관성 있는 개념이 떨어져있으면 읽는 사람이 파일/클래스를 뒤져야함
변수 선언
  • 변수는 사용 위치에 최대한 가까이 선언
  • 좋은(작은)함수는 지역 변수 각 함수 맨 처음에 선언
    JUnit 4.3.1의 함수 예시
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static void readPreferences() {
    InputStream is = null; // 맨 처음에 선언
    try {
    is = new FileInputStream(getPreferencesFile());
    setPreferences(new Properties(getPreferences()));
    getPreferences().load(is);
    } catch (IOException e) {
    try {
    if (is != null)
    is.close();
    } catch (IOException e1) {
    }
    }
    }
    루프 제어 변수는 : 흔히 루프 안에 선언
    1
    2
    3
    4
    5
    6
    public int countTestCases() { 
    int count = 0;
    for (Test each : tests) //이렇게~
    count += each.countTestCases();
    return count;
    }
  • 긴 함수 : 블록 상단이나 루프 직전에 변수 선언 사례
    (아주 드문 케이스)
    TestNG의 아주 긴 함수 중 일부
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    for (XmlTest test : m_suite.getTests()) {
    TestRunner tr = m_runnerFactory.newTestRunner(this, test); //이렇게
    tr.addListener(m_textReporter);
    m_testRunners.add(tr);

    invoker = tr.getInvoker();

    for (ITestNGMethod m : tr.getBeforeSuiteMethods()) {
    beforeSuiteMethods.put(m.getMethod(), m);
    }

    for (ITestNGMethod m : tr.getAfterSuiteMethods()) {
    afterSuiteMethods.put(m.getMethod(), m);
    }
    }
인스턴스 변수
  • 자바 :클래스 맨 처음에 선언
  • C++ : scissors rule(모든 인스턴스 변수를 클래스 마지막에 선언한다)
  • 중요한 것은 인스턴스 변수를 모으고 어디서 찾을 수 있는지 모두가 알고 있어야 함
  • 변수간 세로 거리 X → 정상적인 클래스는 대다수의 클래스 메서드가 인스턴스 변수 사용
    JUnit4.3.1의 TestSuite 클래스 일부분
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

    public class TestSuite implements Test {
    static public Test createTest(Class<? extends TestCase> theClass,
    String name) {
    ...
    }

    public static Constructor<? extends TestCase>
    getTestConstructor(Class<? extends TestCase> theClass)
    throws NoSuchMethodException {
    ...
    }

    public static Test warning(final String message) {
    ...
    }

    private static String exceptionToString(Throwable t) {
    ...
    }

    private String fName; //이거

    private Vector<Test> fTests= new Vector<Test>(10); //이것도

    public TestSuite() { }

    public TestSuite(final Class<? extends TestCase> theClass) {
    ...
    }

    public TestSuite(Class<? extends TestCase> theClass, String name) {
    ...
    }

    ... ... ... ... ...
    }
종속 함수 : 한 함수가 다른 함수 호출
  • 세로로 가까이 배치 - 연관성끼리
  • 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치 - 자연스럽게 읽히도록
  • 일관적으로 규칙 적용 한다면 → 읽는 사람은 호출된 함수가 잠시 뒤에 정의되리라고 예측
  • 호출되는 함수 찾기가 쉬어지며 모듈 전체의 가독성도 올라간ㄷ
    FitNesse의 일부 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // 가장 먼저 호출하는 함수가 바로 아래 정의
    // 그다음 호출하는 함수는 그 다음 정의

    public class WikiPageResponder implements SecureResponder {
    protected WikiPage page;
    protected PageData pageData;
    protected String pageTitle;
    protected Request request;
    protected PageCrawler crawler;

    public Response makeResponse(FitNesseContext context, Request request) throws Exception {
    String pageName = getPageNameOrDefault(request, "FrontPage");
    loadPage(pageName, context);
    if (page == null)
    return notFoundResponse(context, request);
    else
    return makePageResponse(context);
    }

    private String getPageNameOrDefault(Request request, String defaultPageName) {
    String pageName = request.getResource();
    if (StringUtil.isBlank(pageName))
    pageName = defaultPageName;

    return pageName;
    }

    protected void loadPage(String resource, FitNesseContext context)
    throws Exception {
    WikiPagePath path = PathParser.parse(resource);
    crawler = context.root.getPageCrawler();
    crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
    page = crawler.getPage(context.root, path);
    if (page != null)
    pageData = page.getData();
    }

    private Response notFoundResponse(FitNesseContext context, Request request)
    throws Exception {
    return new NotFoundResponder().makeResponse(context, request);
    }

    private SimpleResponse makePageResponse(FitNesseContext context)
    throws Exception {
    pageTitle = PathParser.render(crawler.getFullPath(page));
    String html = makeHtml(context);
    SimpleResponse response = new SimpleResponse();
    response.setMaxAge(0);
    response.setContent(html);
    return response;
    }
    /*makeResponse 함수에서 호출하는 getPageNameOrDefault함수 안에서 "FrontPage" 상수를 사용하지 않고,
    상수를 알아야 의미 전달이 쉬워지는 함수 위치에서 실제 사용하는 함수로 상수를 넘겨주는 방법이
    가독성 관점에서 훨씬 더 좋다*/

    /*
    그렇다면 getPageNameOrDefault 함수안에서 isBlank 판단 이후 상수를 사용하는게 좋아 보일 듯
    */
    ...
  • 위 코드는 상수를 적절한 수준에 두는 좋은 예제
    • 예)
      • makeResponse 함수에서 호출하는 getPageNameOrDefault함수 안에서 “FrontPage” 상수를 사용하지 않고
      • getPageNameOrDefault 함수안에서 isBlank 판단 이후 상수를 사용
    • 위의 예에는 : 기대와 달리 잘 알려진 상수가 적절하지 않은 저차원 함수에 묻힌다
    • [G35-p394]: 구성 정보는 최상위 단계에 두어라 : 상수를 저차원 함수에 숨기지 말고 고차원에 두어야 저차원 함수를 뒤적이지 않게 됨
    • 상수를 알아야 마땅한 함수에서 실제 사용하는 함수로 상수를 넘겨주는 방법이 더 좋다

개념적 유사성

  • 개념적인 친화도가 높은 코드는 서로를 끌어당김 → 친화도가 높을 수록 가까이 배치
  • 친화도 요인 : 종속성, 변수 사용, 비슷한 동작을 수행하는 일군의 함수
Junit4.3.1 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Assert {
static public void assertTrue(String message, boolean condition) {
if (!condition)
fail(message);
}

static public void assertTrue(boolean condition) {
assertTrue(null, condition);
}

static public void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}

static public void assertFalse(boolean condition) {
assertFalse(null, condition);
}
...
  • 개념적 친화도가 매우 높은 함수들임
    • 명명법이 똑깥고 기본 기능이 유사하고 간단
    • 서로가 서로를 호출하는 것은 부차적 요인 → 종속 관계가 없어도 가까이 배치할 함수

세로 순서

  • 함수 호출 종속성 아래 방향 유지 → 자연스럽게 모듈이 고차원→저차원으로 자연스럽게 내려감

    • 호출되는 함수를 호출하는 함수보다 나중에 배치
    • C, C++, Pascal과 정확히 반대 : 함수 호출을 위해 적어도 미리 선언은 해놔야하는 언어
  • 신문 기사와 같이

    • 가장 중요한 개념 가장 먼저 표현 : 세세한 사항을 최대한 배제
    • 세세한 사항은 가장 마지막에 표현
    • 독자들이 첫 함수 몇 개만 읽어도 개념 파악이 되도록
    • 좋은 목록 3-7(ch3마지막), 15-5(p338, )

가로 형식 맞추기

  • 과거 훌러리스(Hollerith) :80자 제한

    • 가로 80, 세로 12단 천공카드, 기계를 고안한 통계학자
  • 가급적 짧은 행이 바람직함

  • 120자 정도로 행 길이 제한 권고 - 인텔리 제이 120으로 되어있음

가로 공백과 밀집도

가로 공백 : 밀접한 개념, 느슨한 개념 표현 가능

1
2
3
4
5
6
7
8
9
10
11
private void measureLine(String line) { 
lineCount++;
int lineSize = line.length(); // 할당 연산자 좌우로 공백 → 좌우 요소가 확실하게 구분.
totalChars += lineSize;

// 괄호 안의 인수끼리는 쉼표 뒤의 공백을 통해 인수가 별개라는 사실을 보여준다.
// 함수명과 괄호는 공백없음 - 함수와 인수의 밀접함
// 괄호 안의 인자끼리는 쉼표 뒤의 공백 → 쉼표 강조 → 각 인자가 별개라는 의미
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}

연산자의 우선순위를 강조하기 위해서도 공백을 사용

  • 불행히 IDE의 포매터가 나중에 해당 공백을 삭제하는 경우가 흔함
  • 포매터 대부분 연산자 우선순위 고려 불가하므로 수식에 같은 간격 적용하기 때문

가로 정렬

  • 밥아저씨의 정렬
    • 어셈블리어 영향
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Reques request;
private Response response;
private FitNesseContex context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;

public FitNesseExpediter(Socket s,
FitNesseContext context) throws Exceptions
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTimeLimit = 10000;
}

좋지 않은 정렬

  • 보기엔 깔끔해 보여도 코드의 엉뚱한 부분이 강조되 진짜 의도가 가려지게 됨
  • 변수유형대신 변수 이름부터 읽게 됨
  • 할당문 : 할당 연산자가 눈에 안보이고 우측 피연산자만 눈에 보임
  • 대다수의 포매터는 위의 정렬을 무시

선언문/할당문 별도 정렬 필요 X

  • 정렬이 필요할 정도로 선언부가 길다면 클래스를 쪼개야 한다

들여쓰기

  • Scope를 한눈에 표현 : 프로그래머들이 크게 의존
들여쓰기 무시하기
  • 들여쓰기를 무식하고 싶은 유혹들 : 간단한 if, while, 짧은 함수
들여쓰기 안하고 한 행에 범위를 뭉뚱그린 코드
1
2
3
4
5
6
public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";

public CommentWidget(ParentWidget parent, String text){super(parent, text);}
public String render() throws Exception {return ""; } //멋져보이나 아니야
}
한 줄 함수라도 들여쓰기를 하자
1
2
3
4
5
6
7
8
9
10
11
public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";

public CommentWidget(ParentWidget parent, String text){
super(parent, text);
}

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

가짜 범위

  • 가능한 피하라
  • 피하지 못할 땐 밑에처럼
    빈 while 문을 어쩔수 없이 쓸때는 괄호로 감싸고 새로운 행에 세미콜론
    1
    2
    while (dis.read(buf, 0, readBufferSize) != -1)
    ;

팀 규칙

  • 팀에 속해있다면 팀 규칙이 가장 우선시 되고 선호되어야 한다
  • 팀은 한가지 규칙에 합의 필요
    • sw 일관적인 코딩스타일 적용 필요
  • 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄지고, 읽기 쉬운 문서는 스타일이 일관적이고 매끄러워야 한다.

밥 아저씨의 형식 규칙

  • 이러한 규칙의 예제
코드 자체가 구현 문서가 되는 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class CodeAnalyzer implements JavaFileAnalysis { 
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;

public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}

public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}

private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}

public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}

private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}

private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}

public int getLineCount() {
return lineCount;
}

public int getMaxLineWidth() {
return maxLineWidth;
}

public int getWidestLineNumber() {
return widestLineNumber;
}

public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}

public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}

public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}

private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}

private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}

정리

  1. 포맷팅이 중요한 이유
  • 가독성에 필수적
  • 아마추어적으로 보이지 않는다
  • 잘못된 포맷팅으로 코드를 잘못 이해해서 버그를 발생할 위험을 줄일 수 있음
  • 어렸을때 띄어쓰기, 문법 공부 - > 이게 잘 되어 있으면 글이 잘 읽힌다
  • 기본 중의 기본
  1. 클린 코드 포매팅
  • 적절한 길이
    • 200라인 이하 권장 , 500라인 이하 권장
    • 현업에서 실제 대부분의 코드도 200라인 유지
    • 200라인이 넘어가면 클래스가 여러가지 일을 수행할 확율이 높은 → SRP 위배
  • 밀접 개념 가까이
    • 행 묶음은 완결된 생각 하나 표현함 → 개념은 빈 행으로 분리
    • 변수는 사용되는 위치에서 최대한 가까이 선언
  1. Java Class Declarations
  1. Team Coding Convention
  • 코딩 스타일에 관한 약속
  • 개발 언어의 컨벤션이 우선, 애매하면(변수명) 팀 컴벤션을 따른다
    • 예) enum타입으로 사용하는 varchar타입의 경우 컬럼명은 _type로 끝나도록 네이밍한다
  • 없다면 함께 만들어가는게 좋다
  • 참고할 만한 컨벤션

Related POST

공유하기