Hayden's Archive
클린코드(Clean Code) 3, 4장 - 로버트 C. 마틴 본문
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단을 넘어서면 안 됨
- 그래야 함수는 읽고 이해하기 쉬워짐
- if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미
- 한 가지만 해라!
- 함수는 한 가지를 해야 함. 그 한 가지를 잘해야 함. 그 한 가지만을 해야 함.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것
- 함수를 만드는 이유 : 큰 개념을 (다시 말해, 함수 이름을) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서임
- 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈
- 함수 내 섹션
- 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다
- 함수당 추상화 수준은 하나로!
- 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 함
- 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈림
- 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가함
- 위에서 아래로 코드 읽기 : 내려가기 규칙
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
- 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 옴
- 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아짐
- 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)보다 좋음
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려줌
- 함수에 인수 1개를 넘기는 경우
- 플래그 인수
- 함수로 부울 값을 넘기는 관례는 좋지 않음.
- 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈
- render(true)라는 코드는 헷갈리기 십상(IDE에서 코드 위로 커서를 가져가면 render(boolean isSuite)라는 정보가 뜨지만 그다지 큰 도움은 안 됨)
- renderForSuite()와 renderForSingleTest()라는 함수로 나눠야 함
- 함수로 부울 값을 넘기는 관례는 좋지 않음.
- 이항 함수
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움
- 이항 함수가 적절한 경우
- 직교 좌표계 점처럼 인수 2개로 한 값을 표현하는 경우
- Point p = new Point(0, 0)
- 직교 좌표계 점처럼 인수 2개로 한 값을 표현하는 경우
- assertEquals(expected, actual)에도 문제가 있음. 두 인수는 자연적인 순서가 없어서 expected 인수에 actual 값을 넣게 될 수 있음.
- 불가피하게 이항 함수를 써야 하는 경우도 있지만, 이항 함수에는 그만큼의 위험이 있으면 가급적 단항 함수로 바꿈
- writeField(outputStream, name)의 경우
- 방법 1) writeField 메서드를 outputStream 클래스 구성원으로 만들어 outputStream.writeField(name)으로 호출
- 방법 2) outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않음
- 방법 3) FieldWriter라는 새 클래스를 만들어 생성자에서 outputStream을 받고 write 메서드를 구현함
- writeField(outputStream, name)의 경우
- 삼항 함수
- 인수가 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)
- 다른 클래스에서 Error enum을 import해서 사용해야 하는 경우
- 반복하지 마라!
- 중복을 없애면 모듈 가독성이 크게 높아짐
- 객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앰
- 어떤 면에서 중복 제거 전략
- 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming)
- 구조적 프로그래밍
- 에츠허르 데이크스트라(Edsger Dijkstra)의 구조적 프로그래밍 원칙
- 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)가 하나만 존재해야 함
- 함수는 return 문이 하나여야 함
- 루프 안에서 break나 continue를 사용해서는 안 되며 goto는 절대로 안 됨
- 이것은 함수가 아주 클 때만 상당한 이익을 제공함
- 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮음
- 에츠허르 데이크스트라(Edsger Dijkstra)의 구조적 프로그래밍 원칙
- 함수를 어떻게 짜죠?
- 소프트웨어를 짜는 행위 = 글짓기
- 처음에는 길고 복잡함. 들여쓰기 단계도 많고 중복된 루프도 많음. 인수 목록도 아주 긺. 이름을 즉흥적이고 코드는 중복됨.
- 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만듦
- 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거함.
- 메서드를 줄이고 순서를 바꿈. 때로는 전체 클래스를 쪼개기도 함.
- 이 와중에도 코드는 항상 단위 테스트를 통과함!
- 결론
- 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기가 쉬워짐
4. 주석
- 프로그래밍 언어를 치밀하게 사용해 의도를 표현하는 능력이 있다면 주석은 거의 필요하지 않음
- 주석은 오래될수록 코드에서 멀어지고, 주석을 유지하고 보수하기란 현실적으로 불가능함
- 주석은 나쁜 코드를 보완하지 못한다
- 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋음
- 코드로 의도를 표현하라!
- 첫번째 코드보다 두번째 코드가 더 나음
- 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분
- 좋은 주석
- 법적인 주석
- 저작권 정보와 소유권 정보
- 정보를 제공하는 주석
- 기본적인 정보를 주석으로 제공하는 것이 유용하더라도 가능하다면, 함수 이름에 정보를 담는 편이 더 좋다.
- 의도를 설명하는 주석
- 때때로 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명함
- 의미를 명료하게 밝히는 주석
- 모호한 인수나 반환값은 그 의미를 읽기 좋게 표현
- 되도록 인수나 반환값 자체를 명확하게 만드는 것이 좋음
- 결과를 경고하는 주석
- 다른 프로그래머에거 결과를 경고할 목적으로 주석을 사용
- //여유 시간이 충분하지 않다면 실행하지 마십시오
- /* SimpleDateFormat은 스레드에 안전하지 못하다. 따라서 각 인스턴스를 독립적으로 생성해야 한다. */
- 요즘에는 @Ignore 속성을 이용해 테스트 케이스를 꺼버리고, 구체적인 설명은 @Ignore 속성에 문자열로 넣어줌
- @Ignore("실행이 너무 오래 걸린다")
- 다른 프로그래머에거 결과를 경고할 목적으로 주석을 사용
- TODO 주석
- 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술함
- 더 이상 필요 없는 기능을 삭제하라는 알림, 누군가에게 문제를 봐달라는 요청, 더 좋은 이름을 떠올려달라는 부탁, 앞으로 발생할 이벤트에 맞춰 코드를 고치라는 주의 등
- 어떤 용도로 사용하든 시스템에 나쁜 코드를 남겨놓는 핑계가 되어서는 안 됨
- 요즘 대다수 IDE는 주기적으로 TODO 주석을 점검해 없애도 괜찮은 주석은 없애라고 권고함
- 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술함
- 중요성을 강조하는 주석
- 자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위함
- /* 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다. 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다. */
- 공개 API에서 Javadocs
- 공개 API를 구현한다면 반드시 훌륭한 Javadocs를 작성할 것
- Javadocs 역시 독자를 오도하거나, 잘못 위치하거나, 그릇된 정보를 전달할 가능성이 존재함
- 법적인 주석
- 나쁜 주석
- 대다수의 주석이 나쁜 주석임.
- 주절거리는 주석
- 주석을 달기로 결정했다면 충분한 시간을 들여 최고의 주석을 달도록 노력
- 같은 이야기를 중복하는 주석
- 주석은 코드보다 더 많은 정보를 제공하지 못함
- 자칫하면 코드보다 주석을 읽는 시간이 더 오래 걸림
- 쓸모없고 중복된 Javadocs는 코드만 지저분하고 기록이라는 목적에 전혀 기여하지 못함
- 오해할 여지가 있는 주석
- 의무적으로 다는 주석
- 모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석음
- 오히려 코드만 헷갈리게 만들며, 거짓말할 가능성을 높이고, 잘못된 정보를 제공할 여지만 만듦
- 이력을 기록하는 주석
- 예전에는 소스 코드 관리 시스템이 없어서 모든 모듈 첫머리에 변경 이력을 기록하고 관리했으나 이제는 혼란만 가중할 뿐이므로 완전히 제거하는 편이 좋음
- 있으나 마나 한 주석
- 너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석
- 무서운 잡음
- 때로는 Javadocs도 문서를 제공해야 한다는 잘못된 욕심으로 잡음을 만들 수 있음
- 함수나 변수로 표현할 수 있다면 주석을 달지 마라
- 주석이 필요하지 않도록 코드를 개선함
- 위치를 표시하는 주석
- 소스 파일에서 특정 위치를 표시하려 주석을 사용하는 경우
- 일반적으로 가독성만 낮추므로 제거해야 마땅함
- 닫는 괄호에 다는 주석
- 닫는 괄호 } 에 주석을 달아야겠다는 생각이 든다면 함수를 줄이려 시도하자
- 공로를 돌리거나 저자를 표시하는 주석
- 소스 코드 관리 시스템이 있으므로 저자 이름으로 코드를 오염시킬 필요가 없음
- 주석으로 처리한 코드
- 주석으로 처리된 코드는 다른 사람들이 지우기를 주저하고, 그러면서 쓸모 없는 코드가 점차 쌓이게 됨
- 소스 코드 관리 시스템이 코드를 기억해주므로 이제는 주석으로 처리할 필요 없이 그냥 삭제할 것
- HTML 주석
- (Javadocs와 같은) 도구로 주석을 뽑아 웹 페이지에 올릴 작정이라면 주석에 HTML 태그를 삽입해야 하는 책임은 프로그래머가 아니라 도구가 져야 함
- 전역 정보
- 주석을 달아야 한다면 근처에 있는 코드만 기술함
- 코드 일부에 주석을 달면서 시스템의 전반적인 정보를 기술하지 말 것
- 너무 많은 정보
- 주석에다 흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓지 말 것
- 모호한 관계
- 주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 함
- 독자가 주석과 코드를 읽어보고 무슨 소리인지 알아야 함
- 주석 자체가 다시 설명을 요구하는 일이 없어야 함
- 함수 헤더
- 짧은 함수는 긴 설명이 필요 없음
- 비공개 코드에서 Javadocs
- 공개하지 않을 코드라면 Javadocs는 쓸모가 없음
- 유용하지 않을 뿐만 아니라 Javadocs 주석이 요구하는 형식으로 인해 코드만 보기 싫고 산만해짐