본문 바로가기

리뷰/책 리뷰

[IT/리뷰] Clean Code

728x90

Clean Code(클린 코드) : 네이버 도서 (naver.com)

 

Clean Code(클린 코드) : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

저번 <리팩터링 2판>에 이어서 <클린 코드>를 읽었다. 이 책에서도 <리팩터링 1판>에 대한 내용을 소개하고 있는 만큼 <리팩터링> 책은 훌륭했다. 그런데 <클린 코드> 역시 꽤나 좋았다. 다만 개인적으로는 <리팩터링 2판>에서도 단계적으로 깔끔한 코드를 작성하는 방법을 설명해준 것이 더 좋았다. <클린 코드>도 단계별로 설명을 해주기는 하나, <리팩터링 2판>이 더 직관적이었던 것 같고 그만큼 더 쉽게 읽혔다. <클린 코드>는 읽는데도 좀 오래 걸렸다.

 

그리고 첫 장부터 커닝험, 부치, 비야네 등 유명한 사람들이 정의한 클린 코드를 설명해주는데 이런 것들이 신기했다. 이 사람들끼리는 서로 친할까 궁금하기도 하고 공부할 때 들어본 이름들이 나오니 반갑기도 했다.

 

이 책은 Java로 Clean Code를 작성하는 방법에 대해 소개를 한다. C++을 위주로 쓰고 Java는 잘 모르지만, Java의 언어적 특성 때문에 이해가 안 될 정도의 난이도는 없다. 코드 자체는 그랬지만 코드와 설명을 왔다 갔다 하면서 읽고 이해하기에는 어느 정도 불편함이 있다.

그래서 앞부분의 클린 코드 작성 규칙들은 정말 쉽게 읽힌다. 뒤쪽 14장부터 코드를 개선해 나가면서 설명을 하는데, 여기서는 정신없어진다. 내용 자체가 여러 생각을 해야 하고, 읽기도 어렵다.

 

책의 전반적인 내용은 <리팩터링 2판>과 겹치는 부분도 많다. 그만큼 코드를 작성 및 개선하는 데 있어 필요한 원칙들은 어느 정도 공통적인 특징을 갖고 있는 것이라 생각됐다. 그러면서 또다시 내가 작성한 코드에 대입해 생각해보는데 아직도 많은 개선이 필요하다는 것을 느꼈다. 회사의 규칙상 지키지 못하는 원칙들도 있지만 적용할 수 있는 원칙들도 꽤나 지켜지지 않고 있다.

 

그리고 이 책은 <Agile Software Development: Principles, Patterns, and Practices>의 프리퀄 격인 책이라고 한다. 위의 책이 실무 기법을 설명한 책이라면, <클린 코드>는 코드 기반의 책이라고 한다. 나중에 찾아서 읽어도 좋을 것 같다. 번역서 이름은 <클린 소프트웨어(애자일 원칙과 패턴, 그리고 실천 방법)>인 것으로 보인다.

 

오늘은 <클린 코드>를 읽으면서 내용 중에서 마음에 들거나 중요하다 생각되는 내용들을 일부분씩만 따다가 나열하는 식으로 정리를 한다. 더 자세한 내용이나 다른 내용들은 유명한 책인 만큼 정리된 다른 블로그들도 많고 도서관 대여도 쉬울 것이라 생각해 생략한다.


  • 장인 정신을 익히는 데는 이론과 실전이 모두 필요하다. 자전거를 타기위해 이론을 모두 설명해 줄 수 있다. 그렇다고 하더라도 처음 타는 사람이라면 100% 넘어진다. 구현도 마찬가지다. 깨끗한 코드를 작성하기 위한 그럴듯한 원칙을 모두 알더라도 깨끗한 코드가 나오지 않는다. 스스로 넘어져 고생도 해보고, 남이 넘어지면서 고생하는 것도 봐야 한다.이 고생을 책을 통해서 먼저 해보는 것이라 생각하자.
  • 인수의 개수는 0 개인 것이 바람직하며, 1개, 2개, 3개 순으로 안 좋아진다. 4개 이상의 인수는 특별한 이유가 있더라도 해서는 안 된다. 보통 3개 정도의 인자가 필요하다면 서브 클래스, 또는 클래스 변수(멤버 변수) 등을 이용해 묶을 수 있는 자료들을 확인해라.
    출력용 인수는 쓰지 않아야 한다. 코드를 볼 때 직관적이지 못하며, 함수 정의를 봐야 그제야 알 수 있다.
    반환도 웬만하면 쓰지 않는 것이 좋다. 아래 3개 중에 마지막 것이 가장 바람직하다.
    • appendFooter(report)
    • report = appendFooter()
    • report.appendFooter()
  • 어떻게 출력도 없고 인자도 0개를 맞춰서 개발하냐면, 함수가 정말 1가지 일만 한다면 가능하다.
    하나의 일만 하라는 건, 같은 추상화 레벨에서 하나의 일만 하라는 것이다. 그러니 점점 아래로 내려갈수록 구체적인 함수들을 쪼개 놓을 수 있다.
  • 반환보다는 예외를 사용해라. 그래야 코드가 깔끔하다.
    다만, try-catch 문은 더럽다. 구조상 혼란을 일으키며 정상 동작과 오류 동작을 섞는다. 그러니 try/catch 블록을 별도 함수로 뽑아 놓는 것이 바람직한 분리다.
    오류 처리도 결국 1가지 작업인 셈이다.
  • 특수사례 패턴 등 같은 작업을 처리하는 방식을 다양하게 보다 나은 방안을 제시해준다. 
    책에서의 예시가 길지 않아 이해는 쉽지만 여러 활용을 보기는 어렵다. 그래도 확실히 보다 낫다는 것이 이해 가는 건 좋다. 코드를 볼 때 같은 기능을 수행하는 함수를 어디에 둬야 할지, 무엇을 wrapping 함수로 만들지 등을 이해할 수 있다.
  • null 반환이 많으면 문제다. 무엇이 null인지 항상 확인해야 한다. 그리고 저 아래에 호출된 함수에서 null이 발생한 것을 누가 어떻게 처리하겠는가. 차라리 예외를 던지고 특수사례 객체를 반환해라.
  • null 반환뿐만 아니라 null 전달도 하면 안 된다. 대다수 프로그래밍 언어에서는 인자로 전달받은 null을 적절히 처리할 방법이 없다. 자바에서 임의 예외를 만들거나 assert 하는 방안이 있겠지만 정해져 있지 않아 혼란스럽다. 
    어차피 null로 인자가 전달되면 문제가 있는 것이다. 그러니 null 전달을 금지시키는 것이 바람직하다.
  • 작은 클래스를 여럿 두면 클래스마다 역할을 확인하느라 힘들다고 하는 사람들이 있다. 하지만 작은 클래스 여럿을 두나 큰 글래스를 조금만 두나 프로그램에 들어가는 부품의 수는 같다. 작은 클래스로 만들어, 클래스를 수정하는 이유가 단 하나만 있도록 유지하라. 즉, SRP를 유지하라는 것이다.
    한 가지 방법으로 클래스 멤버 변수를 줄여라. 함수 안으로 넣으라는 말이 아니다. 특정 멤버 변수들을 사용하는 메서드가 몇 개밖에 없다면 그 변수들은 따로 클래스로 빼라.
    그러면 메서드의 인자도 줄게 된다. 클래스의 응집도는 떨어질 텐데, 걱정할 필요 없다. 몇몇 메서드만 사용하는 몇몇 멤버 변수가 생긴 시점이 클래스를 분리할 시점이라는 것이다.
  • 결합도를 낮추면 테스트 하기 수월해지며 유연성과 재사용성도 당연히 올라간다. 자연스럽게 DIP도 따른다.
  • 켄트 백의 4가지 규칙을 따르면 우수한 설계가 나오고, 코드 구조와 설계 파악이 쉬워지며, SRP나   DIP를 적용하기 쉬워진다. 아래 규칙은 순서도 중요하다. 특히 2~4는 리펙터링 과정이다.
    1. 모든 테스트를 실행한다.
    2. 중복을 없앤다.
    3. 프로그래머의 의도를 표현한다.
    4. 클래스와 메서드의 수를 최소로 줄인다.
  • 객체는 처리의 추상화다. 스레드는 일정의 추상화다.
    - 제임스 O. 코플리엔
  • 동시성과 깔끔한 코드는 양립하기 어렵다.
  • 동시성은 결합(coupling)을 없애기 위함이다. 무엇과 언제를 분리하기 위한 것으로 구조와 효율을 극적으로 나눈다.
    동시성과 관련해 미신과 오해가 있다.
    • 동시성은 항상 성능을 높여준다 -> 때로는 높여준다. 대기시간이 길어 여러 스레드가 프로세서를 공유할 수 있거나 여러 프로세서가 동시에 처리할 독립적 계산이 많을 때가 그렇다.
    • 동시성을 구현해도 설계는 변하지 않는다. -> 무엇과 언제를 분리하면서 크게 달라진다.
    • 웹 또는 EJB 컨테이너를 쓰면 동시성을 이해할 필요가 없다.
  • 동시성에 대한 타당한 설명이다.
    • 동시성은 다소 부하를 유발한다. 성능 측면에서도, 코드 작성 측면에서도.
    • 동시성은 복잡하다.
    • 일반적으로 동시성 버그는 재현이 어렵다. 진짜 결함이 아닌 일회성 문제로 여겨 무시하기 쉽다.
    • 동시성 구현 시 근본적인 설계 전략을 재고해야 한다.
  • 동시성 방어 원칙
    1. 단일 책임 원칙(SRP) : 메서드/클래스/컴포넌트 변경의 이유가 단 하나여야 함. 복잡성 때문이라도 필요한 원칙. 다른 코드랑 동시성 코드는 분리해야 함.
    2. 따름 정리(corollary) - 자료 범위를 제한하라 : 임계 영역을 보호하라. 임계 영역의 수도 줄여라. 자료를 캡슐화하라.
    3. 따름 정리 - 자료 사본을 사용하라 : 처음부터 공유하지 않는 것이 가장 좋다.
    4. 따름 정리 - 스레드는 가능한 독립적으로 구현하라 : 다른 스레드와 동기화가 필요 없는 스레드는 좋다.
  • 스레드 코드 테스트하기
    1. 말이 안 되는 실패는 잠정적으로 스레드 문제로 취급하라.
    2. 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
    3. 다중 스레드 쓰는 부분은 다양한 환경에 쉽게 끼워 넣을 수 있도록 스레드 코드를 구현하라.
    4. 프로세서 수보다 많은 스레드를 돌려보라.
    5. 다른 플랫폼에서 돌려보라.
    6. 코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라.
  • 다중 스레드 환경에서 스레드 동작 순서를 임의로 바꿔가면서 테스트를 할 수 있게 보조 코드를 삽입해야 한다. 그러나 배포용에는 이 코드가 없어야 한다. 
    테스트용이더라도 보조 코드 삽입 위치를 하나씩 넣어주기는 어렵다. 그러니 JIGGLE(흔들기) 클래스에 메서드를 하나 두고 이 메서드를 비우거나 SLEEP/YIELD 등을 수행하게 하라.
    항상 코드의 스레드가 다르게 동작하게 하고 많이 테스트해보면 문제가 드러날 수 있다.
  • 기본적으로 스레드로 인해 문제가 발생하지 않도록 스레드의 영향도 범위를 가장 작게 해야 한다.
  • 14장과 15장의 설명은 그나마 코드가 설명과 붙어있어서 이해하기 쉽다. 리팩터링 과정과 클린 코드 원칙을 쉽게 확인할 수 있다.
    16장은 코드가 별첨에 있어 보기 어렵고 내용도 길어 읽는 호흡이 길어진다. 17장의 냄새도 참고해야 하는데, 냄새는 우선 넘겨서 읽었다.
  • 17장은 <리팩터링 2판>에서 본 것과 같이 나쁜 코드에서 나는 냄새를 표현한다. 실제로 <리팩터링 1판>을 읽고 저자가 몇 개의 냄새를 추가했다고 한다. 그중에서도 몇 개만 정리한다.
    • G5. 중복
      중복을 발견하면 추상화 기회로 간주하라. 
      하위 루틴이나 다른 클래스로 중복을 분리하라. 추상화 수준이 올라야 구현이 빠르고 오류가 적다.
      미묘한 유형으로는 일련의 switch case문이나 if else문으로 같은 조건을 거듭 확인하는 중복이다. 이때는 다형성으로 대체하라.
      더더욱 미묘한 것은 알고리즘은 유사하나 코드가 서로 다른 경우다. 중복은 중복이기 때문에 템플릿 메서드 패턴이나 전략 패턴을 적용해 중복을 제거하라.
    • G6. 추상화 수준이 올바르지 못하다
      저차원 파생 클래스와 고차원 추상 클래스는 철저히 분리되어야 한다. 예를 들어 세부 구현과 관련된 상수, 변수, 유틸리티 함수는 기초 클래스에 있으면 안 된다. 기초 클래스는 구현 정보에 무지해야 하며 섞이면 안 된다.
    • G7. 기초 클래스가 파생 클래스에 의존한다
      기초 클래스는 파생 클래스를 몰라야 한다. 단, FSM(유한 상태 머신)에서 파생 클래스 개수가 확실히 고정되어 있어 기초 클래스에서 파생 클래스를 선택하기도 한다. 이 예외 말고는 몰라야 한다.
    • G8. 과도한 정보
      잘 정의된 모듈은 인터페이스가 아주 작다. 그래서 결합도가 낮다. 그러면서 작은 인터페이스로도 많은 동작이 가능하다.
      부실하게 정의된 인터페이스는 반드시 호출해야 하는 온갖 함수를 제공해, 결합도가 높다.
      우수한 소프트웨어 개발자는 클래스나 모듈 인터페이스에 노출 함수를 제한할 줄 알아야 한다. 함수가 아는 변수도 작을수록 좋다. 클래스에 들어있는 인스턴스 변수 수도 작을수록 좋다.
      자료, 유틸리티 함수, 상수, 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라. 하위 클래스에서 필요하다고, protected 변수나 함수를 마구 생성하지 마라. 인터페이스를 매우 작게, 그리고 매우 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라.
    • G14. 기능 욕심
      클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지, 다른 클래스의 변수와 함수에 관심을 가져서는 안 된다. 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작하거나 모든 값을 빼와서 무언가를 하려고 한다면 그건 그 객체 클래스의 범위를 욕심내는 것이다.
      때로는 어쩔 수 없이 사용하는 경우도 있다. 예를 들어 HourlyEmployeeReoport 클래스의 reportHours 메서드가 HourlyEmployee 클래스의 것들을 뽑아와 보고서를 만들 수도 있다. 그러나, 이 경우는 HourlyEmployee 클래스가 보고서 형식을 알 필요는 없기 때문에 적절한 경우로 볼 수 있다. reportHours 메서드를 HourlyEmployee 클래스로 옮긴다면 객체 지향 설계의 여러 원칙을 위반한 것이다.
    • G15. 선택자 인수
      함수 호출 끝에 Bool 값이 오지 않도록 해라. false를 true로 바꾼다면, 무슨 동작이 바뀌는지 찾아봐야 하는 게 문제다. 부울 인수뿐만 아니라 enum, int 등을 통해 함수 동작을 제어하려 하는 인자는 모두 잘못됐다. 차라리 새로운 함수를 만드는 편이 좋다.
    • G31. 숨겨진 시간적인 결합
      때로는 함수들 간의 시간적 결합이 필요하다. 이를 숨겨서는 안 된다. 일종의 연결 소자로써 리턴 값을 다음 호출 함수의 인자로 넘겨라.
  • 동시성은 테스트하기 어렵다. IBM의 ConTest라는 것을 이용하면 스레드 문제를 찾아내는 확률을 높일 수 있다. 그래도 결국에는 테스트 횟수가 적으면 잘 발견되지 않을 수 있다.
  • 동시성에 대한 공부를 위해서는 더그 리(Doug Lea)의 <Concurrent Programming in Java: Design Principles and Patterns>를 추천한다.
728x90