본문 바로가기

개발 관련/Clean Code

[Clean Code] Ch.3 함수

반응형

 

 

  1. 작게 만들어라

 

함수를 만드는 첫 번째 규칙은 최대한 작게 만드는 것이다. if문/else문/while문 등에 들어가는 블록은 한 줄이어야 하고, 주로 거기서 함수를 호출한다. 즉, 중첩 구조가 생길 만큼 함수가 커져서는 안 된다.

 

1. 한 가지만 해라

 

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

 

아래는 추상화 수준이 하나인 함수의 예시이다. 

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

 

위 코드는 세 작업을 한다고 할 수도 있다. 

1. 페이지가 테스트 페이지인지 판단한다.

2. 그렇다면 설정 페이지와 해제 페이지를 넣는다. 

3. 페이지를 HTML로 렌더링한다. 

하지만, 위 작업들은 모두 추상화 수준이 같기 때문에 한 작업을 수행한다고 한다. 의미를 유지하면서 더 이상 줄이기가 불가능하다. 

 

함수에서 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 것이라고 판단할 수 있다. 

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어렵기 때문이다. 

 

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

 

2. 명령과 조회를 분리하라

 

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 예를 들어 다음의 함수를 살펴보자.

public boolean set(String attribute, String value);

 

이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다. 따라서 다음과 같은 코드가 나오게 된다. 

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

 

독자 입장에서 위 코드를 바라보면 어떤 의미로 생각할 수 있을까? "username"이 "unclebob"으로 설정되어 있는지 확인하는 함수인지, "username"을 "unclebob"으로 설정하는 코드인지 정확히 이해하기 어렵다. set이라는 단어가 동사인지 형용사인지 알기 어렵기 때문이다. 

개발자는 set을 동사로 의도했지만, if문 안에서 사용되었기 때문에 형용사로 느껴진다. 

이런 모호함을 없애기 위해 명령과 조회는 아래와 같이 분리해야 한다. 

if (attributeExists("username")) {
    setAttribute("username", "unclebob");
    ...
}

 

3. 부수 효과를 일으키지 마라

 

부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하곤 남몰래 다른 행동도 하는 것이다. 

예를 들어, 예상치 못하게 클래스 변수를 수정하거나, 함수로 넘어온 인수나 전역 변수를 수정한다. 사용자 이름과 비밀번호를 확인하는 아래의 함수를 살펴보자. 

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 phase = cryptographer.decrypt(codedPhrase, password);
			if("Valid Password".equals(phrase)){
				Session.initialize();
				return true;
			}
		}
		return false;
	}
}

 

두 인수가 올바르면 true, 아니면 false를 반환한다. 하지만, 여기서 함수는 Session.initialize() 호출이라는 부수 효과를 일으킨다. 

checkPassword 함수는 이름 그대로 암호를 확인하지만, 이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다. 따라서 함수 이름만 보고 함수를 호출하는 사용자는 사용자 인증을 하면서 기존 세션 정보를 지워버릴 위험에 처한다.

그러므로, checkPassword 함수는 세션을 초기화해도 괜찮은 특정 상황에서만 호출이 가능하게 된다. 이러한 경우에는 반드시 함수 이름에 명시를 해두어야 한다. 예를 들면 checkPasswordAndInitializeSession 처럼이다. 

 

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

 

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if문에서 명령을 표현식으로 사용하기 쉽기 때문이다. 

if (deletePage(page) == E_OK)

 

위 코드는 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다. 대신 try/catch 같은 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다. 

 

  2. 함수의 형태

 

1. 서술적인 이름을 사용하라

 

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행하는 코드가 좋은 코드이다.

이름이 길어도 좋다. 길고 서술적인 이름이 짧고 어려운 이름보다, 길고 서술적인 주석보다 좋다. 함수 이름을 정할 땐 여러 단어가 쉽게 읽히는 명명법을 사용하고, 그다음 여러 단어를 이용해 함수 기능을 잘 표현하는 이름을 선택한다. 

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

 

2. 함수 인수

 

함수에서 이상적인 인수 개수는 0개이다. 인수의 개수가 적을수록 좋으며, 3개 이상의 인수는 가능한 피하는 것이 좋다. 테스트 관점에서 보면 인수가 많을수록 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 어려워진다. 

 

1) 단항 함수

함수에 인수 1개를 넘기는 가장 흔한 경우는 다음과 같다. 

- 인수에 질문을 던지는 경우

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

- 이벤트 함수인 경우

 

이외의 경우라면 단항 함수는 가급적 피하는 것이 좋다. 예를 들어, 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다. 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다. 

 

2) 이항 함수

인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다. 하지만 Point p = new Point(0, 0)과 같이 이항 함수가 적절한 경우도 있다. 하지만, 여기서 인수 2개는 한 값을 표현하는 두 요소이다. 

 

두 인수가 같은지를 판단하는 assertEquals(expected, actual)이라는 이항 함수도 문제가 있다. expected 인수에 actual 값을 넣는 실수를 범할 가능성이 있기 때문에 인수의 순서를 인위적으로 기억해야 한다. 

이처럼 이항 함수는 위험이 따른다는 사실을 이해하고 가능한 단항 함수로 바꾸도록 애써야 한다.

 

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를 들어, write(name)은 누구나 바로 이해한다. 좀 더 나은 이름은 writeField(name)이다. 그러면 이름이 필드라는 사실이 분명히 드러난다. 

 

마지막으로는 함수 이름에 키워드를 추가하는 형식이다. 즉, 함수 이름에 인수 이름을 넣어 인수 순서를 기억할 필요가 없도록 한다. 예를 들어 assertEquals 보다 assertExpectedEqualsActual이 더 좋다. 

 

3. 구조적 프로그래밍

 

Dijkstra는 모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다고 말했다. 즉, return 문이 하나여야 한다. 루프 안에서 break나 continue를 사용해서는 안되며 goto는 절대로 사용하면 안 된다. 하지만, 함수가 작다면 이 규칙은 별 이익을 제공하지 못한다. 함수가 아주 클 때만 상당한 이익을 제공한다. 

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

 

 

출처) Clean Code (Robert C. Martin)

 

반응형