박수완
1. 📌 핵심 개념 정리
✅ 요약하기
- 형식을 맞추는 목적 오랜 시간이 지나 원래 코드의 흔적의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.
-
적절한 행 길이를 유지하라
-
신문 기사 처럼 직성하라
신문은 다양한 기사로 이뤄진다. 대다수 기사가 아주 짧다. 어떤 기사는 조금 길다. 한 면을 채우는 기사는 거의 없다. 신문이 읽을 만한 이유는 여기에 있다. 신문이 사실, 날짜, 이름 등을 무작위로 뒤섞은 긴 기사 하나만 싣는다면 아무도 읽지 않으리라.
-
개념은 빈 행으로 분리하라
거의 모든 코드는 왼쪽에서 오른쪽으로 그리고 위에서 아래로 읽힌다.각 행은 수식이나 절을 나타내고 일련의 행 묵음은 완겨뢴 생각 하나를 표현한다. 생각 사이는 빈 행을 넣어 분리해야 마땅하다.
-
개선 전
// 빈 행을 넣지 않을 경우 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(); } }
-
-
개선 후
// 빈 행을 넣을 경우 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(); } }
개선 후의 코드 가독성이 더욱 분명하게 드러난다.
-
세로 밀집도
줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다. 즉, 서로 밀접한 코드 행은 세로로 가까이 놓아야 한다는 뜻이다.
-
개선 전
// 의미없는 주석으로 변수를 떨어뜨려 놓아서 한눈에 파악이 잘 안된다. 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); } }
-
개선 후
// 의미 없는 주석을 제거함으로써 코드가 한눈에 들어온다. // 변수 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); } }
-
수직 거리
서로 밀접한 개념은 세로로 가까이 둬야 한다. 물론 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다. 하지만 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. 이게 바로 potected 변수를 피해야 하는 이유 중 하나다.
-
변수 선언
변수는 사용하는 위치에 최대한 가까이 선언한다. 우리가 만든 함수는 매우 짧으므로 지역 변수는 각 함수 맨 처음에 선언한다.
// 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; }
// 드물지만, 긴 함수에서는 블록 상단 또는 루프 직전에 변수를 선언 할 수도 있다. 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++의 경우에는 마지막에 선언하는 것이 일반적이다. 어느 곳이든 잘 알려진 위치에 인스턴스 변수를 모으는 것이 중요하다.
// 도중에 선언된 변수는 꽁꽁 숨겨놓은 보물 찾기와 같다. 십중 팔구 코드를 읽다가 우연히 발견한다. 발견해보시길. // 요즘은 IDE가 잘 되어있어서 찾기야 어렵지 않겠지만, 더러운건 마찬가지 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) { ... } ... ... ... ... ... }
-
종속 함수
한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
/*첫째 함수에서 가장 먼저 호출하는 함수가 바로 아래 정의된다. 다음으로 호출하는 함수는 그 아래에 정의된다. 그러므로 호출되는 함수를 찾기가 쉬워지며 전체 가독성도 높아진다.*/ /*makeResponse 함수에서 호출하는 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; } ...
-
개념적 유사성
어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다.
// 같은 assert 관련된 동작들을 수행하며, 명명법이 똑같고 기본 기능이 유사한 함수들로써 개념적 친화도가 높다. // 이런 경우에는 종속성은 오히려 부차적 요인이므로, 종속적인 관계가 없더라도 가까이 배치하면 좋다. 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); } ...
-
세로 순서
일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 다시 말해, 호출되는 함수를 호출하는 함수보다 나중에 배치한다.
-
-
가로 형식 맞추기
예전에는 오른쪽으로 스크롤할 필요가 절대로 없게 코드를 짰다. 하지만 요즘은 모니터가 아주 크다. 게다가 젊은 프로그래머들은 글꼴 크기를 왕창 줄여 200자 까지도 한 화면에 들어간다. 가급적이면 그렇게 하지 말기를 권헌다.
-
가로 공백과 밀집도
가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.
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; ...
하지만 위와 같은 정렬이 별로 유용하지 못하다는 사실을 깨달았다. 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.
-
들여쓰기
소스 파일은 윤곽도와 계층이 비슷하다. 파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보가 있고, 클래스 내. 적용되는 정보가 있거. 블록 내. 블록에 재귀적으로 적용되는 정보가 있다. 계층에서 각 수준은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위이다.
-
들여쓰기 무시하기
떼로는 간단한 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 ""; } }
-
팀 규칙
팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 스프트웨어가 일관적인 스타일을 보인다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.
- 개념 또는 원칙의 이름
- 어려웠던 부분
해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다. - 궁금한 점
해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.
- 어려웠던 부분
3. 📚 참고 사항
📢 논의하기
관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.
- 관련 자료 공유
- 추가 자료
관련 블로그 글이나 공식 문서 링크를 제공합니다.
- 추가 자료
- 논의하고 싶은 주제
- 주제
논의하고 싶은 내용을 간략히 정리합니다. - 설명
논의하고 싶은 이유를 작성합니다.
- 주제