혼자 정리
5. 형식 맞추기 본문
코드 형식을 맞추기 위해 규칙을 정하고 규칙을 잘 따르자.
팀으로 일한다면 팀이 합의해 규칙을 정하고 모두 그 규칙을 따르자
왜 형식을 맞추는지?
- 의사소통을 원활하게 하기 위해
- 내가 짠 코드를 나중에 누군가 보고 유지보수, 확장하기 위해서는 가독성이 좋아야 한다. → 가독성 좋은 형식
- 처음 구현할 때 잡은 형식은 추후에도 크게 바뀌지 않으니 처음 형식을 잡는 것이 중요
원활한 소통을 장려하는 코드 형식에 대해 보자
세로 형식 맞추기(Vertical Formatting)
- (자바 기준으로) JUnit, FitNesse, Time and Money등의 프로젝트에서 500줄을 넘기는 파일이 없고 대다수가 200줄 미만.
- 모든 프로젝트가 그런 건 아니지만 대개의 프로젝트는 500줄을 넘기지 않고 대부분 200줄 정도의 파일로도 큰 시스템을 구축할 수 있다(FitNesse는 50,000줄 정도의 시스템)
- 일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다
신문 기사처럼 작성하라
- 신문 기사 읽을 때 위에서 아래로 읽으면 헤드라인/요약된 내용을 보여주는 첫 문단/뒤에 이어지는 상세 내용이 순서대로 위치해서 자연스럽게 읽히는 것처럼 코드도 그렇게 작성하자
- 소스 파일 이름은 간단하면서도 설명이 가능하게
- 이름만 보고도 지금 봐야 될 모듈을 보고 있는 게 맞는지(즉 코드 읽는 흐름상 읽어야 될 모듈을 보고 있는 게 맞는지) 알 수 있게 이름을 짓자
- 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명하고, 아래로 내려갈 수록 의도를 세세하게 적는다. 마지막에 가장 저차원 함수하고 세부 내역이 나오도록 한다.
- 흐름에 따라 구체화가 되고 해당 주제에 관한 내용만 담아야 신문이 잘 읽히는 것처럼 코드도 마찬가지
개념은 빈 행으로 분리하라(Vertical Openness Between Concepts)
- 코드의 각 행은 수식(expression)이나 절(clause)을 의미하고, 행 묶음의 sequence는 완결된 생각 하나를 의미한다.
- 생각과 생각 사이는 빈 행을 통해 분리해야 하나의 생각을 명확히 이해할 수 있다.
ex) 좋은 예
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();
}
}
- 패키지 선언부, import문, 각 함수 사이 빈 행이 들어간다.
ex) 나쁜 예
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();
}
}
- 같은 코드인데 훨씬 읽기 어려워졌음을 알 수 있다.
- 이전 코드는 행 묶음이 분리되어 보였는데 아래 코드는 전체가 한 덩어리로 뒤섞여 보인다.
세로 밀집도(Vertical Density)
- 줄바꿈을 통해 개념을 분리하듯이 가까운 연계성(close association)이 있는 것들은 세로로 밀집하게(가까이 있도록) 작성해야 한다.
- 아래의 잘못된 예시를 보면 의미 없는 주석으로 인해 두 인스턴스 변수 코드가 분리되어 있다. 이 때문에 클래스의 변수 구성이 한 눈에 들어오지 않는다.
-
public 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); }
-
- 반면 다음의 개선된 코드는 의미 없는 주석을 제거하여 한 눈에 변수 두 개와 메서드 한 개를 가진 클래스라는 점이 파악된다.
-
public class ReporterConfig { private String m_className; private List<Property> m_properties = new ArrayList<Property>(); public void addProperty(Property property) { m_properties.add(property); }
-
수직 거리(Vertical Distance)
- 밀접하게 서로 관련되어 있는(closely related; 위의 closely associated보다 직접적으로 연결된 느낌) 개념(concepts)들은 수직 거리가 가까워야 한다.
- 물론 다른 파일에 속하면 수직 거리 자체가 성립이 안되지만, 애초에 같은 파일에 놓는 것이 일반적으로 낫다.
- 그래서 protected 변수도 피하는 것이 좋다. (그래야 어느 패키지에 있는 지도 헷갈리는 코드를 찾아 나서는 일이 줄어들 것이다. 전역 변수를 기피하는 이유와 비슷)
- 한 파일에 있을 정도로 가까운 개념들은 수직 거리를 통해 서로가 서로의 개념을 이해하는 데 있어서 얼마나 중요한지를 측정할 수 있어야 한다. 즉, 한 개념 이해에 다른 개념을 참고할 일이 많을수록 더 가까이 있어야 한다(물론 참조할 일이 많으니까 그렇게 하는 것)
- 물론 다른 파일에 속하면 수직 거리 자체가 성립이 안되지만, 애초에 같은 파일에 놓는 것이 일반적으로 낫다.
변수 선언
변수는 사용하는 위치에 최대한 가까이 선언
- 그렇다고 모든 변수를 사용하기 바로 직전에 선언할 필요는 없고, 짧은 함수(우리가 작성하려고 하는 대부분의 함수)에서 지역 변수는 각 함수 맨 처음에 선언.
- ex)
-
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) { } } }
-
- ex)
- 루프의 제어 변수는 보통 루프문 내부에 선언
- ex)
-
public int countTestCases() { int count = 0; for (Test each : tests) // for문 내부에 선언 count += each.countTestCases(); return count; }
-
- ex)
- 드물지만 함수가 다소 긴 경우 블럭 상단이나 루프 직전에 변수를 선언하기도 한다.
- ex)
-
... 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); } } ...
-
- ex)
인스턴스 변수
- 클래스 맨 처음에 선언하고, 변수 간 거리를 두지 않는다.
- 잘 짠 클래스는 보통 많은 클래스 내부 메서드가 인스턴스 변수를 사용하기 때문이다.
- 변수 선언을 어디서 하든 잘 알려진 위치에 인스턴스 변수를 놓아서 모두가 어디서 찾을지 쉽게 알고 있는 것이 중요하다.
- C++에서는 소위 scissors rule로 모든 인스턴스 변수를 마지막에 놓고, 자바에서는 보통 맨 처음에 놓는 등 의견이 갈리지만 중요한 것은 어느 정도 합의된 위치라는 사실이다.
- 다음은 코드 중간에 인스턴스 변수 두 개를 선언한 좋지 못한 예시이다. 이렇게 해놓으면 코드를 읽다가 나중에서야 인스턴스 변수가 더 있다는 것을 알아차릴 수 있을 것이다.
-
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) { ... } ... ... ... ... ... }
-
종속 함수
- 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치
- 가능하다면 caller가 callee보다 앞에 오도록 배치
- 그래야 자연스러운 순서로 읽힌다.
- 이 규칙이 일관적으로 적용되면 독자가 함수 호출을 봤을 때 잠시 후에 해당 함수 정의가 나올 것을 예측할 수 있다.
- ex)
-
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; } ...
-
개념적 유사성(Conceptual Affinity)
- 비슷한 동작을 수행하는 함수들도 서로 가까이 배치하면 좋다.
- ex)
-
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); } ...
- 예시의 함수들은 명명법이 똑같고 기본 기능이 유사하고 간단하다.
-
세로 순서(Vertical Ordering)
- 일반적으로 함수 호출 종속성은 아래 방향으로 유지하여 callee를 caller보다 나중에 배치한다.
- 그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.
- 신문 기사처럼 가장 중요한 개념을 먼저 표현하고 이때는 세부사항을 최대한 배제
- 그러면 독자가 소스파일에서 세세한 사항을 파고들 필요 없이 첫 함수 몇 개만 읽어도 개념을 파악하기 쉽다.
가로 형식 맞추기(Horizontal Formatting)
- 세로 형식에서 살펴 본 프로젝트를 보면 행 길이 20~60자가 40%이고 80자 이후부터는 급격히 비율 감소
- 짧은 행으로 작성하자
- Hollerith의 80자 제안은 다소 인위적이라 100~120까지도 나쁘지 않다. 하지만 그 이상은 주의 부족이다
- 요새는 또 모니터도 크니까..
가로 공백과 밀집도(Horizontal Openness and Density)
- 같은 행 내에서 공백을 사용해서 강하게 관계된 개념을 연계시키기도 하고 그렇지 않은 개념을 분리시키기도 한다.
- 다음 예시는 할당 연산자를 강조하려고 앞 뒤에 공백을 줬다. 밀접한 관계에는 공백을 넣지 않았다.
-
private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); }
- 공백을 통해 할당문 왼쪽 요소/ 할당연산자 / 할당문 오른쪽 요소가 명확히 구분된다.
- 함수와 인수는 서로 밀접한 관계이므로 함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않은 것
- 인수 사이에는 공백을 통해 서로 다른 인수임을 보여준다
-
- 연산자 우선순위가 높은 것에는 공백을 넣지 않음으로써 높은 우선순위를 강조할 수도 있다.
-
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(int a, int b, int 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; } }
- 곱셈은 우선순위가 가장 높으므로 승수 사이에 공백을 주지 않아서 하나의 요소로 볼 수 있게 했다.
- 덧셈과 뺄셈은 우선순위가 곱셈보다 낮으므로 항과 항 사이에는 공백을 주었다.
-
- 다음 예시는 할당 연산자를 강조하려고 앞 뒤에 공백을 줬다. 밀접한 관계에는 공백을 넣지 않았다.
가로 정렬(Horizontal Alignment)
- 선언부의 변수 이름이나 할당문의 오른쪽 피연산자를 정렬하는 것은 엉뚱한 부분을 강조해서 진짜 의도를 가린다
- 아래 예시에서 보면 선언부에서는 변수 유형은 무시하고 변수 이름부터 일게 되고, 할당문에서 연산자는 보이지 않고 오른쪽 피연산자에만 눈이 가게 된다.
-
public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } }
-
- 그런 정렬을 버려야 하나로 연결해서 보아야 할 요소들을 하나로 볼 수 있게 된다.
-
public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } }
-
- 아래 예시에서 보면 선언부에서는 변수 유형은 무시하고 변수 이름부터 일게 되고, 할당문에서 연산자는 보이지 않고 오른쪽 피연산자에만 눈이 가게 된다.
- 정렬이 필요할 정도로 목록이 길다면 문제는 목록 길이다.
- 위의 코드처럼 선언부가 길다면 클래스를 쪼개자
들여쓰기(Indentation)
- 소스 파일은 각 계층별로 scope를 가지고 들여쓰기를 통해 어느 scope에 속하는지 표현
- 클래스 정의처럼 파일 수준인 문장은 들여쓰기 x / 클래스 내 메서드는 하나 들여쓰기 / 메서드 코드는 또 들여쓰기 / 블럭 코드는 또 들여쓰기 / and so on..
- 들여쓰기가 없을 때 얼마나 코드 읽기가 힘든지 눈으로 확인해보자
-
public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } catch(Exception e) { e.printStackTrace(); } } } ----- public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } catch (Exception e) { e.printStackTrace(); } } }
-
가짜 범위(Dummy Scopes)
- 빈 while문이나 for문은 가능한 안 쓰는 것이 좋지만 쓰게 된다면 세미콜론은 같은 행이 아니라 새 행에다 제대로 들여쓰기해서 쓰자. 그래야 눈에 띈다.
-
while (dis.read(buf, 0, readBufferSize) != -1) ;
-
팀 규칙
- 혼자 일하는 게 아니라면 팀원끼리 한 가지 규칙에 합의해서 이를 따라야 소프트웨어가 일관적인 스타일을 보인다.
- 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄지고 이를 위해서 스타일은 일관적이고 매끄러워야 한다.
저자의 Formatting Rules
- 다음의 코드 자체가 저자의 구현 표준 문서라 할 수 있다. 즉, 다음 코드에서 규칙을 파악할 수 있다.
-
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; } }
-
'클린코드' 카테고리의 다른 글
7. 오류 처리 (0) | 2021.10.12 |
---|---|
6. 객체와 자료구조 (0) | 2021.10.10 |
4. 주석 (0) | 2021.09.27 |
3. 함수 (0) | 2021.09.23 |
2. 의미 있는 이름 (0) | 2021.09.19 |