Contents

[Clean Code] Chap3. 함수

함수

Intro

  • 함수 → 프로그램의 가장 기본적인 단위
  • 처음 읽는 사람이 프로그램 내부를 직관적으로 파악하기 위해서 읽기 쉽고 이해하기 쉬운 함수를 만들어야 한다.
  • 함수 - 절망편
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
    public static String testableHtml(PageData pageData, boolean includeSuiteSetup)
        throws Exception {
      Wikipage wikipage = pageData.getWikiPage();
      StringBuffer buffer = new StringBuffer();
      if (pageData.hasAttribute("Test")) {
        if (includeSuiteSetup) {
          WikiPage suiteSetup = PageCrawlerlmpl.getlnheritedPage(
            SuiteResponder.SUITE_SETUP_NAME, wikiPage);
            if (suiteSetup != null) {
              wikiPagePath pagePath =
                suiteSetup.getPageCrawler().getFullPath(suiteSetup);
              String pagePathName = PathParser.render(pagePath);
              buffer.append("include -setup .")
                    .append(pagePathName)
                    .append("\n");
            }
        }
        WikiPage setup =
          PageCrawlerlmpl.getInheritedPage("SetUp", wikiPage);
        if (setup != null) {
          WikiPagePath setupPath =
            wikiPage.getPageCrawler().getFullPath(setup);
          String setupPathName = PathParser.render(setupPath);
          buffer.append("!include -setup .")
                .append(setupPathName)
                .append("\n");
        }
      }
      buffer.append(pageData.getContent());
      if (pageData.hasAttribute("Test")) {
        WikiPage teardown =
          pageCrawlerlmpl.getInheritedPage("TearDown", wikiPage);
        if (teardown != null) {
          WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
          String tearDownPathName = PathParser.render(tearDownPath);
          buffer.append("\n")
                .append("!include -teardown .")
                .append(tearDownPathName)
                .append("\n");
        }
        if (includeSuiteSetup) {
          WikiPage suiteTeardown = PageCrawlerlmpl.getlnheritedPage(
                          SuiteResponder.SUITE_TEARDOWN_NAME,
                          wikiPage
          );
          if (suiteTeardown != null) {
            Wikipagepath pagePath =
              suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
              String pagePathName = PathParser.render(pagePath);
              buffer.append("!include -teardown .")
                    .append(pagePathName)
                    .append("\n");
          }
        }
      }
      pageData.setContent(buffer.toString());
      return pageData.getHtml();
    }
    
  • 1차 리펙토링
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
    	boolean isTestPage = pageData.hasAttribute("Test");
    	if (isTestPage) {
    		WikiPage testPage = pageData.getWikiPage();
    		StringBuffer newPageContent = new StringBuffer();
    		includeSetupPages(testPage, newPageContent, isSuite);
    		newPageContent.append(pageData.getContent());
    		includeTeardownPages(testPage, newPageContent, isSuite);
    		pageData.setContent(newPageContent.toString());
    	}
    	return pageData.getHtml();
    }
    

작게 만들어라!

  • 함수를 만드는 첫번째 규칙 → 작게!
  • 2 ~ 4줄 길이의 함수를 만들 것을 권장
    • 1차 리펙토링( 7 ~ 8 줄 )
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
      	boolean isTestPage = pageData.hasAttribute("Test");
      	if (isTestPage) {
      		WikiPage testPage = pageData.getWikiPage();
      		StringBuffer newPageContent = new StringBuffer();
      		includeSetupPages(testPage, newPageContent, isSuite);
      		newPageContent.append(pageData.getContent());
      		includeTeardownPages(testPage, newPageContent, isSuite);
      		pageData.setContent(newPageContent.toString());
      	}
      	return pageData.getHtml();
      }
      
    • 2차 리펙토링( 3 ~ 4 줄 )
      1
      2
      3
      4
      5
      
      public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
         if (isTestPage(pageData))
         	includeSetupAndTeardownPages(pageData, isSuite);
         return pageData.getHtml();
      }
      

블록과 들여쓰기

  • **if문, if/else문, while문 등에 들어가는 블록은 한 줄** 이어야 한다
    • 바깥을 감싸는 함수가 작아지고, 코드를 이해하기도 쉬워진다
  • 중첩 구조가 생길만큼 함수가 켜져서는 안된다
    • 함수의 들여쓰기 수준은 2단을 넘지 않도록 한다.

한가지만 해라

한 가지의 기준은?

  • 지정된 함수 이름 아래에서 추상화 수준이 하나!
  • 의미를 유지하면서 코드를 줄이기 불가능한 정도
    • 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는여러 작업을 하는 것

함수 당 추상화 수준은 하나로!

  • 모든 문장의 추상화 수준이 동일 참고

내려가기 규칙

  • 위에서 아래로 코드 읽기
  • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다
1
2
3
4
5
6
TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고,
해제 페이지를 포함한다.
	TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한  일반 설정 페이지를 포함한다.
    	TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 `SuiteSetUp` 페이지를 찾아 include 문과
    	페이지 경로를 추가한다.
	TO 부모 계층을 검색하려면, ......

Switch 문

  • switch문 특) 작게 만들기 어렵고, 한가지 작업만 시키기도 어렵다
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    public Money calculatePay(Employee e) throws InvalidEmployeeType {
    	switch (e.type) {
    		case COMMISSIONED:
    			return calculateCommissionedPay(e);
    		case HOURLY:
    			return calculateHourlyPay(e);
    		case SALARIED:
    			return calculateSalariedPay(e);
    		default:
    			throw new InvalidEmployeeType(e.type);
    	}
    }
    
    문제점)
    • 함수가 길다. 유형을 추가하게 되면 더욱 길어진다.
    • 한 가지 작업만 수행하지 않음
    • Single Responsibility Principle(SRP) 위배 → 코드를 변경해야할 이유가 다양함
      • SRP(단일 책임 원칙): 클래스는 단 한 개의 책임을 가져야 한다
    • Open Closed Principle(OCP) 위배 → 새 직원 유형 추가할 때마다 코드를 변경
      • OCP(개방-폐쇄 원칙): 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다
    • 동일한 구조의 함수가 무한정 존재할 수 있음
      • isPayday, deliverPay 같은 함수가 있다면 똑같이 Switch문이 생기고, 코드가 너저분해진다.
  • 다형성을 이용하여 switch문을 ABSTRACT FACTORY에 숨겨 다형적 객체를 생성하는 코드 안에서만 switch를 사용하도록 구성
    • Factory는 Switch문을 사용해 적절한 Employee 파생 클래스 생성
    • 다형성(Polymorphism)을 통해 Switch문 재사용
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    public abstract class Employee {
    	public abstract boolean isPayday();
    	public abstract Money calculatePay();
    	public abstract void deliverPay(Money pay);
    }
    -----------------
    public interface EmployeeFactory {
    	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
    }
    -----------------
    public class EmployeeFactoryImpl implements EmployeeFactory {
    	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    		switch (r.type) {
    			case COMMISSIONED:
    				return new CommissionedEmployee(r) ;
    			case HOURLY:
    				return new HourlyEmployee(r);
    			case SALARIED:
    				return new SalariedEmploye(r);
    			default:
    				throw new InvalidEmployeeType(r.type);
    		}
    	}
    }
    

서술적인 이름을 사용

  • 함수가 하는 일을 좀 더 잘 표현할 수 있도록 서술적인 이름을 사용
    • 깨끗한 코드 → 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행
  • 짧고 어려운 이름 «< 길고 서술적인 이름
  • 일관성이 있어야 한다.
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용

함수 인수

  • 함수에서 이상적인 인수 → 0개(무항) > 1개(단항) > 2개(이항)
    • 3개 이상은 지양
  • 인수는 코드 이해에 방해가 되는 요소이므로, 최소화 해야함

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우 boolean fileExists(“MyFile”)
  • 인수를 뭔가로 변환해 결과를 변환하는 경우 InputStream fileOpen(“MyFile”)
  • 이벤트 함수: 입력 인수만 있음.
    • 이벤트라는 사실이 코드에 명확히 드러나도록 이름과 문맥을 주의해서 선택 passwordAttemptFailedNtimes(int attempts)

바람직하지 않은 단항 형식

  • void transform(StringBuffer in) «< StringBuffer transform(StringBuffer out)
    • 입력 인수를 변환하는 함수라면 변환 결과는 반환해주는 것이 혼동을 방지한다.

플래그 인수

  • 추하다.
  • 함수로 boolean 값을 넘기는 건 끔찍하다.

이항 함수

  • 일반적으로 이항 함수보다 단항 함수가 이해하기 쉽다
    • 예외) 좌표계 관련 함수
  • 일반적으로 2개 인수간의 자연적인 순서가 있어야 함.

삼항 함수

  • 말해 뭐해…

인수 객체

  • 인수가 2~3개 이상 필요할 경우, 일부를 독자적인 클라스로 묶어서 사용하자.

Circle makeCircle(double x, double y, double radius)

Circle makeCircle(Point center, double radius)

동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄아 한다 writeField(name)
  • 함수 이름에 키워드(인수 이름)을 넣기 assertExpectedEqualsActual(expected, actual) : 인수 순서를 기억할 필요가 없어짐

부수효과를 일으키지 마라!

부수효과?

  • 함수 내의 실행으로 인해 함수 외부가 영향을 받는 것
1
2
3
4
5
6
7
8
dump = 10
def addWithDump(a, b):
    global dump
    dump += 10
    return a+b+dump

print(addWithDump(1, 2)) # 23
print(addWithDump(1, 2)) # 33

부수효과는 거짓말이다

한 함수에서는 딱 한가지만 수행

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class UserValidator {
	private Cryptographer cryptographer;
	public boolean checkPassword(String userName, String password) {
		User user = UserGateway.findByName(userName);
		if (user != User.NULL) {
			String codedPhrase = user.getPhraseEncodedByPassword();
			String phrase = cryptographer.decrypt(codedPhrase, password);
			if ("Valid Password".equals(phrase)) {
				**Session.initialize(); // <---- 부수 효과**
				return true;
			}
		}
		return false;
	}
}
  • checkPassword : 암호를 확인
    • 이름만으로 세션을 초기화 한다는 걸 알 수 없음
  • 부수 효과는 시간적인 결합을 초래한다.
    • 함수가 특정 상황에서만 호출하도록 강제함 → 혼란 유발
    • 시간적인 결합이 필요하다면 명시해주어야함 checkPasswordcheckPasswordAndInitializeSession

출력인수

참고자료

  • method의 값 반환 목적으로 method로 전달된 parameter를 사용하는 것
  • 일반적으로 인수는 함수 입력으로 인지
    • appendFooter(s) ← 딱 보면 헷갈린다
      • s를 Footer에 첨부?
      • s에 Footer를 첨부?
  • OOP 환경에서 출력 인수를 사용해야할 이유가 없다
    • this. 로 전부 커버가 되기 때문

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다
  • public boolean set(String attribute, String value);if(set(“username”, “unclebob”))…
    • 의미가 모호함(동사? 형용사?)
    • 명령과 조회를 분리하여 혼란을 제거
      1
      2
      3
      
      if (attributeExists("username")) {
        setAttribute("username", "unclebob");
      }
      

오류 코드 보다 예외를 사용하라!

  • 오류 코드 사용시, 명령/조회 분리 규칙을 위반할 가능성이 높음

  • try/catch를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해 진다.****

  • 오류 코드 사용

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    if (deletePage(page) == E_OK) {
    	if (registry.deleteReference(page.name) == E_OK) {
    		if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
    			logger.log("page deleted");
    		} else {
    			logger.log("configKey not deleted");
    		}
    	} else {
    		logger.log("deleteReference from registry failed");
    	}
    } else {
    	logger.log("delete failed"); return E_ERROR;
    }
    
  • try/catch 사용

    1
    2
    3
    4
    5
    6
    7
    
    try {
      deletePage(page);
      registry.deleteReference(page.name);
      configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logger.log(e.getMessage());
    }
    
  • try/catch 블록도 별도 함수로 분리하는 것이 가독성에 좋다

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public void delete(Page page) {
    	try {
    		deletePageAndAllReferences(page);
      	} catch (Exception e) {
      		logError(e);
      	}
    }
    
    private void deletePageAndAllReferences(Page page) throws Exception {
    	deletePage(page);
    	registry.deleteReference(page.name);
    	configKeys.deleteKey(page.name.makeKey());
    }
    
    private void logError(Exception e) {
    	logger.log(e.getMessage());
    }
    

반복하지 마라

  • 중복은 소프트웨어에서 모든 악의 근원
  • 소스 코드에서 중복을 제거하려는 지속적인 노력 필요

구조적 프로그래밍

  • 다익스트라 왈, 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나여야 된다.
  • return 문이 하나만 존재해야 한다
  • break나 continue를 사용해선 안되며, goto는 쳐다도 보지마라
  • 큰 함수에 대해 효과적인 솔루션
    • 작은 함수에서는 break나 continue를 요령껏 써도 된다
    • goto는 큰 함수에서나 쓸모 있는 거니 작은 함수에서는 쓰면 안된다. (결론: 쓰지마라)

함수를 어떻게 짜죠?

  • 처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많다. 인수목록도 길다.
  • 처음 작성한 코드들을 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고, 순서를 바꾼다. + 동시에 코드는 항상 단위 테스트를 통과한다.