혼자 정리

5. 형식 맞추기 본문

클린코드

5. 형식 맞추기

tbonelee 2021. 10. 6. 17:11

코드 형식을 맞추기 위해 규칙을 정하고 규칙을 잘 따르자.

팀으로 일한다면 팀이 합의해 규칙을 정하고 모두 그 규칙을 따르자

왜 형식을 맞추는지?

  • 의사소통을 원활하게 하기 위해
    • 내가 짠 코드를 나중에 누군가 보고 유지보수, 확장하기 위해서는 가독성이 좋아야 한다. → 가독성 좋은 형식
  • 처음 구현할 때 잡은 형식은 추후에도 크게 바뀌지 않으니 처음 형식을 잡는 것이 중요

원활한 소통을 장려하는 코드 형식에 대해 보자

세로 형식 맞추기(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)
      • public int countTestCases() {
        	int count = 0;
        	for (Test each : tests) // for문 내부에 선언
        		count += each.countTestCases();
        	return count;
        }
  • 드물지만 함수가 다소 긴 경우 블럭 상단이나 루프 직전에 변수를 선언하기도 한다.
    • 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);
        	}
        }
        ...

인스턴스 변수

  • 클래스 맨 처음에 선언하고, 변수 간 거리를 두지 않는다.
    • 잘 짠 클래스는 보통 많은 클래스 내부 메서드가 인스턴스 변수를 사용하기 때문이다.
  • 변수 선언을 어디서 하든 잘 알려진 위치에 인스턴스 변수를 놓아서 모두가 어디서 찾을지 쉽게 알고 있는 것이 중요하다.
    • 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