Clean Code 3. 함수
목차
- 작게 만들어라
- 한가지만 해라
- 함수 당 추상화 수준은 하나로 하라
- switch 문
- 서술적인 이름을 사용하라
- 함수 인수
- 부수 효과를 일으키지 마라
- 명령과 조회를 분리하라
- 오류 코드보다 예외를 사용하라
- 반복하지 마라
- 구조적 프로그래밍
- 함수를 어떻게 짜죠?
- 결론
- 과제
어떤 프로그램이든 가장 기본적인 단위가 함수다. 이 장은 함수를 잘 만드는 법을 소개한다.
작게 만들어라
- 각 함수가 명백하고 하나의 이야기를 표현해야 한다.
- 블록과 들여쓰기
if
/else
/while
문 등에 들어가는 블록은 한 줄이어야 한다.- 블록 내에서 호출하는 함수 이름을 적절히 짓는다면 이해하기 쉽다.
- 들여쓰기 수준은 2단을 넘으면 안된다.
// p.42 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 = new StringBuffer();
newPageContent.append(pageData.getContent());
includeTeardownpages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
// p.43 3-3
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData)
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
한가지만 해라
함수는 한가지를 해야한다. 그 한가지를 잘해야 한다. 그 한가지만을 해야한다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행
- 의미있는 이름으로 다른 함수를 추출할 수 있다면 여러 작업을 하는 것
- 한가지 작업만 하는 함수는 자연스럽게 섹션을 나누기 어려움
함수 당 추상화 수준은 하나로 하라
- 함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일 해야 한다.
- 추상화 수준이 높다 : 디테일 부분을 많이 숨겼다는 의미
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
- 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려운 탓이다.
- 근본 개념과 세부사항을 뒤섞기 시작하면 사람들이 함수에 세부사항을 더 추가한다.
- 내려가기 규칙
- 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
switch 문
switch
문은 작게 만들기 어렵고 한가지 작업만 하도록 만들기도 어렵다.- Polymorphism을 이용하면
switch
문을 저차원 클래스에 숨기고 반복하지 않을 수 있다.- Abstract Factory에 숨기고 Factory는
switch
문을 사용해 적절한 파생 클래스의 인스턴스를 생성
- Abstract Factory에 숨기고 Factory는
// p.47 3-4
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);
}
}
- 위 함수의 문제점
- 길고 한 가지 작업만 수행하지 않는다.
- SRP와 OCP를 위반
- 위 함수와 구조가 동일한 함수를 무한정 양산한다. isPayday(Employee e, Date date), deliverPay(Employee e, Money pay) …
// p.48-49 3-5
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public class 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(e.type);
}
}
}
서술적인 이름을 사용하라
- 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
- 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
- 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
- 함수 기능을 잘 표현하는 이름 선택
- 이름을 붙일때는 일관성이 있어야 한다.
- 모듈 내 함수 이름은 같은 문구, 명사, 동사를 사용
함수 인수
- 함수에서 이상적인 인수 개수는 0개
- 줄줄이 인수를 넘기면 계속 의미를 해석해야 하므로 인스턴스 변수로 선언하는게 좋다.
- 인수가 많아지면 테스트 케이스를 작성하기도 어려워진다.
- 출력 인수는 이해하기 어렵고 코드를 재차 확인하게 만든다.
- 많이 쓰는 단항 형식
// 인수에 질문을 던지는 경우
boolean fileExists("MyFile");
// 인수를 다른 값으로 변환하여 결과를 반환하는 경우
InputStream fileOpen("MyFile");
// 이벤트 함수 : 입력 인수로 상태를 바꾼다. 이벤트라는 사실이 코드에 명확하게 드러나야 한다.
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape) {
Close();
}
}
- 플래그 인수
bool
값을 넘겨서 다른 동작을 하는 함수는 끔찍하다.- 함수가 한꺼번에 여러가지를 한다고 공표하는 셈이다.
void ChangePort(IPort port, bool isLoadingPort)
{
if (isLoadingPort) {
portItem.LoadingPort = port;
}
else {
portItem.DischargingPort = port;
}
}
- 이항 함수
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
- 무조건 나쁘지는 않지만 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸려고 애써야 한다.
// 적절한 이항 함수
Point p = new Point(0, 0);
// 순서를 실수 하기 쉬운 경우
AssertEquals(expected, actual);
// 이항 함수를 단항 함수로 바꾸는 방법
writeField(outstream, name);
// 1. outstream 클래스에 writeField 메서드 추가
outstream.writeField(name);
// 2. outstream을 현재 클래스 구성원 변수로 만든다.
Outstream outstream;
void writeField(string name);
// 3. FieldWriter라는 새 클래스를 만든다.
var fieldWriter = new FieldWriter(outstream);
fieldWriter.Write(name);
- 삼항 함수
- 순서, 주춤, 무시로 야기되는 문제가 두배 이상 늘어난다.
- 인수 객체
- 인수를 줄이면서 개념도 부여할 수 있다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
- 인수 목록
- 인수 개수가 가변적인 함수가 필요하다.
string.Format("Program name:{0}, path:{1}", name, path);
string Format(string format, object[] params); // 사실상 이항 함수
- 동사와 키워드
- 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름은 필수
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
// 함수 이름에 인수 이름을 넣는 것이 오류를 줄이는 해결책이 될 수 있다.
AssertExpectedEqualsActual(expected, actual);
부수 효과를 일으키지 마라
- Side Effect : 함수 내의 실행으로 함수 외부가 영향을 받는 것.
// p.55 3-6
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;
}
}
- 위 예제의 문제점
- 함수 이름으로 세션 초기화 되는 것이 드러나지 않음
- 세션을 초기화해도 괜찮은 경우에만 호출이 가능
- 출력 인수
- 일반적으로 인수를 함수 입력으로 해석하므로 출력 인수는 피해야 한다.
- 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
appendFooter(s);
report.appendFooter();
명령과 조회를 분리하라
- 조회 함수 : 객체 정보를 반환하는 함수
- 명령 함수 : 객체 상태를 변경하는 함수
- 함수는 조회나 명령 중 하나만 수행해야 한다.
if (set(attribute, value)) ... ?
if (attributeExists(attribute)) {
setAttribute(attribute, value);
}
오류 코드보다 예외를 사용하라
- 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
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 {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
- 오류 처리도 한가지 작업이다.
- 함수는 한가지 작업을 해야 한다. 오류 처리도 한가지 작업에 속한다.
- 오류 처리를 하는 함수는 오류만 처리해야 한다.
- Error Code File 의존성 자석
- 오류 코드를 반환한다는 이야기는, 어디선가 오류 코드를 정의한다는 뜻이다.
- Error enum이 변한다면 사용하는 클래스 전부를 다시 컴파일하고 다시 배치 해야 한다.
- 재컴파일/재배치가 번거롭기에 새 오류 코드를 추가하는 대신 기존 오류 코드를 재사용한다.
- 예외는
Exception
클래스에서 파생되기 때문에 재컴파일이 불필요하다.
반복하지 마라
- 중복은 소프트웨어에서 모든 악의 근원이다.
- 코드를 중복해서 쓰면 코드 길이가 길어지고, 알고리즘 변경시 여러 곳을 수정해야 하므로 오류 발생 확률이 높아진다.
구조적 프로그래밍
- Edsger Dijkstra의 구조적 프로그래밍 원칙
- 모든 함수와 함수 내 모든 블록에 입구/출구가 하나만 존재해야 한다.
- 함수는
return
문이 하나여야 한다. - 루프 안에서
break
나continue
를 사용해선 안 되며goto
는 절대로 안된다.
- 함수가 작다면 별 이익을 제공하지 못한다.
- 함수를 충분히 작게 만든다면
return
/break
/continue
를 여러번 사용해도 괜찮다.
함수를 어떻게 짜죠?
- 함수를 처음 짤 때는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 길다. 이름은 즉흥적이고 코드는 중복된다.
- 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
- 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 전체 클래스를 쪼개기도 한다.
- 코드를 다듬는 동안 단위 테스트를 항상 통과한다.
- 최종적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다.
- 처음부터 탁 짜려고 하지 말자.
결론
- 이 장은 함수를 잘 만드는 기교를 소개했다.
- 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다.
- 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기 풀어가기가 쉬워진다.
과제
- 본인이 작성한 코드 중 규칙에 위반되는 함수를 찾고 규칙에 맞게 수정하기
과제코드
Leave a comment