본문 바로가기
Study/클린코드

[클린코드] 3장. 함수

by Nahwasa 2022. 12. 20.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 클린 코드(로버트 C. 마틴 저) 책 입니다.

     


     

    3장 함수

    - 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수를 만드는 방법

     

    작게 만들어라!

    작은 함수가 좋다. 

     

    이하와 같은 수준으로 줄여야 한다.

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

     

     if 문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다.

     

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

     


    한 가지만 해라!

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

     

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

    public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) {
    	if (isTestPage(pageData)) {
        	includeSetupAndTeardownPages(pageData, isSuite);
        }
        return pageData.getHtml();
    }
    1. 페이지가 테스트 페이지인지 판단한다.
    2. 그렇다면 설정 페이지와 해제 페이지를 넣는다.
    3. 페이지를 HTML로 렌더링한다.
    • 세 단계로 지정된 함수 이름 아래에서 추상화 수준이 하나다. 의미를 유지하면서 위 코드를 더 이상 줄이기란 불가능하다. 따라서 한 가지 작업만 한다고 볼 수 있다.

     


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

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

     

    내려가기 규칙

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

     


    Switch 문

    본직절으로 switch문은 N가지를 처리한다.

     

    저자 : 다형적 객체를 생성하는 코드 안에서 사용할 때만 switch 문을 참아준다.

    • switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법.(다형성 이용)

     


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

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

    • testableHtml (X) -> SetupTeardownIncluder.render (O)
    • isTestable (O)
    • includesetupAndTeardown (O)

     

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

     

    이름을 붙일 때는 일관성이 있어야 한다.

    • includeSetupAndTeardownPages
    • includeSetupPages
    • includeSuiteSetupPage
    • includeSetupPage

     


    함수 인수

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

     

    출력 인수는 입력 인수보다 이해하기 어렵다. -> ☆ 출력 인수는 쓰지 말자.

    • 출력 인수 : 예를들어 return은 void인데, 실은 입력된 인자에 출력을 담는 그런거.

     

    많이 쓰는 단항 형식

    • 인수에 질문을 던지는 경우 -> boolean fileExists("MyFile")
    • 인수를 뭔가로 변환해 결과를 반환하는 경우 -> InputStream fileOpen("MyFile")

     

    플래그 인수는 추하다. 함수를 나누는 것이 마땅하다.

     

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

    • Circle makeCircle(double x, double y, double radius) -> Circle makeCircle(Point center, double radius)

     

    가변인자 (Varagrs)

    • e.g. String.format()
    • 이 경우 varargs는 하나의 인수로 취급할 수 있다.

     

    함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.

     


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

    부수 효과 = side effect

     

    함수만 봐서는 한 가지를 하겠다고 생각되는데, 실제론 내부에서 몰래 다른 짓 하지마라.

    • checkPassword 라고 지어놓고 내부적으로 Session.initialize() 호출하고 있는 경우 -> 함수가 한 가지만 한다는 규칙을 위반하긴 하지만, 함수명을 checkPasswordAndInitializeSession 이라는 이름이 훨씬 좋다.

     


    명령과 조회를 분리하라!

    뭔가를 수행하는 명령이거나, 뭔가에 답하는 쿼리 둘 중 하나만 해야 한다.

     

    ☆ "질문이 답변을 수정해서는 안된다.". 쿼리는 객체의 상태를 변경하지 않기 때문에 순서를 자유롭게 변경할 수 있고, 여러번 호출해도 되게 된다. 명령과 쿼리를 분리함으로써 제한적으로 참조 투명성 혜택을 누리게 된다. (오브젝트 책 중)

     


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

    오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

     

     Try/Catch 블록 뽑아내기

    • try/catch 블록은 정상 동작과 오류 처리 동작을 뒤섞으므로 별도 함수로 뽑아내는 편이 좋다.
    • 함수는 한 가지 작업만 해야 한다. 오류 처리도 한 가지 작업에 속한다.

     

    오류 코드를 반환한다는 이야기는 어디선가 오류 코드를 정의한다는 뜻이다. -> 의존성이 생긴다. (오류 코드를 수정하면 그걸 사용하는 모든 클래스를 다시 컴파일해야 한다.)

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

     


    반복하지 마라!

    중복은 소프트웨어에서 모든 악의 근원이다.

     

     


    구조적 프로그래밍

    구조적 프로그래밍 원칙

    • 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다. (즉, 함수의 return 문은 하나여야 한다.)
    • 루프 안에서 break나 continue를 사용해선 안되며, goto는 절대로 안 된다.

     

    위 내용은 함수가 클 때만 상당한 이익을 제공한다.

    • 함수가 작다면 사용해도 된다.
    • goto는 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야만 한다.
    • ☆ 애초에 함수는 작아야 하므로, goto는 절대 쓰면 안되는게 맞고, return문이나 break, continue는 사용해도 된다. 책 읽으면서 goto는 동의하지만 나머진 완전 공감 안되서 무시할까 했었는데 작은 함수에선 의미가 있다고 했고, 애초에 함수는 작아야 한다고 했으므로  저자의 생각과 내 생각이 같아서 다행이었다. 데이크스트라가 말한건 동의 못하지만 dijkstra 알고리즘은 매우 좋아한다.

     


    함수를 어떻게 짜죠?

    소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.

     


    ☆ 실습 해봤다. 

    리팩토링 대상이 된 코드는 반년 전쯤 짰던 스프링부트 컨트롤러 코드이다. 로직은 다음과 같다.

    1. 요청을 기준으로 로그를 기록한다.
    2. roleChecker를 통해 userInfo(인증 토큰을 통해 획득한 유저 정보)에 그 뒤의 인자들에 해당하는 역할이 있는지 확인한다. 없다면 403 상태를 리턴한다.
    3. Pager를 사용하는 공통 entity를 생성한다.
    4. 리스트를 조회한다.
    5. ResponseDTO에 내용들을 담아 리턴한다. 이 때 logAndGetSame은 응답값에 대한 로그를 쌓고, 자기자신을 다시 리턴하라는 의미로 작성했었다.

    • 문제점 : 함수명이 적절하지 못하다. chkRole 만 봐서는 어떤 역할인지 알기 힘들다. logAndGetSame 역시 마찬가지다. 이건 이 책과는 관계 없지만, 시작 시 로그와 리턴 시 로그는 AOP로 빼주면 더 깔끔하다.

     

    리팩토링 결과이다. 변경 시 적용된 사항은 다음과 같다.

    1. 로그 부분은 AOP로 빼줬다. (책 내용과는 관계없다.)
    2. 역할 확인 함수명을 userInfoHas로 변경해줬다. varargs를 썼으므로 인자는 2개이다.
    3. 역할이 통과하지 못할 경우 기존엔 boolean으로 리턴해주었으나, 예외로 처리해서 catch에서 403을 리턴해주도록 변경했다.
    4. list와 전체 글의 수는 하나로 묶어서 객체로 리턴해주도록 했다.

    • 애초에 서비스쪽으로 빼서 처리하는게 더 타당하긴 했을 것이다. 그건 스프링부트의 영역인거고, 어쨌든 함수로만 보고 리팩토링 해봤다.
    • roleChecker의 경우 사실 DB에 각 API별 권한이 존재했다면 따로 코드로 넣지 않아도 자동으로 처리되게 할 수 있다. 이번엔 리팩토링 대상이 아니었다.
    • try/catch 문을 별도의 함수로 빼는 것도 생각해봤는데 굳이 try/catch만을 위해 함수를 하나 더 빼는게 정말 이해하기 더 좋을지 잘 모르겠다. 즉, 추가 로직 없이 단순히 try/catch가 바로 보이지 않게 하려고 동일한 코드를 빼서 함수로 감싸는게 좋은지 모르겠다.
    • 추상화 수준이 동일한지 확신이 서지 않는다. "요청에서 역할을 확인한다.", "요청에 해당하는 데이터를 얻는다.", "요청에 응답한다." 정도로 볼 수 있으니 맞는 것 같다. 추가로 더 함수를 써서 빼보려 했는데 오히려 지저분하고 더 이해하기 어렵게 느껴졌다.

    댓글