[클린코드] Chapter5-형식 맞추기
jeonyoungho Apr 17, 2023 2023-04-17T00:00:00+09:00
Jul 3, 2023 2023-07-03T20:54:51+09:00 23 min
프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다. 코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다. 필요하다면 규칙을 자동으로 적용하는 도구를 활용한다. (ex. intellij의 codestyle이 이러한 도구 중 하나가 될 것 같다)
형식을 맞추는 목적
- 코드 형식은 중요하다!
- 오늘 구현한 기능이 다음 버전에서 ㅂ바뀔 확률은 아주 높다.
- 그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드 품질에 지대한 영향을 미친다.
- 오랜 시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아높은 구현 스타일과 가독성 수준은 유지보수 숑이성과 확장성에 계속 영향을 미친다.
- 원래 코드는 사라질지라도 개발자의 스타일과 규율은 사라지지 않는다.
- 그렇다면 원활한 소통을 장려하는 코드 형식은 무엇일까?
적절한 행길이를 유지하라
- 표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.