과거 포트란(Fortran)과 PL/1 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴다.
현재에는 함수만 살아남았으며, 어떤 프로그램이든 가장 기본적인 단위가 함수다.
함수를 잘 이용하면 읽기 쉽고 이해하기 쉬운 코드로 작성이 가능하다. 클린 코드에서는 처음 읽는 사람이 내부 프로그램을 직관적으로 파악할 수 있고, 의도를 분명히 표현하는 함수 작성하는 방법에 대해서 소개한다.
작게 만들어라!
함수를 만드는 가장 중요한 규칙은 작게 만드는 것이다.
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
- if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다. (이곳에서 함수를 호출)
- 블록 안에서 호출하는 함수 이름을 적절히 지어야 한다.
- 함수에서 들여 쓰기 수준은 1단이나 2단을 넘어서면 안된다.
한 가지만 해라!
“함수는 한 가지를 해야 한다. 그 한 가지를 잘해야 한다. 그 한 가지만을 해야 한다.”
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
- 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위함이다.
- 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
함수 당 추상화 수준은 하나로!
- 함수가 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
내려가기 규칙
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
- 한 함수 다음에는 추상화 수준이 한 단계 낮아지는 것이 좋다.
Switch 문
switch 문은 한 가지 작업을 하도록 만들기 어렵고 작게 만들기도 어렵다.
- 본질적으로 switch 문은 N가지 작업을 처리한다.
하지만, 저 차원 클래스에 숨기고 반복하지 않는 방법은 있다.
다음과 같이 swith 문을 사용하는 함수가 있다.
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);
}
}
calculatePay 함수에는 몇 가지 문제가 있다.
- 함수가 길다.(새 직원 유형을 추가하면 더 길어진다.)
- 한 가지 작업만을 수행하지 않는다.
- 단일 책임 원칙(SRP)를 위반한다.
- 개방 폐쇄 원칙(OCP)를 위반한다.
이 문제를 해결하기 위해 추상 팩토리(Abstract factory)에 숨겨 아무에게도 보여주지 않는 방법이 있다.
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);
}
}
}
서술적인 이름을 사용하라!
코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 할 수 있다. - 워드 커닝햄(Ward Cunningham)
- 함수가 하는 일을 잘 표현하는 것이 좋은 이름이다.
- 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
- 이름을 붙일 때는 일관성이 있어야 한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
- includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage, …
함수 인수
함수에서 이상적인 인수 개수는 0개(무항)다. 다음은 1개(단항)고, 다음은 2개(이항)다. 3개(삼항)는 가능한 피하는 편이 좋다. 최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.
많이 쓰는 단항 형식
- 인수에 질문을 던지는 경우
- ex) boolean fileExists(”MyFile”)
- 인수를 변환하여 결과를 반환하는 경우
- InputStream fileOpen(”MyFile”)
- String 형의 파일 이름을 InputStream으로 변환
- 단항 함수 형식의 이벤트는 다소 드물게 사용
- 이 외의 경우에는 단항 함수는 가급적 피한다.
플래그 인수
- 플래그 인수는 함수가 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
이항 함수
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
- 2항 함수가 적절한 경우
- Point p = new Point(0, 0)과 같은 좌표계
- 이항 함수가 무조건 나쁘다는 것은 아니며, 개발을 하다 보면 불가피한 경우도 생긴다.
- 그럼에도 가능하면 단항 함수로 바꾸도록 애써야 한다.
삼항 함수
- 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다.
- 삼항 함수를 만들 때는 신중히 고려해야 한다.
인수 객체
- 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
- Circle makeCircle(double x, double y, double radius);
- Circle makeCircle(Point center, double radius);
인수 목록
- 때로는 인수 개수가 가변적인 함수도 필요하다.
- String.format(”%s worked %.2f hours.”, name, hours);
- String.format은 사실상 이항 함수이다.
- public String format(String format, Object… args)
- 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있지만, 이를 넘어서는 인수를 사용할 경우에는 문제가 있다.
동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
- 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
- write(name)
- 함수 이름에 키워드(인수)를 추가한다.
- assertExpectedEqualsActual(expected, actual)
부수 효과를 일으키지 마라!
부수 효과는 거짓말
- 함수에서 한 가지를 하겠다고 약속하고는 남몰래 다른 것도 한다.
- 예상치 못하게 클래스 변수를 수정
- 인수나 시스템 전역 변수를 수정
- 시간적인 결합이나 순서 종속성을 초래
다음은 userName과 password를 인수로 참이면 true, 거짓이면 false를 반환한느 함수이다.
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;
}
}
- Session.initialize() : 세션을 초기화하는 함수
- 함수 이름에는 세션을 초기화한다는 사실이 드러나지 않는다.
- 위 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 수 있다.
출력 인수
일반적으로 우리는 인수를 함수 입력으로 해석한다.
- 출력 인수는 사용하지 않아야 하며, 객체지향 언어에서는 사용할 필요가 거의 없다.
- appendFotter(s) 보다는 report.appendFooter()
명령과 조회를 분리하라
함수는 객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나다.
public boolean set(String attribute, String value);
- attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패하면 false 반환하는 함수
if (set(”username”, “unclebob”))…
- username을 unclebob으로 설정하는 것인지, 설정되어 있음을 확인하는 것인지 모호하다.
- set이라는 단어가 동사인지 형용사인지 분간하기 어렵기 때문
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
- 명령과 조회를 분리하여 혼란으로부터 방지해야 한다.
오류 코드보다 예외를 사용하라!
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
- if 문에서 명령을 표현식으로 사용하기 쉽기 때문
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 블록 뽑아내기
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
- 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());
}
- 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
- 함수에 try가 있다면, 해당 함수는 try 문으로 시작해 catch/finally 문으로 끝나야 한다.
Error.java 의존성 자석
오류 코드를 반환한다는 것은 클래스, 열거형 변수 등 어디선가 오류 코드를 정의한다는 것이다.
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
- 위와 같은 클래스는 의존성 자석(magnet)이다.
- 다른 클래스에서 Error enum을 import 해서 사용해야 하므로 Error enum이 변하면, Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 한다.
- Error 클래스 변경이 어려워진다.
- 재컴파일/재배치가 번거롭기에 새 오류코드를 정의하지 않고, 기존 오류 코드를 재사용한다.
- 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다.
- 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.
반복하지 마라!
- 중복은 소프트웨어에서 모든 악의 근원이다.
- 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
- 중복을 제거하려는 지속적인 노력이 필요하다.
구조적 프로그래밍
- 다익스트라의 구조적 프로그래밍 원칙
- 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)는 하나만 존재해야 한다.
- 즉, 함수는 return 문이 하나여야 한다.
- 루프 안에서 break나 continue를 사용해선 안되며, goto는 절대로 안된다.
- 구조적 프로그래밍의 목표와 규율은 공감하지만, 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다.
- 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮다.
- 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
- goto 문은 피해야 한다.
함수를 어떻게 짜죠?
소프트웨어를 짜는 행위는 어느 글짓기와 비슷하다. 처음부터 한 번에 모든 규칙을 지키면서 함수를 작성하는 사람은 없다.
- 처음에는 길고 복잡하다.
- 들여 쓰기도 많고 중복된 루프도 많다.
- 인수 목록도 아주 길다.
- 이름은 즉흥적이고 코드는 중복된다.
- 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
- 이후 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
- 메서드를 줄이고 순서를 바꾸며, 때로는 전체 클래스를 쪼개기도 한다.
- 이 와중에도 코드는 항상 단위 테스트를 통과한다.
결론
- 이 장은 함수를 잘 만드는 기교를 소개한다.
- 설명한 규칙을 따른다면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수가 나올 것이다.
https://link.coupang.com/a/V3Ggo
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
'IT Book > 클린 코드(Clean Code)' 카테고리의 다른 글
[Clean Code] 6. 객체와 자료구조 - 클린 코드 정독하기 (0) | 2023.07.20 |
---|---|
[Clean Code] 5. 형식 맞추기 - 클린 코드 정독하기 (0) | 2023.06.08 |
[Clean Code] 4. 주석 - 클린 코드 정독하기 (1) | 2023.05.12 |
[Clean Code] 2. 의미 있는 이름 - 클린 코드 정독하기 (0) | 2023.03.18 |
[Clean Code] 1. 깨끗한 코드 - 클린 코드 정독하기 (0) | 2023.03.17 |