[Clean Code] 5장 형식 맞추기

ASk·2021년 12월 30일
0

Clean Code

목록 보기
5/17
post-thumbnail

Intro

  • 질서정연하고 깔끔하며, 일관적인 코드를 본다면 사람들에게 전문가가 짰다는 인상을 심어줄 수 있다.
  • 반대로, 코드가 어수선해 보인다면 프로젝트 전반적으로 무성의한 태도로 작성했다고 생각할 것이다.
  • 프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야한다.
  • 코드 형식을 맞추기 위한 간단한 규칙을 정하고, 그 규칙을 착실히 따라야 하며,
  • 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다.
  • 필요하다면 규칙을 자동으로 적용하는 도구를 활용한다 (ex. IntelliJ Code Style)

형식을 맞추는 목적

  • 코드 형식은 중요하다! 너무 중요해서 무시하기 어렵다.
  • 코드 형식은 의사소통의 일환이며, 의사소통은 전문 개발자의 일차적인 의무다.
  • 오늘 구현한 코드의 스타일과 가독성 수준은 유지보수의 용이성과 확정성에 지속적인 영향을 미친다.

코드는 사라져도 스타일과 규율은 사라지지 않는다!

적절한 행 길이를 유지하라 (코드 세로 길이)

  • 소스코드는 얼마나 길어야 적당할까?
  • 500줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다.
  • (실제로 자바 프로젝트들(JUnit, FitNesse, Time and Money 등)이 이렇게 구현되어있다)
  • 코드 길이를 200줄 정도로 제한하는 것은 반드시 지킬 엄격한 규칙은 아니지만,
  • 일반적으로 큰 파일보다는 작은 파일이 이해하기 쉽다.

신문 기사처럼 작성하라

  • 이름만 보고도 올바른 모듈을 살펴보고 있는지를 판단 할 정도로 신경써서 짓는다.
  • 소스파일의 첫 부분은 고차원 개념과 알고리즘을 설명한다.
  • 아래로 내려갈수록 의도를 세세하게 묘사한다.
  • 마지막에는 가장 저차원 함수와 세부 내역이 나온다.
  • 만약 신문이 사실, 날짜, 이름 등을 무작위로 뒤섞은 긴 기사 하나만 싣는다면 아무도 신문을 읽지 않을 것이다.

개념은 빈 행으로 분리하라

  • 코드의 각 줄은 수식이나 절을 나타내고, 여러 줄의 묶음은 완결된 생각 하나를 표현한다.
  • 생각 사이에는 빈 행을 넣어 분리해야한다. 그렇지 않다면 코드 가독성이 현저히 떨어진다.
  • 예시 1
// Bad, 빈 행을 넣지 않을 경우
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();
  } 
}

// Good, 빈 행을 넣을 경우
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();
  } 
}

세로 밀집도

  • 줄바꿈이 개념을 분리한다면, 세로 밀집도는 연관성을 의미한다.
  • 즉, 서로 밀집한 코드 행은 세로로 가까이 놓여야 한다.
  • 예시 1
// Bad
// 의미없는 주석으로 변수를 떨어뜨려 놓아서 한눈에 파악이 잘 안된다.
public class ReporterConfig {
  /**
  * 리포터 리스너의 클래스 이름
  */
  private String m_className;
  
  /**
  * 리포터 리스너의 속성
  */
  private List<Property> m_properties = new ArrayList<Property>();
  public void addProperty(Property property) {
    m_properties.add(property);
  }
}

// Good
// 의미 없는 주석을 제거함으로써 코드가 한눈에 들어온다.
// 변수 2개에 메소드가 1개인 클래스라는 사실이 드러난다.
public class ReporterConfig {
  private String m_className;
  private List<Property> m_properties = new ArrayList<Property>();

  public void addProperty(Property property) {
    m_properties.add(property);
  }
}

수직 거리

  • 서로 밀접한 개념은 세로로 가까이 둬야 한다.
  • 물론 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다.
  • 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다, 이게 protected 변수를 피해야 하는 이유중 하나다.
  • 같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성(한 개념을 이해하는 데 다른 개념이 중요한 정도)을 표현한다.

변수선언

  • 변수는 사용하는 위치에서 최대한 가까이 선언한다.
  • 우리가 만든 함수는 매우 짧으므로 각 함수 맨처음에 선언한다.
  • 예시 1
// InputStream이 함수 맨 처음에 선언 되어있다.
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) {
    }
  }
}

// 루프 제어 변수는 Test each처럼 루프 문 내부에 선언
public int countTestCases() { 
  int count = 0;
  for (Test each : tests)
    count += each.countTestCases(); 
  return count;
}

// 드물지만, 긴 함수에서는 블록 상단 또는 루프 직전에 변수를 선언 할 수도 있다.
// TestRunner 선언 부분
...
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);
  } 
}
...

인스턴스 변수

  • 인스턴스 변수는 클래스 맨 처음에 선언한다(자바의 경우).
  • 변수 간 세로로 거리를 두지 않는다.
  • 잘 설계한 클래스는 대다수 클래스 메서드가 인스턴스 변수를 사용하기 때문.
  • 어느 곳이든 잘 알려진 위치에 인스턴스 변수를 모으는 것이 중요하다.
  • 예시 1
// 도중에 선언된 변수는 꽁꽁 숨겨놓은 보물 찾기와 같다. 십중 팔구 코드를 읽다가 우연히 발견한다.
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) { 
    ...
  }
}

종속 함수

  • 한 함수가 다른 함수를 호출한다면(종속 함수) 두 함수는 세로로 가까이 배치한다.
  • 가능하면 호출되는 함수를 호출하는 함수보다 뒤에 배치한다.
  • 이러한 규칙을 일관되게 적용한다면 독자는 방금 함수에서 호출한 함수가 잠시 후에 정의될 것이라고 자연스레 예측한다.
  • 예시 1
/*
getPageNameOrDefault 함수 안에서 "FrontPage" 상수를 사용하지 않고 인수로 넘겨준다.
상수를 알아야 마땅한 함수에서 실제로 사용하는 함수로 상수를 넘겨주는 방법이 더 좋다.
그렇지 않으면 상수가 적절하지 않은 저차원 함수에 묻힌다.
*/
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;
  }
}

개념의 유사성

  • 개념적인 친화도가 높을 수록 코드를 서로 가까이 배치한다.
  • 앞서 살펴보았듯이 한 함수가 다른 함수를 호출하는 종속성, 변수와 그 변수를 사용하는 함수가 그 예다.
  • 그 외에도 비슷한 동작을 수행하는 함수 무리 또한 개념의 친화도가 높다.
  • 예시 1
// 명명법이 똑같고 기본 기능이 유사한 함수들로써 개념적 친화도가 높다.
// 이러한 경우 종속적인 관계가 없더라도 가까이 배치하면 좋다.
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);
  }
}

세로 순서

  • 일반적으로 함수 호출 종속성은 아래방향으로 유지하므로, 호출되는 함수를 호출하는 함수보다 뒤에 배치한다.
  • 그러면 소스코드 모듈이 고차원에서 저차원으로 내려간다.
  • 가장 중요한 개념을 가장 먼저 표현하고, 세세한 사항은 마지막에 표현한다.
  • 그렇게 하면 첫 함수 몇개만 읽어도 개념을 파악하기 쉬워질 것이다.

가로 형식 맞추기

  • 대다수의 프로그래머들은 명백히 짧은 행을 선호하므로 짧은 행이 바람직하다.
  • Hollerith가 제안한 80자 제한은 다소 인위적이므로 조금 더 늘여도 좋다. 하지만 120자 이상을 넘어간다면 주의 부족이다.
  • 필자 개인적으로는 120자 정도로 길이를 제한한다.

가로 공백과 밀집도

  • 가로로는 공백을 사용해 밀접/느슨한 개념을 표현한다.
  • 예시 1
private void measureLine(String line) { 
  lineCount++;
  
  // 할당 연산자 좌우로 공백을 주어 왼쪽,오른쪽 요소가 확실하게 구분된다.
  int lineSize = line.length();
  totalChars += lineSize; 
  
  // 반면 함수이름과 괄호 사이에는 공백을 없앰으로써 함수와 인수의 밀접함을 보여준다.
  // 괄호 안의 인수끼리는 쉼표 뒤의 공백을 통해 인수가 별개라는 사실을 보여준다.
  lineWidthHistogram.addLine(lineSize, lineCount);
  recordWidestLine(lineSize);
}

가로 정렬

  • 보기엔 깔끔해 보일지 모르나, 코드가 엉뚱한 부분을 강조해 변수 유형을 자연스레 무시하고 이름부터 읽게 된다.
  • 게다가 Code Formatter 대부분들은 이렇게 해놔봤자 무시하고 원래대로 돌려놓는다.
  • 그러므로 선언문과 할당문을 별도로 정렬할 필요가 없다.
  • 정렬이 필요할 정도로 목록이 길다면 목록의 길이가 문제이지 정렬이 부족해서가 아니다.
  • 선언부가 길다는 것은 클래스를 쪼개야 한다는 것을 의미한다.
  • 예시 1
// 아래와 같은 정렬은 별로 유용하지 못하다.
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;
  ... 

들여쓰기

  • 들여쓰기한 파일은 구조가 한눈에 들어온다.
  • 들여쓰기를 하지 않은 코드는 열심히 분석하지 않는한 보기 힘들다.

들여쓰기 무시하기

  • 간단한 if문, while문, 짧은 함수에서 들여쓰기를 무시하고픈 유혹이 생긴다.
  • 이런 유혹이 생기더라도 원점으로 돌아가 항상 들여쓰기를 넣어야 한다.
  • 예시 1
// Bad
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 ""; } 
}

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

가짜 범위

  • 빈 while문이나 for문을 접할 때가 있다. 가능한 피해야 된다.
  • 피하지 못 할 경우엔 빈 블록을 올바로 들여쓰고 괄호로 감싸라.

팀 규칙

  • 프로그래머라면 각자 선호하는 규칙이 있다. 하지만 팀에 속해있다면 선호해야 할 규칙은 바로 팀 규칙이다.
  • 그래야 소프트웨어가 일관적인 스타일을 보인다.
  • 코딩 스타일을 의논하여 IDE 형식기로 지정하여 구현하는 것이 옳은 방식이다.
  • 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄지고, 읽기 쉬운 문서는 스타일이 일관적이고 매끄러워야 한다.

밥 아저씨의 형식 규칙

  • 책의 코드에 Intellij Google Java Code Style 적용
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;
  }
}
profile
Backend Developer

0개의 댓글