CleanCode 5장 형식 맞추기 에 대해 정리한 포스트입니다.

Overview

프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 합니다. 코드 형식을 맞추기 위해 규칙을 정하고 일관성 있게 지켜야 합니다

  • 코드를 본 독자들이 깔끔하고 일관적이며 꼼꼼하다고 감탄하면 좋겠다.
  • 질서 정연하다고 탄복하면 좋겠다.
  • 전문가가 짰다는 인상을 심어주면 좋겠다.
  • 규칙을 적용하는 툴을 활용하는 것도 좋다.

형식을 맞추는 이유

일단 코드 형식은 의사 소통의 일환이고 전문 개발자의 일차적인 의무이기 때문에 아주 중요합니다.

  • 오늘 구현한 기능이 다음 버전에서 바뀔 확률이 아주 높다.
  • 그 때문에 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미침
  • 원래 코드의 흔적이 없어지더라도 처음 잡은 구현 스타일가독성 수준은 계속 영향을 미침.

형상 관리 도구에서 포멧으로 인한 Diff를 방지하기 위해서도 필요합니다.

적절한 행 길이를 유지하라

큰 파일보다 작은 파일이 일반적으로 이해하기 쉽습니다.

  • 200줄 미만의 코드로도 충분히 크고 복잡한 시스템을 만들 수 있습니다.

신문 기사처럼 작성하라

소스 파일은 신문 기사와 비슷하게 작성합니다.

  1. 이름은 간단하면서도 설명이 가능하게 짓는다.
  2. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명
  3. 아래로 내려갈수록 의도를 세세하게 묘사
  4. 마지막에는 가장 저차원 함수와 세부 내역

대다수 기사는 아주 짧습니다(몇몇개는 길지만). 신문이 아주 긴 하나의 기사만 싣는다면 아무도 읽지 않을 것입니다.

C,C++ 등은 호출되는 함수를 먼저 정의, 아니 최소한 미리 선언해야 하지만 대부분의 언어들은 순서에 구애받지 않는다.

개념은 빈 행으로 분리하라

  • 각 행은 수식이나 절을 나타내고 일련의 행 묶음은 완결된 생각 하나를 표현
  • 생각 사이에는 빈 행을 넣어 분리
package fitnesse.wikitext.widgets;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

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();
	}
}
package fitnesse.wikitext.widgets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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();
	}
}

세로 밀집도

세로 밀집도는 연관성을 의미합니다.

  • 즉 서로 밀접한 코드 행은 세로로 가까이 놓여야 합니다.
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);
}

수직 거리

서로 밀접한 개념은 같은 파일에 속하는 것이 좋습니다.

  • 무엇을 하는지 이해하기 위해 이 조각 저 조각이 어디에 있는지 찾고 기억해야 함

함수나 변수가 정의된 코드를 찾으러 상속 관계를 거슬러 올라간 경험이 있다면 공감할 것이다.

같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현합니다.

  • 연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 여기저기 뒤져야 함

1. 변수 선언

  • 변수는 사용하는 위치에 최대한 가까이 선언
  • 지역 변수는 각 함수 맨 처음에 선언(우리가 만들 함수는 짧을 테니)

      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) {
              }
          }
      }
    
  • loop문 제어 변수는 loop문 내부에 선언

      public int countTestCases() {
          int count = 0;
          for (Test each : tests)
              count += each.countTestCases();
          return count;
      }
    

2. 인스턴스 변수

  • 변수를 선언하는 위치는 잘 알려진 위치여야 합니다.
    • 클래스 맨 처음에 선언(java), 클래스 마지막에 선언(c/c++, scisors rule)
  • Method 중간에 숨겨두면 찾기 어려워진다.

3. 종속 함수

  • 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치
  • 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치
    • 코드가 자연스럽게 읽힘(높은 추상화 -> 낮은 추상화)
    • 이 규칙이 일관적으로 적용되면 독자는 방금 호출된 함수가 곧 정의될 거라 예측할 수 있습니다.
      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;
          } 
      ...
    

4. 개념적 유사성

개념적인 친화도가 높은 코드는 서로 가까이 배치합니다.

  • 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성
  • 변수와 그 변수를 사용하는 함수
  • 비슷한 동작을 사용하는 함수
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);
} 
...

가로 형식 맞추기

행 길이는 짧은 편이 바람직합니다. 80자까지는 아니라도 100~120자 정도는 나쁘지 않습니다.

  • 그 이상은 주의 부족
  • 120자 정도로 제한하는 것을 권장

옛날 Hollerith가 내놓은 80자 제한은 천공 카드 기계의 규격(가로 80자리 세로 12단)에 따른 것입니다.

가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현합니다.

private void measureLine(String line) { 
    lineCount++;

	// 할당 연산자가 강조되어 왼쪽/오른쪽 요소가 나뉨
    int lineSize = line.length();
    totalChars += lineSize; 
	
	// 함수와 인수는 밀접하기에 공백을 넣지 않는다.
	// 인수들은 공백으로 분리(별개라는 점을 보여줌)
    lineWidthHistogram.addLine(lineSize, lineCount);
    recordWidestLine(lineSize);
}

가로 정렬

public class FitNesseExpediter implements ResponseSender {
    private     Socket         socket;
    private     InputStream    input;
    private     OutputStream   output;
    private     Reques         request;      
    private     Response       response; 
    private     FitNesseContex 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 FitNesseContex 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;
	}

들여쓰기

소스파일은 윤곽도와 계층이 비슷합니다.

  • 파일 전체에 적용되는 정보
  • 파일 내 개별 클래스에 적용되는 정보
  • 클래스 내 각 메서드에 적용되는 정보
  • 블록 내 블록에 재귀적으로 적용되는 정보

계층에서 각 수준은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위입니다.

이 범위를 표현하기 위해 들여쓰기를 사용합니다. 여기서 각 코드를 들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례합니다.

  • 파일 수준인 문장은 들여쓰지 않음(클래스 정의와 같은)
  • 클래스 내 메서드는 클래스보다 한 수준 들여씀
  • 블록 코드는 블록을 포함하는 코드보다 한 수준 들여씀

이와 같은 들여쓰기를 통해 왼쪽으로 코드를 맞춰 코드가 속하는 범위를 시각적으로 표현할 수 있습니다.

이를 통해 범위간 이동을 빠르게 할 수 있고 현재 상황과 무관한 if/while 코드를 일일이 살펴보지 않을 수 있다. 또한 왼쪽으로만 훑으면서 메서드 변수 클래스를 찾을 수 있다.

1. 들여쓰기 무시하기

짧은 if/while문이나 함수에서 들여쓰기를 무시하지 않도록 한다.

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 ""; } 
}
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 ""; 
    } 
}

2. 가짜 범위

빈 while문이나 for문을 쓰지 않는 것이 좋습니다.

만약 쓰게 된다면 세미콜론(;)을 새 행에 제대로 들여써서 넣어준다.

while (dis.read(buf, 0, readBufferSize) != -1)
;

저자의 형식이 잘 맞춰진 코드

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;
    } 
}

reference

Clean Code 클린코드 : 애자일 소프트웨어 장인 정신