[Clean Code] Chapter 3. 함수

joyful·2024년 7월 14일
0

개발서적

목록 보기
3/9
post-custom-banner

들어가기 앞서

이 글은 개발자 필독서인 클린 코드를 읽으며 습득한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


  • 프로그래밍 초창기에는 시스템을 루틴과 하위 루틴으로 나눴으며, 포트란과 PL/1 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴다. 지금은 함수만 살아남았으며, 프로그램의 가장 기본적인 단위가 함수다.
  • 의도를 분명히 표현하는 함수를 구현하는 방법, 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있도록 함수에 알맞은 속성을 부여하는 방법에 대해 생각해볼 필요가 있다.



1. 작게 만들어라

  • 함수의 길이는 짧아야 한다.
  • 함수는 명백하며 이야기 하나를 표현해야 한다.

1.1 블록과 들여쓰기

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



2. 한 가지만 해라

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

  • 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다.
  • 함수가 지정된 함수 이름 아래서 추상화 수준이 하나인 단계만 수행한다면, 그 함수는 한 가지 작업만 한다.
  • 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 가지 작업을 하는 셈이다.

2.1 함수 내 섹션

  • 한 가지 작업만 하는 함수는 섹션으로 나누기 어렵다.



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

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

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

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. → 내려가기 규칙
  • 짧으면서 '한 가지'만 하는 함수가 핵심이다.
  • 위에서 아래로 TO 문단을 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.



4. Switch 문

  • 작고 '한 가지' 작업만 하는 switch 문은 만들기 어렵다. 이는 본질적으로 switch 문이 N 가지를 처리하기 때문이다.
  • 다만 다형성을 이용하여 각 swtich 문을 저차원 클래스에 숨기고 반복하지 않을 수 있다.

🖥️ 개선 전 함수

public Money calculatePay(Employee e) throws InvalidEmployeeType {
	switch (e.type) {
    	case COMMISSIONED:
        	return calculateCommissionedPay(e);
        case HOURLY:
        	return calculateHourlyPay(e);
        case SALARIED:
        	return calculateSalariedPay(e);
        default:
        	throw new InvalidEmployeeType(e.type);
    }
}
  • 문제점
    • 길이가 길다. 새 직원 유형을 추가하면 더 길어지게 된다.
    • 여러 가지 작업을 수행한다.
    • SRP를 위반한다. 코드를 변경할 이유가 여럿이다.
    • OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하게 된다.
    • 위 함수와 구조가 동일한 함수가 무한정 존재한다.

🖥️ 개선 후 함수

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 {
    	swtich (r.type) {
        	case COMMISSIONED:
            	return new CommissionedEmployee(r);
            case HOURLY:
            	return new HourlyEmployee(r);
            case SALARIED:
            	return new SalariedEmploye(r);
            default:
            	throw new InvalidEmployeeType(r.type);
        }
    }
}
  • switch 문을 추상 팩토리에 숨기고, 팩토리는 switch 문을 사용해서 적절한 파생 클래스의 인스턴스를 생성한다.
  • 함수는 인터페이스로 호출되어, 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
  • 상속 관계로 숨긴 후에는 다른 코드에 노출하지 않도록 한다.



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

  • 길고 서술적인 이름이 짧고 어려운 이름보다, 길고 서술적인 주석보다 낫다.
  • 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용하고, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
  • IDE의 도움을 받아 최대한 서술적인 이름을 고르는 것도 좋은 방법이다.
  • 서술적인 이름을 사용하면 설계가 뚜렷해져서 코드를 개선하기 쉬워진다.
  • 이름을 붙일 때는 일관성이 있어야 한다.
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.



6. 함수 인수

  • 함수에서 이상적인 인수 개수는 0개(무항)이다. 3개(삼항)는 가능한 피하는 편이 좋으며, 4개 이상(다항)은 웬만해서는 사용하지 않는 것을 권고한다.
  • 인수는 개념을 이해하기 어렵게 만든다.
    • 코드를 읽는 사람이 인수를 발견할 때마다 의미를 해석해야 한다.
    • 함수 이름과 인수는 추상화 수준이 다르며, 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부 사항까지 알아야 한다.
  • 테스트 관점에서 인수가 없다면 간단하지만, 인수의 개수가 많아질수록 각 인수에 유효한 값으로 모든 조합을 구성하기가 어려워진다.
  • 출력 인수는 입력 인수보다 이해하기 어렵다.
    • 일반적으로 함수에 인수로 입력을 넘기고 반환 값으로 출력을 받는다는 개념에 익숙하기 때문이다.
  • 최선은 입력 인수가 없는 경우며, 차선은 입력 인수가 1개뿐인 경우다.

6.1 많이 쓰는 단항 형식

  • 가장 흔한 경우는 두 가지로, 하나는 인수에 질문을 던지는 경우고, 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우다.
    • 이 경우 독자가 당연하게 받아들인다.
    • 함수 이름을 지을 때는 두 경우를 분명히 구분하며, 일관적인 방식으로 두 형식을 사용해야 한다.
  • 드물게 사용하지만 유용한 단항 함수 형식으로는 이벤트가 있다.
    • 입력 인수만 있으며 출력 인수는 없다.
    • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.
    • 이벤트 함수는 이벤트라는 사실이 코드에서 명확히 드러나야 하므로, 이름과 문맥을 주의해서 선택하도록 한다.
  • 이 외에는 단항 함수는 가급적 피하는 것이 좋다.
    • 변환 함수에서 출력 인수를 사용하지 않도록 한다.
    • 입력 인수를 변환하는 함수라면 변환 결과는 반환 값으로 돌려준다.
    • 입력 인수를 그대로 돌려주는 함수라 할지라도 변환 함수 형식을 따르는 편이 낫다.

6.2 플래그 인수

  • 플래그 인수는 함수가 한꺼번에 여러 가지를 처리한다고 공표하는 셈이다.
  • 플래그 인수를 사용한 코드는 헷갈리기 십상이며, 인수 대신 함수를 사용하는 것을 권장한다.
    ex) render(boolean isSuite) → renderForSuite()와 renderForSingleTest()

6.3 이항 함수

  • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
    • 의미가 명확한 경우라도 인수가 1개인 함수가 더 쉽게 읽히고 더 빨리 이해된다.
  • 이항 함수가 적절한 경우
    • 인수 2개가 한 값을 표현하고, 자연적인 순서가 있는 경우
      ex) 직교 좌표계 점 - Point p = new Point(0, 0)
  • 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 한다.

6.4 삼항 함수

  • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다.
    • 순서 고려, 주춤, 무시로 야기되는 문제가 늘어난다.
  • 주춤하게 될지라도 그만한 가치가 충분한 경우는 괜찮다.
    ex) 부동소수점 비교 - assertEquals(1.0, amout, .001)

6.5 인수 객체

  • 인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 수 있는지 생각해본다.
  • 객체를 생성해 인수를 줄이는 방법은 눈속임이 아니다. 변수를 묶어서 넘기는 경우 이름을 붙여야 하므로 개념을 표현하게 된다.

6.6 인수 목록

  • 인수 개수가 가변적인 함수가 필요한 경우도 있다.

  • 가변 인수를 전부 동등하게 취급하는 경우, List 형 인수 하나로 취급할 수 있다.

    /* String.format */
    // 구현
    String.format("%s worked %.2f" hours.", name, hours);
    
    // 선언부
    public String format(String format, Object... args);
  • 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있다. 다만, 이를 넘어서는 인수는 쓰지 않도록 한다.

6.7 동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
  • 함수 이름에 키워드를 추가하면 인수 순서를 기억할 필요가 없어진다.
    ex) assertEqualsassertExpectedEqualsActual(expected, actual)



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

  • 때로는 함수에서 한 가지만 수행할 것 같지만 다른 일도 수행하는 경우가 있으며, 클래스 변수를 수정하고 함수로 넘어온 인수나 시스템 전역 변수를 수정한다.
    • 이는 일시적인 결합이나 순서 종속성을 초래한다. 일시적인 결합은 혼란을 일으키며, 부수 효과로 숨겨진 경우에는 더더욱 혼란이 커진다.
  • 일시적인 결합이 필요하다면 함수 이름에 분명히 명시하도록 한다.

7.1 출력 인수

  • 일반적으로 개발자들은 인수를 함수 입력으로 해석한다.
  • 출력 인수를 사용하게 될 경우 함수 선언부를 찾아보게 될 가능성이 크며, 이는 인지적으로 거슬린다는 뜻이므로 피해야 한다.
  • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. this가 출력 인수로 사용하라고 설계한 변수이기 때문이다.
  • 일반적으로 출력 인수는 피해야 하며, 함수에서 뭔가의 상태를 변경해야 한다면 함수가 속하는 객체의 상태를 변경하는 방법을 택하도록 한다.



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

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 혼란을 초래한다.
  • 명령과 조회를 분리해서 혼란을 애초에 뿌리 뽑아야한다.

✅ 개선 전

if (set("username", "unclebob")) ...
  • 함수를 호출하는 코드만 봐서는 의미가 애매하다.
    • 'set'이라는 단어가 동사인지 형용사인지 분간하기 어려운 탓이다.

✅ 개선 후

if (attributeExists("username")) {
	setAttribute("username", "unclebob");
    ...
}



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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 위반한다.
    • 자칫하면 if 문에서 명령을 표현식으로 사용하기 쉽다.
    • 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.
  • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되어 코드가 깔끔해진다.

9.1 Try/Catch 블록 뽑아내기

  • try/catch 블록은 코드 구조에 혼란을 일으키며, 정상적인 동작과 오류 처리 동작을 뒤섞는다.
    • 별도 함수로 뽑아내는 편이 낫다.
  • 정상적인 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.

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

  • 오류를 처리하는 함수는 오류만 처리해야 한다.
  • 함수에 키워드 try가 있다면, 함수는 try문으로 시작해서 catch/finally 문으로 끝나야 한다.

9.3 Error.java 의존성 자석

  • 오류 코드를 반환한다는 것은 어디선가 오류 코드를 정의한다는 뜻이다.
    public enum Error {
    	OK,
      INVALID,
      NO_SUCH,
      LOCKED,
      OUT_OF_RESOURCES,
      WATING_FOR_EVENT;
    }
    • Error enum이 변한다면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 배치해야 하므로, Error 클래스를 변경하기가 어려워진다.
    • 프로그래머는 재컴파일/재배치가 번거로워서 새 오류 코드를 정희하기 싫어하므로, 기존 오류 코드를 재사용하게 된다.
  • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생한다.
    • 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.



10. 반복하지 마라

  • 중복이 초래하는 문제
    • 코드 길이가 늘어난다.
    • 알고리즘이 변하면 이를 사용하는 코드 모두 손봐야 한다. 어느 한 곳이라도 빠뜨리면 오류가 발생할 확률도 높아진다.



11. 구조적 프로그래밍

  • 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다. 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
  • goto 문은 큰 함수에서만 의미가 있으므로 피하는 편이 낫다.



12. 함수를 짜는 방법

  • 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 단위 테스트 케이스도 만든다.
  • 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메소드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 단위 테스트는 통과하도록 한다.



13. 결론

진정한 목표는 시스템이라는 이야기를 풀어가는 데 있다. 함수는 분명하고 정확한 언어에 깔끔하게 맞아야 이야기를 풀어가기가 쉬워진다.

  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 응용 분야 언어로 만들어진다.
    • 함수는 그 언어에서 동사며 클래스는 명사며, 프로그래밍은 언어 설계의 기술이다.
  • 대가 프로그래머는 시스템을 풀어갈 이야기로 여긴다.
    • 프로그래밍 언어를 사용해 표현력 있는 언어를 만들고, 이를 통해 시스템의 동작을 설명하는 함수 계층을 구축한다.
    • 재귀를 통해 각 동작은 응용 언어로 자신만의 이야기를 전개한다.

📖 참고

  • 로버트 C. 마틴, 『Clean Code 클린 코드 애자일 소프트웨어 장인 정신』, 박재호·이해영 옮김, 케이앤피북스(2010), p71-93.
profile
기쁘게 코딩하고 싶은 백엔드 개발자
post-custom-banner

0개의 댓글