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
16package 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
23package 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 | ublic class ReporterConfig { |
1 | public class ReporterConfig { |
수직 거리
이런 경험이 있는가?
- 함수 연관 관계, 동작을 이해하려고 함수를 오가며 소스 파일을 위로 오가는데 못찾게 되는 경험?
- 함수/변수가 정의된 코드를 찾으려 상속 관계를 줄줄히 올라간 경험?
- 시스템을 이해하려는 목적과 달리 각 조각이 어디에 있는지 찾고 기억해야 함
- 시간과 노력을 엄청나게 소모
- 서로 밀접한 개념은 세로로 가까이 둬야 한다
- 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다.
- 타당한 근거가 없다면 밀접한 개념은 한 파일에 → protected 변수를 피해야 하는 이유 중 하나
- 연관성 : 한 개념을 이해하는데 다른 개념이 중요한 정도
- 연관성 있는 개념이 떨어져있으면 읽는 사람이 파일/클래스를 뒤져야함
변수 선언
- 변수는 사용 위치에 최대한 가까이 선언
- 좋은(작은)함수는 지역 변수 각 함수 맨 처음에 선언
JUnit 4.3.1의 함수 예시 1
2
3
4
5
6
7
8
9
10
11
12
13
14private 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
6public 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
15for (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]: 구성 정보는 최상위 단계에 두어라 : 상수를 저차원 함수에 숨기지 말고 고차원에 두어야 저차원 함수를 뒤적이지 않게 됨
- 상수를 알아야 마땅한 함수에서 실제 사용하는 함수로 상수를 넘겨주는 방법이 더 좋다
- 예)
개념적 유사성
- 개념적인 친화도가 높은 코드는 서로를 끌어당김 → 친화도가 높을 수록 가까이 배치
- 친화도 요인 : 종속성, 변수 사용, 비슷한 동작을 수행하는 일군의 함수
1 | public class Assert { |
- 개념적 친화도가 매우 높은 함수들임
- 명명법이 똑깥고 기본 기능이 유사하고 간단
- 서로가 서로를 호출하는 것은 부차적 요인 → 종속 관계가 없어도 가까이 배치할 함수
세로 순서
함수 호출 종속성 아래 방향 유지 → 자연스럽게 모듈이 고차원→저차원으로 자연스럽게 내려감
- 호출되는 함수를 호출하는 함수보다 나중에 배치
- C, C++, Pascal과 정확히 반대 : 함수 호출을 위해 적어도 미리 선언은 해놔야하는 언어
신문 기사와 같이
- 가장 중요한 개념 가장 먼저 표현 : 세세한 사항을 최대한 배제
- 세세한 사항은 가장 마지막에 표현
- 독자들이 첫 함수 몇 개만 읽어도 개념 파악이 되도록
- 좋은 목록 3-7(ch3마지막), 15-5(p338, )
가로 형식 맞추기
과거 훌러리스(Hollerith) :80자 제한
- 가로 80, 세로 12단 천공카드, 기계를 고안한 통계학자
가급적 짧은 행이 바람직함
120자 정도로 행 길이 제한 권고 - 인텔리 제이 120으로 되어있음
가로 공백과 밀집도
가로 공백 : 밀접한 개념, 느슨한 개념 표현 가능
1 | private void measureLine(String line) { |
연산자의 우선순위를 강조하기 위해서도 공백을 사용
- 불행히 IDE의 포매터가 나중에 해당 공백을 삭제하는 경우가 흔함
- 포매터 대부분 연산자 우선순위 고려 불가하므로 수식에 같은 간격 적용하기 때문
가로 정렬
- 밥아저씨의 정렬
- 어셈블리어 영향
1 | public class FitNesseExpediter implements ResponseSender { |
좋지 않은 정렬
- 보기엔 깔끔해 보여도 코드의 엉뚱한 부분이 강조되 진짜 의도가 가려지게 됨
- 변수유형대신 변수 이름부터 읽게 됨
- 할당문 : 할당 연산자가 눈에 안보이고 우측 피연산자만 눈에 보임
- 대다수의 포매터는 위의 정렬을 무시
선언문/할당문 별도 정렬 필요 X
- 정렬이 필요할 정도로 선언부가 길다면 클래스를 쪼개야 한다
들여쓰기
- Scope를 한눈에 표현 : 프로그래머들이 크게 의존
들여쓰기 무시하기
- 들여쓰기를 무식하고 싶은 유혹들 : 간단한 if, while, 짧은 함수
1 | public class CommentWidget extends TextWidget { |
1 | public class CommentWidget extends TextWidget { |
가짜 범위
- 가능한 피하라
- 피하지 못할 땐 밑에처럼
빈 while 문을 어쩔수 없이 쓸때는 괄호로 감싸고 새로운 행에 세미콜론 1
2while (dis.read(buf, 0, readBufferSize) != -1)
;
팀 규칙
- 팀에 속해있다면 팀 규칙이 가장 우선시 되고 선호되어야 한다
- 팀은 한가지 규칙에 합의 필요
- sw 일관적인 코딩스타일 적용 필요
- 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄지고, 읽기 쉬운 문서는 스타일이 일관적이고 매끄러워야 한다.
밥 아저씨의 형식 규칙
- 이러한 규칙의 예제
1 | public class CodeAnalyzer implements JavaFileAnalysis { |
정리
- 포맷팅이 중요한 이유
- 가독성에 필수적
- 아마추어적으로 보이지 않는다
- 잘못된 포맷팅으로 코드를 잘못 이해해서 버그를 발생할 위험을 줄일 수 있음
- 어렸을때 띄어쓰기, 문법 공부 - > 이게 잘 되어 있으면 글이 잘 읽힌다
- 기본 중의 기본
- 클린 코드 포매팅
- 적절한 길이
- 200라인 이하 권장 , 500라인 이하 권장
- 현업에서 실제 대부분의 코드도 200라인 유지
- 200라인이 넘어가면 클래스가 여러가지 일을 수행할 확율이 높은 → SRP 위배
- 밀접 개념 가까이
- 행 묶음은 완결된 생각 하나 표현함 → 개념은 빈 행으로 분리
- 변수는 사용되는 위치에서 최대한 가까이 선언
- Java Class Declarations
- 오라클 : https://www.oracle.com/java/technologies/javase/codeconventions-fileorganization.html#1852
- static variable → instance variables → Constructors → Methods
- 변수는 : public → protected → package → private 순서
- 메서드
- 스코프와 접근에 따라 그루핑이 되어야 한
- public 메서드에서 private 메서드는 그 아래에 둔다 → 그래야 가독성 높아짐
- 가독성 위주로 그룹핑
- Team Coding Convention
- 코딩 스타일에 관한 약속
- 개발 언어의 컨벤션이 우선, 애매하면(변수명) 팀 컴벤션을 따른다
- 예) enum타입으로 사용하는 varchar타입의 경우 컬럼명은 _type로 끝나도록 네이밍한다
- 없다면 함께 만들어가는게 좋다
- 참고할 만한 컨벤션
- mysql 컨벤션 : snake_case
- Google Java Style Guide: https://google.github.io/styleguide/javaguide.html
- 현업에서 많이 참고
- 네이버 헥데이 코딩 컨벤션 : https://naver.github.io/hackday-conventions-java
- 네이버에서 헥데이라는 행사에서 쓰기 위한 컨벤션
- 네이버 현업과 비슷하고 한글로 되어 있음
Related POST
- [Clean Code] Ⅰ. 깨끗한 코드
- [Clean Code] Ⅱ.의미 있는 이름
- [Clean Code] Ⅲ. 함수
- [Clean Code] Ⅳ. 주석
- [Clean Code] Ⅴ. 형식 맞추기
- [Clean Code] Ⅵ. 객체와 자료구조
- [Clean Code] Ⅶ. 오류 처리
- [Clean Code] Ⅷ. 경계
- [Clean Code] Ⅸ. 단위 테스트
- [Clean Code] Ⅹ. 클래스
- [Clean Code] Ⅺ. 시스템
- [Clean Code] Ⅻ. 창발성(創發性)
- [Clean Code] XIII. 동시성
- [Clean Code] XIV. 점진적 개선(SUCCESSIVE REFINEMENT)
- [Clean Code] XV. JUnit 들여다보기
- [Clean Code] XVI. SerialDate 리팩터링
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] 다 읽었다~