Skip to main content

박수완

1. 📌 핵심 개념 정리

✅ 요약하기

  1. 형식을 맞추는 목적 오랜 시간이 지나 원래 코드의 흔적의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.

  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 변수를 피해야 하는 이유 중 하나다.

    
    

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

    반면, 인스턴스 변수는 클래스 맨 처음에 선언한다. 변수 간에 셀로 거리를 두지 않는다. 잘 설계된 클래스는 클래스의 많은 메서드가 인스턴스 변수를 사용하기 때문이다. 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) { 
          ...
      }
    
      ... ... ... ... ...
    }
    

  1. 종속 함수

    한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다.

        /*첫째 함수에서 가장 먼저 호출하는 함수가 바로 아래 정의된다.
    다음으로 호출하는 함수는 그 아래에 정의된다. 그러므로 호출되는 함수를 찾기가 쉬워지며
    전체 가독성도 높아진다.*/
    
    /*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;
    	} 
    ...
    

  1. 개념적 유사성

    어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다.

     // 같은 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);
     	} 
     ...
    
    • 세로 순서

      일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 다시 말해, 호출되는 함수를 호출하는 함수보다 나중에 배치한다.


  1. 가로 형식 맞추기

    예전에는 오른쪽으로 스크롤할 필요가 절대로 없게 코드를 짰다. 하지만 요즘은 모니터가 아주 크다. 게다가 젊은 프로그래머들은 글꼴 크기를 왕창 줄여 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;
    
     	... 
    

    하지만 위와 같은 정렬이 별로 유용하지 못하다는 사실을 깨달았다. 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.

  • 들여쓰기

    소스 파일은 윤곽도와 계층이 비슷하다. 파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보가 있고, 클래스 내. 적용되는 정보가 있거. 블록 내. 블록에 재귀적으로 적용되는 정보가 있다. 계층에서 각 수준은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위이다.


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

  1. 팀 규칙

    팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 스프트웨어가 일관적인 스타일을 보인다.



2. 🤔 이해가 어려운 부분

🔍 질문하기

책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다.
    • 궁금한 점
      해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.

3. 📚 참고 사항

📢 논의하기

관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.

  1. 관련 자료 공유
    • 추가 자료
      관련 블로그 글이나 공식 문서 링크를 제공합니다.

  1. 논의하고 싶은 주제
    • 주제
      논의하고 싶은 내용을 간략히 정리합니다.
    • 설명
      논의하고 싶은 이유를 작성합니다.