혼자 정리

3. 함수 본문

클린코드

3. 함수

tbonelee 2021. 9. 23. 21:53
  • FitNesse에서 길고, 중복이 많고, 이상한 문자열과 낯설고 모호한 자료 유형, API가 많은 코드를 부정적인 예시로 보여줌
    • (3-1) 
    • 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 = 
      				PageCrawlerImpl.getInheritedPage(
      								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 stup = 
      			PageCrawlerImpl.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 = 
      			PageCrawlerImpl.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 =
      				PageCrawlerImpl.getInheritedPage(
      								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();
      }​
       
    • 위 코드를 리팩토링해서 다음의 결과를 보여준다
      • (3-2) 
      • public static String renderPageWithSetupsAndTeardowns(
        	PageData pageData, boolean isSuite
        ) throws Exception {
        	boolean isTestPage = pageData.hasAttribute("Test");
        	if (isTestPage) {
        		WikiPage testPage = pageData.getWikiPage();
        		StringBuffer newPageContent = newStringBuffer();
        		includeSetupPages(testPage, newPageContetn, isSuite);
        		newPageContent.append(pageData.getContent());
        		includeTeardownPages(testPage, newPageContent, isSuite);
        		pageData.setcontent(newPageContent.toString());
        	}
        	return pageData.getHtml();
        }
    • 완벽히 이해하긴 어려워도 setup 페이지와 teardown 페이지를 테스트 페이지에 넣고 해당 테스트 페이지를 HTML로 렌더링한다늣 사실은 파악할 수 있다.

이제 함수를 어떻게 구현하고 어떤 속성을 부여해야 읽기 쉽고 이해하기 쉽게 만들 수 있을지 살펴 보자.


작게 만들어라!

  • 함수는 작으면 작을수록 좋다.
  • 직전의 코드도 사실 다음과 같이 줄일 수 있다.
    • (3-3) 
    • public static String renderPageWithSetupsAndTeardowns(
      	PageData pageData, boolean isSuite) throws Exception {
      	if (isTestPage(pageData))
      		includeSetupAndTeardownPages(pageData, isSuite);
      	return pageData.getHtml();
      }

블록과 들여쓰기

  • if문/else문/while문 등에 들어가는 블록은 한 줄로 만들자.
    • 그러면 바깥을 감싸는 함수가 작아질 수 밖에 없다.
    • 또한 블록 안에서 호출하는 함수 이름을 적절히 지으면 코드를 이해하기도 쉬워진다.
  • 중첩 구조가 생길만큼 함수가 커지면 안 된다는 의미
    • 들여쓰기 수준이 1단이나 2단을 넘지 않게 하자.
    • 중첩이 커질수록 복잡해지니 이해하기 어려워질 수 밖에..

한 가지만 해라!

(3-1)코드는 버퍼 생성/페이지 가져오기/상속된 페이지 검색/경로 렌더링/문자열 추가/HTML 생성 등 많은 일을 한다.

반면 (3-3)은 설정 페이지와 해제 페이지를 테스트 페이지에 넣는 한 가지 일만 한다.

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.

이 말이 항상 전해져 내려오는 조언인데 그 '한 가지'를 파악하는 게 쉽지 않을 수 있다.

(3-3)만 봐도 다음 세 가지를 하는 것으로 이해할 수도 있다.

  1. 페이지가 테스트 페이지인지 판단.
  2. 맞으면 설정 페이지와 해제 페이지를 넣는다.
  3. 페이지를 HTML로 렌더링.

그렇지만 다음과 같이 설명할 수 있다.

  • 함수를 만드는 이유는 큰 개념(함수 이름)을 다음 추상화 단계에서 여러 작업으로 나누어 수행하기 위함이다.
  • 따라서 지정된 함수 이름 아래에서 세 업무가 동일한 추상화 단계라면 하나의 작업으로 볼 수 있다.

그런 의미에서 함수가 '한 가지'만 하는지 다음과 같이 판단할 수 있다.

  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있으면 그 함수는 여러 작업을 하는 셈이다.
    • (3-3)의 if문을 includeSetupsAndTeardownsIfTestPage라고 고쳐도 똑같은 내용을 다르게 표현한 것일뿐 추상화 수준은 바뀌지 않는다. → 더 이상 줄일 수 없으므로 한 가지 일만 하는 셈이다.

함수 내 섹션

다음 함수는 declarations, initializations, sieve라는 세 섹션으로 나뉜다.

한 가지 작업만 하는 함수는 이것처럼 자연스럽게 섹션으로 나누기 어렵다.

import java.util.*;

public class GeneratePrimes
{
    /**
    * @param maxValue is the generation limit.
    */
    public static int[] generatePrimes(int maxValue)
    {
        if (maxValue >= 2) // the only valid case
        {
            // declarations
            int s = maxValue + 1; // size of array
            boolean[] f = new boolean[s];
            int i;

            // initialize array to true.
            for (i = 0; i < s; i++)
                f[i] = true;

            // get rid of known non-primes
            f[0] = f[1] = false;

            // sieve
            int j;
            for (i = 2; i < Math.sqrt(s) + 1; i++)
            {
                if (f[i]) // if i is uncrossed, cross its multiples.
                {
                    for (j = 2 * i; j < s; j += i)
                        f[j] = false; // multiple is not prime
                }
            }

            // how many primes are there?
            int count = 0;
            for (i = 0; i < s; i++)
            {
                if (f[i])
                count++; // bump count.
            }

            int[] primes = new int[count];

            // move the primes into the result
            for (i = 0, j = 0; i < s; i++)
            {
                if (f[i]) // if prime
                    primes[j++] = i;
            }

            return primes; // return the primes
        }
        else // maxValue < 2
            return new int[0]; // return null array if bad input.
    }
}

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

  • 함수 내 모든 문장의 추상화 수준을 일치시켜라
    • 한 함수 내에 추상화 수준을 섞으면 읽는 사람을 헷갈리게 한다.
      • 어떤 표현이 근본적인 개념인지 세부사항인지 구분하기 어렵다.
    • 또한 근본개념과 세부사항이 뒤섞인 순간 다른 사람들이 해당 함수에 세부사항을 더 추가해도 되는 것으로 생각해서 코드는 더 복잡해질 것이다.

탑-다운으로 코드 읽기: 'Stepdown' 규칙

한 함수의 내용은 바로 한 단계 낮은 추상화 수준의 문장들로 이루어져있다.

  • 즉, 프로그램을 위에서 아래로 쭉 읽었을 때 함수 단계가 한 단계 낮아질 때마다 추상화 단계도 한 단계 낮아진다.
  • 다음의 TO 문단을 읽는 것처럼 프로그램이 읽혀야 한다.
    • ex)
      • To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.
        	To include the setups, we include the suite setup if this is a suite, then we include the regular setup.
        	To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page.
        	To search the parent. . .
  • 각 함수는 다음 단계의 함수를 소개하고, 각각의 함수는 일정한 추상화 수준을 유지한다.

Switch문

본질적으로 switch 문 자체가 N가지 일을 처리한다.

따라서 이를 완전히 피할 수는 없지만 다형성을 이용하여 low-level클래스에 숨기고 반복하지 않는 방법은 있다.

  • 다음은 직원 유형에 따라 다른 값을 계산해서 반환하는 함수
    • public Money calculatePay(Employee e)
      throws InvalidEmployeeType {
      	switch (e.type) {
      		case COMMISSIONED:
      			return calculateCommissionedPay(e);
      		case HOURLY:
      			return calculateHourlyPay(e);
      		case SALARIED:
      			return calculatedSalariedPay(e);
      		default:
      			throw new InvalidEmployeeType(e.type);
      	}
      }​
       
    • 위 함수의 문제는 다음과 같다.
      1. 함수가 길다. 새 직원 유형이 추가되면 더 길어질 수 밖에 없다.
      2. '한 가지' 작업만 하지 않는다.
      3. SRP(Single Responsibility Principle; 단일 책임 원칙)를 위반.
        • 코드를 변경할 이유가 하나가 아니기 때문
          • ex) HOURLY직원에 관한 게 바뀌어도 수정, SALARIED직원에 관한 게 바뀌어도 수정
      4. OCP(Open Closed Principle; 개방 폐쇄 원칙)를 위반.
        • 새 직원 유형을 추가할 때마다 함수 자체를 변경해야 하기 때문. 확장을 통해 해결해야 함
      5. 위 함수의 직접적 문제는 아니지만 직원 유형별로 달라지는 메소드가 모두 위 함수와 같은 구조로 존재할 것이다.
        • ex)
          • isPayday(Employee e, Date date);
            deliverPay(Employee e, Money pay);
  • 아래 코드는 switch 문을 추상 팩토리에 숨김으로써 문제를 해결했다.
    • 팩토리는 switch 문을 사용해서 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
    • calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 참조변수를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
    • 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 SalariedEmployee(r);
      			default:
      				throw new InvalidEmployeeType(r.type);
      		}
      	}
      }

서술적인 이름을 사용하라!

  • 워드가 말한 원칙 : "코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다."
  • 함수가 작고 단순할수록 함수가 하는 일을 더 잘 표현하는 서술적인 이름을 고르기도 쉬워진다.
  • 그렇지 못하더라도 '길고 서술적인 이름'이 '짧고 어려운 이름'이나 '길고 서술적인 주석'보다 좋다.
    • 여러 단어가 쉽게 읽히는 네이밍 컨벤션을 정하고 여러 단어를 통해 함수 기능을 잘 표현하는 이름을 선택하자
    • 시간을 들여서라도 최대한 설명을 잘하는 이름을 고르자
  • 서술적인 이름을 사용하면 개발자 머리에서도 설계가 뚜렷해지기 때문에 코드 개선이 쉬워진다.
  • 서술적인 이름을 쓸 때 일관성을 유지하는 것이 중요하다.
    • 그래야 여러 함수를 보더라도 기능을 짐작하기 쉽고,
    • 설계에 필요한 부분이 무엇인지 짐작하여 채우기 쉽다. 즉, 이야기를 순차적으로 풀어가기도 쉬워진다.
      • ex)
        • includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage가 있으면 자연스럽게 includeTeardownPages, includeSuiteTeardownPage, includeTeardownPage도 떠오르게 될 것이다.

함수 인수

  • 인수는 적을수록 좋다. 세 개 이상은 가능한 피하자.
  • 어떤 객체를 멤버 변수로 선언해서 메소드에서 사용하는 방법과 메소드에서 인수로 받아서 사용하는 것을 비교해보면, 후자의 경우 코드를 읽는 사람이 메소드에 넘겨진 인수의 의미를 파악해야 한다.
    • ex)
      • includeSetupPageInto(newPageContent)보다 includeSetupPage()가 더 이해하기 쉽다.
      • includeSetupPageInto(newPageContent)는 함수 이름과 인수 사이에 추상화 수준이 다르고, 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부사항(여기서 newPageContentStringBuffer인 예시인데 그러면 StringBuffer라는 세부사항)까지 알아야 한다.
  • 인수가 많으면 테스트해야 할 케이스가 지수 함수 꼴로 증가하므로 테스트하기 부담스러워진다.
    • 모든 인수 조합에 대해 테스트해야 하므로
  • 출력값을 넘겨받는 인수는 더 이애하기 어렵다. 보통은 그렇게 잘 하지 않기 때문에 코드 독자가 더 노력을 기울여서 읽게 만든다.

많이 쓰는 단항 형식

함수에 인수 한 개를 넘기는 가장 흔한 경우는 다음 두 가지

  • 인수와 관련된 질문을 하는 함수
    • ex) boolean fileExists("MyFile")
  • 인수를 다른 것으로 변환해서 결과를 반환하는 경우
    • ex) InputStream fileOpen("MyFile")

아주 드물게 다음과 같은 함수를 사용하기도 한다.

  • 입력 인수의 상태로 시스템 상태를 바꾸는 이벤트 함수.
    • 함수 호출을 이벤트로 해석해서 입력 인수로 시스템 상태를 바꾼다.
      • passwordAttemptFailedNtimes(int attempts)와 같은 함수

위와 같은 케이스들이 아니라면 인수는 가급적 피하자.

  • 예를 들어 void includeSetupPageInto(StringBuffer pageText)와 같이 변환 함수에서 출력 인수를 사용하면 혼란만 일으킨다. 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려주자.
    • StringBuffer transform(StringBuffer in)void transform(StringBuffer out)보다 낫다.
      • 출력값이 인수와 같더라도 적어도 transformation의 형태일 때는 그게 낫다.

플래그 인수

  • 함수로 불 변수를 넘기는 것은 함수가 여러 가지 작업을 한다고 보여주는 것과 다름없다.
    • 플래그가 참이면 a를 하고, 거짓이면 b를 하고..

이항 함수

  • 인수가 한 개인 함수보다 두 개인 함수가 이해하기 어렵다.
    • ex) writeField(name)writeField(outputStream, name) 중 전자가 이해하기 쉽다. 후자에서 첫번째 인자가 덜 중요하다는 것은 코드를 살펴봐야지 알 수 있는 것이고, 알게 되더라도 오히려 그로 인해 첫번째 인자를 신경 쓰지 않게 됨으로써 후에 버그가 생길 가능성도 있다.
  • Point p = new Point(0, 0)과 같이 인수 사이에 자연적 순서가 있으면 적절할 수도 있다.
    • 하지만 위의 outputStreamname은 그런 순서가 없다.
    • assertEquals(expected, actual)에도 그런 순서가 없기에 잦은 실수가 생길 수 있다.
  • 이항 함수를 무조건 배제할 수는 없지만 리스크를 알고 써야 하고, 가능하다면 단항 함수로 바꾸자.
    • writeField(outputStream, name)의 경우 writeField메소드를 outputStream클래스 구성원으로 만들어서 outputStream.writeField(name)으로 호출하자.
    • 아니면 outputStream을 현재 클래스 구성원 변수로 만들어서 인수로 넘기지 않는다.
    • 또 다른 방법으로는 FieldWriter라는 새 클래스를 만들어서 constructor에서 outputStream을 받고 write메소드를 구현한다.

삼항 함수

  • 이항 함수보다 더 이해하기 어렵다.
  • assertEquals(message, expected, actual)함수를 보면 첫 인수가 expected라고 실수하기 쉽다.

객체를 인수로 받기(Argument Objects)

  • 인수가 2~3개 필요할 때 일부를 독자적 클래스 변수로 선언하는 것이 나을 수도 있다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
  • 변수를 묶어서 객체로 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 돼서 더 나을 수 있다.

인수 목록(Argument Lists)

  • 가변 인수로 사실상 인수 갯수가 줄어들 수도 있다.
    • ex) String.format의 경우가 그 예시이다.
      • String.format("%s worked %.2f hours.", name, hours);​
         
      • 예시처럼 가변 인수 전부를 동등하게 보면 List형 인수 하나로 취급할 수 있다.
      • 그런 논리로 보면 String.format은 사실상 이항 함수이다.
        • 선언부만 봐도 그렇다
          • public String format(String format, Object... args)

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 이름이 필요.

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다
    • ex) write(name)은 누구나 쉽게 이해할 수 있다.
      • writeField(name)은 조금 더 낫다. namefiled라는 사실을 쉽게 알 수 있다.
  • 함수 이름에 인수 이름을 넣을 수도 있다.
    • assertEquals 대신에 assertExpectedEqualsActual을 사용하면 인수 순서를 더 쉽게 알 수 있다.

부수 효과(Side effects)를 일으키지 마라!

  • 사이드 이펙트는 함수가 한 가지 일만 하지 않게 하는 것이므로 별로 좋지 못한 습관이다.
  • 많은 경우 '시간적 결합(temporal coupling)'이나 '순서 종속성(order dependency)'을 초래한다.

출력 인수

  • 일반적으로 인수는 함수 입력으로 해석되기 쉽다.
    • appendFooter(s);가 무언가에 s를 바닥글로 첨부하는지, s에 바닥글을 첨부하는지, s는 입력인지 출력인지 알려면 함수 선언부를 찾아봐야 한다.
      • public void appendFooter(StringBuffer report)​
         
      • 선언부를 보고 나서야 출력 인수라는 것을 파악할 수 있었고, 이는 좋지 못한 코드라 할 수 있다.
  • 객체지향 언어 이전에는 출력 인수가 불가피할 때도 있었지만 객체 지향 언어에서는 this 를 사용하면 된다.
    • appendFooter는 다음과 같이 호출하도록 수정하는 것이 좋다.
      • report.appendFooter()
  • 일반적으로 출력 인수는 피하고, 만약 함수에서 상태를 변경해야 한다면 해당 함수를 포함하고 있는 객체의 상태를 직접 변경하는 방식을 택하자.

명령과 조회를 분리하라!

  • 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 하자.
  • ex)
    • public boolean set(String attribute, String value);는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환한다.
    • 이를 사용하면 if (set("username", "unclobob"))...와 같은 꼴이 된다.
    • if문에 들어가면 "set"이 의도한대로 동사로 읽히지 않기 때문이다.
    • 즉, 다음과 같이 명령과 조회를 분리해서 혼란을 없애자.
      • if (attributeExists("username")) {
        	setAttribute("username", "unclebob");
        }

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

  • 명령 함수에서 오류 코드를 반환하게 하면 if문에서 명령을 표현식으로 사용하기 쉽기 때문에 명령과 조회를 분리하라는 규칙을 어기기 쉽다.
  • 또한 동사/형용사 혼란을 일으키지 않더라도 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 하므로 여러 단계로 중첩되는 코드를 만들게 된다.
    • ex) if(deletePage(page) == E_OK)
      • 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 {
      	deletePage(page);
      	registry.deleteReference(page.name);
      	configKeys.deleteKey(page.name.makeKey());
      }
      catch (Exception e) {
      	logger.log(e.getMessage());
      }

Try/Catch 블록 추출하기

  • try/catch 블록은 본래 코드 구조에 혼란을 일으키고, 정상 동작과 오류 처리 동작을 뒤섞기 때문에 try/catch 블록을 별도 함수로 추출해내는 것이 낫다.
    • 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());
      }​
       
    • delete함수가 모든 오류를 처리해주므로 코드를 이해하기 쉽다.

오류 처리도 한 가지 작업이다.

  • 오류 처리도 하나의 작업이므로 오류 처리하는 함수는 오류만 처리하는 것이 마땅하다.
    • 함수에 키워드 try가 있다면 함수는 try문으로 시작해 catch/finally 문으로 끝나야 한다는 말이다.

Error.java 의존성 자석

  • 오류 코드를 반환한다는 말은 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 말이고, 이는 의존성 자석을 낳는다.
    • 만약 다음과 같이 enum변수로 오류 코드를 정의하면 Error enum이 변하는 경우 Error enum을 import해서 사용하는 클래스 전부를 재컴파일하고 재배치해야 하므로 번거롭다. 그러면 새 오류 코드를 추가하는 대신 기존 오류 코드를 재사용하게 된다.
    • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되기 때문에 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라!

  • 코드에 중복이 늘어나면 코드도 길어지고 나중에 수정하기도 힘들다.
  • 또 한 곳에만 실수해서 오류가 날 가능성도 크다.
  • 중복을 제거하는 것은 모든 소프트웨어 개발에서 지속적으로 이루어진 노력이다.

구조적 프로그래밍

  • 다익스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다는 구조적 프로그래밍 원칙을 얘기했다.
    • 함수는 return 문이 하나여야 하고, 루프 안에서 break나 continue를 사용해선 안 되며 goto는 절대로 안 된다.
  • 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작으면 큰 이득이 없다. 이는 함수가 아주 클 때만 큰 이익을 제공한다.
  • 그러므로 함수를 작게 만든다면 가끔 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
    • 반면, goto문은 큰 함수에서만 의미 있으므로, 작은 함수에서는 피해야만 한다.

함수를 어떻게 짜죠?

  • 저자도 처음에는 길고 복잡하게 쓴다.
  • 들여쓰기 단계도 많고, 중복된 루프도 많고 인수 목록도 아주 길다.
  • 이름은 즉흥적이고 코드도 중복된다.
  • 하지만 그 와중에도 단위 테스트 케이스도 만들어서 통과하는 것을 확인한다.
  • 그 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메소드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다.
  • 처음부터 깨끗한 코드가 뚝딱하고 나오는 것이 아니다.

결론

앞서 말한 규칙을 따르면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수를 얻을 수 있다.

하지만 진짜 목표는 우리가 만들 시스템이라는 이야기를 풀어나가는 데 있다.

우리가 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어나가기 쉽기 때문에 깔끔한 함수를 작성하려고 한다는 것을 기억하자.

'클린코드' 카테고리의 다른 글

6. 객체와 자료구조  (0) 2021.10.10
5. 형식 맞추기  (0) 2021.10.06
4. 주석  (0) 2021.09.27
2. 의미 있는 이름  (0) 2021.09.19
1. 깨끗한 코드  (0) 2021.09.14