개발자라면 형식을 깔끔하게 맞춰 코드를 작성해야 한다. 코드 형식을 맞추기 위해 간단한 규칙을 정하고 그 규칙을 착실하게 따라야 한다.
형식을 맞추는 목적
코드의 형식은 매우! 아주! 중요하다! 코드 형식은 의사소통의 일환이다. 의사소통은 전문 개발자의 일차적인 의무다. 작성한 코드는 매우 불안정하다. 오늘 작성한 코드가 내일 바뀔 수 도 있다. 이때 코드의 가독성이 낮다면 앞으로 바뀔 코드 품질에 지대한 영향을 미친다. 오랜 시간이 흘러 초기의 코드 흔적을 찾아볼 수 없을지라도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 지속적인 영향을 미친다.
원활한 소통을 위해서는 다음과 같은 코드 형식을 지켜야 한다.
적절한 행 길이를 유지하라.
하나의 파일의 행 길이가 적어야 이해하기 쉽다. 그리고 작은 파일들 만으로도 충분히 큰 프로젝트를 할 수 있다. JUnit의 코드 파일을 예로 들면 JUnit에는 500줄이 넘어가는 파일이 없다. 대다수가 200줄 미만이다.
신문 기사처럼 작성하라
좋은 신문 기사는 최상단에 기사를 요약하는 표제가 나온다. 첫 문단은 전체 기사 내용을 요약해서 세세한 사실은 숨기고 커다란 그림을 보여준다. 기사를 읽으며 내려가면 세세한 사실이 조금씩 드러난다. 신문은 다양한 기사로 이뤄지며 대다수 기사가 짧다. 긴 기사도 한 면을 꽉 채우는 기사는 거의 없다. 신문이 읽을만한 이유가 여기서 들어난다.
소스 파일도 좋은 신문 기사처럼 작성해야 한다. 이름은 간단하면서도 명료하게 지어야 한다. 이름만 보고도 어떤 모듈인 지 파악이 되야 한다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈 수록 의도를 세세하게 묘사 한다. 마지막에는 가장 저차원 함수와 세부 내역이 나온다.
개념은 빈 행으로 분리하라
거의 모든 코드는 왼쪽에서 오른쪽으로, 위에서 아래로 읽힌다. 각 행은 수식이나 절은 나타내고, 일련의 행 묶음은 완결 된 생각 하나를 표현한다. 생각 사이는 빈 행을 넣어 분리해야 한다. 아래의 예시를 보면 빈 행을 넣지 않은 코드는 암호문 같아 보인다.
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
|
package fitness.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public staticc final STring REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(PatternWidget 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();
}
}
package fitness.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public staticc final STring REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(PatternWidget 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();
}
}
|
cs |
세로 밀집도
줄바꿈은 개념을 분리하고 세로 밀집도는 연관성을 의미한다. 즉, 서로 연관된 코드는 세로로 가까이 놓아야 한다. 아래의 예시를 보자. 주석이 있는 예시는 연관된 코드를 분리해 놓아서 코드가 한눈에 들어오지 않는다.
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
|
public class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
private String className;
/**
* 리포터 리스너의 속성
*/
private List<Property> properties = new ArryaList<>();
public void addProperty(Property property) {
properties.add(property);
}
}
public class ReporterConfig {
private String className;
private List<Property> properties = new ArryaList<>();
public void addProperty(Property property) {
properties.add(property);
}
}
|
cs |
수직 거리
서로 밀접하게 관련된 개념은 세로로 가까이 둬야 한다. 그렇지 않으면 함수의 연관 관계를 파악하기 위해 온갖 파일을 뒤 지게 되고 이는 혼란만 야기한다. 연관이 있지만 두 개념을 서로 다른 파일에 두는 것은 타당한 근거가 없다면 피해야 한다. 이것이 protected 변수를 피해야 하는 이유다. 같은 파일에 속할 정고로 밀접한 두 개념은 세로 거리로 연관성을 표현한다.
변수 선언. 변수는 사용하는 위치에 최대한 가까이 선언해야 한다.
인스턴스 변수. 인스턴스 변수는 클래스 맨 앞에 선언해야 한다. 잘 설계한 클래스는 많은 클래스 메서드가 인스턴스 변수 를 사용하기 때문이다.
종속 함수. 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하면 호출하는 함수를 호출되 는 함수보다 먼저 배치해야 한다. 그래야 프로그램의 가독성이 높아진다.
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
|
public class WikiPageResponder implements SecureResponder {
protected WikiPage page;
protected PageData pageData;
protected String pageTitle;
protected Request request;
protected PageCrawler crawler;
public Response makeReponse(FitNesseContext context, Request request) throw 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;
}
private 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 notFountResponse(FitNesseContext context, Request request) throw Exception {
return new NotFoundResponder().makeReponse(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 respnose;
}
}
|
cs |
개념적 유사성. 친화도가 높은 코드는 서로를 끌어당긴다. 친화도가 높을 수록 코드를 가까이 해야 한다. 한 함수가 다른 함수를 호출하는 경우, 변수가 함수에 사용되는 경우, 비슷한 동작을 수행하는 함수들이 그 예시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Assert {
staic 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);
}
}
|
cs |
세로 순서
일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 즉, 호출되는 함수를 호출하는 함수보다 나중에 배치하라. 그래야 소스 코드 모듈이 고차원에서 저차원으로 자연스레 내려간다.
가로 형식 맞추기
한 행은 짧아야 보기 편하다. 그러니 최대 120자를 넘기지 말아야 한다.
가로 공백과 밀집도
가로로 공백을 사용해 밀접한 개념과 느슨한 개념을 표시한다. 예를 들어 할당 연산자는 왼쪽 요소와 오른쪽 요소로 나뉜다. 따라서 공백을 넣어 두 가지 요소가 확실히 나뉜다는 것을 표시한다. 함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않는다. 함수와 인수는 서로 밀접히 연관되 있다. 정리하면 공백을 사용해 나우어야 하는 개념을 표시해야 한다.
1
2
3
4
5
6
7
|
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totlaChars += lineSize();
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
|
cs |
들여쓰기
소스 파일은 윤곽도와 계층이 비슷하다. 파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보 등이 있다. 계층에서 각 수준은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위(scope)다.
이렇든 범위로 이뤄진 계층을 표현하기 위해 코드를 들여쓴다. 들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례한 다.
개발자는 이런 들여쓰기 체계에 크게 의존한다. 왼쪽으로 코드를 맞춰 코드가 속하는 범위를 시작적으로 표현한다. 그러 면 하나의 범위에서 다른 범위로 이동하기 쉬워진다. 들여쓰기가 없다면 코드를 읽는 것은 거의 불가능하다.
1
2
3
4
5
6
7
8
9
10
|
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totlaChars += lineSize();
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void measureLine(String line) { lineCount++; int lineSize = line.length(); totlaChars += lineSize();
lineWidthHistogram.addLine(lineSize, lineCount);recordWidestLine(lineSize);}
|
cs |
때로는 빈 반복문을 사용해야 한다. 이런 반복문은 눈에 띄지 않으므로 ';'을 새로운 항에 넣어주어야 한다.
1
2
|
while(dis.read(buf, 0, readBufferSize) != -1)
;
|
cs |
팀 규칙
개발자는 자신들이 선호하는 규칙이 존재한다. 하지만 본인이 팀의 일원으로 코드를 작성한다면 팀의 코드 규칙을 우선순위에 두어야 한다. 그래야 소프트웨어가 일관적인 코드 스타일을 유지할 수 있다.
좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 주어야 한다.
출처 - 클린 코드