Language/Java

[클린 코드] Ch.3 - 함수

비소_ 2022. 8. 3.

클릭하시면 네이버 Book으로 연결됩니다!

해당 서적을 참고하여 개인 공부용으로 정리한 글입니다.


1. 함수는 최대한 작게 작성

한 함수가 100줄을 넘어가게 하지 말아야 한다. (100줄도 많다)

if/else 문, while 문 등에 들어가는 블록은 한 줄인 것이 좋다.

그 안에서 함수를 호출한다.

중첩 구조를 최대한 피하라는 뜻이다.

따라서, 들여 쓰기 수준이 1단이나 2단을 넘어서면 안 된다.

최대한 간단해져야 읽고 이해하기 쉬워진다.


2. 한 가지만 수행할 것

한 가지만 수행해야 한다는 점은 동의한다.

그런데 한 가지의 기준이 참 애매하다.

따지고 보면 코드 한 줄 한 줄이 다 한 기능을 담당하고 있지 않는가.

 

작가가 말하는 한 가지는 '지정된 함수 이름 아래에서 추상화 수준이 하나인 것'을 의미한다.

혹은 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 수행하는 것이다.

빼더라도 추상화 수준이 바뀌지 않는다면, 기존 함수는 이미 한 가지 작업을 수행하고 있는 것이다.


3. 추상화 수준을 섞지 말 것

  getHtml() PathParser.render(pagepath) sb.append("\n")
추상화 수준 높음 중간 낮음

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

근본 개념인지 세부사항인지 구분하기 어렵기 때문이다.

한 번 뒤섞인 코드는 점점 더러워진다.

 

또한, 코드는 이야기처럼 읽혀야 좋다.

그래서 함수 추상화 수준이 한 번에 한 단계씩 낮아지는 게 좋다.

이를 내려가기 규칙이라 한다.

쉽게 말해 각 함수는 다음 함수를 소개해야 한다.


4. Switch 문

switch 문은 근본적으로 작게 만들기 어렵다.

그래도 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않을 수 있다.

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(Single Reponsibility Principal)을 위배한다.
  • OCP(Open Closd Principal)을 위배한다.
    • 새 직원 유형을 추가할 때마다 코드를 변경해야 하기 때문
  • 동일 구조가 무한정 존재할 수 있다. (ex. isPayday, deliverPay 등)

따라서, 해당 switch 문을 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);
		} 
	}
}

5. 서술적인 이름으로 표현할 것

함수를 서술적인 이름으로 표현해 함수의 기능을 짐작할 수 있게 한다.

이름이 길어지더라도 주석보다 낫다.


6. 함수 인수(parameter)

이상적인 인수 개수는 0개다.

최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.

이후는 최대한 피한다.

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우 (ex. boolean fileExists("MyFile"))
  • 인수를 변환해 결과를 반환하는 경우 (ex. InputStream fileOpen("MyFile"))
  • 이벤트 함수 (단, 코드에 이벤트라는 사실이 명확히 드러나야 함)

이러한 상황이 아니라면 단항 함수는 가급적 피한다.

플래그(Flag) 인수

쓰지말 것. 함수가 한 번에 여러 작업을 처리한다고 공표하는 셈

true면 이거 하고, false면 저거 하고.. 안된다.

이항 함수

두 요소가 한 값을 표현하거나, 자연적인 순서가 있다면 오히려 괜찮을 수 있다.

assertEquals(expected, actual) 생각해보면 두 인수는 자연적인 순서가 없다.

그래서 expected에 actual를 넣는 실수를 간간히 한다.

이처럼 프로그래머가 인위적으로 외워야 하는 경우는 별로 좋지 않다.

물론, 어쩔 수 없는 경우도 생기지만 그만큼 위험이 따른다는 사실을 이해하고 단항으로 바꾸기 위해 노력해야 한다.

인수 객체

인수가 2-3개 이상 필요할 경우 독자적인 클래스로 묶을 수 있는지 살펴본다.

x, y를 넘기는 것보다 Point 클래스로 묶어서 넘기는 것이 낫다.

또한, 개념을 여전히 표현하고 있다.

인수 목록

가변 인수를 취하는 함수는 가변 인수 전부를 동등하게 취급하면 List 혹은 배열형 인수 하나로 취급할 수 있다.

public String format(String format, Object... args)

동사와 키워드

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

write(name)
writeField(name) //좀 더 명확함

함수 이름에 키워드를 추가할 수도 있다.

assertExpectedEqualsActual(expected, actual)로 바꾸면 인수의 순서를 외울 필요가 없다.


7. 부수 효과를 일으키지 말 것

부수 효과는 한 가지를 하겠다고 하고선 남몰래 다른 작업을 수행하는 짓이다.

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

위 코드에서 checkPassword 함수는 이름만 봐서는 패스워드를 확인해서 참/여부를 알려주는 함수다.

그러나 중간에 Session.initialize()가 세션을 초기화하는 과정을 거치는 부수 효과를 부른다.

따라서, 이름만 보고 사용한 사용자는 세션이 초기화돼버리는 문제가 발생한다.

이처럼 부수효과는 대게 시간적인 결합(Temporal Copuling)이나 순서 종속성(Order Dependency)을 초래한다.

출력 인수

일반적으로 우리는 인수를 함수 입력으로 해석한다.

따라서, 출력 인수는 피하고 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다. (this 이용)


8. 명령과 조회를 분리할 것

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

public boolean set(String attribute, String value);

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

위 set 메서드는 username을 unclebob으로 설정하는 것에 성공/여부를 판단하는지,

username이 unclebob으로 설정되어 있는지를 판단하는지 분간하기 어렵다.

함수명은 동사인데 if 문 안에 들어가 마치 형용사처럼 느껴진다.


9. 오류 코드보다 예외를 사용할 것

오류 코드 대신 예외(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());
}

의존성 자석(magnet)

오류 코드를 사용한다는 것은 어디선가 다음과 같이 enum 형태로 정의한다는 뜻이다.

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

하지만 Error enum이 변하면 이를 사용하는 모든 클래스를 전부 재컴파일/재배치해야 한다.

예외를 사용한다면 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 없이 추가할 수 있다.


10. 반복하지 말 것

중복은 모든 소프트웨어에서 모든 악의 근원이다.

중복만 없애도 모듈 가독성이 크게 높아진다.


11. 구조적 프로그래밍

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

다익스트라(Dijkstra)의 단일 입/출구 규칙(single entry-exit rule)보다 의도를 표현하기 쉬워진다.


12. 함수 짜는 법

러프(rough)하게 초안을 작성하고 단위 테스트를 수행하면서 위 규칙을 만족하는 함수가 나오도록 꾸준히 리팩터링한다.

댓글