함수를 만드는 방법

Jarban·2022년 3월 14일
0

클린코드 리뷰

목록 보기
2/2

함수를 만들 때 항상 하는 생각은 함수의 규모를 어떻게 비슷하게 설정할 수 있을지? 였다.
이번 챕터를 읽었으니 함수를 원자화 시키고 SOLID의 SRP처럼 함수도 하나의 추상화를 설정할 수 있도록 실천해봐야겠다.

"작게 만들어라!"

함수를 만드는 첫째 규칙은 '작게', 두번 째 규칙은 '더 작게'다.
사실 함수를 작게 만드는 것이 더 좋다는 증거나 자료를 제시하기는 어렵다.
하지만 저자의 경험을 토대로 비춰볼 때, 저자는 작은 함수가 좋다고 확신했다. 저자는 함수가 한화면서 가로 150자 세로 100줄을 넘겨서, 아니 20줄도 넘겨선 안된다고 한다.

다시 말해, if 문/ else문/while 문 등에 들어가는 블록은 한 줄 이어야 한다는 의미이다. 
그러면 바깥을 감싸는 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 이름을 적절히 짓는다면, 코드를 이해하기도 쉽다.
(이 말은, 중첩구조가 생길만큼 함수가 커져서는 안 된다는 뜻이다.)

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

함수를 만드는 이유는 큰 개념을 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다. 따라서 함수는 한 가지를 해야한다.
함수가 한 가지만 하는지에 대해 알아보기 위해서는 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있는지 알아보는 것이다.

예시

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

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

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
한 함수 내 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부 사항인지 구분하기 어렵다. 하지만 문제는 이가 깨진 창문처럼 사람들이 함수에 세부사항을 한 개씩 덧붙인다는 것이다.

"내려가기 규칙" : 코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
즉, 위에서 아래로 내려갈 수록 추상화 단계가 낮아진다는 것이다.

좋은 예시

public class SetupTeardownIncluder {
 private PageData pageData;
 private boolean isSuite;
 private WikiPage testPage;
 private StringBuffer newPageContent;
 private PageCrawler pageCrawler;
 public static String render(PageData pageData) throws Exception {
 	return render(pageData, false); 
 }
 public static String render(PageData pageData, boolean isSuite) throws Exception {
 	return new SetupTeardownIncluder(pageData).render(isSuite); 
 }
 private SetupTeardownIncluder(PageData pageData) {
   this.pageData = pageData;
   testPage = pageData.getWikiPage(); 
   pageCrawler = testPage.getPageCrawler();
   newPageContent = new StringBuffer();
 }
 private String render(boolean isSuite) throws Exception {
   this.isSuite = isSuite;
   if (isTestPage())
    includeSetupAndTeardownPages();
   return pageData.getHtml(); 
 }
 private boolean isTestPage() throws Exception {
 	return pageData.hasAttribute("Test");
 }
 private void includeSetupAndTeardownPages() throws Exception {
   includeSetupPages();
   includePageContent();
   includeTeardownPages();
   updatePageContent();
 }
 private void includeSuiteSetupPage() throws Exception {
 	include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
 }
 private void includeSetupPage() throws Exception {
 	include("Setup", "-setup");
 }
 private void includePageContent() throws Exception {
 	newPageContent.append(pageData.getContent());
 }
 private void includeTeardownPages() throws Excepton {
 	includeTeardownPage();
 	if (isSuite)
  		includeSuiteTeardownPage();
 }
 private void includeTeardownPage() throws Exception {
  	include("TearDown", "-teardown");
 }
 private void includeSuiteTeardownPage() throws Exception {
  	include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
 }
 private void updatePageContent() throws Exception {
   	pageData.setContent(newPageContent.toString());
 }
 private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if ( inheritedPage != null ) {
    String pagePathName = getPathNameForPage(inheritedPage);
    buildIncludeDirective(pagePathName, arg);
    }
 }
 private WikiPage findInheritedPage(String pageName) throws Exception {
  	return PageCrawlerImpl.getInheritedPage(pageName, testPage);
 }
 private String getPathNameForPage(WikiPage page) throws Exception {
   WikiPagePath pagePath = pageCrawler.getFullPath(page);
   return PathParser.render(pagePath);
 }
 private void buildIncludeDirective(String pagePathName, String arg) {
   newPageContent
   .append("\n!include ")
   .append(arg)
   .append(" .")
   .append(pagePathName)
   .append("\n");
  }
}

"Switch 문"

Switch 문은 작게 만들기 어렵다?

case 분기가 단 두개인 switch 문도 내 취향에는 너무 길며, 단일 블록이나 함수를 선호한다.
한 가지만 하는 switch문을 만들기도 매우 어렵다. 본질적으로 switch 문은 N가지를 처리한다. 

이는 다형성을 이용하여 저차원 클래스에 숨기는 방법이 있다.

public abstract class Employee {
    public static boolean isPayday();

    public static Money calculatePay();

    public static 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 SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

사용예시

@Autowired
EmployeeFactory employeeFactory;

public Money calculatePay(Employee e) throws InvalidEmployeeType {
    Employee employee = employeeFactory.makeEmployee(COMMISSIONED);
    return employee.calculatePay();
}

저자는 switch문을 단 한번 다형적 객체를 생성하는 코드 안에서만 참는다. 이렇게 상속 관계로 숨긴 후에는 절대로 다른 코드에 노출하지도 않는다. 물론 불가피한 상황도 있다.

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

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
한 가지만 하는 작은 함수에 좋은 이름을 붙인다면 이런 원칙을 달성함에 있어 이미 절반은 성공했다.
이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 이름을 정하느라 시간을 들여도 괜찮다.

includeTeardownPages, includeSuiteTearDownPage, includeTeardownPage가 그 예시이다.

"함수 인수"

함수에서 이상적인 인수 개수는 0개다. 다음은 1개, 2개이다. 3개는 피하는 편이 좋다. 4개 이상은 특별한 이유가 필요하다.

1. 많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우
    boolean fileExists(“MyFile”);
  • 인수를 뭔가로 변환해 결과를 변환하는 경우
    InputStream fileOpen(“MyFile”);
  • 이벤트 함수일 경우 (이 경우에는 이벤트라는 사실이 코드에 명확하게 드러나야 한다.)

위의 3가지가 아니라면 단항 함수는 가급적 피하는 것이 좋다.

2. 플래그 인수 

플래그 인수는 추하다. 함수로 부울 값을 넘기는 관레는 정말 끔찍하다. 함수가 여러 일을 처리한다고 대놓고 공표하는 셈이니 말이다. 플래그가 참이면 이걸하고 거짓이면 저걸 한다는 말이니까!

private String render(boolean isSuite) throws Exception {

	this.isSuite = isSuite;
    	if (isTestPage())
    	includeSetupAndTearDownPages();
    	return pageData.getHtml();
}

위 예제도 결국 끔찍한 함수이다. render(true)로는 별 의미를 찾을 수 없기 때문이다.
원래는 renderForSuite()와 renderForsSingleTest()라는 함수로 나눠야 마땅하다.

3. 이항 함수

인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다.
예를 들어 writeField(name)는 writeField(outputStream,name)보다 이해하기 쉽다. 둘다 의미는 명백하지만 전자가 더 쉽게 읽히고 더 빨리 이해된다.
물론 이항 함수가 적절한 예시도 있다. Point p=new Point(0,0)
Point에는 자연적인 순서가 있지만 writeFiled에는 자연적인 순서가 없어서 이해하기 더 어렵다.
오히려 바꿔 outputStream 클래스 구성원으로 만들어 outputStream.writeFiled(name)으로 호출한다.

4. 삼항 함수

신중히 고려하는 것을 권고한다.
예를 들어 assertEqauls(message,expected,actual)을 보면 첫 인자를 Expected로 예상하지 않은가? 이 함수를 볼떄마다 잠시 주춤한다. 반면 assertEquals(1.0,amout,.001)은 위 보다 괜찮다. 부동소수점 비교가 상대적이라는 사실은 언제든 주지할 중요한 사항이기 때문이다.

5. 인수 객체

인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어 본다.

예시

Circle makeCircle(double x,double y,double radius);
Circle makeCircle(Point center,double radius);

6. 인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메서드가 좋은 예시이다.
String.format의 인수는 List형 인수이기 때문에 이항함수라고 할 수 있다. 하지만 삼항을 넘어서는 인수는 문제가 있다.
7. 동사와 키워드
함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항함수는 인수와 동사/명사 쌍을 이뤄야한다. 예를들어 write(name)은 누구나 곧바로 이해한다. 좀더 나아지자면 writeField(name)이다.
마지막 예제는 함수 이름에 키워드를 추가하는 형식이다. 즉, 함수 이름에 인수 이름을 넣는다. 
예를 들어 assertEquals보다 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; 
	}
}

위 코드는 Session.initialize()에서 부수효과를 일으킨다.
checkPassword함수는 이름 그대로 암호를 확인한다. 이름만 봐서는 세션을 초기화 하는것이 드러나지 않는다.
그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다.
이런 부수적인 효과가 시간적인 결합을 초래한다. 즉, checkPassword 함수는 특정 상황에서만 호출이 가능하다.(세션 초기화가 상관없는 작업에서만) 차라리 함수 이름이 checkPasswrodAndInitializeSession이 훨씬 좋다.

1. 출력 인수

출력 인수는 사용할 필요가 없다. 출력 인수로 사용하라고 나온 것이 this 키워드이기 때문이다. 따라서 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

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

함수는 뭔갈 수행하거나 하거나 둘중 하나만 해야한다. 둘다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘중 하나다. 둘다 하면 혼란을 초래한다. 예를 들어 다음 함수를 살펴보자.

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

위는 무슨뜻일까? username을 unclebob으로 설정되있는지 확인하는것인가 설정하라는것인가?
물론 이름을 setAndCheckIfExists라고 바꿀 수도 있겠지만 여전히 어색하다

해결책은 명령조회를 분리해 혼란을 애초에 뿌리잡는 것이다.

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

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

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

따라 오류 코드 대신 예외를 사용하면 코드가 깔끔해진다.

public void delete(Page page){
	try{
    	deletePageAndAllReference(page);
    }
    catch(Exception e){
		logError(e);
    }
}

private void deletePageAndAllReference(Page page) throws Exception{
	deletePage(page);
    registy.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}


private void logError(Exception e){
	logger.log(e.getMessage());
}
profile
안녕하세요 자르반입니다.

0개의 댓글