개발노트

Clean Code | 함수 본문

Computer Science/Software Enginerring

Clean Code | 함수

개발자? 2023. 5. 21. 20:40

목차

3장 함수

  • 작게 만들어라!
  • 한 가지만 해라!
  • 함수 당 추상화 수준은 하나로!
  • Switch 문
  • 서술적인 이름을 사용하라!
  • 함수 인수는 최대한 작게!
  • 부수 효과를 일으키지 마라!
  • 명령과 조회를 분리하라!
  • 오류 코드보다 예외를 사용하라!
  • 반복하지 마라!
  • 구조적 프로그래밍
  • 함수를 어떻게 짜죠?

 

어떤 프로그램이든 기본적인 단위가 함수다.

 

작게 만들어라!

함수를 만들 때 최대한 '작게' 만들어라

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception{
        boolen isTestPage = pageData.hasAttribute("Test");
        if(isTestPage){
            WikiPage testPage = pageData.getWikiPage();
            StringBuffer newPageContent = new StringBuffer();
            includeSetupPages(testPage, newPageContent, isSuite);
            newPageContent.append(pageData.getContent());
            includeTeardownPages(testPage, newPageContent, isSuite);
            pageData.setContent(newPageContent.toString());
        }
        return pageData.getHtml();
    }

위 코드도 길다.

되도록 한 함수당 3~5줄 이내로 줄이는 것을 권장한다.

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

블록과 들여쓰기

중첩구조(if/else, while 문 등)에 들어가는 블록은 한 줄이어야 한다. 대개 이 한 줄은 함수를 호출하는데, 그러면 함수가 작아질 뿐만 아니라 블록 안에서 호출하는 함수 이름이 적절하다면 코드 이해도 쉬워진다.

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

 

한 가지만 해라!

다음은 지난 30여년 동안 여러 가지 다양한 표현으로 프로그래머들에게 주어진 충고다.

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

지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.

함수가 '한 가지'만 하는지 판단하는 방법이 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

 

함수 내 섹션

함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러 작업을 하는 셈이다. 아라 generatePrimes 함수는 여러 섹션으로 나눠지므로, 여러 가지 일을 한다라고 볼 수 있다.

public static int[] generatePrimes(int maxValue){
        if(maxValue >= 2){
            // 선언 -> 섹션 1
            int s = maxValue + 1; // 배열 크기
            boolean[] f  = new boolean[s];
            int i;
            
            // 초기화 -> 섹션 2
            for(i=0;i<s;i++)
                f[i] = true;
            
            // 소수 아닌 알려진 숫자를 제거
            f[0] = f[1] = false;
            
            // 체 -> 섹션 3
            int j;
            for(i=2;i<Math.sqrt(s)+1;i++){
                if(f[i]){ // i 가 남아 잇는 숫자라면 이 숫자의 배수를 구한다.
                    for(j=2*1;j<s;j+=i)
                        f[j] = false; // 배수는 소수가 아니다.
                    
                }
            }
            
            // 소수 개수는? -> 섹션 4
            int count = 0;
            for(i=0;i<s;i++){
                if(f[i])
                    count++;
            }

            // 소수를 결과 배열로 이동 -> 섹션 5
            int[] primes = new int[count];
            for(i=0,j=0;i<s;i++){
                if(f[i]) // 소수인 경우
                    primes[j++] = i;
            }
            
            return primes;
        }else{
            // 예외 처리 -> 섹션 6
            return new int[0]; // 입력이 잘못되면  비어 있는 배열을 반환한다.
        }
    }

 

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

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

만약 한 함수 내에 추상화 수준이 섞이게 된다면 읽는 사람이 헷갈린다.

 

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

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.

함수 추상화 부분이 한번에 한 단계씩 낮아지는 것이 가장 이상적이다. → 내려가기 규칙

 

Switch 문

switch 문은 본질적으로 N가지를 처리하기 때문에 switch 문(if/else 의 연속도 동일함)은 작게 만들기 어렵다.

하지만 switch 문을 다형성을 이용하여 abstract factory 에 숨겨 다형적 객체를 생성하는 코드로 사용한다면 반복을 피할 수 있다. 

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

 

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

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
- 워드

작은 함수는 그 기능이 명확하므로 이름을 붙이기가 더 쉬우며, 일관성 있는 서술형 이름을 사용한다면 코드를 순차적으로 이해하기도 쉬워진다.

이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다면(문체가 비슷하다면) 이야기를 순차적으로 풀어가기도 쉬워진다.

 

함수 인수

함수에서 이상적인 인수 개수는 0개이다.

인수는 코드 이해에 방해가 되는 요소이므로 최선은 0개이고, 차선은 1개뿐이다.

출력인수(= 함수의 반환 값이 아닌 입력 인수로 결과를 받는 경우)는 이해하기 어려우므로 왠만하면 쓰지 않는 것이 좋다.

 

많이 쓰는 당항 형식

  • 인수에 질문을 던지는 경우
    boolean fileExists("MyFile");
  • 인수를 뭔가로 변환해 결과를 변환하는 경우
    InputStream fileOpen("MyFile");
  • 이벤트 함수인 경우
    passwordAteemptFailedNtimes(int attempts)

플래그 인수

플래그 인수는 추하다. (추하다는 표현까지 쓰다니😅)

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

 

이항 함수

단항 함수보다 이해하기가 어렵다.

물론 이항함수가 필요한 경우도 있다. Point 클래스의 경우에는 이항 함수가 적절하다. 2개의 인수 간의 자연적인 순서가 있어야 하니깐.

이항 함수가 무조건 나쁜것은 아니지만 인수가 2개이면 이해가 어렵고 위험이 따르므로 가능하면 단항으로 바꾸자.

 

삼항 함수

이항 함수보다 이해하기가 훨씬 어렵다. 위험도 2배 이상 늘어난다. 삼항 함수를 만들 때는 신중히 고려하자.

 

인수 객체

인수가 많이 필요한 경우, 일부 인수를 독자적인 클래스 변수로 선언할 가능성을 살펴보자. x, y 를 인자로 넘기는 것보다 Point 를 넘기는 것이 더 낫다.

 

인수 목록

때로는 String.format 같은 함수들처럼 인수 개수가 가변적인 함수도 필요하다. String.format 의 인수는 List형 인수이기때문에 이항함수라고 할 수 있다.

 

동사와 키워드

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

writeField(name); // name 이 filed 라는 사실이 드러난다.

함수 이름에 키워드를 추가하면 인수 순서를 기럭할 필요가 없어진다.

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

출력 인수

일반적으로 출력 인수는 피해야 한다.

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

public void appendFooter(StringBuffer report)

→ report 에 바닥글을 첨부하는 함수이다

report.appendFooter()

→ report 객체 안에 appendFooter 함수를 추가해서 사용할 것

 

명령과 조회를 분리하다!

함수는 뭔가 객체 상태를 변경하거나, 갹체 정보를 반환하거나 둘 중 하나다.

둘 다 수행해서는 안 된다.

-- bad

public boolean set(Strign attribute, String value)

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

--- good

if(attributeExists("username"){

  setAttribute("username", "unclebob");

}

 

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

try/catch 를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

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

정상 작동과 오류 처리 동작을 뒤섞는 추한 구조이다.

if/else 와 마찬가지로 블록을 별도 함수로 뽑아내는 것이 좋다.

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 문으로 끝나야 한다는 말이다.

 

반복하지 마라!

중복은 모든 소프트웨어에서 모든 악의 근원이므로 늘 중복을 없애도록 노력해야 한다.

중복은 문제다. 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러 곳을 손봐야 한다. 오류 발생할 확률도 높다.

 

구조적 프로그래밍

구조적 프로그래밍 원칙은 함수는 return 문이 하나여야 한다는 말이다. Loop 안에서 break 나 continue 를 사용해선 안 된다는 말이다.

이 원칙은 함수가 클 때만 상당한 이익을 제공한다. 작은 함수에서는 피하자.

 

함수를 어떻게 짜죠?

함수는 처음부터 탁 짜지지 않는다.

처음에는 길고 복잡하며, 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다. 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.

최종적으로 클린한 코드의 함수가 만들어지는 것이다.

 

 

이 내용은 로버트 C.마틴 의 『Clean Code』 를 보고 정리하였습니다.
반응형

'Computer Science > Software Enginerring' 카테고리의 다른 글

Clean Code | 객체와 자료구조  (0) 2023.05.24
Clean Code | 주석  (0) 2023.05.24
TDD (Test Driven Development)  (0) 2023.05.04
SOLID 객체지향 5가지 원칙  (0) 2023.04.24
Refactoring 리팩토링  (0) 2023.04.24
Comments