Hayden's Archive

클린코드(Clean Code) 3, 4장 - 로버트 C. 마틴 본문

Book

클린코드(Clean Code) 3, 4장 - 로버트 C. 마틴

_hayden 2021. 2. 16. 20:35

book.naver.com/bookdb/book_detail.nhn?bid=7390287

 

Clean Code

『CLEAN CODE(클린 코드)』은 오브젝트 멘토(OBJECT MENTOR)의 동료들과 힘을 모아 ‘개발하며’ 클린 코드를 만드는 최상의 애자일 기법을 소개하고 있다. 소프트웨어 장인 정신의 가치를 심어 주며

book.naver.com

 


3. 함수

  • 이전 역사
    • 프로그래밍 초창기 : 시스템 = 루틴 + 하위 루틴
    • 포트란과 PL/I 시절 : 시스템 = 프로그램 + 하위 프로그램 + 함수
    • 지금은 함수만 살아남음. 어떤 프로그램이든 가장 기본적인 단위가 함수!
  • 작게 만들어라
    • 함수를 만드는 첫째 규칙은 '작게!', 둘째 규칙은 '더 작게!'
    • 함수는 100줄을 넘어서는 안 됨. 아니 20줄도 길다.
    • 블록과 들여쓰기
      • if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미
        • 대개 여기에서 함수를 호출함
        • 바깥을 감싸는 함수(Enclosing Function)가 작아질 뿐만 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기 쉬워짐
      • 중첩 구조가 생길만큼 함수가 커져서는 안 됨
        • 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 됨
        • 그래야 함수는 읽고 이해하기 쉬워짐
  • 한 가지만 해라!
    • 함수는 한 가지를 해야 함. 그 한 가지를 잘해야 함. 그 한 가지만을 해야 함.
    • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것
    • 함수를 만드는 이유 : 큰 개념을 (다시 말해, 함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서임
    • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈
    • 함수 내 섹션
      • 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다
  • 함수당 추상화 수준은 하나로!
    • 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 함
    • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈림
      • 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가함
    • 위에서 아래로 코드 읽기 : 내려가기 규칙
      • 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
      • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 옴
      • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아짐
  • Switch 문
    • switch 문은 작게 만들기 어려움(당연히 if/else가 여럿 이어지는 구문도 포함됨)
    • 본질적으로 switch 문은 N가지를 처리함
    • 하지만 각 switch 문을 저차원 클래스에 숨기고 절대 반복하지 않는 방법이 있음
  • 서술적인 이름을 사용하라!
    • 서술적인 이름은 함수가 하는 일을 좀 더 잘 표현함
    • 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워짐
    • 길고 서술적인 이름이 짧고 어려운 이름보다 좋음
    • 이름을 붙일 때는 일관성이 있어야 함
      • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용
      • includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetuptPage 등
        • 문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워짐.
        • 위의 함수를 보면 "includeTeardownPages, includeSuiteTeardownPage, includeTeardownPage도 있는지 질문이 떠오르게 됨.
  • 함수 인수
    • 함수의 이상적인 인수 개수는 0개(무항), 다음은 1개(단항), 다음은 2개(이항), 3개(삼항)는 가능한 피하는 편이 좋음. 4개 이상(다항)은 특별한 이유가 필요하며, 특별한 이유가 있어도 사용하면 안 됨.
    • 최선은 입력 인수가 없는 경우, 차선은 입력 인수가 1개 뿐인 경우.
    • SetupTeardwonIncluder.render(pageData)는 이해하기 쉬움. pageData 객체 내용을 렌더링하겠다는 뜻
    • 많이 쓰는 단항 형식
      • 함수에 인수 1개를 넘기는 경우
        • 인수에 질문을 던지는 경우
          • boolean fileExists("MyFile")
        • 인수를 뭔가로 변환해 결과를 반환하는 경우
          • InputStream fileOpen("MyFile")은 String 형의 파일 이름을 InputStream으로 변환함
        • 이벤트 함수
          • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꿈
          • ex) passwordAttemptFailedNtimes(int attempts)
          • 이벤트 함수는 이벤트라는 사실에 코드에 명확히 드러나도록 조심해서 사용
      • 위의 경우가 아니라면 단항 함수는 가급적 피함
        • 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려줌
          • void includeSetupPageInto(StringBuffer pageText)는 피함.
          • StringBuffer transform(StringBuffer in)이 void transform(StringBuffer out)보다 좋음
    • 플래그 인수
      • 함수로 부울 값을 넘기는 관례는 좋지 않음.
        • 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈
        • render(true)라는 코드는 헷갈리기 십상(IDE에서 코드 위로 커서를 가져가면 render(boolean isSuite)라는 정보가 뜨지만 그다지 큰 도움은 안 됨)
          • renderForSuite()와 renderForSingleTest()라는 함수로 나눠야 함
    • 이항 함수
      • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움
      • 이항 함수가 적절한 경우
        • 직교 좌표계 점처럼 인수 2개로 한 값을 표현하는 경우
          • Point p = new Point(0, 0)
      • assertEquals(expected, actual)에도 문제가 있음. 두 인수는 자연적인 순서가 없어서 expected 인수에 actual 값을 넣게 될 수 있음.
      • 불가피하게 이항 함수를 써야 하는 경우도 있지만, 이항 함수에는 그만큼의 위험이 있으면 가급적 단항 함수로 바꿈
        • writeField(outputStream, name)의 경우
          • 방법 1) writeField 메서드를 outputStream 클래스 구성원으로 만들어 outputStream.writeField(name)으로 호출
          • 방법 2) outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않음
          • 방법 3) FieldWriter라는 새 클래스를 만들어 생성자에서 outputStream을 받고 write 메서드를 구현함
    • 삼항 함수
      • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어려움
      • assertEquals(message, expected, actual)은 좋지 않은 함수
      • 반면 assertEquals(1.0, amount, .001)은 그럴 수 있음. 부동소수점 비교가 상대적임.
    • 인수 객체
      • 인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성 살피기
      • Circle makeCircle(double x, double y, double radius); 를 Circle makeCircle(Point center, double radius);로 변경
        • 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 됨
    • 인수 목록
      • 때로는 인수 개수가 가변적인 함수도 필요함
      • 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있음. 하지만 이를 넘어서는 인수 사용은 문제가 있음.
    • 동사와 키워드
      • 함수의 의도나 인수의 순서와 의도를 표현하려면 좋은 함수 이름이 필수
      • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 함
        • write(name)보다 writeField(name)이 더 적절. 써야 할 이름이 Field라는 사실이 분명히 드러남
      • 함수 이름에 키워드를 추가하는 형식(함수 이름에 인수 이름을 넣음)
        • assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋음. 인수 순서 기억할 필요 없음.
  • 부수 효과를 일으키지 마라!
    • 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하는 것
      • 예상치 못하게 클래스 변수를 수정하거나 함수로 넘어온 인수나 시스템 전역 변수를 수정
    • 부수 효과는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래함
    • ex) 암호를 확인하는 checkPassword 함수에서 Session.initialize()를 호출하여 부수 효과로 세션을 초기화함
      • 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처함
      • 시간적인 결합 초래 → checkPassword 함수를 특정 상황(세션을 초기화해도 괜찮은 경우)에서만 호출 가능하게 함
      • 시간적인 결합이 필요하다면 함수 이름에 checkPasswordAndInitializeSession이라고 분명히 명시하는 게 낫다(물론 함수가 한 가지만 한다는 규칙 위반)
    • 출력 인수
      • 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없음
      • 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문
      • 함수에서 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경하는 방식을 택함(ex: report.appendFooter())
  • 명령과 조회를 분리하라!
    • 함수는 뭔가를 수행하거나 뭔가에 답하거나(객체 상태를 변경하거나 객체 정보를 반환하거나) 둘 중 하나만 해야 하고 둘 다 하면 안 됨
    • 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하는 true를 반환하고 실패하면 false를 반환하는 함수
      • public boolean set(String attribute, String value);로 선언되면, if(set("username", "unclebob"))...과 같은 괴상한 코드가 나옴
      • set이라는 함수 이름을 setAndCheckIfExists라고 바꾸는 방법도 있지만, if 문에 넣고 보면 여전히 어색함
      • 진짜 해결책은 명령과 조회를 분리해서 혼란을 애초에 뿌리뽑는 것
  • 오류 코드보다 예외를 사용하라!
    • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반
    • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해짐
    • Try/Catch 블록 뽑아내기
      • 정상 동작과 오류 처리 동작이 뒤섞이지 않도록, try/catch 블록을 별도 함수로 뽑아내는 편이 좋음
    • 오류 처리도 한 가지 작업이다.
      • 오류를 처리하는 함수는 오류만 처리해야 마땅함
      • 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally 문으로 끝나야 함
    • Error.java 의존성 자석
      • 다른 클래스에서 Error enum을 import해서 사용해야 하는 경우
        • Error enum이 변한다면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 하므로, Error 클래스 변경이 어려워짐
        • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있음(OCP : Open Closed Principle)
  • 반복하지 마라!
    • 중복을 없애면 모듈 가독성이 크게 높아짐
    • 객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앰
    • 어떤 면에서 중복 제거 전략
      • 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming)
  • 구조적 프로그래밍
    • 에츠허르 데이크스트라(Edsger Dijkstra)의 구조적 프로그래밍 원칙
      • 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)가 하나만 존재해야 함
      • 함수는 return 문이 하나여야 함
      • 루프 안에서 break나 continue를 사용해서는 안 되며 goto는 절대로 안 됨
      • 이것은 함수가 아주 클 때만 상당한 이익을 제공함
    • 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮음
  • 함수를 어떻게 짜죠?
    • 소프트웨어를 짜는 행위 = 글짓기
    • 처음에는 길고 복잡함. 들여쓰기 단계도 많고 중복된 루프도 많음. 인수 목록도 아주 긺. 이름을 즉흥적이고 코드는 중복됨.
      • 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만듦
      • 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거함.
      • 메서드를 줄이고 순서를 바꿈. 때로는 전체 클래스를 쪼개기도 함.
      • 이 와중에도 코드는 항상 단위 테스트를 통과함!
  • 결론
    • 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기가 쉬워짐

 


4. 주석

  • 프로그래밍 언어를 치밀하게 사용해 의도를 표현하는 능력이 있다면 주석은 거의 필요하지 않음
  • 주석은 오래될수록 코드에서 멀어지고, 주석을 유지하고 보수하기란 현실적으로 불가능함
  • 주석은 나쁜 코드를 보완하지 못한다
    • 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋음
  • 코드로 의도를 표현하라!
    • 첫번째 코드보다 두번째 코드가 더 나음
    • 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분
  • 좋은 주석
    • 법적인 주석
      • 저작권 정보와 소유권 정보
    • 정보를 제공하는 주석
      • 기본적인 정보를 주석으로 제공하는 것이 유용하더라도 가능하다면, 함수 이름에 정보를 담는 편이 더 좋다.
    • 의도를 설명하는 주석
      • 때때로 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명함
    • 의미를 명료하게 밝히는 주석
      • 모호한 인수나 반환값은 그 의미를 읽기 좋게 표현
      • 되도록 인수나 반환값 자체를 명확하게 만드는 것이 좋음
    • 결과를 경고하는 주석
      • 다른 프로그래머에거 결과를 경고할 목적으로 주석을 사용
        • //여유 시간이 충분하지 않다면 실행하지 마십시오
        • /* SimpleDateFormat은 스레드에 안전하지 못하다. 따라서 각 인스턴스를 독립적으로 생성해야 한다. */
      • 요즘에는 @Ignore 속성을 이용해 테스트 케이스를 꺼버리고, 구체적인 설명은 @Ignore 속성에 문자열로 넣어줌
        • @Ignore("실행이 너무 오래 걸린다")
    • TODO 주석
      • 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술함
        • 더 이상 필요 없는 기능을 삭제하라는 알림, 누군가에게 문제를 봐달라는 요청, 더 좋은 이름을 떠올려달라는 부탁, 앞으로 발생할 이벤트에 맞춰 코드를 고치라는 주의 등
      • 어떤 용도로 사용하든 시스템에 나쁜 코드를 남겨놓는 핑계가 되어서는 안 됨
      • 요즘 대다수 IDE는 주기적으로 TODO 주석을 점검해 없애도 괜찮은 주석은 없애라고 권고함
    • 중요성을 강조하는 주석
      • 자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위함
      • /* 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다. 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다. */
    • 공개 API에서 Javadocs
      • 공개 API를 구현한다면 반드시 훌륭한 Javadocs를 작성할 것
      • Javadocs 역시 독자를 오도하거나, 잘못 위치하거나, 그릇된 정보를 전달할 가능성이 존재함
  • 나쁜 주석
    • 대다수의 주석이 나쁜 주석임.
    • 주절거리는 주석
      • 주석을 달기로 결정했다면 충분한 시간을 들여 최고의 주석을 달도록 노력
    • 같은 이야기를 중복하는 주석
      • 주석은 코드보다 더 많은 정보를 제공하지 못함
      • 자칫하면 코드보다 주석을 읽는 시간이 더 오래 걸림
      • 쓸모없고 중복된 Javadocs는 코드만 지저분하고 기록이라는 목적에 전혀 기여하지 못함
    • 오해할 여지가 있는 주석
    • 의무적으로 다는 주석
      • 모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석음
      • 오히려 코드만 헷갈리게 만들며, 거짓말할 가능성을 높이고, 잘못된 정보를 제공할 여지만 만듦
    • 이력을 기록하는 주석
      • 예전에는 소스 코드 관리 시스템이 없어서 모든 모듈 첫머리에 변경 이력을 기록하고 관리했으나 이제는 혼란만 가중할 뿐이므로 완전히 제거하는 편이 좋음
    • 있으나 마나 한 주석
      • 너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석
    • 무서운 잡음
      • 때로는 Javadocs도 문서를 제공해야 한다는 잘못된 욕심으로 잡음을 만들 수 있음
    • 함수나 변수로 표현할 수 있다면 주석을 달지 마라
      • 주석이 필요하지 않도록 코드를 개선함
    • 위치를 표시하는 주석
      • 소스 파일에서 특정 위치를 표시하려 주석을 사용하는 경우
      • 일반적으로 가독성만 낮추므로 제거해야 마땅함
    • 닫는 괄호에 다는 주석
      • 닫는 괄호 } 에 주석을 달아야겠다는 생각이 든다면 함수를 줄이려 시도하자
    • 공로를 돌리거나 저자를 표시하는 주석
      • 소스 코드 관리 시스템이 있으므로 저자 이름으로 코드를 오염시킬 필요가 없음
    • 주석으로 처리한 코드
      • 주석으로 처리된 코드는 다른 사람들이 지우기를 주저하고, 그러면서 쓸모 없는 코드가 점차 쌓이게 됨
      • 소스 코드 관리 시스템이 코드를 기억해주므로 이제는 주석으로 처리할 필요 없이 그냥 삭제할 것
    • HTML 주석
      • (Javadocs와 같은) 도구로 주석을 뽑아 웹 페이지에 올릴 작정이라면 주석에 HTML 태그를 삽입해야 하는 책임은 프로그래머가 아니라 도구가 져야 함
    • 전역 정보
      • 주석을 달아야 한다면 근처에 있는 코드만 기술함
      • 코드 일부에 주석을 달면서 시스템의 전반적인 정보를 기술하지 말 것
    • 너무 많은 정보
      • 주석에다 흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓지 말 것
    • 모호한 관계
      • 주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 함
      • 독자가 주석과 코드를 읽어보고 무슨 소리인지 알아야 함
      • 주석 자체가 다시 설명을 요구하는 일이 없어야 함
    • 함수 헤더
      • 짧은 함수는 긴 설명이 필요 없음
    • 비공개 코드에서 Javadocs
      • 공개하지 않을 코드라면 Javadocs는 쓸모가 없음
      • 유용하지 않을 뿐만 아니라 Javadocs 주석이 요구하는 형식으로 인해 코드만 보기 싫고 산만해짐