본문 바로가기
Programming/Clean Code

3장. 함수

by JKROH 2023. 10. 17.
반응형
  • 어떤 프로그램이든 가장 기본적인 단위는 함수다.

작게 만들어라!

  • 함수를 만드는 첫 번째 규칙은 '작게'다. 두 번째 규칙은 '더 작게'다.
  • 각 함수는 하나의 이야기를 명백하게 표현해야한다.

- 블록과 들여쓰기

  • 중첩 구조가 생길만큼 함수가 커져서는 안된다. 그래야 함수는 읽고 이해하기 쉬워진다.

한가지만 해라!

  • 함수는 한 가지를 해야한다. 그 한가지를 잘 해야 한다. 그 한 가지만을 해야한다.
  • 그럼 한 가지는 무엇인가? 여기에서 한 가지는 지정된 함수 이름 아래에서 추상화 수준이 하나인 것을 의미한다.
    • 즉, 함수 이름에서 여러 단계에 나눠진 일을 수행한다고 하더라도, 그 일이 하나의 추상화 수준에 해당한다면 이는 한 가지 일을 하는 것이다.
    • 대신, 큰 덩어리로 이루어진 하나의 일을 더욱 잘게 쪼개 더 작은 추상화 레벨에서 하나씩 수행하는 작은 함수를 만들어주면 된다.
  • 함수가 한 가지 일만을 하는지 판단하는 다른 방법은, 의미 있는 이름으로 다른 함수를 추출할 수 있는지의 여부다.

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

  • 함수가 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야한다.
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려워지기 때문이다.

- 추상화 수준

  • 추상화 수준이라는 말이 처음에 이해가 잘 안됐다. 한 번 '출근 준비를 한다'는 간단한 예시로 살펴보자.
    • 출근 준비를 한다는 동작은 '아침 밥을 먹는다', '씻는다', '옷을 입는다', '필요한 물품을 챙긴다' 의 네 가지 동작으로 구분될 수 있다.
    • '출근 준비를 한다' 와 구분 동작의 차이는 얼마나 구체적으로 표현하는가에 있다.
    • 출근 준비를 한다고 하면, 어떤 일을 하는지 명확하게 알 수 없다. 그러나 동작을 구분하면 구체적으로 어떤 일을 하는 지 알 수 있다. 추상화 레벨이 높고 낮음은 이렇게 나눌 수 있다.
    • 아래에는 해당 과정을 직접 나눠보는 수도 코드를 작성해놓았다. 
더보기
    private void 출근_준비를_한다(){
        while(밥이 남아있다){
            밥을 먹는다;
            반찬을 먹는다;
            국을 먹는다;
        }
        머리를 감는다;
        세수를 한다;
        몸을 닦는다;
        양치를 한다;
        머리를 말린다;
        속옷을 입는다;
        셔츠를 입는다;
        바지를 입는다;
        양말을 신는다;
        while(필요한 물품이 남아있다){
            필요한 물품을 찾는다;
            필요한 물품을 챙긴다;
        }
    }
    
    /////////////////////
    
    private void 출근_준비를_한다(){
        아침밥을_먹는다();
        씻는다();
        옷을_입는다();
        필요한_물품을_챙긴다();
    }

    private void 필요한_물품을_챙긴다() {
        while(필요한 물품이 남아있다){
            필요한 물품을 찾는다;
            필요한 물품을 챙긴다;
        }
    }

    private void 옷을_입는다() {
        속옷을 입는다;
        셔츠를 입는다;
        바지를 입는다;
        양말을 신는다;
    }

    private void 씻는다() {
        머리를 감는다;
        세수를 한다;
        몸을 닦는다;
        양치를 한다;
        머리를 말린다;
    }

    private void 아침밥을_먹는다(){
        while(밥이 남아있다){
            밥을 먹는다;
            반찬을 먹는다;
            국을 먹는다;
        }
    }

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

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이를 내려가기 규칙이라 한다.
    • 위의 예시를 다시 한 번 사용해보자.
    • 출근 준비를 하려면, 밥을 먹고, 씻고, 옷을 입고, 준비물을 챙겨야 한다.
      • 밥을 먹으려면, 남은 밥이 없을 때까지 밥과 반찬, 국을 먹는다.
      • 씻으려면, 머리를 감고, 세수를 하고, 몸을 닦고, 양치를 한 뒤, 머리를 말린다.
      • 옷을 입으려면 ...
  • 추상화 수준이 하나인 함수를 구현하는 것은 매우 중요하지만, 동시에 매우 어렵다.
  • 핵심은 짧으면서도 한 가지만 하는 함수다. 위에서 아래로 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.

Switch 문

  • Switch 문(If - else문)은 작게 만들기 어렵다. 당연하다, 분기가 여러 개로 나뉘는만큼 코드가 길어질 수밖에 없기 때문이다.
    • 본질적으로 Switch 문을 사용하면 n가지 일을 처리한다. 이는 단일 책임의 원칙을 위반한다.
    • 새로운 분기가 생길 때마다 코드를 변경해야한다. 이는 개방 - 폐쇄 원칙을 위반한다.
  • 그러나 Switch 문을 완전히 사용하지 않기는 어렵다. 문제 해결 과정에서 완벽하게 분기를 지울 수는 없기 때문이다.
  • 그러나 왜 Switch 문을 사용하는지를 알면 Switch 문을 public한 API에서 숨길 수 있다
  • Switch 문을 사용하는 이유는 하나다. 비슷한 개념인데 여러 갈래로 나뉘는 경우가 발생하기 때문이다.
  • '비슷한 개념'이라는데서 힌트를 얻을수 있다. 우리는 비슷한 개념을 하나의 큰 개념으로 추상화하고 나머지를 구체화해서 사용할 수 있다.
  • 결국, Switch문에 들어가는 조건 자체를 큰 개념으로 추상화하고, 이를 적절한 구현체로 바꿔주는 팩터리 메서드를 활용하면, Switch문은 팩터리 메서드에 숨기고 public한 API에는 Switch문을 사용하지 않고 해당 구현체에 맞는 결과를 반환할 수 있다.
  • 아래 예시는 이를 바탕으로 과거 프로젝트에서 사용했던 코드를 리팩터링 해본 결과다.
    /**
     * 카테고리를 선택했는지, 태그를 선택했는지 등에 따라 검색 결과가 달라진다.
     */
    public MultiResponseDto readFilteredCocktails(String email, String category, String tag, String sortValue) {
        Sort sort = setSort(sortValue);
        if (isNotSelectCategoryAndTag(category, tag)) {
            List<Cocktail> cocktails = cocktailQueryService.readAllCocktails(sort);
            log.info("# CocktailService#readFilterdCocktails 성공");
            return createCocktailsSimpleMultiResponseDtos(email, cocktails);
        }
        if (isNotSelectCategory(category)) {
            return filterByTagCocktailsSimpleResponse(email, tag, sort);
        }
        if (isNotSelectTag(tag)) {
            return filterByCategoryCocktailsSimpleResponse(email, category, sort);
        }
        return filterByTagsAndCategoryCocktails(email, category, tag, sort);
    }
    
// 위 코드에서는, 선택한 조건에 따라 분기가 달라진다. 위 코드를 아래와 같이 리팩토링 해보면 어떨까.

    public MultiResponseDto readFilteredCocktails(String email, String category, String tag, String sortValue) {
        Sort sort = setSort(sortValue);
        FilteringCondition filteringCondition = FilteringConditionFactory.createFilteringCondition(category, tag);
        return createFilteredCocktailsList(email, filteringCondition, sort);
    }

// 위와 같이 간결하게 public API의 코드가 변경될 수 있으며, 카테고리와 태그의 선택 여부는 내부 로직으로 숨길 수 있다.

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

  • 2장의 내용을 기억하자. 좋은 이름의 가치는 상상 그 이상으로 중요하다.
  • 이름은 좀 길어도 괜찮다. 괜히 짧게 만들어서 기능을 제대로 표현하지 못하는 이름보다는, 좀 길더라도 기능을 제대로 표현할 수 있는 이름이 더 좋다. 대신, 쉽게 읽을 수 있게 만드는게 좋다.
  • 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기도 더 쉬워진다.
  • 이름을 붙일 때는 일관적으로 작성하자. 한 개념에는 하나의 단어를 사용하고, 비슷한 문체를 사용하면 이야기를 순차적으로 풀어가기도 좋다.

함수 인수

  • 인수는 적으면 적을수록 좋다. 넘어가는 인수가 많으면 많을수록 코드를 읽는 사람이 인수의 의미를 해석해야한다.
  • 테스트 코드를 작성한다고 생각하면, 인수가 많은 함수는 더더욱 어려워진다. 인수가 여러 개가 되면, 각 인수의 조합마다 테스트를 해야한다.
  • 출력 인수는 입력 인수보다 더 이해하기 어렵다. 일반적으로 함수는 A라는 인수를 넣어서 B라는 결과가 나온다고 생각한다. A를 넣었는데 A`이 나올 것이라고는 기대하지 않는다.
    • 출력 인수와 관련해 좋은 글이 있어 링크로 남겨놓는다.

- 많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 가장 흔한 경우는 '인수 자체에 질문을 던지거나', '인수를 뭔가로 변환해 결과를 반환하는' 두 가지 경우다.
  • 입력 인수를 변환하는 함수라면 변환 결과는 반환 값으로 돌려주자. 물론, 불가피한 경우가 아니라면 객체의 멤버 메서드로 사용해 변환하는게 좋다
    • boolean isCorrectAnswer(writtenAnswer) : 작성한 답안이 맞는지 질문을 던지고 있다.
    • Developer studyAndBecomeDeveloper(student) : 학생 객체를 개발자 객체로로 변환하여 반환한다.
    • Stringbuffer transform(Stringbuffer in) : 최소한 반환 형태는 지켰다.
    • pokemon.evolve(); 가 evolvePokemon(name); 보다 낫다.
  • 이벤트는 드물게 사용하지만 유용하게 사용할 수 있는 단항 함수 형식이다.
    • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.
    • 이벤트 함수는 이벤트라는 사실이 코드에 명확히 드러나게 조심히 사용해야 한다.
    • 따라서 이름과 문맥을 주의해서 선택하자.

- 플래그 인수

  • 플래그 인수는 정말 가급적 쓰지말자. 함수가 여러 가지 일을 한다고 공표하는 꼴이다.
    • 참이면 A를 하고, 거짓이면 B를 한다의 형태가 되어버린다.

- 이항 함수와 삼항함수

  • 당연히 이항 함수가 단항 함수보다, 삼항 함수가 이항 함수보다 인수를 분석하는데 어렵다.
  • 일례로 assert.equlas(expected, actual)에만 해도 수없이 순서를 헷갈렸던 경험이 있을 것이다.
  • 가급적 인수를 줄일 수 있도록 노력해보자.

- 인수 객체

  • 인수가 여러 개 필요하면, 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어보자.
  • 예를 들어 Circle makeCircle(doublex, double y, double radius); 의 x,y좌표를 Point 값으로 바꿔 Circle makeCircle(Point center, double radius);의 형태로 바꿔볼 수 있겠다.

- 인수 목록

  • 가변 인수는 하나로 취급할 수 있다.
  • 예를 들어, String.format(String format, Object... args); 의 경우, 가변적인 갯수의 Object가 오지만 사실상 이항 함수다.
  • 물론 가변 인수를 취하는 함수를 사용하더라도 인수가 많아지지 않도록 주의하자.

- 동사와 키워드

  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
  • 단항 함수의 경우에는 인수가 무엇을 의미하는지를 이름에 명확하게 표현하자.
  • 함수 이름에 키워드를 추가할 수도 있다. assertEquals보다 assertExpectedEaqualsActual(expected, actual)이 인수 순서를 기억할 필요가 없어 좋다.

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

  • 한 함수에서 부수 효과가 일어난다는 것은 명백히 함수가 두 가지 이상의 일을 한다는 말이다.
  • 이렇게 일어나는 부수 효과는 예상치 못한 시스템의 변화를 야기할 수도 있다.

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다.
  • 동시에 둘 다 하는 함수는 기본적으로 여러 일을 하고 있는 것이며, 코드를 읽는 이에게 혼란을 초래한다.

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

  • 오류 코드를 사용하면 코드가 중첩되기 마련이다.
    • 만약 A라는 기능을 수행했는데 오류 코드를 반환한다면 B를 한다.
    • B를 수행했는데 오류 코드를 반환한다면 ... 의 무한 반복이다.
  • 예외를 사용하면 오류 처리 코드가 원래 코드에서 catch 문으로 분리되므로 코드가 깔끔해진다.

- Try / Catch 블록 뽑아내기

  • try / catch 블록은 정상 동작과 오류 동작을 뒤섞는다. 따라서 try / catch 블록을 별도 함수로 뽑아내는 편이 좋다.
    • try / catch 블록 내에서 실제로 예외를 발생시키는 메서드에만 try / catch 블록을 사용하도록 뽑아내자.

- 오류 처리도 한 가지 작업이다.

  • 함수는 한 가지 작업만을 해야하며, 오류 처리도 한 가지 작업에 속한다. 따라서 오류를 처리하는 함수는 오류만 처리해야한다.
  • 함수에 try 키워드가 있다면 함수는 try로 시작해 catch / finally로 끝나야 한다는 말이다.

반복하지 마라!

  • 하나의 로직이 여러 메서드에서 반복되는 코드 중복은 여러 면에서 좋지 않다.
  • 일단 코드가 길어진다. 코드가 길어지면 그만큼 읽기 힘들다.
  • 반복이 많아지면 수정이 어려워진다. 로직 하나를 수정했을 뿐인데, 해당 로직이 적용되는 모든 곳을 수정해야한다.
  • 반복되는 로직이 있다면 혹시 기능을 잘못 분리한 것은 아닌지 고민해보거나 반복되는 로직을 메서드로 빼는 시도를 해보자.

함수를 어떻게 짜죠?

  • 한 번에 탁 완벽하게 함수를 짤 수는 없다.
  • 일단은 길고 복잡하게 작성하고 이를 위한 테스트를 공들여 만든다. 그 다음, 함수를 새롭게 빼고, 중복을 제거하는 등의 리팩터링을 진행한다.
반응형

'Programming > Clean Code' 카테고리의 다른 글

5장. 형식 맞추기  (0) 2023.11.20
4장. 주석  (0) 2023.11.10
2장. 의미 있는 이름  (1) 2023.10.07
1장. 깨끗한 코드  (0) 2023.09.21
0장. 들어가며  (0) 2023.09.21

댓글