클린코드 발췌 기록

개발자 생존기·2023년 1월 8일
0

실력

목록 보기
1/2

클린코드 책을 읽으면서 중요한 부분을 발췌하여 기록한다.

다른 사람이 정리한 것들
https://determined-floss-33d.notion.site/ecfe8552e1c04aa997e4f74e22c9fb52
http://amazingguni.github.io/blog/
https://velog.io/@junnkk?tag=%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C

1장 깨끗한 코드

(4pg) 우리 모두는 자신이 짠 쓰레기 코드를 쳐다보며 나중에 손보겠다고 생각한 경험이 있다. 우리 모두는 대충 짠 프로그램이 돌아간다는 사실에 안도감을 느끼며 그래도 안 돌아가는 프로그램보다 돌아가는 쓰레기가 좋다고 스스로를 위로한 경험이 있다. 다시 돌아와 나중에 정리하겠다고 다짐했다. 물론 그 때 그 시절 우리는 르블랑의 법칙을 몰랐다. 나중은 다시 오지 않는다
(5pg) 나쁜 코드가 쌓일수록 생산성을 떨어진다. 마침내 0에 근접한다.
(7pg) 나쁜 코드의 위험을 이해하지 못하는 관리자의 말을 그대로 따르는 행동은 전문가답지 못하다. 어느 환자가 시간이 너무 오래 걸리기 때문에 손을 씻지 말라고 의사에게 요구한다. 그 요구를 들어줄 것인가? ... 나쁜 코드의 위험을 이해하지 못하는 관리자의 말을 그대로 따르는 행동은 전문가답지 못하다.
(9pg) 오류는 명백한 전략에 의거해 철저히 처리해야 한다.
(10pg) 깨끗한 코드는 한 가지를 잘해야 한다.
(11pg) 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 (여러 가지가 아니라) 하나만 제공한다.... 깨끗한 코드란 다른 사람이 고치기 쉽다고 단언한다.
(12pg) 모든 테스트를 통과하고 중복이 없어야 한다.
(13pg) 중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기, 내게는 이 세가지 깨끗한 코드를 만드는 비결이다.

2장 의미 있는 이름

좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 많다.
1. 문제는 코드의 단순성이 아니라 함축성이다. 단순한 코드라도 의미를 명확하게 전달해야 한다

public List<int[]> getFlaggedCells() {
  List<int[]> flaggedCells = new ArrayList<int[]>(); // 더 명확하게 명명함
  for (int[] cell: gameBoard) { // for element도 고심해서 짓자
    if (cell[STATUS_VALUE] == FLAGGED) { // STATUS_VALUE, FLAGGED 모두 숫자였으나 상수형으로 변경
      flaggedCells.add(cell);
    }
  }
}

이건 더 좋은 코드

public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<Cell>(); // 더 명확하게 명명함
  for (Cell cell: gameBoard) { // for element도 고심해서 짓자
    if (cell.isFlagged()) { // STATUS_VALUE, FLAGGED 모두 숫자였으나 상수형으로 변경
      flaggedCells.add(cell);
    }
  }
}
  1. 그릇된 정보를 피하라. 여러 계정을 그룹으로 묶을 때, 실제 List가 아니라면, ~~List라고 명명하지 않는다.
  2. 유사한 개념은 유사한 표기법을 사용한다. 그렇지 않다면 서로 흡사한 이름을 사용해서는 안된다.
  3. 발음하기 쉬운 이름을 사용하라. 의사 소통을 염두에 두고 명명하라.
  4. 검색하기 쉬운 이름을 사용하라. 이름 길이는 범위 크기에 비례해야 한다. 만일 전역으로 사용되는 변수라면 이름 길이가 다소 길어도 좋다.
  5. 인코딩을 하지 마라. 굳이 변수명예 접미사로 Str 등등을 붙이지마라. 타입이 바뀌게되면 더 큰 오해를 초래한다.
    접두어의 경우 고민해봐야 한다. python이나 javascrip의 경우 전역으로 hoisting 되기 때문에 접두어를 붙이고는 한다._
  6. 자신의 기억력을 자랑하지 마라. 너는 잊어버리고 다른 사람을 알지 못할 것이다.
  7. 한 개념에 한 단어를 사용하라. 예를 들어 정보를 가져올 때, fetch, retrieve, get 중 무엇을 선택할지 통일하자.
    팀원들끼리 고민해볼만한 주제이다
  8. 해법영역에서 가져온 이름을 사용하라. 읽는 사람은 프로그래머다. 어떤 알고리즘이나 디자인패턴인지 힌트를 주어라.
  9. 의미 있는 맥락을 추가하라. 불필요한 맥락을 없애라.
  • 지나치게 길다면 나누어서 맥락을 만들어라. 변수명에 불필요한 접두사 등은 제거하라.

3장 함수

42pg - 작게 만들어라. 100줄을 넘겨서는 안되고, 20줄도 길다. 각 함수는 이야기 하나를 표현했다. 각 함수가 너무도 멋지게 다음 무대를 준비했다.
43pg - if/else, while 문에 들어가는 블록은 단 한줄이어야 한다. (...) 중첩 구조가 생길만큼 함수가 커져서는 안된다.
44pg - 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.
45pg - 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서가 아니던가?
46pg - 내려가기 규칙 - 코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉 위에서 아래로 프로그램을 읽으면서 함수 추상화 수준이 한 단계씩 낮아진다.
47pg - switch 문을 숨겨라. 추상 팩토리에 숨기고 노출해서는 안된다.
49pg - 서술적인 이름을 사용하라. 함수가 하는 일을 잘 표현해야 한다. (...) 이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. (...) 일관적인 법칙을 사용하라. 예를 들어 include를 사용할지, contains를 사용할지 통일하라.
50pg - 함수 인수는 작을 수록 좋다. 0이 가장 이상적이고, 3항 이상은 피하도록 하라. (4항은 절대 금지다.)
52pg - flag 인수는 추하다. 함수에 boolean 을 넘기는 관례는 끔찍하다.
53pg - 인수의 순서는 직관적으로 이해할 수 있어야 한다. 함수명에 인수 순서를 자연스럽게 유추할 수 있도록 짓는 것도 유용하다. 또한 인수명도 중요하다.
54pg - 부수 효과를 일으키지 마라. 함수로 넘어온 인수나 시스템 전역 변수를 수정하여 부수 효과를 일으키면 안된다. 또한 Session 초기화처럼 무의식적으로 작성한 부분도 부수 효과를 일으킬 수 있다.
56pg 출력 인수는 가급적 피해라. 만일 함수에서 상태를 변경해야 한다면, 함수가 속한 객체 상태를 변경하는 방식을 선택하라

public void appendFooter(StringBuffer report) {
}
보다는 아래가 낫다.
report.appendFooter();

56pg 명령과 조회를 분리해라. 함수는 뭔가를 수행하거나, 뭔가를 답하는 둘 중 하나를 해야 한다. 예를 들어 set함수에서 조회 후 명령을 한다면 이를 분리해야 한다.
57pg 오류코드보다는 예외를 사용하라. 오류코드를 사용하게 되면 , 여러 단계로 중첩되는 코드를 야기한다.

try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) { // 예외 코드 사용
}

58pg try-catch는 추하다. 따라서 try-catch 블록을 별도 함수로 뽑아내는 편이 좋다.

public void deletePage(Page page) {
  try {
  }
  catch {
  }
}

59pg 오류 처리도 한 가지 작업이므로 오류만 처리해야 한다.
60pg 반복하지 마라. (...) 중복은 큰 문제이다. 코드 길이를 늘어나게 하고, 알고리즘이 변하면 여러 곳을 손봐야 한다.
61pg 구조적 프로그래밍 (...) 함수 안에 입구와 출구는 하나여야 한다. return을 남발하지 마라. 한 군데서 return해야 한다.
62pg 일단 되게 만들고, 테스트 케이스를 만들고, 리팩토링(코드를 다듬고, 함수를 만들고, 중복을 제거 등)한다. 이 와중에 항상 단위테스트를 통과해야 한다. 시스템은 이야기를 풀어나가는 것이다.

4장 주석 (필요악이다.)

나라면 코드를 깔끔하게 정리하고, 표현력을 강화하는 방향으로, 그래서 애초에 주석이 필요 없는 방향으로 에너지를 쏟겠다.
주석은 절대 나쁜 코드를 보완하지 못한다.
그러나 어떤 주속은 필요하거나 유익하다.
1. 법적인 주속 (라이센스 고지)
2. 정보를 제공하는 주석 --> 함수명, 인수명을 명확하게 하는데 집중하자.
3. 의도를 설명하는 주석 --> 특정 알고리즘을 사용했거나, 일반적이지 않은 방법으로 구현했을 경우에 한하여!!
4. 의미를 명료하게 밝히는 주석

assertTrue(bb.compareTo(ba) == 1); // bb > aa
  1. 결과를 경고하는 주석. 예를 들어 시간이 오래 걸리는 함수에 대해 경고한다.
  2. todo
  3. 중요함을 알리는 주석

나머지는 다 나쁘다. 위의 것도 필요없다면 나쁜 것이다. 주석은 관리 대상이 아니므로 혼란을 야기할 것이다.

5장 형식 맞추기

구성원들끼리 code formatter 는 통일해야 한다.
1. 적절한 행 길이를 유지하라
2. 신문 기사처럼 작성하라
3. 개념(패키지 선언부, import, 함수 사이 등)은 빈 행으로 구분하라
4. 서로 연관된 개념은 한 눈에 들어오게 하라

  • 변수 선언 : 사용하는 위치에 최대한 가깝게 선언한다.
  • 인스턴스 선언 : 클래스 초반에 선언한다.
  • 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
  1. 가로도 120자 정도 행 길이가 제한되어야 한다.;

그 외에는 팀의 ide의 formatter를 통일해서 사용하면 될 듯 하다.

6장 객체와 자료 구조

단순히 변수를 private으로 선언하고, getter, setter를 사용하는 것이 아니라, 개념의 추상화가 필요하다.
119 - 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다. 자료 구조는 자료를 그대로 공개하며 별다른 함수는 공개하지 않는다.
객체는 클래스 변경에는 유리하나, 함수 추가에는 불리하다. 자료구조는 함수 추가에는 유리하나 클래스 변경에 불리하다.
따라서 객체와 자료 구조는 근본적으로 양분된다.

Pros

(자료 구조를 사용하는) 절차적인 코드는 기존 자료 구조를 변경하지 않으면서, 새 함수를 추가하기 쉽다. 반면 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

Cons

절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 모든 함수를 고쳐야 한다. 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 모든 클래스를 고쳐야 한다.

디미터 법칙

모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다. 객체는 자료를 숨기고 함수를 공개한다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratDir();
final String outputDir = scratchDir.getAbsolutePath();

객체라면 내부 정보를 숨겨야 하므로, 디미터 법칙을 위반한다. 반면 자료구조라면 내부 구조를 노출하므로 디미터 법칙에 위반되지 않는다.
ctxt가 객체라면 무엇가를 하라고 말해야지 속을 드러내면 안된다. 아래와 같다면 어떨까? 객체에게 맡기기 적당한 임무로 보인다.

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

자료 전달 객체 --> vo를 생각하면 된다. 말 그대로 정보 전달을 위한 객체이다.
어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.

7장 오류 처리

130pg - 동어 반복! 오류 코드보다 예외를 사용하라.
132pg - try/catch 문부터 작성하라. close가 내제된 경우에는 try with resource를 사용하라
133pg - 미확인 예외를 사용하라. (확인된 예외란 RuntimeException을 제외한 ExceptionClass의 모든 하위 클래스이다. 즉 Compile시점에서 처리되는 IoException, FileNotFoundException이다.
아래가 BestPractice

public class LocalPort {
  privagte ACMEport innerPort;
  
  public LocalPort (int portNumber) {
    innerPort = new ACMEPort();
  }
  
  public void open() {
    try {
      innerPort.open();
    } catch (DeviceResponseException e)  {
       throw new PortDeviceFailure(e);
    } catch (ATM~~Exception e) {
       throw new PortDeviceFailure(e);
    }
  }
}

138pg - 절대 null을 반환하지 마라. null을 return하는 코드는 일거리를 늘릴 뿐이 아니라 호출자에게 문제를 떠넘긴다. 그리고 null을 전달하지 마라. 이유는 마찬가지다. null check, assert와 같은 부가적인 처리가 필요하다.

8장 경계

145pg - 프로그램에서 Map 인스턴스를 여기저기 넘긴다면, Map 인터페이스가 변할 경우, 수정할 코드가 많아진다. 따라서 class 내부로 숨기는 것이 권장된다. 코드는 이해하기 쉽고 오용하기 어려워진다. (...) ㅡMap 인스턴스를 공개 API의 인수로 넘기거나 반환하지 않도록 한다.
147pg - log4j 익히기
150~151pg -- 아직 이해가 더 필요함, 다만 Adapter 패턴에 대한 이야기로 보임
https://johngrib.github.io/wiki/pattern/adapter/
151pg - 경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예이다. 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. (...) 경계에 위치하는 코드는 깔끔하게 분리한다. 또한 기대치를 정의하는 테스트케이스도 작성한다.

9장 단위테스트

TDD 법칙 세 가지
1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
2. 컴파일을 실패하지 않으면서 실행이 실패하는 정도만 단위테스트를 작성한다.
3. 현재 실페하는 테스트를 통과할 정도만 실제 코드를 작성한다.

156pg - 단위 테스트도 유지 보수가 필요하다. 실제 코드가 변경되면 단위 테스트도 변경해야 하기 때문이다. 따라서 테스트 코드도 유연성, 유지보수성, 재사용성을 고려해야 한다.

161pg - 코드 작성의 표준과 테스트 작성의 표준은 다를 수 있다. 아래 예를 참고하자

assergEquals("HBchL", hw.getState()); //5가지 상태의 결과를 한 번에 확인하는 방법이다.

167pg - FIRST
F(Fast): 테스트는 빨라야 한다. 빠르고 자주 돌아야 한다.
I(Independent): 각 테스트는 서로 의존하면 안된다. 한 테스트가 다른 테스트의 실행 환경을 준비해서는 안된다.
R(Repeat): 테스트는 어떤 환경에서도 반복 가능해야 한다. (...) 네트워크가 연결되지 않은 환경에서도 실행할 수 있어야 한다. 이건 생각해보자. DB 연계가 필요한 테스트가 필요할 수 있으니
S(Self-validating): 테스트는 boolean으로 결과를 내야 한다. 통과 여부를 알기 위해서 로그를 읽어서는 안된다.
T(Timely): 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

10장 클래스

http://amazingguni.github.io/blog/2016/06/Clean-Code-10-%ED%81%B4%EB%9E%98%EC%8A%A4

  1. 클래스 체계
  • 변수 목록의 순서: public static -> private static -> instance -> public method -> private method (추상화 단계가 순차적으로 내려간다. 따라서 프로그램은 신문 기사처럼 읽힌다.)
  • 테스트 코드에서 접근이 필요한 경우에 protected로 선언하기도 한다. 그러나 일반적으로는 private으로 선언해야 한다.
  1. 클래스는 작은 책임을 가져야 한다. (SRP: Single Responsibility Principle)
  • 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다.
  • Class 이름에 Processor, Manager와 같은 모호한 이름이 있다면, 클래스에 여러 책임을 떠넘겼다는 이야기다.

소프트웨어를 돌아가게 만드는 활동과 소프트웨어를 깨끗하게 만드는 활동은 완전히 별개다. 우리들 대다수는 두뇌 용량에 한계가 있어 '깨끗하고 체계적인 소프트웨어'보다 '돌아가는 소프트웨어'에 초점을 맞춘다. 전적으로 올바른 태도다. 관심사를 분리하는 작업은 프로그램뿐만이 아니라 프로그래밍 활동에서도 마찬가지다.문제는 우리가 프로그램이 돌아가면 일이 끝났다고 여기는데 있다. '깨끗하고 체계적인 소프트웨어'라는 다음 관심사로 전환하지 않는다. (...) 규모가 어느 수준에 이르는 시스템은 논리도 많고 복잡하다. 이런 복잡성을 다루려면 체계적인 정리가 필수다.

  • 큰 클래스 몇개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다. 작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.
  1. 응집도(Cohesion)
    유용한 링크 (https://devkingdom.tistory.com/300)
    결합도가 낮고, 응집도가 높아야 유지보수하기 쉬운 좋은 프로그램이다.
    적정 수준의 응집도를 유지한다면 작은 클래스 여러 개가 나온다.

    신호 --> 몇몇 메서드만이 쓰는 변수들이 많아진다. --> 클래스를 분리해야 한다는 신호다.

  2. 변경으로부터 닫혀 있어야 한다.
    OCP (Open-Closed Principle): 확장에는 열려있고, 수정에는 폐쇄적이어야 한다.
    대다수 시스템은 지속적인 변경이 가해진다. (...) 기능 추가를 위해 다른 기능을 포함하고 있는 클래스에 손을 대야 한다면, 다른 코드를 망가뜨릴 잠재적인 위험이 존재한다. (...)
    새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지 않는다.

데코레이션 패턴이 좋은 예이다.
https://velog.io/@drimdrim2002/%ED%97%A4%EB%93%9C%ED%8D%BC%EC%8A%A4%ED%8A%B83-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%EC%85%98%ED%8C%A8%ED%84%B4

public interface StockExchange {
  Money currentPrice(String symbol);
}

public Portfolio {
  private StockExchange exchange;
  public Portfolio (StockExchange exchange) {
    this.exchange = exchange;
  }
}

이렇게 결합도를 줄이면 자연스럽게 또 다른 클래스 설계 원칙인 DIP(Dependency Inversion Principle)를 따르는 클래스가 나온다. 본질적으로 DIP는 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.
우리가 개선한 Portfolio 클래스는 StockExchange라는 인터페이스에 의존한다. 해당 인터페이스가 주식 기호를 받아 현재 주식 가격을 반환한다는 추상적인 개념을 표현한다. 이와 같은 추상화로 실제 주가를 얻어오는 방식 등 구체적인 사실은 모두 숨긴다.

11장 시스템

https://velog.io/@junnkk/11%EC%9E%A5-%EC%8B%9C%EC%8A%A4%ED%85%9C

profile
NP-Hard 문제를 풀어봅니다.

0개의 댓글