Today I../Today I Read

[Clean Code] Chapter 03. 함수

HJChung 2022. 2. 1. 14:55
Clean Code 클린 코드 - 로버트 C. 마틴 저 를 읽고, clean code 해설 강의를 통해 보충 이해한 내용을 정리한 글입니다. 

 

 

현업에서 어떤 code를 refactoring 해야하는 경우가 있었다. 

그 때 

1. 함수 인자가 8개가 넘어가고, 

2. 한 파일에 모든 함수가 모여있고

3. 한 함수에 기능과 추상화 수준이 섞여있어서 한 함수당 코드가 백 몇 줄이 넘어가는 경우도 많았다. 

 

그 때 동료분께서 Clean Code 책을 읽어보는 것을 추천해주셨는데,

그 당시에는 이 많은 내용 중에 어떤 부분에 중점을 두어서 읽고 적용해보는 것이 좋을지 막막한 느낌이 들었다면,

그 일을 다 마치고 시간이 조금 흐른 지금 이 책을 다시 읽어보니 가장 많은 도움이 된 부분이 이 chapter인 것 같다. 

 

 

1. 안전하고 간결한 함수 작성하기

1) 함수를 작게 만들어라

왜?

논리적인 근거를 대기는 어렵지만 저자의 오랜 경험상 작은 함수가 좋다. 그리고 보통 함수가 길면 여러가지 기능이나 추상화 수준이 섞여 있을 가능성이 높다. 추상화 수준이 섞여있으면 함수를 읽는 사람이 어떤 코드가 근본 개념이고 세부개념인지 헷갈리고, 깨어진 창문처럼 세부사항을 덕지덕지 추가하는 경우가 생길 수 있다. 

얼마나? 

public static String renderPageWithSetupsAndTeardowns(
	pageData pageData, boolean isSuite) throws Exception {
    	if(isTestPage(pageData)) {
        	includeSetupAndTeardownPage(pageData, isSuite);
        }
        return pageData.getHtml();
    }

이 정도거나 이것보다 더  짧게.

어떻게?

  • 블록과 들여쓰기
    • if/else/while 문 등에 들어가는 블록은 한 줄이어야 한다. (위의 코드예시에서 if문 블록처럼)
    • 이렇게 되면 보통 그 줄에서 함수를 호출하게 될 텐데 chapter 02. 의미 있는 이름 에서 배운 것처럼 이 함수명을 잘 지으면 코드를 이해하기도 쉬워진다. 
    • 그리고 한 줄이니까 곧 중첩 구조도 사용하지 않을 수 있다. 즉, 함수에서 들여쓰기 수준은 2단을 넘어서면 안 된다. 

 

  • 각 python의 파일은 500 라인 이하로 유지해보도록 하자. 
    • 정적분석 툴 등을 사용해서 자동으로 검사해주는 도움을 받아보자. 

 

2) 한 가지만 해라 & 추상화 수준은 하나로!

왜? 

객체지향 설계의 SOLID 원칙 중 SRP, OCP 원칙과 일치하는 사항이다. 해당 원칙에 대한 설명에서 이유가 설명된다.

  • SRP(단일 책임 원칙; Single Responsibility) 
    출처: https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898
    • 클래스는 하나의 기능만 가지며, 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. 
    • 클래스가 하나의 기능만 하게 되면 그 역할에 대한 책임이 분명하기 때문에 해당 클래스의 변경에 대한 연쇄작용(변경으로 인한 버그의 연쇄 등이 될 수 있다.)에서 자유로워진다. unrelated behaviors에는 영향을 미치지 않을 것이기 때문이다. 
    • 또한 클래스당 역할이 명확하므로 구현도 명확해진다. 이것은 곧 가독성 향상과 유지보수 용이로 이어진다.

 

  • OCP(개방-폐쇄 원칙; Open-Closed)
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다. 
    • 어떤 class 를 변경하게 되면 기존에 해당 class를 사용하던 시스템에 영향을 미치게 된다. 그래서 요구사항의 변경이나 추가사항이 발생하게 되면, 기존 기능들을 변경하는 것이 아닌 기능 확장을 하는 것이 좋다. 
    • 그렇게 할 수 있으려면 객체지향의 추상화와 다형성 개념을 사용해서 구현해야 한다.

 

얼마나?

이렇게 판단해보자. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 기능을 하고 있는 셈이다. 

어떻게?

계산과 타입 관리를 분리하고, 타입에 대한 처리는 Factory에서만 하도록 하는 것도 이 원칙을 따르는 것 중 하나이다. (해설 강의에서 구체적인 예시로 설명을 해주었기에 그 예시를 첨부하고자 한다. )

public abstract class Employee {
	// 추상 class로 이 Employee에 필요한 method들을 다 추상으로 정의해놓았음
    // 이것으로 '계산'과정 분리
	public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}

public interface EmployeeFactory {
	// Employee를 생성해놓는 과정을 분리해서 해당 interface에 넣어둠
    // 이것으로 '타입관리'과정 분리
	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 CommissionedEmplyee(r);
            case HOURLY:
            	return new HourlyEmplyee(r);
            case SALARIED:
            	return new SalariedEmplyee(r);
            default:
            	throw new InvalidEmplyeeType(r.type);
            }
      }
}

예시

  • JavaScript
    • Bad
      function emailClients(clients: Client[]) {
        clients.forEach((client) => {
          const clientRecord = database.lookup(client);
          if (clientRecord.isActive()) {
            email(client);
          }
        });
      }​
    • Good
      function emailClients(clients: Client[]) {
        clients.filter(isActiveClient).forEach(email);
      }
      
      function isActiveClient(client: Client) {
        const clientRecord = database.lookup(client);
        return clientRecord.isActive();
      }​
  • Python
    • Bad
      from typing import List
      
      class Client:
          active: bool
      
      
      def email(client: Client) -> None:
          pass
      
      
      def email_clients(clients: List[Client]) -> None:
          """Filter active clients and send them an email.
          """
          for client in clients:
              if client.active:
                  email(client)​
    • Good
      from typing import Generator, Iterator
      
      
      class Client:
          active: bool
      
      
      def email(client: Client):
          pass
      
      
      def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]:
          """Only active clients"""
          return (client for client in clients if client.active)
      
      
      def email_client(clients: Iterator[Client]) -> None:
          """Send an email to a given list of clients.
          """
          for client in active_clients(clients):
              email(client)​

 

3) 서술적인 이름 사용

왜?

이름이 길어도 괜찮다. 겁먹지 마라. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 

어떻게?

일관성 있는 이름 붙이기, 모듈 내 함수 이름을 같은 문구, 명사, 동사 사용하기 등..

예시

includeSetupAndTeardownPages();

includeSetupPages();

includeSiteSetupPage();

 

4) 함수 인수

왜?

인수는 개념을 이해하기 어렵게 만든다. 또한 테스트 작성시에도 갖가지 인수 조합으로 함수를 검증하는 테스트를 작성할 때도 어려워진다.

어떻게? 

방법 1. 객체를 인자로 넘기기 (현업에서는 이 방법을 많이 사용한다.)

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

방법 2. 가변 인자를 넘기기 (하지만 이는 특별한 경우가 아니면 잘 사용하지 않는다. )

String.format(String format, Object...args);

예시

  • Javascript
    • Bad
      function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
        // ...
      }
      
      createMenu('Foo', 'Bar', 'Baz', true);​
    • Good
      type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
      
      function createMenu(options: MenuOptions) {
        // ...
      }
      
      createMenu({
        title: 'Foo',
        body: 'Bar',
        buttonText: 'Baz',
        cancellable: true
      });​
  • Python
    • Bad
      def create_menu(title, body, button_text, cancellable):
          pass​
    • Good
      from typing import NamedTuple
      
      
      class MenuConfig(NamedTuple):
          """A configuration for the Menu.
      
          Attributes:
              title: The title of the Menu.
              body: The body of the Menu.
              button_text: The text for the button label.
              cancellable: Can it be cancelled?
          """
          title: str
          body: str
          button_text: str
          cancellable: bool = False
      
      
      def create_menu(config: MenuConfig):
          title, body, button_text, cancellable = config
          # ...
      
      
      create_menu(
          MenuConfig(
              title="My delicious menu",
              body="A description of the various items on the menu",
              button_text="Order now!"
          )
      )​
    • Even fancier, Python3.8+ only
      from typing import TypedDict, Text
      
      
      class MenuConfig(TypedDict):
          """A configuration for the Menu.
      
          Attributes:
              title: The title of the Menu.
              body: The body of the Menu.
              button_text: The text for the button label.
              cancellable: Can it be cancelled?
          """
          title: Text
          body: Text
          button_text: Text
          cancellable: bool
      
      
      def create_menu(config: MenuConfig):
          title = config["title"]
          # ...
      
      
      create_menu(
          # You need to supply all the parameters
          MenuConfig(
              title="My delicious menu",
              body="A description of the various items on the menu",
              button_text="Order now!",
              cancellable=True
          )
      )​

5) 부수효과 없는 함수를 작성하라

왜?

부수 효과란 값을 반환하는 함수가 외부 상태를 변경하는 경우를 말한다. 내가 해야하는 기능만 수행하는 것이 아니라 외부에 어떤 변화를 일으키는 것은 위험하다. 

어떻게?

함수가 관계없는 외부 상태를 변경시키는 함수는 작성하지 말아야 한다. 

대표적인 부수효과 예시로 너무 많은 전역 변수가 있다. 

전역 변수는 당장 쓰기에는 편할 수 있지만, 항상 사이드이펙트를 가져온다. 그래서 가능하면, 환경 값들은 환경 변수를 활용하고, 함수에 명시적으로 파라미터를 전달하고 받아오는 방식으로 고친다. 

예시

  • Javascript
    • Bad
      // Global variable referenced by following function.
      let name = 'Robert C. Martin';
      
      function toBase64() {
        name = btoa(name);
      }
      
      toBase64();
      // If we had another function that used this name, now it'd be a Base64 value
      
      console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='​
    • Good
      const name = 'Robert C. Martin';
      
      function toBase64(text: string): string {
        return btoa(text);
      }
      
      const encodedName = toBase64(name);
      console.log(name);​
  • Python
    • Bad
      # type: ignore
      
      # This is a module-level name.
      # It's good practice to define these as immutable values, such as a string.
      # However...
      fullname = "Ryan McDermott"
      
      def split_into_first_and_last_name() -> None:
          # The use of the global keyword here is changing the meaning of the
          # the following line. This function is now mutating the module-level
          # state and introducing a side-effect!
          global fullname
          fullname = fullname.split()
      
      split_into_first_and_last_name()
      
      # MyPy will spot the problem, complaining about 'Incompatible types in
      # assignment: (expression has type "List[str]", variable has type "str")'
      print(fullname)  # ["Ryan", "McDermott"]
      
      # OK. It worked the first time, but what will happen if we call the
      # function again?​
    • Good
      from typing import List, AnyStr
      
      
      def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]:
          return name.split()
      
      fullname = "Ryan McDermott"
      name, surname = split_into_first_and_last_name(fullname)
      
      print(name, surname)  # => Ryan McDermott​

 

2. 함수 리팩터링 과정

  1. 기능을 구현하는 서투른 함수를 작성한다. 
  2. 테스트 코드를 작성한다. 리팩터링 과정 앞에 테스트 코드를 짜는 것이 중요한 이유는 리팩터링이라는 건 현재 기능은 동일하다는 전제하에 코드의 가독성과 성능을 높이는 과정이므로 내가 건든 코드가 정상 작동을 한다는 전제를 가지고 리팩터링을 진행해야 하기 때문이다. 
  3. 리팩터링 한다. (코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.)

 

3. 실습 코드

https://github.com/Gracechung-sw/clean-code-practice/tree/chapter3-function

 

GitHub - Gracechung-sw/clean-code-practice

Contribute to Gracechung-sw/clean-code-practice development by creating an account on GitHub.

github.com

 

 

Reference

https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898