본문 바로가기

개발 서적/클린 코드(Clean Code)

[Clean Code(클린 코드)] 3장 함수

본 게시글은 <Clean Code>를 학습한 내용을 정리한 글입니다. (문제시 삭제하겠습니다.)

 

Clean Code(클린 코드) - 교보문고

애자일 소프트웨어 장인 정신 | 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

www.kyobobook.co.kr

어떤 프로그램이든 가장 기본적인 단위가 함수다. 이 장은 함수를 잘 만드는 법을 소개한다.

 

작게 만들어라!

  • 함수를 만드는 첫째 규칙은 '작게!다. 함수를 만드는 둘째 규칙은 '더 작게!'다.
  • 블록과 들여쓰기 - if문 /else 문 /while 문 등에 들어가는 블록은 한 줄이어야 한다. 이 말은 중첩 구조가 생길만큼 함수가 커져서는 안된다는 뜻이다.

한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
  • 이 충고에서 문제라면 그 '한 가지가' 무엇인지 알기 어렵다는 점이다.
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다. 우리가 함수를 만드는 이유는 큰 개념(다시 말해, 함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서가 아니던가.
  • 함수가 '한 가지'만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

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

  • 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다.
  • 근본 개념과 세부 사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.
  • 위에서 아래로 코드 읽기: 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

Switch 문

  • switch 문은 작게 만들기 어렵다. 본질적으로 switch문은 N가지를 처리한다.
  • 불행하게도 switch문을 완전히 피할 방법은 없다.
  • 하지만 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 다형성을 이용한다.
  • switch 문을 추상 팩토리(Abstract Factory)에 꽁꽁 숨긴다. 팩토리는 switch문을 사용해 적적절한 파생 클래스의 인스턴스를 생성한다.
    • 추상 팩토리 패턴 : 서로 관련있는 객체들의 조합을 만드는 인터페이스를 제공하는 패턴. 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성하는 겨웅 유용
    • 팩토리 메서드 패턴 : 조건에 따른 객체 생성을 팩토리 클래스로 위임하여, 팩토리 클래스에서 객체를 생성하는 패턴
    → 책에서 설명하는 건 팩토리 메서드 패턴인 것 같다. 여기서 말하는 추상 팩토리는 ≠ 추상 팩토리 패턴 인 듯하다..
  • 나는 switch문을 단 한 번만 참아준다. 다형적 객체를 생성하는 코드 안에서다. 이렇게 상속 관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.

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

  • 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
  • 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개(무항)이다.
  • 인수는 개념을 이해하기 어렵게 만든다.
  • 코드를 읽는 사람이 인수를 발견할 때마다 의미를 해석해야한다.
  • 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부사항을 알아야한다.
  • 테스트 관점에서 보면 인수는 더 어렵다. 인수가 3개를 넘어가면 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스러워진다.
  • 많이 쓰는 단한 형식
    • 인수에 질문을 던지는 경우 - boolean fileExists("MyFile)
    • 인수를 뭔가로 변환해 결과를 반한하는 경우 - InputStream fileOpen("MyFile)
    • 이벤트 - 입력 인수만 있고 출력 인수는 없음. 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꿈. passwordAttemptFailedNtiems(int attempts)
    • 지금까지 설명한 경우가 아니라면 단항 함수는 가급적 피한다.
  • 플래그 인수
    • 플래그 인수는 추하다.
    • 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까! 플래그가 참이면 이걸 하고 거짓이면 저걸 한다는 말이니까!
  • 이항 함수
    • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
    • 물론 이항 함수가 적절한 경우도 있다. Point p = new Point(0, 0)가 좋은 예다. 하지만 여기서 인수 2개는 한 값을 표현하는 두 요소다. 두 요소에는 자연적인 순서도 있다.
    • 심지어 아주 당연하게 여겨지는 이항 함수 assertEquals(expected, actual)에도 문제가 있다. 두 인수는 자연적인 순서가 없다. 순서를 인위적으로 기억해야 한다.
    • 이항 함수가 무조건 나쁘다는 소리는 아니다. 하지만 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
  • 삼항 함수
    • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다.
    • 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다. 그래서 삼항 함수를 만들 때는 신중히 고려하라 권고한다.
  • 인수 객체
    • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어 본다.
  • 인수 목록
    • 때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메서드가 좋은 예다.
  • 동사와 키워드
    • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. - write(name)
    • 함수 이름에 키워드를 추가한다. 즉, 함수 이름에 인수 이름을 넣는다. 그러면 인수 순서를 기억할 필요가 없어진다. - assertExpectedEqualsActual(expected, actual)

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

  • 부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓도 하니까.
  • 때로는 예상치 못하게 클래스 변수를 수정한다. 때로는 함수로 넘어온 인수나 시스템 전역 변수를 수정한다. 어느 쪽이든 교활하고 해로운 거짓말이다. 많은 경우 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래한다.
  • 출력 인수
    • 일반적으로 우리는 인수를 함수 입력으로 해석한다.
    • 인수를 출력으로 사용하는 함수 → 함수 선언부를 찾아보는 행위를 초래. 코드를 보다가 주춤하는 행위와 동급이다. 인지적으로 거슬린다는 뜻이므로 피해야 한다.
    • 일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
  • public boolean set(String attribute, String value)
    • if (set("username", "unclebob"))... → 개발자는 set을 동사로 의도. 하지만 if문에 넣고 보면 형용사로 느껴짐
    • if (attributeExists("username")) {...
    • }
    • setAttribute("username", "unclebob");

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if문에서 명령을 표현식으로 사용하기 쉬운 탓이다.
  • 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.
  • 반면 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
  • Try/Catch 블록 뽑아내기
    • 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
  • 오류 처리도 한 가지 작업이다.
    • 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
    • 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally문으로 끝나야 한다는 말이다.
  • Error.java 의존성 자석
    • 오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는 뜻이다.
    • 의존성 자석과 같은 클래스에 변경이 생긴다면 해당 클래스를 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야한다.
    • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라!

  • 중복은 문제다. 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 n곳이나 손봐야 하니까.
  • 게다가 어느 한 곳이라도 빠뜨리는 바람에 오류가 발생할 확률도 n 배나 높다.
  • 중복을 없앴더니 모듈 가독성이 크게 높아졌다는 사실을 깨달으리라.

구조적 프로그래밍

  • 데이크스트라는 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)가 하나만 존재해야 한다고 말했다.
    • 즉, 함수는 return문이 하나여야 한다는 말이다.
    • 루프 안에서 break나 continue를 사용해서는 안 되며 goto는 절대로, 절대로 안된다.
  • 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다. 함수가 아주 클 때만 상당한 이익을 제공한다.
  • 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.
  • 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
  • 반면, goto문은 큰 함수에서만 의미가 있으므로, 작은 함수에서는 피해야만 한다.

함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다.
  • 처음에는 길고 복잡하다.
  • 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다.
  • 그런 다음 나는 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다.
  • 이 와중에도 코드는 항상 단위 테스트를 통과한다.
  • 최종적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다.

결론

  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다.
  • 함수는 그 언어에서 동사며, 클래스는 명사다.
  • 마스터 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다.
  • 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다.
  • 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.
  • 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하기 바란다.
반응형