오늘은 어떻게 하면 함수를 깨끗이 짤 수 있을지에 대해서 알아보도록 하겠습니다. 하나의 함수는 어느정도의 길이로 짜야하는지, 함수를 짤 때 주의해야할 것들은 무엇이 있는지 고민을 많이 해보시는게 좋습니다.

1. 작게 만들어라!

함수는 얼마나 짧아야할까요? 길어도 20~30줄을 넘기지 않으려고 하고있습니다. if/else/while문 등에 들어가는 블록은 한줄이어야 합니다. 그러면 바깥을 감싸는 함수가 작아질 뿐만 아니라 블록 안에서 호출하는 함수 이름을 적절하게 짓는다면 코드를 이해하기 쉬워집니다. 이 말은 중첩구조가 생길만큼 함수가 커져서는 안된다는 뜻입니다.그러므로 함수에서 들여쓰는 수준은 1단이나 2단을 넘어서면 안됩니다.

2. 한가지만 해라!

함수는 한가지만 해야합니다. 그 한가지를 잘해야 하고 그 한가지만 해야합니다. 함수가 한가지만 하는지 판단하는 방법으로는 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈입니다.

3. 함수당 추상화 수준은 한가지로!

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

왼쪽의 {A B C} 기능을 하는 큰 개념을 분해하여 A,B,C기능을 만들었습니다. 그리고 나눠진 기능을 단계 별로 수행하는 {} 만들어 연결해 주었습니다. 결국 A,B,C가 하나의 추상화 수준이 되는 것입니다.

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

코드는 위에서 아래로 내려가면서 읽어야 읽기가 편합니다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 옵니다. 즉, 위에서 아래로 내려가면서 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아집니다.

5. switch문

switch문은 작게 만들기 어렵습니다. 또한 '한 가지' 작업만 하는 switch문도 만들기 어렵습니다. 본질적으로 switch문은 N가지처리를 하기 때문입니다. 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 InvalidaEmployeeType(e.type);
    }

}

위 함수에는 몇가지 문제가 있습니다. 첫번째로 함수가 길고, 두번째로 한가지 작업만 수행하지 않습니다. 코드를 변경할 이유가 여럿이기 때문에 단일책임원칙을 벗어납니다. 새 직원을 추가할때마다 코드를 변경해야하기 때문에 개방폐쇄원칙도 위반합니다. 가장 심각한 문제는 위 함수와 동일한 함수가 무한정 존재한다는 사실입니다. 

isPayDay(Employee e, Date date);
deliverPay(Employee e, Money pay);

다음은 위의 코드의 문제점을 해결한 코드입니다. switch문을 추상팩토리에 꽁꽁 숨깁니다.

public abstract class Employee{
	public abstract boolean isPayDay();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money money);
}
------------------------------------------------------------------------------
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 SalariedEmployee(r);
            default: 
            	throw new InvalidEmployeeType(r.type);
        }
    }
    
}

팩토리는 switch문을 사용해 적절한 Employy 파생 클래스의 인스턴스를 생성합니다. calculatePay, isPayDay, deliverPay등과 같은 함수는 Employee 인터페이스를 거쳐 호출됩니다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행됩니다. 이렇게 switch문을 상속 관계로 숨긴 후 다른 코드에 노출하지 않게합니다. 물론 불가피한 상황도 생깁니다.

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

함수가 작을수록 서술적인 이름을 선택하기 쉬워집니다. 이름이 길어도 괜찮습니다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋습니다. 함수 이름을 정할때는 여러 단어가 쉽게 읽히는 명명법을 사용합니다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘표현하는 이름을 선택합니다. 또한 모듈내에서 이름을 붙일때는 일관성이 있어야합니다. 같은 문구, 같은 명사, 동사를 사용합니다.

7. 함수 인수

  • 이상적인 인수의 개수는 0개, 다음은 1개, 다음은 2개입니다. 삼항 이상은 가능한 피하는 편이 좋습니다.
  • 함수로 부울 값을 넘기는 관례는 정말로 끔찍합니다. 왜냐하면 함수가 한꺼번에 여러가지를 처리한다고 대놓고 공표하는 셈입니다. 플래그가 참이면 이걸하고 거짓이면 저걸 한다는 뜻이기 때문입니다.
  • 이함 함수는 인수가 1개인 함수보다 이해하기 어렵습니다. 하지만 이함 함수가 무조건 나쁘다는 소리는 아니고 프로그램을 짜다보면 불가피한 경우도 생깁니다. 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단함함수로 바꾸도록 애써야 합니다.
  • 삼항함수는 인수가 2개인 함수보다 이해하기 훨씬 어렵습니다. 예를들어 assertEquals(message, expected, actual)이라는 함수를 보고 주춤하게 됩니다. 순서, 주춤, 무시로 야기되는 문제가 2배 이상 늘어납니다.
  • 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 가능성을 살펴봅니다. 예를들어 Circle makeCircle(double x, double y, double radius); 와 Circle makeCircle(Point center, double radius); 가 있을때 x와y를 묶어서 표현하는 것은 변수에 이름을 붙이기 때문에 개념을 표현하게 됩니다.
  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름은 필수입니다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야합니다. 그리고 함수 이름에 인수 이름을 넣는 방식이 있습니다. 예를들어, assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋습니다. 그러면 인수 순서를 기억할 필요가 없습니다.

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

함수에서 한가지를 하겠다고 약속하고 남몰래 다른짓을 하면 안됩니다. 예상치 못하게 클래스 변수를 수정하거나, 넘어온 인수나 시스템 전역 변수를 수정합니다. 많은 경우 시간적인 결합이나 순서 종속성을 초래합니다. 예를들어 유저 아이디와 비밀번호를 확인하는 함수에서 세션을 초기화해버리는 기능을 넣는게 이에 해당합니다.

9. 출력 인수는 피하라

일반적으로 우리는 인수를 함수 입력이라고 해석합니다. 인수를 출력으로 사용하는 함수에 어색함을 느끼게 됩니다. 함수 선언부를 찾아보고 나서야 알 수 있습니다. 인지적으로 거슬리므로 피해야합니다.

appendFooter(s); 
public void appendFooter(StringBuffer report)

appendFooter는 다음과 같이 호출하는게 좋습니다.

 report.appendFooter(); 

일반 적으로 출력 인수는 피해야합니다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택합니다.

 

9. 명령과 조회를 분리하라!

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘중 하나만 해야합니다. 

public boolean set(String attribute, String value);
if(set("username", "unclebob"))...

이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정 후 성공하면 true, 실패하면 false를 반환합니다. 그래서 다음과 같이 괴상한 코드가 나타납니다.

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

username이 "unclebob"으로 설정되어 있는지 확인하는 코드인지 아니면 username을 "unclebob"으로 설정하는 코드인지 코드만 봐서는 모호합니다. "set"이라는 단어가 동사인지 형용사인지 분간하기 어려운 탓입니다. 이문제의 해결책은 명령과 조회를 분리해서 혼란을 뿌리뽑는 것입니다.

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

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

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

if(deletePage(page) == E_OK)

위 콛는 여러 단계로 중첩되는 코드를 야기합니다. 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야한다는 문제에 부딪힙니다.  반면 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해집니다.

try{
	deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e){
	logger.log(e.getMessage());
}

11. try-catch 블록도 한가지의 일이다.

try catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞습니다. 별도 함수로 뽑아내는 편이 좋습니다. 오류 처리도 한가지 작업 입니다.

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

이렇게 정상 처리동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워집니다.

12.반복하지 말아라!

같은 코드를 복사해서 한번이라도 붙여넣는다면 이 코드는 함수로 만들어서 재사용이 가능하게 해야합니다.

13. 구조적 프로그래밍

구조적 프로그래밍 원칙은 entry와 출구가 하나만 존재한다. 루프 안에서 break나 continue를 사용해서는 안되며 goto 는 절대로 안된다. 구조적 프로그래밍 목표와 규율은 공감하지만 함수가 작다면 위 규칙은 별이익을 제공하지 못합니다. 함수가 아주 클 때만 상당한 이익을 제공합니다. 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮습니다.

 

 

위의 방법들을 활용한다면 하나의 함수가 수백줄이 되고, 여러가지 일을 하는 코딩 스타일에서 여러개의 함수로 쪼개고, 코드를 읽는 사람, 관리하는 사람 모두 유지보수를 하는데 큰 도움이 될 것입니다. 이러한 좋은 습관들을 초기부터 키워나가는게 중요합니다. 대가프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여깁니다. 

REFERENCE

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기