Clean Code 책 정리

Updated:


Clean Code 책을 선택한 이유

클린코드

제목을 보고 본능적으로 끌렸던 것 같다. 어느 언어, 기술, 프레임워크를 접하든 개발을 하는 사람이라면 코드는 계속 보고 작성할 수 밖에 없다. 내 경험상 개인프로젝트를 할 때는 정해진 기한이 없는 경우가 많아서 마음의 여유가 있고 그래서 중간에 코드를 리팩토링 하거나 원하는 방식으로 주석을 다는 방식으로 코드의 방식에 대한 고민이 별로 없었다. 하지만 협업을 하다보면 제한된 프로젝트 기한이 주는 촉박함도 있고 같이 협업하는 사람과의 코드 작성 방식의 차이도 존재하다보니 개발을 하면 할수록 소위 말해 ‘개판’이 되기가 쉬운 것 같다. 프로젝트 내에 코드의 일관성도 줄어들고 프로젝트가 고도화될 수록 다른 사람의 코드는 물론이고 내 코드를 해석하는 것도 점점 시간이 오래걸리면서 생산성이 갈수록 떨어지는 것 같다. 이런 경험을 하고 나니 이 책의 제목을 보고 목차를 본 순간 무조건 읽어야 한다는 생각이 들었다. 이 글에는 책의 수백페이지를 다 읽지 않아도 책에서 말하고자 하는 바를 몇분 이내로 다 알 수 있도록, 그래서 다른 사람에게도 도움이 되지만 내가 다시 봐도 2독, 3독한 효과를 받을 수 있도록 각 챕터별로 요약한 내용을 정리해 보았다.

목차

  1. 깨끗한 코드
  2. 의미 있는 이름
  3. 함수
  4. 주석
  5. 형식 맞추기
  6. 객체와 자료구조
  7. 오류 처리
  8. 경계
  9. 단위 테스트
  10. 클래스
  11. 시스템
  12. 창발성
  13. 동시성
  14. 점진적인 개선
  15. JUnit
  16. SerialDate 리팩터링
  17. 냄새와 휴리스틱

1. 깨끗한 코드

Clean Code란

클린 코드의 정의는 아마 개발자의 수만큼이나 정의도 다양할거라고 생각한다. 아래는 세계적인 개발자들이 정의한 ‘클린 코드’이다.

  • Bjarne Stroustrup(C++ 창시자) : 우아하고 효율적인 코드이다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 깨끗한 코드는 한가지를 제대로 한다.
    • 효율 강조
  • Grady Booch(UML 개발자) : 깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 잘 쓴 문장처럼 읽힌다. 깨끗한 코드는 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.
    • 가독성 강조
  • Dave Thomas(OTI 창립자) : 깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미있는 이름이 붙는다. 특정 목적을 달성하는 방법은 하나만 제공한다. 의존성은 최소이며 각 의존성을 명확히 정의한다.
    • 가독성 강조, TDD(Test Driven Development) 강조
  • Michael Feathers(레거시 코드 활용 전략 저자) : 깨끗한 코드는 언제나 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 손 댈 곳이 없다. 작성자가 이미 모든 사항을 고려했기 때문에 고칠 궁리를 하다 보면 언제나 제자리로 돌아온다.
    • 주의를 기울인 코드 강조
  • Ron Jeffries(Extreme Programming 개발 방법론 창시자) : 나는 주로 중복에 집중한다. 같은 작업을 여러 차례 반복한다면 코드가 아이디어를 제대로 표현하지 못한다는 증거다. 나는 문제의 아이디어를 찾아내 좀 더 명확하게 표현하려 애쓴다. 집합을 추상화 하면 ‘진짜’ 문제에 신경 쓸 여유가 생긴다.
    • 중복을 피하고 추상화된 코드 강조
  • Ward Cunningham(Wiki 창시자) : 코드를 읽으면서 짐작했던 기능을 그대로 수행한다면 깨끗한 코드이다.
    • 명백하고 단순한 코드 강조

보이스카우트 규칙

  • ‘캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라’
    • 코드는 시간이 지나도 언제나 깨끗하게 유지해야 한다
    • 체크아웃 할 때 보다 깨끗한 코드를 확인한다면 코드는 절대 나빠지지 않는다
    • 한꺼번에 많은 시간과 노력을 투자해 코드를 정리할 필요가 없다 (변수 이름 하나 개선, 긴 함수 하나 분할, 복잡한 if문 하나 제거 등)

2. 의미 있는 이름

프로그래머는 코드를 최대한 이해하기 쉽게 짜야 한다. 집중적인 탐구가 필요한 코드가 아닌 대충 훑어봐도 이해할 코드 작성을 목표로 해야 한다. 그러기 위해서는 변수의 이름을 신중하게 결정할 필요가 있다. 소프트웨어에서 이름은 어디에나 쓰인다. 변수, 함수, 인수와 클래스, 패키지, 소스파일, jar/war 파일 등에도 이름을 붙인다. 그렇기 때문에 이름을 잘 지으면 여러모로 편하다.

의도 명확하게 하기

좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다. 변수, 함수, 클래스 이름은 다음의 질문에 모두 답해야 한다.

  • 존재 이유
  • 수행 기능
  • 사용 방법

따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 말이다.

// 나쁜 예시
int d; 

// 좋은 예시
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

클래스 이름

클래스 이름과 객체 이름은 명사가 적합하다.

메서드 이름

메서드 이름은 동사가 적합하다.

한 개념에 한 단어만 사용하기

추상적인 개념 하나에 단어 하나를 선택해 이를 고수해야 한다.

똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다. 요즘 에디터들은 좋은 메서드 이름을 자동으로 추천해주는 기능이 있다.

한 단어를 두가지 목적으로 사용하지 않기

같은 개념처럼 보여도 맥락이 다르다면 다른 단어를 사용해야 한다.

add가 들어간 메서드들이 있다고 가정했을 때 어떤 add 메서드는 기존 값 두개를 더해서 새로운 값을 만들고 어떤 add는 집합에 값 하나를 추가하는 경우가 있다고 하자. 이 경우에는 add라는 단어의 맥락이 다르기 때문에 add 라는 단어가 아닌 insert나 append라는 단어로 구분할 필요가 있다.

의미있는 맥락 추가하기

클래스, 함수, 혹은 접두어 등에 맥락을 부여하는게 좋다.

firstName, lastName, street, houseNumber, city, state 라는 변수들을 훑어보면 주소라는 사실을 금방 알 수 있다. 하지만 어느 메서드가 state 변수 하나만 사용한다면 state가 주소를 의미하는지 알기 어렵다. addr라는 접두어를 추가해 addrFirstName, addrLastName, addrState라고 쓰면 맥락이 분명해진다. 다른 방법으로는 Address라는 클래스를 생성하는 방법도 좋다.

3. 함수

어떤 프로그램이든 가장 기본적인 단위는 함수다. 함수를 잘 활용하면 가독성 있는 코드를 작성할 수 있다.

작게 만들기

함수는 길면 안된다. 최대 100줄을 넘겨서도 안된다. 중첩 구조가 생길만큼 함수가 커져서도 안된다. 그래야 함수는 읽고 이해하기 쉬워진다.

한가지만 하기

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다. 함수가 한 가지만 하느지의 여부를 알 수 있는 방법은 함수의 이름을 지을 때 의미 있는 이름으로 다른 함수를 추출할 수 있다면 함수는 한 가지의 일을 하는게 아닌 것이다.

함수 인수

  • 함수 인수의 가장 이상적인 개수는 0개(무항)이다. 그 다음은 1개이고 그 다음은 2개이다. 3개 이상은 가능한 피하는 편이 좋다. 인수가 많을 수록 이해하기 어려운 코드가 된다.

  • 함수 인수에 bool 값을 넘기는 것도 좋지 않다. 함수가 한꺼번에 여러가지 일을 한다고 말하는 것과 같다.

  • 인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 수 있는지 확인하는게 좋다.

  • Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius); // 가독성 향상
    

부수효과를 일으키지 않기

함수에서 한가지를 하지 않고 몰래 다른 일을 하게 된다면 심각한 부작용을 가져올 수 있다. 함수로 넘어온 인수나 전역변수를 수정할 수도 있다. 많은 경우 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)를 초래한다.

명령과 조회를 분리하기

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나만 해야 한다. 두가지를 같이 하면 혼란을 초래하게 된다.

Try/catch 사용하기

Try/catch를 사용하면 오류 처리 코드가 원래 코드에서 분리되어서 코드가 깔끔해진다. 적용한 try/catch 블록을 별도의 함수로 뽑아내면 더 좋다.

반복하지 않기

중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러 군데를 손 봐야 한다. 소프트웨어의 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다. 관계형 데이터베이스의 정규형식, 객체지향 프로그래밍, 구조적 프로그래밍, AOP, COP 모두 중복 제거를 위해 나온 개념이다. 중복은 소프트웨어에서 모든 악의 근원이다.

결론

모든 시스템은 프로그래머가 설계한 ‘도메인 특화 언어’이다. 함수는 그 언어에서 동사고 클래스는 명사다. 프로그래밍 기술은 언어 설계의 기술이다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 그 언어이다. 이 장의 함수를 잘 만드는 규칙을 잘 따른다면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수를 만들 수 있다. 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는데에 있다. 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 맞아 떨어져야 이야기를 풀어가기가 쉬워진다.

4. 주석

나쁜 코드에 주석을 달지 마라. 새로 짜라 - 브라이언 W.커니핸, P.J. 플라우거

잘 달린 주석은 그 어떤 정보보다 유용하다. 하지만 대부분의 주석은 필요하지 않다. 코드로 보통 의도를 표현하지 못해, 다른말로 실패를 만회하기 위해 주석이 사용되는 경우가 많다. 자신이 만든 난장판을 주석으로 설명하는게 아닌 난장판을 깨끗이 치우는게 낫다.

주석이 필요한 상황이 되면 스스로 생각해야 한다. 코드로 의도를 표현할 방법이 없을까?

주석에 이렇게 부정적인 이유는 주석은 오래될수록 코드에서 멀어지기 때문이다. 프로그래머들이 주석을 유지하고 보수하기는 현실적으로 어렵기 때문에 시간이 지날수록 주석은 거짓말을 하게 된다.

코드는 일부가 여기 저기로 옮겨지기도 하면서 변화하고 진화한다. 하지만 주석이 언제나 코드를 따라가지는 않는다.

프로그래머들이 주석을 엄격하게 관리해야 한다고 주장할 수도 있지만 그것보다 더 좋은 것은 코드를 깔끔하게 정리하고 표현력을 강화하는 방향으로, 그래서 애초에 주석이 필요 없는 방향으로 에너지를 쏟는게 더 낫다.

부정확한 주석은 독자를 현혹하고 오도하기 때문에 주석이 아예 없는것보다 훨씬 나쁘다. 부정확한 주석은 더 이상 지킬 필요가 없는 규칙이나 지켜서는 안되는 규칙을 명시한다.

진실은 코드 한곳에만 존재한다. 코드만이 정확한 정보를 제공하는 유일한 출처다. 아주 간혹 주석이 필요한 경우를 제외하고는 주석을 가능한 줄이도록 노력해야 한다.

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다
if((employee.flags & HOURLY_FLAG) && (employee.age > 65)) ... // 안좋은 코드 + 주석
    
if(employee.isEligibleForFullBenefits()) ... // 좋은 코드 (주석이 필요 없음) 

좋은 주석

  • 법적인 주석
    • 회사가 각 소스 파일의 첫 머리에 주석으로 저작권 정보와 소유권 정보를 넣어야 한다고 정하는 경우는 주석을 다는게 필요하고 타당하다.
  • 의도를 설명하는 주석
    • 단순 구현을 이해하는 수준이 아닌 결정에 깔린 의도까지 설명하는 경우 유용한 주석이 될 수 있다.
  • 결과를 경고하는 주석
    • 프로그래머가 주석을 보고 실수를 면하게 할 수 있다.
  • TODO 주석
    • 필요하지만 당장 구현하기 어려운 경우 앞으로 할 일을 남겨놓으면 편하다. 요즘 IDE는 TODO를 전부 찾아주는 기능을 제공하기 때문에 찾기도 쉽다. 하지만 TODO 주석 역시도 주기적으로 점검해서 없애도 괜찮으면 없애주는게 좋다.
  • 공개 API를 구현할때는 Javadocs를 열심히 작성해야 한다. 공개 API가 아닌 경우에는 Javadocs 주석이 요구하는 형식으로 인해 코드만 보기 싫고 산만해진다.

나쁜 주석

  • 명확하지 않은 주석

  • 같은 이야기를 반복하는 주석

  • 의무적으로 다는 주석

  • 있으나 마나 한 주석

    • 코드상으로도 이미 명확한데 주석을 다는 경우에 주석은 무의미하다.
  • 이력을 기록하는 주석

    • 예전에는 소스코드 관리 시스템이 없어서 필요했을지 몰라도 지금은 아니다.
  • 위치를 표시하는 주석

    • 소스파일에서 특정 위치를 표시하려고 주석을 사용할 때가 있다. 예를들어 ///// Actions///////////////// 처럼 다는 경우이다. 유용할 때도 많지만 많이 사용하는 경우 가독성만 낮추게 된다.
  • 닫는 괄호 오른쪽에 다는 주석

    try {
        ...  코드 ...
        while {
            ...  코드 ...
        } // while
        ...  코드 ... 
    } // try
    
    • 중첩이 심하고 장황한 함수라면 의미가 있을 수 있지만 중첩이 심하고 장황한 함수가 아닌 작고 캡슐화된 함수를 만드는게 더 좋다.
  • 공로를 돌리거나 저자를 추가하는 주석

    • 소스코드 관리 시스템이 있는 현재 시점에서 이런 주석은 점차 부정확하고 쓸모없는 정보로 변하기 쉽다.
  • 주석으로 처리한 코드

    InputStreamResponse response = new InputStreamResponse();
    response.setBody(formatter.getResultStream(), formatter.getByteCount());
    // InputStream resultsStream = formatter.getResultStream();
    // StreamReader reader = new StreamReader(resultStream);
    // response.setContent(reader.read(formatter.getByteCount()));
    

    주석으로 처리된 코드는 다른 사람이 봤을 때 이유가 있어서 남겨 놓았을 거라고 생각하기 때문에 지우기 어렵다. 1960년대에는 주석으로 처리한 코드가 유용했지만 요즘은 소스코드 관리 시스템이 있기 때문에 코드를 삭제하는게 낫다.

  • 너무 많은 정보
    • 주석에 역사적인 일이나 관련 없는 정보를 장황하게 늘어놓는건 안좋다.
  • 주석과 코드의 모호한 관계
    • 주석을 달아야 한다면 코드를 명확하게 설명하는 주석을 달아야 한다.

5. 형식 맞추기

프로그램머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다. 그러기 위해 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 코드를 읽는 독자들이 코드가 깔끔하고 일관적이며 꼼꼼하며 질서 정연하다고 감탄해야 한다.

형식을 맞추는 목적

어떤 개발자들은 ‘돌아가는 코드’가 개발자의 일차적인 의무라고 여길 수도 있다. 하지만 코드 특성상 오늘 구현한 기능이 다음 버전에서 바뀔 확률은 아주 높다. 그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다. 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.

세로 형식 맞추기

보통 한 소스코드 파일의 크기가 작을수록 이해하기가 쉽다. JUnit 같은 경우 500줄을 넘어가는 파일이 없고 대다수가 200줄 미만이다. JUnit같은 커다란 시스템도 이렇게 적은 줄의 소스코드 파일들로 구축이 가능하다면 다른 시스템도 충분히 가능하다.

  • 신문 기사처럼 작성하기
    • 신문기사는 위에서 아래로 기사를 읽는다. 최상단에 기사를 몇마디로 요약하는 표제가 나온다. 첫 문단은 전체 기사 내용을 요약한다. 쭉 읽으며 내려가면 세세한 사실이 조금씩 드러난다. 소스파일도 신문기사처럼 작성해야 한다. 이름은 간단하면서도 설명이 가능하도록, 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명하고 아래로 내려갈수록 의도를 세세하게 알 수 있도록 작성해야 한다.
  • 개념은 빈 행으로 분리하기
  • 연관있는 코드는 가까이 배치하기
    • 변수는 변수끼리 함수는 함수끼리 배치하는게 한눈에 읽기 쉽다
  • 인스턴스 변수는 클래스 맨 처음에 선언하기
  • 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치하기
  • 개념적 유사성이 있는 코드를 가까이 배치하기

가로 형식 맞추기

대형 프로젝트 7가지를 조사한 결과 대부분의 행이 20자~60자였다. 80자 이후의 행은 거의 존재하지 않았다. 결론적으로 짧은 행이 무조건 좋다.

6. 객체와 자료구조

객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면 기존 객체에 새 동작을 추가하기는 어렵다.

자료 구조는 별다른 동작 없이 자료를 노출한다(예 - DTO). 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나 기존 기존 함수에 새 자료 구조를 추가하기는 어렵다.

어떤 시스템을 구현할 때 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 반면에 새로운 동작을 추가하는 유연성이 필요하면 자료구조와 절차적인 코드가 더 적합하다.

7. 오류 처리

오류 처리는 깨끗하고 튼튼한 코드에 핵심적인 요소이다. 뭔가 잘못되면 바로 잡을 책임은 해당 코드를 작성한 프로그래머에게 있다. 그렇다고 오류 처리를 너무 많이 쓰면 실제 코드가 하는 일을 파악하기가 어려워 프로그램의 논리를 이해하기 어려워질 수 있다. 오류 처리를 통해 우아하고 고상하게 오류를 처리하면서 깨끗하고 튼튼한 코드를 작성하는 것은 기술이다.

Try-Catch-Finally 문부터 작성하기

Try-catch 구조로 범위를 정의하면 TDD를 자연스럽게 사용하게 된다.

예외에 의미 제공하기

예외를 던질 때는 전후 상황을 충분히 덧붙이는게 좋다. 자바는 모든 예외에 호출 스택을 제공하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족할 때가 있다. 오류메세지에 정보(실패한 연산 이름과 실패 유형 등)를 담아 예외와 함께 던지거나 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하는 방식으로 정보를 제공하는게 좋다.

8. 경계

소프트웨어를 개발할 때 직접 개발하는 경우도 있지만 외부 오픈소스나 사내 다른 팀이 제공하는 컴포넌트를 사용할 때가 많다. 이 때 직접 개발한 코드와 외부 코드의 경계를 깔끔하게 처리하는 기술이 필요하다.

경계 살피고 익히기

외부 패키지를 제대로 사용하기 위해서는 테스트 케이스를 통해 외부 코드를 먼저 익히는 것이 좋다. 그렇게 했을 때 외부 패키지 사용 방법을 빠르게 익히고 내부 코드와의 호환성을 보장할 수 있다.

깨끗한 경계

경계에 위치하는 코드는 깔끔히 분리해야 한다. 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리해야 한다. 통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 커지지 않도록 주의해야 한다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 것이 낫다. 자칫하면 오히려 외부 코드에 휘둘릴 수 있다.

9. 단위 테스트

테스트 코드는 실제 코드만큼 프로젝트 건강에 중요하다. 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문이다. 테스트 코드는 지속적으로 깨끗하게 관리해야 한다.

유연성, 유지보수성, 재사용성

테스트 케이스가 있다면 변경이 두렵지 않다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그이다. 따라서 테스트 코드가 지저분하면 코드를 변경하거나 코드 구조를 개선하는 능력이 떨어진다.

깨끗한 테스트 코드

깨끗한 테스트코드의 핵심은 가독성이다.

테스트당 assert 하나

given-when-then이라는 관례를 사용하면 테스트 코드를 읽기가 쉬워진다. 테스트 함수 하나 당 개념 하나로 제한해야 한다. assert문은 상황에 따라 많이 들어가야 할 때도 있지만 최대한 한 개념 당 assert문의 수를 최소로 줄이는게 가독성이 좋다. assert문이 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.

FIRST

깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.

  1. F(Fast) : 테스트는 빨라야 한다. 빨라야 부담 없이 자주 돌릴 수 있다.
  2. I(Independent) : 각 테스트는 독립적이어야 하고 서로 의존하면 안된다. 순서에 관계 없이 실행해도 괜찮아야 한다. 테스트가 서로 의존하면 실패나 성공이 서로 의존하게 되고 그럼 실패나 성공의 원인을 진단하기 어려워진다.
  3. R(Repeatable) : 테스트는 어떤 환경(QA환경, 실제 환경, 네트워크가 없는 환경 등)에서도 반복 가능해야 한다. 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
  4. S(Self-Validating) : 테스트는 bool 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알기 위해 로그 파일을 읽거나 텍스트 파일 두개를 수작업으로 비교하면 안된다. 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다.
  5. T(Timely) : 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

10. 클래스

단일 책임 원칙

단일 책임 원칙(SRP - Single responsibility prinsiple)은 하나의 책임을 가져야 한다는 원칙을 의미한다. 큰 클래스 몇개가 아니라 작은 클래스 여러개로 이루어져야 좋은 시스템이라고 할 수 있다.

응집도

클래스는 응집도가 높아야 한다. 메서드가 변수를 많이 사용할 수록 메서드와 클래스는 응집도가 높다. 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리하는게 좋다. 그렇게 하면 프로그램이 더 체계적이고 구조가 투명해진다.

변경으로부터 격리

요구사항이 변함에 다라 코드도 변하기 마련이다. 객체 지향 프로그래밍에서는 구체적인 클래스와 추상 클래스가 있다. 좋은 코드는 인터페이스와 추상클래스를 사용해 구현의 변경이 미치는 영향을 격리한다. 인터페이스를 사용하면 테스트도 가능해지고 시스템의 결합도를 낮춰 유연성과 재사용성을 높일 수 있고 각 요소를 이해하기도 더 쉬워진다. 이렇게 결합도를 최소로 줄이면 DIP(Dependency Inversion Principle) 이라는 ‘클래스는 상세한 구현이 아니라 추상화에 의존해야 한다’는 원칙이 나온다.

11. 시스템

시스템은 도시와 비슷하다. 도시는 적절한 추상화와 모듈화 덕분에 매우 복잡하지만 체계적으로 돌아갈 수 있다. 수도 관리 팀, 전력 관리 팀, 교통 관리 팀, 치안 관리 팀, 건축물 관리 팀 등 각 분야를 관리하는 팀이 있듯이 시스템도 추상화와 모듈화를 잘 해 놓으면 시스템 수준에서 깨끗함을 유지할 수 있다.

깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다. 도메인 논리가 흐려지면 버그가 숨어들기 쉬워져서 제품 품질이 떨어진다. 기민성이 떨어지면 생산성이 낮아져 TDD가 제공하는 장점이 사라진다.

12. 창발성

착실하게 따르기만 하면 우수한 설계가 나오는 간단한 규칙 네가지가 있다.

  1. 모든 테스트를 실행하기
  2. 중복을 없애기
  3. 의도 표현하기
  4. 클래스와 메서드 수를 최소로 줄이기

모든 테스트를 실행하기

테스트 케이스를 게속 만들고 돌리면 시스템은 낮은 결합도와 높은 응집력이라는 객체 지향 방법론이 지향하는 목표를 저절로 달성한다. 즉, 테스트 케이스를 작성하면 설계 품질이 높아진다.

중복을 없애기

우수한 설계에서 중복은 큰 적이다. 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 의미하기 떄문이다.

의도 표현하기

코드를 짠 사람은 본인의 코드를 이해하기 쉽다. 하지만 나중에 유지보수 하는 사람은 코드를 짜는 사람만큼 코드를 깊이 이해하기 어렵다. 소프트웨어 프로젝트 비용 중 대다수는 장기적인 유지보수에 들어간다. 코드를 변경하면서 버그의 싹을 심지 않으려면 유지보수 개발자가 시스템을 제대로 이해해야 한다. 하지만 시스템이 복잡해지면서 유지보수 개발자가 시스템을 이해하느라 보내는 시간은 점점 늘어나고 동시에 코드를 오해할 가능성도 점점 커진다. 그러므로 코드는 개발자의 의도를 분명히 표현해야 한다. 개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다. 그래야 결함이 줄어들고 유지보수 비용이 적게 든다.

그러기 위해서는 다음과 같이 코드를 짜야 한다.

  1. 좋은 이름 선택하기
  2. 함수와 클래스 크기를 가능한 줄이기.
  3. 표준 명칭을 사용하기 - 디자인 패턴은 의사소통과 표현력 강화가 주요 목적이다. 클래스가 COMMAND나 VISITOR와 같은 표준 패턴을 사용해 구현된다면 클래스 이름에 패턴 이름을 넣어주면 다른 개발자가 클래스 설계 의도를 이해하기 쉬워진다.
  4. 단위 테스트 케이스를 꼼꼼히 작성하기 - 테스트 케이스는 소위 ‘예제로 보여주는 문서’다.

가장 중요한 것은 자신의 코드에 더 주의를 기울이는 것이다. 함수와 클래스와 이름에 더 많은 시간을 투자하는 것이다. 주의는 대단한 재능이다.

클래스와 메서드 수를 최소로 줄이기

클래스와 메서드 수를 최소로 줄이는 것은 매우 중요하지만 위의 세가지 원칙 만큼 중요하지 않다. 위의 세가지 원칙을 다 지켰을 때 클래스와 메서드 수를 줄이는 것에 신경 쓰면 된다.

13. 동시성

다중 스레드는 올바로 구현하기 어렵다. 다중 스레드 코드를 작성한다면 각별히 깨끗하게 코드를 짜야 한다. 주의하지 않으면 희귀하고 오묘한 오류에 직면하게 된다. 다중 스레드를 올바로 구현하기 위해 다음과 같은 원칙을 준수해야 한다.

  1. SRP(Single Responsibility Principle) 을 준수한다. POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다.
  2. 동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다. 보통 여러 스레드가 공유 자료를 조작하거나 자원 풀을 공유할 때 동시성 오류가 발생한다. 루프 반복을 끝내거나 프로그램을 깔끔하게 종료하는 등 경계 조건의 경우가 까다로우므로 특히 주의한다.
  3. 사용하는 라이브러리와 기본 알고리즘을 이해한다. 특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악한다.

스레드는 출시하기 전까지 최대한 오랫동안 돌려봐야 한다. 깔끔한 접근 방식을 취한다면 코드가 올바로 돌아갈 가능성이 높아진다.

14. 점진적인 개선

단순히 코드가 돌아간다고 해서 만족하는 개발자는 직업의식이 없는 사람이다. 설계와 구조를 개선할 시간이 없다고 변명하는 것은 말 그대로 변명이다. 나쁜 코드만큼 개발 프로젝트에 악영향을 미치는 요인도 없다. 나쁜 일정, 나쁜 요구사항, 나쁜 팀 역학은 바꾸면 된다. 하지만 나쁜 코드는 썩어 문드러진다. 그 무게가 점점 늘어나 팀의 발목을 잡는다. 속도가 점점 늘어나다 못해 기어가는 팀도 많다. 너무 서두르다가 이후로 영원히 자신들의 운명을 지배할 악성 코드라는 굴레를 짊어진다.

나쁜 코드도 깨끗한 코드로 개선할 수 있다. 하지만 엄청난 비용이 든다. 코드가 썩어가며 모듈은 서로 얽히고 뒤엉켜서 숨겨진 의존성이 수도 없이 생긴다. 오래된 의존성을 찾아내 깨려면 상당한 시간과 인내심이 필요하다. 반면 처음부터 코드를 깨끗하게 유지하기란 상대적으로 쉽다. 아침에 엉망으로 만든 코드를 오후에 정리하기는 어렵지 않다. 5분전에 엉망으로 만든 코드는 지금 당장 정리하기 아주 쉽다.

그렇기 때문에 코드는 항상 깔끔하고 단순하게 정리해야한다. 절대로 썩어가게 방치하면 안된다.

15. JUnit 들여다보기

(JUnit 프레임워크의 코드를 평가하고 개선할 점을 찾는 내용으로 지엽적인 내용이 많아 생략)

16. SerialDate 리팩터링

(SerialDate 클래스의 코드를 평가하고 개선할 점을 찾는 내용으로 지엽적인 내용이 많아 생략)

17. 냄새와 휴리스틱

다음은 코드를 읽으면서 나쁜 냄새가 나는 코드와 그 해결 방법을 정리한 목록이다.

주석

  • 부적절한 정보 : 주석에 장황한 정보를 넣는 것은 좋지 않다. 특히 변경 이력은 필요 없다. 작성자, 최종 수정일 정도만 넣는게 좋다.

  • 쓸모 없는 주석 : 오래된 주석, 잘못된 주석은 빨리 처리하는게 좋다. 코드와 무관하게 따로 놀며 코드를 그릇된 방향으로 이끌 수 있다.

  • 중복된 주석 : 코드만으로 충분한데 구구절절 설명하는 주석은 중복된 주석이다

    i++; // i 증가
    
  • 성의 없는 주석 : 작성할 필요가 있다면 간결하고 명료하게 단어를 신중하게 선택해서 작성해야 한다.
  • 주석 처리된 코드 : 코드를 읽다가 주석으로 처리된 코드는 신경이 쓰인다. 얼마나 오래된 코드인지, 중요한 코드인지 아닌지 알 수가 없다. 그럼에도 삭제하기 어렵다. 누가 필요하거나 사용할 수 있을 수도 있다고 생각하기 때문이다. 주석으로 처리된 코드는 흉물 그 자체이다. 그런 주석은 과감하게 삭제할 필요가 있다. 소스코드 관리 시스템이 기억하기 때문에 걱정할 필요가 없다. 누군가 정말 필요하다면 이전 버전에서 가져오면 된다.

환경

  • 빌드 : 빌드는 간단히 한 단계로 끝나야 한다. 여러 파일을 뒤적이며 빌드하는건 좋지 않다. 한 명령으로 빌드가 가능해야 한다.
  • 테스트 : 모든 단위 테스트는 한 명령으로 돌릴 수 있는게 좋다. 모든 테스트를 한 번에 실행하는 건 빠르고 쉽다.

함수

  • 너무 많은 인수 : 함수에서 인수 개수는 작을수록 좋고 아예 없는게 가장 좋다. 넷 이상은 그 가치가 의심스러우므로 최대한 피하는게 좋다.
  • 출력 인수 : 출력 인수는 직관을 정면으로 위배한다. 일반적으로 독자는 인수를 입력으로 받아들인다. 함수에서 뭔가의 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경하는게 좋다.
  • 플래그 인수 : boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다. 플래그 인수는 혼란을 초래하므로 피하는게 좋다.
  • 죽은 함수 : 아무도 호출하지 않는 함수는 과감히 삭제해야 한다. 죽은 코드는 낭비다. 소스 코드 관리 시스템이 기억하기 때문에 걱정하지 않아도 된다.

일반

  • 한 소스 파일에 여러 언어를 사용하기 : 요즘 프로그래밍 환경은 한 소스 파일에 여러 언어를 사용할 수 있지만 좋게 말하자면 혼란스럽고 나쁘게 말하자면 조잡하다. 이상적으로는 한 소스파일에 하나의 언어만 사용하는게 좋다. 현실적으로 불가능하지만 최대한 언어 수를 줄이도록 애써야 한다.

  • 경계 처리 : 개발자들의 안좋은 습관중 하나는 머릿속에서 코드를 돌려보고 끝내는 것이다. 개발자는 모든 경계 조건을 찾아내고 모든 경계 조건을 테스트하는 테스트 케이스를 작성해야 한다.

  • 중복 : 이 책에서 말하는 가장 중요한 규칙 중 하나이다. 모든 소프트웨어 설계자는 모두가 이 규칙을 말한다. 코드에서 중복을 발견할 때마다 추상화 할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어난다. 추상화 수준을 높였으므로 구현이 빨라지고 오류가 적어진다. 중복을 없애는 방법은 함수, 다형성, 디자인패턴 등이 있다. BCNF(Boyce-Codd Normal Form) 역시 데이터베이스 스키마에서 중복을 제거하는 전략이다.

  • 죽은 코드 : 죽은 코드는 삭제해야 한다. 설계가 변해도 수정되지도 않고 예전의 코드로 그대로 남아있게 된다.

  • 수직분리 : 변수와 함수는 사용되는 위치에 가깝게 정의한다. 지역변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다. 선언한 위치로부터 몇백 줄 아래에서 사용하면 안된다.

  • 일관성 : 변수명이나 함수명을 특정한 규칙에 따라 일관성있게 만들어야 한다. 이러한 일관성이 적용되었다면 코드를 읽고 수정하기 쉬워진다.

  • 잡동사니 : 아무도 사용하지 않는 변수, 함수, 정보를 제공하지 않는 주석들은 코드만 복잡하게 만들기 때문에 제거해야 한다. 소스파일은 언제나 깔끔하게 정리해야 한다.

  • 매직 숫자는 명명된 상수로 교체하기 : 어떤 공식은 그냥 숫자를 쓰는게 좋을 수 있지만 대부분의 경우에 숫자는 명명된 상수 뒤로 숨기는게 좋다.

  • 조건을 캡슐화하기 : boolean 논리는 이해하기 어려운 경우가 많기 때문에 조건이 의도를 분명히 밝히는 함수로 표현하는 것이 좋다.

    if(timer.hasExpired() && !timer.isRecurrent()) // 1차원적인 평범한 방법
    if(shouldBeDeleted(timer)) // 가독성이 좋은 방법
    
  • 부정 조건 피하기 : 부정조건은 긍정조건보다 이해하기 어렵다. 가능하면 긍정조건으로 표현해야 한다.

    if(!buffer.shouldNotCompact()) // 부정 조건 (가독성 떨어짐)
    if(buffer.shouldCompact()) // 긍정 조건 (가독성 더 좋음)
    
  • 함수는 한가지만 하기 : 함수는 한가지 업무만 해야 한다. 여러 업무를 한다면 여러 함수로 분리하는 것이 좋다.

  • 설정 정보는 최상위 단계에 두기 : 기본값 상수나 설정 관련 상수는 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘길 수 있도록 최상위 단계에 둬야 한다.

테스트

  • 사소한 테스트 : 사소한 테스트는 짜기 쉽다. 하지만 사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.
  • 경계 조건 테스트 : 경계 조건은 각별히 신경써서 테스트 해야 한다. 알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 많다.
  • 버그 주변 철저히 테스트 하기 : 버그는 서로 모이는 경향이 있다. 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다. 십중 팔구 다른 버그도 있을 확률이 놓다.
  • 실패 패턴을 살피기 : 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 꼼꼼하게 합리적인 순서로 정렬된 테스트 케이스는 실패 패턴을 드러낸다.
  • 테스트 속도 : 느린 테스트 케이스는 실행하지 않게 된다. 일정이 촉박하면 느린 테스트 케이스는 건너 뛰게 된다. 테스트 케이스는 최대한 빨리 돌아가게 노력해야 한다.

Leave a comment