본문 바로가기

책책책/Clean Code

[Clean Code] 3장 - 함수

작게 만들어라!

함수가 작을 수록 그 하는 일이 명백해진다. 

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
   if (isTestPage(pageData)) 
   	includeSetupAndTeardownPages(pageData, isSuite); 
   return pageData.getHtml();
}

되도록 위 예시만큼 짧아야 한다. 

블록과 들여쓰기

if/else, while문 등에 들어가는 블록은 한 줄이어야 한다. 대개 여기서 함수를 호출한다. 
각 함수 별 들여쓰기 수준은 2단을 넘어서지 않아야 한다. 

한 가지만 해라!

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

지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다. 
함수를 만드는 이유 자체가 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이기 때문이다.

의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈이다. 
한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다. 

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

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

위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 

Switch 문

작고, 한가지 일만 하는 switch 문은 만들기 어렵다. 
이를 다형성으로 해결해야한다. 

// Bad
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를 위반한다. (새 직원 유형을 추가할 때마다 코드를 변경하기 때문)

다음과 같은 코드로 위 문제를 해결한다. 

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);
		} 
	}
}

switch 문을 추상 팩토리에 숨기고, 적절한 파생 클래스의 인스턴스를 생성한다. 

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

길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 
서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. 

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

함수 인수

함수의 인수는 적을 수록 좋다. 
4개 이상의 인수는 특별한 이유가 필요하다. 

테스트 관점에서도 인수가 적은 함수가 더 용이하다. 

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 경우

인수에 질문을 던지는 경우

boolean fileExists(“MyFile”);

인수를 뭔가로 변환해 결과를 반환하는 경우

InputStream fileOpen(“MyFile”);

이벤트 함수

입력 인수만 있으며, 출력 인수는 없다.
이벤트라는 사실이 코드에 명확히 드러나야 한다.

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

플래그 인수

플래그 인수는 쓰지마라. boolean 값을 넘기는 것 자체가 그 함수는 한꺼번에 여러가지 일을 처리한다고 공표하는 것과 마찬가지다.

이항 함수, 삼항 함수

단항 함수보다 이해하기가 어렵고 실수하기 쉽다. 
두개의 인수간의 자연적인 순서가 있어야한다. 
무조건 나쁜다는 소리는 아니지만 그만큼 주의해서 써야한다. 

인수 객체 

인수가 여러개 필요하면, 일부를 독자적인 클래스 변수로 선언할 가능성을 생각본다. 

// 위 보다 아래 예시가 더 Good
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다. 

String.format("%s worked %.2f hours", name, hours);

동사와 키워드

단항 함수는 함수와 인수가 동사 / 명사 쌍을 이뤄야 한다. 

writeField(name);

함수 이름에 키워드를 추가한다. 즉 함수 이름에 인수 이름을 넣는다.
그러면 인수 순서를 기억할 필요가 없어진다.

assertExpectedEqualsActual(expected, actual);

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

많은 경우 시간적 결합이나 순서 종속성을 초래한다. 

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; 
	}
}

이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다. 

출력 인수

객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 
출력 인수로 사용하라고 설계한 변수가 바로 this 이다. 

// Bad
appendFooter(s);
// Good
report.addFooter(s);

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

명령과 조회를 분리하라!

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

public boolean set(String attribute, String value);

속성 값 설정 성공 시 true를 반환하는 모호한 코드가 된다. 

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

오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다. 

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 / 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());
}

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

Error.java 의존성 자석

public enum Error { 
	OK,
	INVALID,
	NO_SUCH,
	LOCKED,
	OUT_OF_RESOURCES, 	
	WAITING_FOR_EVENT;
}

다른 클래스에서 Error Enum을 import해야한다. 
이 경우 Error Enum이 변하면 이를 사용하는 클래스를 전부 다시 컴파일하고 다시 배치해야 한다. 
그러므로 예외를 사용하는 것이 더 안전하다.

오 에러 코드를 쓰지 않아도 되는 이유....

반복하지 마라!

중복을 제거하면 모듈 가독성이 크게 높아진다. 
객체 지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다.

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다. 
즉 함수는 return 문이 하나여야 한다. 
루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로, 절대로 안된다.

위 규칙은 함수가 클 경우이고, 함수를 작게 만든다면 간혹 return, break, continue를 사용해도 괜찮다.  

 

하지만 진짜 목표는 시스템이라는 이야기를 풀어나가는 데 있다는 사실을 명심하기 바란다. 여러분이 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어가기가 쉬워진다는 사실을 기억하기 바란다.

 

'책책책 > Clean Code' 카테고리의 다른 글

[Clean Code] 2장 - 의미 있는 이름  (0) 2021.08.18
[Clean Code] 1장 - 깨끗한 코드  (0) 2021.08.18