본문 바로가기
도서

[클린코드 핥아먹기 시리즈] 2. 함수 잘 만드는 법 알려줄까?

by 무자비한 낭만주먹 2024. 1. 3.
목차
0. 개요
1. 개떡같은 코드
2. 작게 쪼개라
3. 함수의 들여쓰기 수준은 1단이나 2단을 넘어서지 마라
4. 한 가지만 해라 (SRP: 단일 책임 원칙)
5. 이상적인 함수의 인수는 0개(무항)이다.
6. 플래그 인수는 추하다
7. 인수 객체
8. 부수 효과는 거짓말을 하는거소가 마찬가지다
9. 일반적으로 출력 인수는 피하는게 좋다.
10. 함수에서 값의 변경과 조회 작업을 꼭 분리해라
11. 반복하지 마라
12. return은 하나만 (구조적 프로그래밍)
13. 마치며

 

 

0. 개요
 

[클린코드 핥아먹기 시리즈] 1. 의미 있는 이름

목차 0. 개요 1. 의도를 분명히 밝혀라 2. 그릇된 정보를 피하라 3. 의미 있게 구분하라 4. 발음하기 쉬운 이름을 사용해라 5. 검색하기 쉬운 이름을 사용해라 6. 너 분명히 기억 못하니까 i, j, k 같은

https-blog-navercom-cksgurwkd12.tistory.com

지난 기록에서는 "의미 있는 이름"을 짓는 법에 대해서 배웠다.
이번 기록에서는 클린 코드 3장 ["함수"]에 대해서 정리하려 한다.
1. 개떡같은 코드

 

public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
    WikiPage wikiPage = pageData.getWikiPage();
    StringBuffer buffer = new StringBuffer();

    if (pageData.hasAttribute("Test")) {
        if (includeSuiteSetup) {
            WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
            if (suiteSetup != null) {
                WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
                String pagePathName = PathParser.render(pagePath);
                buffer.append("!include -setup .").append(pagePathName).append("\n");
            }
        }

        WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
        if (setup != null) {
            WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup);
            String setupPathName = PathParser.render(setupPath);
            buffer.append("!include -setup .").append(setupPathName).append("\n");
        }

        buffer.append(pageData.getContent());

        if (pageData.hasAttribute("Test")) {
            WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
            if (teardown != null) {
                WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
                String tearDownPathName = PathParser.render(tearDownPath);
                buffer.append("\n").append("!include -teardown .").append(tearDownPathName).append("\n");

                if (includeSuiteSetup) {
                    WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                    if (suiteTeardown != null) {
                        WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
                        String pagePathName = PathParser.render(pagePath);
                        buffer.append("!include -teardown .").append(pagePathName).append("\n");
                    }
                }
            }
        }
    }

    pageData.setContent(buffer.toString());
    return pageData.getHtml();
}

 

[그림1]. 한국판 로버트 C. 마틴

저자는 대뜸 위 코드를 보여주며 3분 안에 이해해 보라고 독자를 무시했다. 사실 거지같은 코드라면 우리 회사도 어디가서 꿀리지 않는 레거시의 전통 강호이고 반영 10분 전에도 기능 수정이 발생하는 애자일 끝판왕 환경에서 단련했기에 "3분도 많다. 1분 안에 이해해주마!" 라는 마음으로 도전했다.

 

[그림2]. 실패 ~

호기롭게 도전했지만 당연히 실패했고, 저자가 제안한 개선안을 확인했다.

 

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

 

위 코드처럼 짧은 기능 단위로 메서드를 분리해 읽기 편하게 만들면 좀 더 이해하기 쉬운 코드가 될 거라고 주장했다. 사실 저 코드도 FitNesse(Wiki 및 소프트웨어용 자동화 테스트 도구) 가 익숙하지 않으면 이해하기 어려울 거라고 말하지만, 그래도 대략적인 내용은 파악할 수 있었다.

 

2. 작게 쪼개라

 

[그림3]. 사과를 쪼개는 개발자 호동씨

저자는 코드는 무조건 작게 쪼개라고 주장한다. 이 부분에 있어서 좀 극단적인 면이 있을 정도인데 그 근거가 "겁나 훌륭한 개발자인 캔트백의 코드를 봤을 때 거의 1~2줄 사이였다"여서 그다지 공감이 가지는 않는다. 저자가 제안한 코드의 예시는 아래와 같다.

public static String testableHtml(PageData pageData, boolean isSuite) throws Exception {
    if (isTestPage(pageData))
        includeSetupAndTeardownPages(pageData, iusSuite);

    return pageData.getHtml();
}

 

3. 함수의 들여쓰기 수준은 1단이나 3단을 넘어서면 안된다.

 

fun gotoMarry(choichanhyeok: Choichanhyeok): Unit{
   if (choichanhyeok.get재산() > 5억){
        if (choichanhyeok.get나이() < 30) {
             if(choichanhyeok.isGirlfriendApprove()){
                 choichanhyeok.changeStatus("기혼")
            }
        }
    }
}

 

위 코드는 내가 결혼을 할 수 있는지에 대해 판별하는 간단한 함수이다. 저자가 하지 말라는 3단 들여쓰기를 도입해봤는데 여기서 들여쓰기를 줄이려면 어떻게 해야할까? 메서드로 해당 기능을 분리해보자.

 

fun gotoMarry(choichanhyeok: Choichanhyeok): Unit{
    if (choichanhyeok.canMarry()){ choichanhyeok.changeStatus("기혼") }
}

 

사실 위 코드처럼 메서드로 분리할 필요도 없이 and 연산으로 비교하면 될 거 같긴 하지만 일단 들여쓰기 줄였으니 만족한다. 로버트 씨도 잘했다고 박수를 쳐주지 않을까?

 

4. 한 가지만 해라 (SRP: 단일책임 원칙)

[그림4]. 가지가지 하지 말고 하나만 해라

"의미 있는 다른 이름으로 함수를 추출할 수 있다면 그 함수는 아직도 여러 작업을 하는 셈이다"

 

저자는 메서드는 하나의 책임만 지는게 좋다고 주장한다. 위 gotoMarry 함수는 사실 모든 책임을 다른 메서드에 전가해버려서 무슨 일을 한다고 보기도 애매하긴 한데 어찌됐든 .. 저자는 이런 내용에 대해 "추상화 수준을 하나로!"라고 표현했다. 이 때 "내려가기 규칙"이라는 걸 준수해 읽기 좋은 코드를 만들어야 한다고 강조했는데 내용은 아래와 같다.

(1) 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
(2) 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아져야 한다.

 

5. 이상적인 함수의 인수는 0개(무항)이다.

 

사실 이 내용은 이래저래 말이 많은 내용인 걸로 알고 있지만, 본 기록의 요지는 "독서 기록"이기에 조금 더 열린 자세로 받아들여보려 한다. 저자는 인수는 무조건 적을수록 좋다고 주장하는데, 최소 3항부터 피하는 편이 좋고 4항을 사용하는데는 특별한 이유가 있어야 한다고 주장한다. 테스트 코드 짤 때 좋을 거 같긴 한데 .. 아직 나는 잘 모르겠다.

 

최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.
(로버트 C. 마틴)

 

6. 플래그 인수는 추하다
사실 저자도 별 도리가 없을 땐 플래그 인수를 사용하지만, 플래그 인수를 사용한다는 사실 자체가 SRP를 위배하고 있다거나 추상화 레벨이 1이 아니라는 반증이 된다고 한다.

# 근데 애초에 플래그 인수는 추하다는 주장을 받아들이려면 메소드의 추상화 레벨이 1이어야 좋다는 내용이나, 무조건 SRP를 준수해야 한다는 내용에 대해 동의해야 성립하는 논점이어서 이것도 잘 모르겠다.

# 개인적으로 "플래그 인수는 추하다"라는 강력한 표현보단, "플래그 인수를 사용한다는 건 SRP를 위배하고 있다는 반증이 될 수 있다" 정도로 표현하는게 "조금 더 의도를 잘 전달할 수 있지 않았을까?" 하는 아쉬움이 있었다.

 

7. 인수 객체

사실 저자가 파라미터는 왠만하면 1항 넘기지 말라고 할 때 부터 왠지 이렇게 말할 거 같았는데 내 예상이 맞았다.

"객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만 그렇지 않다. 파라미터를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다."

결국 저자가 주장하는 건 의미있는 컨택스트를 갖는 데이터 클래스로 인수를 묶어 전달하라는 거 같은데, 클래스가 그렇게 많아지는게 과연 모든 경우에 가독성을 높이는 방식인지는 모르겠다. (코드가 클린해지는 건 인정하지만) 어쨌든 뭔 말인지 코드로 보면 ..

 

Circle makeCircle(double x, double y, double z, double radius); // 개선 전
Circle makeCircle(Point center, double radius);                 // 개선 후

 

8. 부수 효과는 거짓말을 하는것과 마찬가지다
부수 효과는 '거짓말'이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른짓도 하니까
- 로버트 C. 할리 -

 

SideEffect 가 발생하는 메서드는 나쁜거라는 걸 저런식으로 표현했다. 이 내용에 대해서 깊이 생각해보려면 함수형 프로그래밍이나 다양한 부분들을 적어야 할 거 같아서 일단 마무리한다.

 

9. 일반적으로 출력 인수는 피하는게 좋다

 

처음으로 저자가 본인의 주장에 "일반적으로"라는 밑밥을 깔 수 있는 표현을 사용했다. 아래 코드를 먼저 보자

 

appendFooter(s);

 

위 코드를 보고 무슨 코드인지 생각을 해보면, "바닥글에 s면 뭐 String을 넣는다는 걸까? s라는걸 바닥글로 첨부를 한다는건지 s라는 객체에 바닥글이라는 걸 첨부한다는건지 헷갈린다" 내 생각엔 전자가 더 자연스럽지만 어쨌든 출력 자체를 저렇게 인자로 넘기는 건 좋지 않다고 한다. 그 이유로 저자는 this가 나온 이유를 근거로 들었다.

 

/* this가 나온 이유는 출력 인수를 사용하기 위해서기에 s라는 걸 출력 하려면 해당 객체의 메서드를 이용하는게 좋다." */

report.appendFooter()

 

사실 무슨 말인지 아직 이해 안간다 (ㅋㅋ)

 

10. 함수에서 값의 변경과 조회 작업을 꼭 분리해라
객체 상태를 '변경하거나' 아니면 객체 정보를 '반환하거나' 둘 중 하나만 해라. 둘 다 하면 안된다.

 

이건 당최 무슨 의도인지 모르겠다. getter, setter 같은 데이터 관리를 위한 클래스에선 그럴 수 있을 거 같은데 일반적으로 가능한 소리인가 싶다.

 

fun updateProfile(updateUserRequest: UpdateUserRequest): Int{
    .
    .
    val updatedUserId = userRepository.update(updateUserRequest)
    return updatedUserId
}

 

위 처럼 데이터 수정 및 조회의 역할을 동시에 수행해야 하는 경우도 있지 않은가? userRepository의 update 메서드는 데이터 수정 작업과 결과를 반환하는 작업을 동시에 수행하는데 이것도 하지 말라는 얘기인건가? 그럼 어떻게 구현해야하지? DB에 또 한번 조회 요청을 보내 확인해야하나?

 

11. 반복하지 마라
하위 루틴을 발명한 이래로 소프트웨어 개발에서 지금까지 일어난 혁신은 '소스 코드에서 중복을 제거'하려는 지속적인 노력으로 보인다.

 

사실 이것도 논란의 여지가 많긴 하다. 최소 3번 ~ 4번 정도 반복되는 코드에 한해서 저자가 제안하는 ["구조적 프로그래밍", "AOP", "COP"] 등의 중복 제거 전략을 활용할 수 있을 거 같은데 (사실 메서드나 클래스 분리 아니면 최소 10번 넘어야 고려가 필요하지 않을까 싶다.) 중복 제거를 위한 시도 전에 이걸 공통로직으로 뺐을 때 발생할 수 있는 향후 유지보수의 어려움도 고민해봐야 할 거 같다.

 

12. return은 하나만 (구조적 프로그래밍)

 

break나 continue를 사용해선 안되며 goto는 '절대!!!' 로 안된다!

 

[그림5]. 라고할뻔 ~

 

사실 위 주장은 그 유명한 "에츠허르 데이크스트라"의 구조적 프로그래밍 원칙이라고 하는데 '클린 코드'의 저자인 로버트 아저씨는 저 원칙의 목표와 규율은 공감하지만 자신이 주장하는 "작은 함수"에 있어서 위 규칙은 별 이익을 제공하지 못한다고 얘기했다. 작은 함수는 return, break, continue를 여러 차례 사용해도 괜찮다고 주장했는데 .. 작은 함수에 return이나 break, continue가 여러개 들어갈 일이 있나?  어쨌든 알아서 입장정리 하시고, 공통된 의견으로 goto문 절대 쓰지 마라 정도는 합의가 된 거 같다.

 

[그림6]. 그 유명한 클린코드와 다익스트라의 숨막히는 자존심 싸움

# 근데 그럼 다익스트라 아저씨는 최단 경로 탐색할 때 재귀함수 절대 안쓰나?

 

13. 마치며
'함수는 동사'며 '클래스는 명사'이다.

대가 프로그래머는 시스템을 "구현해야 하는 프로그램"이 아니라 "풀어갈 이야기"로 여긴다. 이 장에서는 함수를 잘 만드는 기교를 소개했다. 이를 믿고 따른다면 체계 잡힌 좋은 함수가 나오리라 믿는다. 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하라. 여러분이 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어가기 쉬울 것이다.

 

전반적으로 로버트 아저씨는 "프로그래머는 작가다"라는 메타포를 가지고 마치 좋은 책 처럼 "읽기 좋은" 코드를 쓰고 싶어하는 거 같다.

 

오늘 하루도 공부할 수 있어 크게 감사합니다. 2023-12-10

개발자 최찬혁

 

'도서' 카테고리의 다른 글

[클린코드 핥아먹기 시리즈] 1. 의미 있는 이름  (2) 2024.01.03