개발놀이터

리팩토링 (2) : 함수 본문

리팩토링

리팩토링 (2) : 함수

마늘냄새폴폴 2023. 7. 22. 14:39

이번 포스팅은 함수입니다. 우리는 개발할 때 수십개 수백개도 더 함수를 만들어냅니다. 정작 함수가 어떻게 읽기 좋게 쓰여지는지는 별로 고민하지 않는 제 모습을 봤습니다. 

 

여기서 로버트 C.마틴의 말을 인용하면서 시작해보겠습니다. 

 

"함수를 만드는 첫번째 방법은 작게 만드는 것이다. 그리고 두번째 방법은 더 작게 만드는 것이다. 함수는 100줄을 넘어서는 안된다. 아니! 20줄도 넘어서는 안된다!" -로버트 C.마틴

 

함수

한 가지만 해라

함수 하나가 버퍼도 처리하고, 페이지도 가져오고, 상속된 페이지를 검색하고, 경로를 렌더링하고, 문자열을 덧붙이고, HTML 파일을 생성하는 만능 함수를 한번쯤 만들어보셨을겁니다. 

 

이에 로버트 C.마틴은 "함수는 한 가지를 해야하고, 그 한가지를 잘해야하며, 그 한가지만을 해야한다." 라고 말합니다. 

 

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

.getHtml()

이 코드는 추상화 수준이 굉장히 높습니다. 

 

그럼 이 코드를 볼까요?

String pagePathName = PathParser.render(page.path);

이 코드는 추상화 수준이 중간입니다. 

 

그럼 이 코드는요?

.append("\n");

추상화 수준이 매우 낮습니다. 

 

저는 어떤 느낌인지 바로 왔는데 어떤 느낌인지 곧바로 감이 안잡히실 수도 있습니다. 즉, 간단하게 말하자면 추상화 수준을 높일 거면 함수만 존재해야 하고, 추상화 수준을 중간으로 유지하고 싶으면 다른 추상화 수준을 도입하면 안된다는 말이죠. 

 

추상화 수준이 일정하지 않으면 읽기 어렵기 때문에 우리는 추상화 수준을 맞춰야할겁니다. 

 

Switch 문

Switch문은 애초에 한 가지 일만 할 수 없습니다 (if - else 문도 마찬가지) 이런 경우 추상 클래스로 감싸 추상화를 진행해야합니다. 

    public Money calculatePay(Employee e) throws InvalidEmployeeType {
        switch (e.type) {
            case COMMISSTION -> {
                return calculateCommisstionPay(e);
            }
            case HOURLY -> {
                return calculateHourlyPay(e);
            }    
            case SALARIED -> {
                return calculateSalariedPay(e);
            }
            default -> {
                throw new InvalidEmployeeType(e.type);
            }
        }
    }

 

이 코드를 추상 클래스를 이용해 리팩토링 하는 방법은 아래의 링크에서 자세하게 확인하실 수 있습니다! 너무 길어질 것을 우려해 다른 포스팅으로 따로 뺐습니다. 

 

https://coding-review.tistory.com/448

 

리팩토링 (2-1) : switch 문 인터페이스, 추상클래스로 리팩토링

이번 포스팅에선 책 클린코드에서 나온 예제인 switch문을 인터페이스, 추상클래스로 리팩토링 하는 방법에 대해서 포스팅해보도록 하겠습니다. 사실 책에서는 두루뭉실하게 얘기하기 때문에 조

coding-review.tistory.com

 

서술적인 이름을 사용하라

서술적인 이름은 함수가 하는 일을 조금 더 명확하게 표현할 수 있습니다. 

 

예를 들어서 isTestable() 혹은 includeSetupAndTeardownPages() 등 서술적인 이름을 작성하는 것이 좋습니다. 

 

"이름이 좀 길어도 괜찮다. 겁먹을 필요 없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다." -로버트 C.마틴

 

함수 인수 (Argument / 인자)

함수에서 이상적인 인자의 개수는 0개입니다. 다음은 1개, 다음은 2개, 3개는 가능하다면 작성하지 않는 것이 좋고 4개는 사용하지 않아야합니다. 

 

단항함수

단항함수를 사용하는 경우는 두 가지로 나눠서 볼 수 있습니다. 

 

  1. 인수에게 질문하는 경우 boolean fileExists("Myfile");
  2. 인수를 뭔가로 변환하여 결과를 반환하는 경우 InputStreamfileOpen("Myfile");

이 두가지 경우를 제외하면 단항함수를 사용해서는 안됩니다. 

 

그리고 단항함수 중 플래그 함수라고 불리는 것은 절대절대 사용해서 안된다고 강조합니다. 

 

플래그 함수가 뭐냐면 그냥 render(true) 이렇게 boolean값을 넘기는 것을 말합니다. 

 

읽기가 정말 까다로워지고 안에 들어가보지 않으면 어떤 값을 참으로 만드는지 알 수 없습니다. 

 

이항함수

인자가 2개인 함수는 1개인 함수보다 더 이해하기 어렵습니다. 

writeField(name) VS writeField(outputStream, name);

이 두가지는 딱 봐도 가독성에서 차이가 납니다. 첫 번째 인자값인 outputStream은 내부 로직에서 사용하는 것이라고 알 수 있습니다. 

 

이런 경우에는 차라리 OutputStream을 상속하여 outputStream.writeField(name); 이렇게 바꾸는 것이 더 좋은 방법입니다. 

 

하지만 이항함수가 더 잘어울리는 경우도 있습니다. 바로 좌표를 나타낼 때입니다. 

Point p = new Point(1, 2);

좌표는 두개인 것이 조금 더 자연스럽기 때문에 이런 경우 이항함수를 사용하는 것이 좋겠습니다. 

 

이항함수가 이해하기 어렵다는 것은 이항함수의 대표주자인 JUnit 프레임워크의 assertEquals가 대표적인 예가 있습니다. 

assertEquals(expect, actual);

assertEquals같은 경우는 첫 번째 인자가 기대하는 값이고 두 번째 인자가 실제 값이라는 사실을 알고 있어야 (인지하고 있어야) 헷갈리지 않고 사용할 수 있습니다. 

 

물론 모든 개발자가 assertEquals는 알지만 가끔 저도 멈칫멈칫할 때가 있었습니다. 이런 코드는 클린코드가 아니라는 저자의 말이겠죠? 

 

삼항함수

삼항함수는 더더욱이 이해하기 어렵습니다. 되도록이면 쓰지 말자는 것이 저자의 말입니다. 

 

만약 인자값이 2개 3개씩 늘어난다면 하나로 합치는 것도 고려해볼만 합니다. 

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

이렇게 세개인 인자를 아래의 코드로 리팩토링할 수 있습니다. 

Circle makeCircle(Point center, double radius);

이런 방식으로 리팩토링 하는 것이 아마 가독성에서는 더 뛰어날 것입니다. 물론 위의 코드가 쉬운 코드이긴 하지만요. 

 

명령과 조회를 분리해라

객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나만 하는 것이 좋습니다. 이는 맨 처음 소개해드렸던 한 가지만 해라 라는 말과 일맥상통할 수 있겠습니다. 

 

예제를 한번 보시죠. 

 

public boolean set(String attribute, String value);

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

set이라는 함수를 사용하면서 boolean값을 반환합니다. 그리고 if 문에 집어넣었더니 뜻이 모호해졌습니다. 

 

  1. username이 bob으로 변경되어 있는지 확인하는 코드인가?
  2. username을 bob으로 변경하는 코드인가?

 

if 문 때문에 우리는 "username이 bob으로 설정되어 있다면..." 으로 읽힙니다. 

 

때문에 우리는 아래와 같은 방식으로 리팩토링 해야 합니다. 

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

 

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

    if (deletePage(page) == E_OK) {
        if (registry.deleteReference(page.name) == E_OK) {
            if (configKeys.deleteDey(page.name.makeKey()) == E_OK) {
                logger.log("page deleted")();
            }
            else {
                logger.log("config key is not deleted");
            }
        }
        else {
            logger.log("deleteReference from registry failed");
        }
    }
    else {
        logger.log("delete failed");
    }

먼저 예제 코드를 보여드렸습니다. if 문에 오류 여부를 확인하기 때문에 굉장히 읽기 힘들어진 모습입니다. 

 

우리는 try catch문으로 예외처리를 통해 리팩토링할 수 있습니다. 

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

하지만 저자는 여기서 멈추지않고 try catch문이라면 반드시 한줄로 요약하는 것이 좋다고 설명합니다. 바로 이렇게요!

    try {
        deletePageAndAllReferences(page);
    }
    catch (Exception e) {
        logError(e);
    }
    
    private void deletePageAndAllReferences(Page page) throws Exception {
        deletePage(page);
        registry.deleteReference(page.name);
        configKey.deleteKey(page.name.makeKey());
    }
    
    private void logError(Exception e) {
        logger.log(e.getMessage());
    }

 

마치며

이번엔 함수에 대해 알아봤습니다. 제 프로젝트에서 고쳐야할 부분이 정말 많겠네요 ㅎㅎ... 저자의 Switch 문을 추상 클래스로 리팩토링 하는 부분에서 직접 실습이 필요하다고 생각되어 실습을 마쳤습니다! 

 

객체지향 언어에서 다형성을 어떻게 사용하는지 대충 감을 잡았습니다. 좋은 공부가 된 것 같아서 정말 좋네요. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 보내세요~