3장 - 함수
어떤 프로그램이든 가장 기본적인 단위가 함수다. 이 장은 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있는지, 더욱 읽기 쉽고 이해하기 쉬운지, 의도를 분명히 표현하는 함수를 구현할 수 있는 지에 대해 소개한다.
작게 만들어라
- 함수를 만드는 첫째 규칙은
작게
, 둘째 규칙은더 작게
- 얼마나 짧아야 좋을까? 이 정도가 좋다.
function renderPageWithSetupsAndTeardown(pageData, isSuite) {
if (isTestPage(pageDate)) includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
블록과 들여쓰기
- 중첩 블록(if/else문, while문)은 한 줄이어야 하고, 대게 거기서 함수를 호출한다.
- 함수에서 들여쓰기 수준은 1단이나 2단을 넘으면 안 된다. 그래야 함수를 읽고 이해하기 쉬워진다.
한 가지만 해라
함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
한가지
란? 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행하는 것
함수 내 섹션
한 함수를 여러 섹션으로 나눌 수 있다면 여러 작업을 한다는 것이다.
한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.
함수 당 추상화 수준은 하나로
- 함수가 확실히
한 가지
작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. - 한 함수 내에 추상화 수준을 섞으면 읽는 사람이 헷갈린다.
위에서 아래로 코드 읽기: 내려가기 규칙
- 위에서 아래로 이야기처럼 읽히는 코드
- 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
Switch문
switch문은 본질적으로 N가지를 처리한다. 불행히도 switch문을 완전히 피할 순 없으나, interface나 abstract factory 등 상속 관계로 숨기고 다른 코드에 노출하지 않는다.
function calculatePay(employee) {
switch (employee.type) {
case COMMISSIONED:
return calculateCommissionedPay(employee);
case COMMISSIONED:
return calculateHourlyPay(employee);
case COMMISSIONED:
return calculateSalariedPay(employee);
default:
throw new InvalidEmployeeType(employee.type);
}
}
하지만 불가피하게 switch를 사용할 수 밖에 없는 상황도 생기므로 상황에 따라 사용한다.
서술적인 이름을 사용하라
- 길고 서술적인 이름이 짧고 어려운 이름보다, 긴 주석보다 좋다.
- tips
- 함수 이름은 여러 단어가 쉽게 읽히는 명명법을 사용한다.
- 그 다음 가장 여러 단어를 사용해 함수의 기능을 더 잘 표현하는 이름을 선택한다.
- 비슷한 문체 사용 (같은 문구, 명사, 동사를 사용하는 일관성)
- 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
함수 인수
- 이상적인 인수 개수는 0개(무항)이고, 차선은 입력 인수가 1개인 경우다. 가능하면 삼항은 피하고, 4개 이상(다항)은 특별한 이유가 필요하다.
- 인수가 많아지면 코드를 이해하기에도 더 어렵고 테스트 관점에서도 부담스럽다.
많이 쓰는 단항 형식
- 인수에 질문을 던지는 경우
fileExists('myFile')
- 인수를 뭔가로 변환해 결과를 반환하는 경우
fileOpen('myFile')
- 이벤트를 사용하는 경우 이벤트라는 사실이 코드에 명확이 드러나야 한다.
플래그(flag) 인수
- 플래그 인수는 추하다. 함수로 boolean을 넘기는 것 자체가 함수가 여러 가지를 처리 한다고 대놓고 공표하는 셈이다.
이항 함수
writeField(name)
이writeField(outputStream, name)
보다 이해하기 쉽다.- 이항 함수가 적절한 경우
- 인수 2개가 한 값을 표현하는 요소인 경우 (자연적인 순서가 있다)
Point p = new Point(0,0)
- 이항 함수는 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
삼항 함수
- 이항함수보다 훨씬 더 이해하기 어렵고, 무시로 야기되는 문제가 두 배 이상 늘어나므로 신중이 사용을 고려해야 한다.
assertEquals(message, expected, actual)
의 경우 첫 번째 인수를 무시해야 한다.
인수 객체
- 인수가 2~3개 필요하다면 인수 일부를 독자적인 클래스 변수로 선언한 가능성을 살펴보자. (?)
인수 목록
- 때로는 인수 개수가 가변적인 함수도 필요하다. 가변 인수 전부를 동등하게 취급하면 List형 인수 하나로 취급할 수 있다.
동사와 키워드
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
- ex)
writeField(name)
- ex)
- 함수 이름에 키워드를 추가한다
assertEquals
보다assertExpectedEqualsActual(expected, actual)
이 좋다.- 이러면 인수 순서를 기억할 필요가 없어진다.
부수 효과를 일으키지 마라
- 부수효과는 때로는 예상치 못하게 지역/전역변수 및 인수를 수정한다. 많은 경우 시간적 결합이나 순서 종속성을 초래한다.
function userValidator() {
let cryptographer;
function checkPassword(userName, password) {
const user = UserGateWay.findByName(userName);
if (user !== User.NULL) {
const codePhrase = user.getPhraseEncodedByPassword();
const phrase = cryptographer.decrypt(codePhrase, password);
if ("Valid Password" === phrase) {
Session.initialize();
return true;
}
}
return false;
}
}
위의 코드에서 부수효과는 Session.initialize()
이다. 이름만 봐서는 함수 내부에서 세션을 초기화한다는 것을 알 수 없으므로, 이름만 보고 함수를 호출하면 기존의 세션 정보를 지워버릴 위험에 처한다.
이런 부수 효과가 시간적 결합을 초래한다. 즉, checkPassword
는 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다. 만약 시간적 결합이 필요하다면 함수 checkPasswordAndInitializeSession
으로 이름에 분명이 명시한다.
출력 인수
- 일반적으로 출력 인수는 피해야한다.
- 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
명령과 조회를 분리하라
- 함수는 뭔가를 수행하거나(ex. 객체 상태를 변경) 답하거나(ex. 객체 정보를 반환) 둘 중 하나만 해야한다.
if (set('username', 'unclebob'))
개발자는 set
을 동사로 의도했지만 if문에 넣고 보면 형용사로 느껴진다. 해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 것이다.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
}
오류 코드보다 예외를 사용하라
오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
Try/Catch 블록 뽑아내기
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;
}
위의 코드에서 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.
function deletePage(page) {
try {
deletePageAndAllReferences(page);
} catch (e) {
logError(e);
}
}
function deletePageAndAllReferences(page) {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKye());
}
function logError(e) {
logger.log(e.getMessage());
}
오류 처리도 한 가지 작업이다
- 오류를 처리하는 함수는 오류만 처리해야 한다. 함수에 키워드
try
가 있다면 함수는catch
/finally
문으로 끝나야 한다. - 오류 코드를 반한환다는 이야기는 어디선가 오류 코드를 정의한다는 뜻이다. 이는
의존성 자석
이다. Error enum이 변하면 이것을 사용하는 클래스나 함수를 전부 재컴파일, 재배치해야 한다.
enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_DEVENT
}
- 오류 코드 대신 예외를 사용하는 것이 더 좋다.
반복하지 마라
- 중복이 많아지면 오류가 발생할 확률도 많아진다. 소스코드에서 중복을 제거하려 노력해야 한다.
구조적 프로그래밍
Dijkstra 구조적 프로그래밍 원칙
: 모든 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다.- 함수는 return문이 하나여야 한다. 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮다.
- 절대 goto를 사용해서는 안된다.
함수를 어떻게 짜죠?
- 먼저 생각을 기록한 후 읽기 좋게 다듬는다.
- 처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많다. 이름은 즉흥적이고 코드는 중복된다.
- 그런 다음 단위 테스트 케이스를 만들고, 코드를 다듬고, 함수를 바꾸고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고 순서를 바꾼다.
- 처음부터 함수를 잘 짜내는 것은 불가능하다.
결론
대게 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.