Clean code 3장 - 함수
포스트
취소

Clean code 3장 - 함수

클린 코드 책을 읽고 정리한 내용입니다.

1. 작게 만들어라!

  • 블록과 들여쓰기
    • if/else문, while문 등에 들어가는 블록은 한 줄이어야 함
      • 여기서 함수를 호출
      • 바깥을 감싸는 함수가 작아질 뿐 아니라 블록 안에서 호출하는 함수 이름을 적절히 지으면 코드를 이해하기도 쉬워짐
    • 즉, 중첩 구조가 생길만큼 함수가 커져서는 안됨
    • 들여쓰기 수준은 1단이나 2단을 넘어서도 안됨

2. 한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
  • 함수가 ‘한 가지’만 하는지 판단하는 방법
    • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
    • 해당 함수 내에서 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈
      • 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어려움!

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

  • 함수 내 모든 문장의 추상화 수준이 동일해야 함
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈림
    • 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려움
  • 위에서 아래로 코드 읽기: 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와야 함(=위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아져야 함, 내려가기 규칙!)

4. Switch 문

  • switch 문은 작게 만들기 어려움

  • 본질적으로 N가지 일을 처리 함

  • 일반적인 Switch문의 문제점 예시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    public Money calculatePay(Employee e) throw InvalidEmployeeType {
    	switch(e.type){
    		case COMMISSIONED:
    			return calcuateCommisionedPay(e);
    		case HOURLY:
    			return calculateHourlyPay(e);
    		case SALARIED:
    			return calculateSalariedPay(e);
    		default:
    			return new InvalidEmployeeType(e.type);
    }
        
    
    • 문제점1. 함수가 길다
    • 문제점2. 한가지 작업만 수행하지 않는다.
    • 문제점3. SRP(Single Responsibility Principle)을 위반한다.
    • 문제점4. OCP(Open Closed Principle)을 위반한다.
      • 새 직원타입을 추가할 때마다 코드 변경이 필요함.
    • 문제점5. 위 함수와 구조가 동일한 함수가 무한정 존재한다.
  • switch문을 완전히 피할 방법은 없으나 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않을 수는 있음 → 다형성을 이용

    • 아래와 같이 다형적 객체를 생성하는 코드 안에서만 사용 할 것
    • 상속 관계로 숨긴 후에는 다른 코드에 노출하지 않도록!
    • 물론 불가피한 상황도 있음
  • 아래 코드는 위 코드의 문제점을 해결하기 위해 switch 문을 추상 팩토리에 꽁꽁 숨긴 것이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public abstract class Employee {
    	public abstract boolean isPayday();
    	public abstract Money calculatePay();
    	public abstract void deliverPay(Money pay);
    }
    ---------------------
    public interface EmployeeFactory{
    	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 CommissionedEmployee(r) ;
    			case HOURLY:
    				return new HourlyEmployee(r);
    			case SALARIED:
    				return new SalariedEmployee(r);
    			default:
    				throw new InvalidEmployeeType(r.type);
    		}
    	}
    }
        
    

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

  • 길고 서술적인 이름 > 짧고 어려운 이름
    • 함수가 하는 일을 잘 표현하는 이름이 훨씬 좋은 이름.

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

    • 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워짐.

  • 서술적인 이름을 사용하면 설계도 뚜렷해져 코드 개선하기도 쉬워짐
  • 이름을 붙일 때는 일관성이 있어야 함
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용

6. 함수 인수

  • 함수에서 이상적인 인수 개수는 0개

  • 3개부터는 가능한 피하는 것이 좋고 4개 이상은 특별한 이유가 필요함. 특별한 이유가 있어도 사용하면 안됨.

  • 테스트 관점에서 보면 인수는 더 어렵다.

  • 출력 인수는 입력 인수 보다 이해하기 어렵다.

  • 단항 형식

    • 단항 형식을 사용하는 3가지 경우

      1. 인수에 질문은 던질 때 (ex. boolean fileExists(”filename”) )
      2. 인수를 뭔가로 변환해 결과를 반환하는 경우
      3. 이벤트 함수

      → 그 외의 경우에는 단항 함수는 가급적 피할 것

  • 플래그 인수

    • 함수로 부울 값을 넘기는 것은 매우 끔찍!
    • 함수가 한꺼번에 여러 가지를 처리한다는 것과 동일
      • flag가 참이면 A를 하고, 거짓이면 B를 하라는 것이니까!
  • 이항 함수

    • 인수가 2개면 1개일때보다 이해하기 어려워짐
    • 물론 이항함수가 적절한 경우도 있음 (ex. 좌표계)
      • 보통 이런 경우는, 두 개의 인수가 하나의 값을 표현하는 경우(ex. x, y가 있어야 하나의 좌표)
      • 두 요소에 자연스러운 순서가 있어야 함
    • 당연하게 여겨지는 이항 함수들도 문제가 있는 경우가 있음.
      • Ex. assertEquals(expected, actual)
        • expected 인수에 actual 값을 집어넣는 실수를 하기 쉬움.
        • expected 다음에 actual이 온다는 순서를 인위적으로 기억해야함.
  • 삼항 함수

    • 순서, 주춤, 무시로 야기되는 문제가 인수가 2개인 함수보다 두 배 이상 늘어남.
      • Ex. assertEquals(message, expected, actual)
        • 첫 인수가 expected가 아니란 사실에 주춤. message 무시해야 한다는 사실 상기
    • 반면, assertEquals(1.0, amount, .001)은 그리 음험하지 않은 삼항 함수.
      • 여전히 주춤하게 되지만 그만한 가치가 충분함.
      • 부동소수점 비교가 상대적이라는 사실은 언제든 주지할 중요한 사항.
  • 인수 객체

    • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언해 볼 것
  • 인수 목록

    • 인수 개수가 가변적인 함수도 있음

    • 가변 인수를 동등하게 취급 시 List 형 인수 하나로 취급 가능

      1
      2
      3
      4
      5
      6
      
      // 예시
      public String format(String format, Object... args)
      void monad(Integer... args)
      void dyad(String name, Integer... args);
      void triad(String name, int count, Integer... args)
              
      
  • 동사와 키워드

    • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수
    • 단항 함수는 함수와 인수가 동사/명사 쌍을 이루어야 함
      • ex) writeField(name)
    • 함수 이름에 키워드로 인수 이름을 추가할 것
      • ex) assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋음

        → 인수 순서를 기억할 필요가 없어짐

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

❓ 부수 효과란?

함수 내의 실행으로 인해 함수 외부가 영향을 받는 것 함수의 매개 변수의 값이 변경되어, 이로 인해 함수를 사용하는 코드에 영향을 주거나, 함수의 외부 세계인 데이터베이스, 파일 시스템, 네트워크로 데이터 이동이 발생하는 것

  • 부수 효과는 결국 함수에서 한 가지만 하는게 아니므로 거짓말!

    • ex1. 클래스 변수 수정
    • ex2. 함수로 넘어온 인수나 전역변수 수정
  • 부수 효과는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)를 초래함

    ❓ temporal coupling

    • 실행의 순간이 연결된 것 ex) 순서를 바꾸어도 말이 되는가?
    1
    2
    3
    
    print("Hello")
    print("What is your name?")
        
    
    1
    2
    3
    
    print("Orange")
    print("Apple")
        
    
    • Temporal coupling은 유지보수 용이성을 떨어트림

    • 만약 시간적인 결합이 필요하다면 함수 이름에 명시하는 것을 추천

  • 출력 인수

    • 일반적으로 인수를 함수 입력으로 해석함. 출력 인수는 피하는 것이 좋음
    • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택할 것
    • 함수 선언부를 찾아보는 행위는 코드를 보다가 주춤하는 행위와 동급인데 출력 인수가 있으면 함수 선언부를 찾아보게 됨
    • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없음
      • this 변수가 있으니!
    1
    2
    3
    4
    5
    6
    7
    8
    
      appendFooter(s); // 무언가에 바닥글로 s를 첨부하는지, s에 바닥글을 첨부하는지 모름.
        
      public void appendFooter(StringBuffer report)
      // 함수를 찾아보면 s에 바닥글을 첨부한다는 것을 알 수 있음.
      // 그러나 출력 인수 추천하지 않음. 특히 객체 지향 언어에서는.
        
      report.appendFooter(); // 이렇게 호출하는 방식이 가장 좋음. 
        
    

8. 명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 함! ( = 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나)
  • 둘 다 하면 혼란을 초래하게 됨
1
2
3
4
5
6
7
8
9
10
11
12
// 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패 시 false 반환
public boolean set(String attribute, Strinb value);

// 다음과 같이 사용 시 :"username"이 "unclebob"으로 설정되어 있는지 확인하는 것인지
// 설정하는 코드인지 호출 부분만으로는 모호
if (set("username", "unclebob"))

// 따라서 명령과 조회를 아예 분리하는 것을 추천
if (AttributeExists("username")) {
	setAttribute("username", "unclebob");
}

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반

  • if문에서 명령을 표현식으로 사용하기 쉬운 탓

    • 오류 코드 반환 시 호출자는 오류 코드를 곧바로 처리해야한다는 문제 발생

      1
      2
      3
      4
      5
      6
      7
      8
      
      if (deletePage(page) == E_OK) {
      	if registry.deleteReference(page.name) == E_OK) {
      		if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
      			logger.log("page deleted");
      		else {
      			logger.log("configKey not deleted");
      		}
              
      
    • 오류 코드 대신 예외 사용 시 다음과 같이 깔끔하게 수정 가능

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      try{
      	deletePage(p{age);
      	registry.deleteReference(page.name);
      	configKeys.deleteKey(page.name.makeKey());
      }
      catch (Exception e) {
      	logger.log(e.getMessage());
      }
              
      
    • Golang에서는 내장 함수 대부분이 error 코드를 반환해주고 있어 위와 같이 코드를 짜야함

      [Golang] 에러 처리

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      package main
              
      import "fmt"
              
      func main() {
              var test map[string]int
              test = make(map[string]int)
              test["A"] = 1
              test["B"] = 2
              
              tmpVar, err := test["A"]
              fmt.Println(tmpVar, err)
      }
              
      
      1
      2
      3
      
      $ go run main.go
      1 true
              
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      if resp, err := client.Do(req); err != nil {
      		file_logger.ErrorLogger.Println(err)
      		return ""
      } else if bodyText, err := ioutil.ReadAll(resp.Body); err != nil {
      		file_logger.ErrorLogger.Println(err)
      		return ""
      } else {
      		s := string(bodyText)
      		return s
      }
              
      
  • Try/Catch 블록 뽑아내기

    • try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞음

    • 따라서 try/catch 블록도 별도 함수로 뽑아내는 편이 좋음

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      public void delete(Page page) {
      	try {
      		deletePageAndAllReferences(page);
      	}
      	catch (Exception e) {
      		logError(e);
      	}
      }
              
      private void deletePageAndAllReferences(Page page) throws Exception {
      	deletePage(page);
      	registry.deleteReference(page.name);
      	configKeys.deleteKey(page.name.makeKey());
      }
              
      private void logError(Exception e) {
      		logger.log(e.getMessage());
      }
              
      
    • 정상 동작(deletePageAndAllReferences)과 오류 처리 동작(delete)을 분리하면 코드를 이해하고 수정하기 쉬워짐.

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

  • Error.java 의존성 자석

    • 오류 코드를 반환한다 = 오류 코드를 정의를 해두었다는 뜻
    • Error 코드가 변환되면 해당 Error 코드를 사용하는 클래스 전부를 다시 컴파일하고 배치해야 함
    • 따라서 Error 클래스 변경이 어려워짐
    • 그러나 오류 코드 대신 예외 사용 시 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 없이도 새 예외 클래스 추가 가능

10. 반복하지 마라!

  • 코드 중복 시 코드 길이가 늘어날 뿐 아니라 알고리즘 변경 시 모두 손봐주어야 함
    • 손볼 곳 하나라도 빠뜨리는 날에는.. 오류..
  • 중복은 소프트웨어에서 모든 악의 근원.
    • 관계형 데이터베이스에 정규 형식(자료에서 중복을 제거할 목적)
    • 객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앰

11. 구조적 프로그래밍

  • 에츠허르 데이크스트라의 구조적 프로그래밍 원칙
    • 모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다.
      • 함수는 return문 하나여야 함.
      • 루프 안에서 break, continue 사용하지 말아야 함.
      • goto는 절대로 안됨.
  • 구조적 프로그래밍의 목표와 규율은 공감하지만, 함수가 작다면 이는 별 이익을 제공하지 못함.
    • 함수가 아주 클 때만 상당한 이익 제공.
  • 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 여러차례 사용해도 괜찮다.
    • goto 문은 큰 함수에만 의미가 있으므로 작은 함수에서는 피하자.

12. 함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 여느 글짓기와 비슷!
    • 먼저 생각 기록한 후 읽기 좋게 다듬으면 된다.
    • 처음엔 길고 복잡하고, 들여쓰기 단계도 많고 루프도 많고, 인수도 많고, 이름도 즉흥적이고 그럴 수 있음. 초안이니까!
    • 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복 제거해 나가면 된다.
    • 처음부터 탁! 짜낼 수 있는 사람은 없다.

13. 결론

  • 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.