Posts [클린코드] Chapter5-형식 맞추기
Post
Cancel

[클린코드] Chapter5-형식 맞추기

프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다. 코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다. 필요하다면 규칙을 자동으로 적용하는 도구를 활용한다. (ex. intellij의 codestyle이 이러한 도구 중 하나가 될 것 같다)

형식을 맞추는 목적

  • 코드 형식은 중요하다!
  • 오늘 구현한 기능이 다음 버전에서 ㅂ바뀔 확률은 아주 높다.
    • 그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드 품질에 지대한 영향을 미친다.
    • 오랜 시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아높은 구현 스타일과 가독성 수준은 유지보수 숑이성과 확장성에 계속 영향을 미친다.
    • 원래 코드는 사라질지라도 개발자의 스타일과 규율은 사라지지 않는다.
  • 그렇다면 원활한 소통을 장려하는 코드 형식은 무엇일까?

적절한 행길이를 유지하라

  • 소스 코드는 얼마나 길어야 적당할까?

image

  • 표5-1 이 우리에게 말하는 바는 다음과 같다.
    • 500줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다는 사실이다.
    • 반드시 지킬 엄격한 규칙은 아니지만 바람직한 규칙으로 삼으면 좋겠다.
    • 일반적으로 큰 파일보단 작은 파일이 이해하기 쉽다.

신문 기사처럼 작성하라

  • 독자는 위에서 아래로 기사를 읽는다.
    • 최상단에 기사를 몇 마디로 요약하는 표제가 나온다.
    • 독자는 표제를 보고서 기사를 읽을지 말지를 결정한다.
    • 첫 문단은 전체 기사 내용을 요약한다.
    • 세세한 사실은 숨기고 커다란 그림을 보여준다.
    • 쭉 읽으며 내려가면 세세한 사실이 조금씩 드러난다.
    • 날짜, 이름, 발언, 주장, 기타 세부사항이 나온다.
  • 소스 파일도 위의 신문 기사와 비슷하게 작성한다.
    • 이름은 간단하면서도 설명이 가능하게 짓는다.
    • 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경써서 짓는다.
    • 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다.
    • 아래로 내려갈수록 의도를 세세하게 묘사한다.
    • 마지막에는 가장 저차원 함수와 세부 내역이 나온다.
  • 신문은 다양한 기사로 이뤄진다.
    • 대다수 기사가 아주 짧다.
    • 어떤 기사는 조금 길다. 한 면을 채우는 기사는 거의 없다.
    • 신문이 읽을 만한 이유는 여기에 있다.
    • 신문이 사실, 날짜, 이름 등을 무작위로 뒤섞은 긴 기사 하나만 싣는다면 아무도 읽지 않으리라.

개념은 빈행으로 분리하라.

  • 각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이는 빈 행을 넣어 분리해야 마땅하다.
  • 패키지 선언부, import문, 각 함수 사이에는 빈행이 들어간다.
    • 빈 행은 새로운 개념을 시작한다는 시각적 단서다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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
24
25
//안 좋은 예시 - 의미없는 주석과 공백은 코드를 다른 개념으로 인식시킨다.
public class ReporterConfig {
    /*
    * 리포터 리스너의 클래스 이름
    */
    private String m_className;
    
    /*
    * 리포터 리스너의 속성
    */
    private List<Property> m_properties = new ArrayList<Property>();
    public void addProperty(Property property) {
    	m_properties.add(property);
    }
}

//개선 - 공백을 제거하여 같은 개념임을 표현할 수 있다. (코드가 훨씬 눈에 잘 들어온다)
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 변수를 피해야 하는 이유 중 하나다.

변수 선언

  • 변수는 사용하는 위치에 최대한 가까이 선언한다.
  • 우리가 만든 함수는 매우 짧으므로 지역 변수는 각 함수 맨 처음에 선언한다.

인스턴스 변수

  • 인스턴스 변수는 클래스 맨 처음에 선언한다.
  • 변수 간에 세로로 거리를 두지 않는다.
  • 잘 설계한 클래스는 많은 (혹은 대다수) 클래스 메서드가 인스턴스 변수를 사용하기 때문이다.
  • 인스턴스 변수를 선언하는 위치는 아직도 논쟁이 분분하다.
    • 일반적으로 C++ 은 클래스 마지막에 선언하고, 자바는 맨 처음에 선언한다.
    • 하지만 이 논쟁보단 잘 알려진 위치에 인스턴스 변수를 모은다는 사실이 중요하다. 변수 선언을 어디서 찾을지 모두가 알고 있어야 한다.

종속 함수

  • 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다.
  • 또한, 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽히게 될 것이다.
  • 규칙을 일관적으로 적용한다면 독자는 방금 호출한 함수가 잠시 후에 정의되리라는 사실을 예측할 수 있다.
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
// 목록5-5
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;
  }
}
...
  • 위 코드의 getPagenameOrDefault 함수 안에서 "FrontPage" 상수를 사용하는 방법도 있다.
    • 하지만 그러면 기대와는 달리 잘 알려진 상수가 적절하지 않은 저차원 함수에 묻힌다.
    • 상수를 알아야 마땅한 함수에서 실제로 사용하는 함수로 상수를 넘겨주는 방법이 더 좋다.

개념 유사성

  • 친화도가 높을수록 코드를 가까이 배치한다.
  • 친화도가 높은 요인은 여러가지다.
    • 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예다.
    • 변수와 그 변수를 사용하는 함수도 한 예다.
    • 비슷한 동작을 수행하는 일군의 함수도 좋은 예다.

세로 순서

  • 호출되는 함수를 호출하는 함수보다 나중에 배치한다.
  • 그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.
    • 그러면 마치 신문처럼 읽히게 될 것이다.
    • 세세한 사항은 가장 마지막에 표현하게 될텐데 그러면 독자가 소스 파일에서 첫 함수 몇 개만 읽어도 개념을 파악하기 쉬워진다.
    • 코드를 처음 접한 사람 또는 유지보수할 때 굳이 세세한 사항까지 매번 파고들 필요가 없어지고 가독성도 좋아지게 될 것이다.
    • 목록5-5가 좋은 예다.

가로 형식 맞추기

  • 가로길이는 20자~60자 사이인 행이 총 행수의 40%에 달한다는 말이다.
  • 10자 미만은 30%정도로 보인다.
  • 프로그래머는 짧은 행을 선호한다.
  • 100자나 120자에 달해도 나쁘지 않다. 하지만 그 이상은 솔직히 주의부족이다.
  • 예전보다 큰 모니터들을 많이 사용해서 한 화면에 200자까지도 들어가지만 가급적으론 120자 정도로 행 길이를 제한하자.

가로 공백과 밀집도

1
2
3
4
5
6
7
private void measureLine(String line) {
  lineCount++;
  int lineSize += line.length();
  totalCHars += lineSize;
  lineWithHistogram.addLine(lineSize, lineCount);
  recordWidestLine(lineSize);
}
  • 할당 연산자를 강조하기 위해 앞뒤에 공백을 주었다.
  • 하지만 함수 이름과 이어지는 괄호 사이에는 공백이 없다.
    • 함수와 인수는 인접하기 때문이다.
    • 공백을 넣게되면 한 개념이 아니라 별개로 보이게 된다.
  • 함수를 호출하는 코드에서 괄호 안 인수는 공백으로 분리했다.
    • 쉼표를 강조해 인수가 별개라는 사실을 보여주기 위해서다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Quadratic {

    public static double root1(double a, double b, double c) {
        double determinant = determinant(a, b, c);
        return (-b + Math.sqrt(determinant) / (2*a));
    }

    public static double root2(double a, double b, double c) {
        double determinant = determinant(a, b, c);
        return (-b - Math.sqrt(determinant) / (2*a));
    }

    private static double determinant(double a, double b, double c) {
        return b*b - 4*a*c;
    }
}
  • 연산자 우선순위를 강조하기 위해서도 공백을 사용한다.
  • 승수 사이는 공백이 없다.
    • 곱셉은 우선순위가 가장 높기 때문이다.
  • 항 사이에는 공백기 들어간다.
    • 덧셈과 뺼셈은 우선순위가 곱셈보다 낮기 때문이다.
  • 불행히도 코드 형식을 자동으로 맞춰주는 도구는 대다수가 연산자 우선순위를 고려하지 못하므로, 수식에 똑같은 간격을 적용한다.
    • 따라서 위와 같이 공백을 넣어줘도 나중에 도구에서 없애는 경우가 흔하다.

가로 정렬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FitNesseExpediter implements ResponseSender
{
  private   Socket          socket;
  private   InputStream     input;
  private   OutputStream    output;
  private   Request         request;
  private   Response        response;
  private   FitnesseContext context;
  private   long            requestParsingTimeLimit;
  private   long            requestProgress;
  private   long            requestParsingDeadline;
  private   boolean         hasError;
 
  public FitNessExpediter(Socket          s,
                          FitNesseContext context) throws Exception
  {
    this.context =        context;
    socket =                  s;
    input =                   s.getInputStream();
    output =                  s.getOutputStream();
    requestParsingTimeLinit = 10000;
  }
}
  • 위와 같은 정렬은 유용하지 않다.
  • 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.
    • 예를 들어, 위 선언부를 읽다 보면 변수 유형은 무시하고 변수 이름부터 읽게 된다.
    • 마찬가지로, 위 할당문을 훑어보면 할당 연산자는 보이지 않고 오른쪽 피연산자에 눈이 간다.
  • 설상가상으로 코드 형식을 자동으로 맞춰주는 도구는 대다수가 위와 같은 정렬을 무시한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FitNesseExpediter implements ResponseSender
{
  private Socket socket;
  private InputStream input;
  private OutputStream output;
  private Request request;
  private Response response;
  private FitnesseContext context;
  private long requestParsingTimeLimit;
  private long requestProgress;
  private long requestParsingDeadline;
  private boolean hasError;
 
  public FitNessExpediter(Socket s, FitNesseContext context) throws Exception
  {
    this.context = context;
    socket = s;
    input = s.getInputStream();
    output = s.getOutputStream();
    requestParsingTimeLinit = 10000;
  }
}
  • 위와 같이 정렬하지 않게되면 오히려 중대한 결함을 찾기 쉬워진다.
  • 정렬이 필요할 정도로 목록이 길다면 문제는 길이지 정렬 부족이 아니다.
    • 즉, 클래스를 쪼개야 한다는 의미다.

들여쓰기

1
2
3
4
5
6
7
8
public class Product {

  private String name;

  public String getName() {
    return this.name;
  }
}
  • 들여쓰기한 파일은 구조가 한눈에 들어온다.
  • 변수, 생성자 함수, 접근자 함수, 메서드가 금방 보인다.
  • 반면, 들여쓰기 하지 않은 코드는 열심히 분석하지 않는 한 거의 불가해하다.

들여쓰기 무시하기

  • 간단한 if문, 짧은 while문, 짧은 함수에서도 들여쓰기로 범위를 제대로 표현한 코드가 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
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 ""; } 
}


// good
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문이나 for문은 가능한 피해라.
  • 피하지 못할땐 빈 블록을 올바로 들여쓰고 괄호로 감싼다.
  • 세미콜론(;)은 새행에다 제대로 들여써서 넣어준다.
    • 그렇게 하지 않으면 눈에 띄지 않는다.

팀 규칙

  • 팀에 속한다면 자신이 팀의 규칙을 따르는 것이 무엇보다 중요하다.
  • 그래야 소프트웨어가 일관적인 스타일을 보인다.
    • 개개인이 따로국밥처럼 맘대로 짜대는 코드는 피해야 한다.
  • 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다는 사실을 기억하기 바란다.
  • 스타일은 일관적이고 매끄러워야 한다.
    • 한 소스 파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 줘야 한다.
    • 온갖 스타일을 뒤섞어 소스 코드를 필요 이상으로 복잡하게 만드는 실수는 반드시 피한다.

밥 아저씨의 코드 규칙

  • 밥 아저씨의 코드 규칙은 아래 코드를 통해 확인할 수 있다.
  • 코드 자체가 최고의 구현 표준 문서가 되는 예다.
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
91
92
// 목록5-6

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;
    } 
}

예제 코드 출처

This post is licensed under CC BY 4.0 by the author.

[flyway] DB 마이그레이션을 위한 flyway

[도메인주도개발시작하기] Chapter1-도메인 모델 시작