[Programming] - CleanCode 03. 함수


작게 만들어라!


함수를 만드는 첫번째 규칙은 ‘작게’다.

두번째 규칙은 ‘더 작게’다.

거의 2~3줄로 함수를 줄여라

블록과 들여쓰기


if/else문 while문 등에 들어가는 블록은 한줄이어야 한다.

이 말은 ‘중첩 구조’가 생길만큼 함수가 커져서는 안 된다는 뜻이다.

한 가지만 해라


함수는 한 가지를 해야 한다.

우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다.

함수가 ‘한가지’만 하는지 판단하기 어렵다.

그 방법은 2가지다.

  1. 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다.
  2. 의미 있는 이름으로 다른 함수를 추출 할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

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


함수가 확실히 한가지 작업만 하려면 함수내 모든 문장의 추상화 수준이 동일 해야 한다.

한 함수 내에서 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.

ex) getHtml()은 추상화 수준이 매우 높다. .append()는 추상화 수준이 매우 낮다.

위에서 아래로 내려가기 규칙


코드는 위에서 아래로 이야기처럼 읽혀야 한다. (코드의 배치가 그래야 한다)

즉, 위에서부터 함수의 추상화 수준이 한단계씩 내려간다.

하지만, 추상화 수준이 한 단계인 함수를 구현하기란 쉽지 않다.

그렇지만 매우 중요한 규칙이다!

Switch 문


switch문을 작게 만들기 어렵다.

기본적으로 switch문은 N가지를 처리한다.

불행하게도 switch문을 완전히 피할 방법은 없다.

하지만, 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.

다형성을 활용하자.

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


함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.

함수 이름을 붙일때는 일관성이 있어야 한다. 특히 모듈 내에서는 같은 문구, 명사, 동사를 사용한다.

함수 인수


함수에서 이상적인 인수 개수는 0개(무항)이다.

다음은 1개, 2개이고 3개,4개부터는 피하는게 좋다.

4개 이상은 특별한 이유가 있어도 사용하면 안된다.

인수가 많아지면 테스트 관점에서도 어려워진다.

또한 출력인수는 입력인수보다 이해하기 어렵다. 함수에서 파라미터(인수)로 출력을 하지 말아라. ⇒ 함수의 반환값으로 출력을 해야지, 인수에다가 출력하지 말아라 ⇒ 사이드 이펙트 (그래서 요즘 불변형 변수 사용이 트랜드)

많이 쓰는 단항 함수


단항 함수에는 크게 2가지 종류가 있다.

  1. 질문을 던지는 경우 boolean fileExists(”myFile”) ⇒ 파일이 존재하는지 체크 (질문)
  2. 인수를 뭔가로 변환해서 반환하는 경우 InputStream fileOpen(”myFile”) ⇒ string을 inputStream으로 변환

드물게 사용하지만 아주 유용한 방식으로 단항 함수 형식 이벤트가 있다.

입력 인수만 있으며 출력 인수는 없다.

프로그램은 함수 호출을 이벤트로 해석해 입력 인수에 따라 시스템 상태를 바꾼다. passwordAttemptFailedNtimes(int attempts)

이벤트 함수는 이벤트라는 것이 명확히 코드에 드러나야 한다.

위 3가지 경우가 아니라면 단항 함수는 가급적 피한다.

ex) void includeSetupPageInto(StringBuffer pageText)는 피한다.

변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다. (위 함수는 pageText를 받아가서 출력내용을 담는다)

입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.

ex) StringBuffer transform(StringBuffer in)이 void transform(StringBuffer out)보다 좋다.

입력 인수를 그대로 돌려주는 함수라 할지라도 변환 함수 형식을 따르는 편이 좋다.

플래그 인수


플래그 인수는 정말 끔찍하다. 대놓고 2가지 일을 하겠다고 공표하는 것이다.

참과 거짓에 따라 다른 행동을 하겠다는 것이다.

이항 함수


이항 함수보다 단항 함수가 이해하기 쉽다.

writeField(name)이 writeField(outputStream, name)보다 이해하기 쉽다

물론 이항 함수가 필요한 경우가 있다. ex) point (x,y)

outputStream 내부에 writeField라는 함수를 만들어 outputStream.writeField(name)으로 처리한다. 혹은 outputStream을 클래스 내부 변수로 만들어 파라미터에서 제거 한다.

삼항 함수


인수가 3개면 당연히 2개보다 이해하기 어렵고, 그만큼 주의를 기울여야 한다.

인수 객체


만약 인수가 2~3개면 독자적 클래스로 변수를 선언할 가능성을 봐보자.

ex) Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);

객체를 생성해 인수를 줄이며 개념을 표현할 수 있다.

동사와 키워드


write(name)은 보자마자 누구나 이해한다.

writeField(name)은 더욱 명확하다. name이 field라는 것도 알 수 있다.

마지막은 함수이름에 키워드를 추가하는 것이다.

assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. (인수 순서 햇갈릴 일이 없다)

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


부수 효과는 거짓말이다. 남몰래 무언가를 하지 말아라.

때로는 예상치 못하게 클래스 변수를 수정한다.

ex) checkPassword 내부에서 session을 초기화함 (이름만 보고 비밀번호만 확인하는 줄 알고 사용하다가 버그 발생)

출력 인수


인수는 입력으로 사용해야 한다.

void appendFooter(s); ⇒ 인수에 내용을 더했다. 즉, 출력으로 사용했다.

위 함수는 어디에 바닥글을 추가하는가? s에 추가하는가? 아니면 어딘가에 s를 추가하는건가?

report.appendFotter()로 수정해야 한다.

함수 상태에서 변경해야 한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!


함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다.

객체의 상태를 변경하던지, 객체의 정보를 반환하거나 둘 중 하나만 해야 한다.

public boolean set(String attribute, String value);

if(set("username", "unclebob")) ...

위 함수는 attribute를 찾아서 value로 값을 설정하고 성공했으면 true를 실패했으면 false를 반환한다.

if문에서 사용중인 내용을 보아라. 괴상한 코드가 나온다.

if(attributeExists("username") {
	setAttribute("username", "unclebob");
	...
}

위와 같이 명령과 조회를 분리하라.

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


명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 방식을 미묘하게 위반한다.

명령 함수의 오류값을 아래와 같이 사용하기 시작한다. (오류를 조회하듯)

if(deletePage(page) === E_OK)

try/catch로 오류 처리를 예외로 해주자.

Try/Catch 블록 뽑아내기


try/catch 블록은 추하다. 코드의 구조 혼란을 일으키며, 동작을 뒤섞는다.

별도로 뽑아내는 편이 좋다.

public void delete(Page page) {
	try { 
		deletePageAndAllReference(page);
	} catch (Exception e) {
	logError(e);
	}
}

delete 함수는 모든 오류를 처리한다. 실제로 페이지를 제거하는 함수는 deletePageAndAllReference 이다. deletePageAndAllReference 는 예외 처리를 하지 않는다.

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


하나의 함수에서 한가지 작업을 해야한다.

즉, 오류 처리를 하려면 오류 처리만 해라.

또한 오류코드 말고, 예외 클래스를 사용하면 추상 클래스를 통한 확장성 또한 챙길 수 있다.

반복하지 마라!


코드의 중복은 문제다.

코드의 길이가 늘어날 뿐 아니라, 알고리즘이 변하면 반복된 모든 곳의 코드를 변경 시켜야 한다.

구조적 프로그래밍


모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야한다.

루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로 안된다. (C언어 goto)

함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.

함수를 어떻게 짜죠?


소프트웨어는 글쓰기와 비슷하다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.

하지만, 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만들어서 진행한다.

코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.

코드는 항상 단위 테스트를 통과해야한다.

결론


함수는 그 언어에서 동사며, 클래스는 명사다.

프로그래밍의 기술은 언제나 언어 설계의 기술이다.

대가 프로그래머는 시스템을 구현할 프로그래밍 아니라 풀어갈 이야기로 여긴다.

좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다.