우당탕탕 우리네 개발생활

[클린코드] 17장 정리(냄새와 휴리스틱) 본문

tech

[클린코드] 17장 정리(냄새와 휴리스틱)

미스터카멜레온 2024. 6. 23. 23:13
<클린코드> 책을 통해 공부한 내용을 정리하기 위해 작성하였습니다. 제 개인적인 각색과 의견이 첨가되어 있어 실제 책의 내용과는 차이가 있을 수 있습니다.

17장. 냄새와 휴리스틱

- 경험상 당연한 플로우의 동작을 구현해야 한다. 당연한 동작을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵다. 저자를 신뢰하지 못하므로 코드를 일일이 살펴야 한다.

 

- 경계를 올바로 처리해야 한다. 코드는 올바로 동작해야 한다. 너무나도 당연한 말이다. 그런데 우리는 올바른 동작이 아주 복잡하다는 사실을 자주 간과한다. 흔히 개발자들은 머릿속에서 코드를 돌려보고 끝낸다. 자신의 직관에 의존할 뿐 모든 경계와 구석진 곳에서 코드를 증명하려 애쓰지 않는다. 부지런함을 대신할 지름길은 없다. 모든 경계 조건, 모든 구석진 곳, 모든 기벽, 모든 예외는 우아하고 직관적인 알고리즘을 좌초시킬 암초다. 스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성하라.

 

- 안전 절차를 무시하지 마라. 체르노빌 원전 사고는 책임자가 안전 절차를 차례로 무시하는 바람에 일어났다. 실험을 수행하기에 번거롭다는 이유에서였다. 결국 실험은 제대로 수행되지 않았고, 세계는 사상 최악의 상업용 원자력 발전 사고를 목격했다. 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 신용카드가 공짜 돈이라는 생각만큼 위험하다.

 

- DRY(Do not Repeat Yourself, 가장 중요한 원칙). 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 좀 더 미묘한 유형은 여러 모듈에서 일련의 switch/case나 if/else문으로 똑같은 조건을 거듭 확인하는 중복이다. 이런 중복은 다형성(polymorphism)으로 대체해야 한다.

 

- 추상화 수준을 올바르게 설정해야 한다. 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다. 예를 들어, 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안 된다. 기초 클래스는 구현 정보에 무지해야 마땅하다.

 

- 과도한 정보를 피해라

  • 잘 정의된 모듈은 인터페이스가 아주 작다. 하지만 작은 인터페이스로도 많은 동작이 가능하다. 부실하게 정의된 모듈은 인터페이스가 구질구질하다. 그래서 간단한 동작 하나에도 온갖 인터페이스가 필요하다. 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다. 부실하게 정의된 인터페이스는 반드시 호출해야 하는 온갖 함수를 제공한다. 그래서 결합도가 높다.
  • 우수한 소프트웨어 개발자는 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다. 클래스가 제공하는 메서드 수는 작을수록 좋다. 함수가 아는 변수 수도 작을수록 좋다. 클래스에 들어있는 인스턴스 변수 수도 작을수록 좋다.
  • 자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스를 피하라. 하위 클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 마라. 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라.

- 죽은 코드를 철저하게 제거하라. 죽은 코드란 실행되지 않는 코드를 가리킨다. 불가능한 조건을 확인하는 if 문과 throw 문이 없는 try 문에서 catch 블록이 좋은 예다.

- (내 생각) try/catch문을 사용하면서 catch에서 또 다른 에러를 던지는 것이 아닌 logging만으로 처리하는 경우가 있다. 이러한 경우는 정상적으로 exception을 던지는 방법으로 리팩터링을 하는 편이 낫다고 생각한다.

 

일관성을 지켜야 한다. 어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다. 앞서 언급한 '최소 놀람의 원칙(The Principle of Least Surprise)'에도 부합한다. 표기법은 신중하게 선택하며, 일단 선택한 표기법은 신중하게 따른다. 착실하게 적용한다면 이처럼 간단한 일관성만으로도 코드를 읽고 수정하기가 대단히 쉬워진다.

- (내 생각) 이미 전해져내려오는 레거시 규칙들이 답답하게 느껴지는 상황에서도 유사하게 구현하는게 맞는지는 고민해볼 필요가 있는 듯 하다. 때로는 심사숙고끝에 새판을 짜는 것도 방법이지 않을까?

 

- 서로 무관한 개념을 인위적으로 결합하지 말자. 일반적으로 인위적인 결합은 직접적인 상호작용이 없는 두 모듈 사이에서 일어난다. 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한 위치에 (물론 잘못된 위치에) 넣어버린 결과다. 게으르고 부주의한 행동이다. 함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치를 고민한다. 그저 당장 편한 곳에 선언하고 내버려두면 안 된다.

- (내 생각) enum이 특정 클래스의 내부 사정을 알 필요가 있을까? 이 객체가 다른 객체의 내부사정을 알 필요가 있을까? 이 메서드가 다른 메서드의 내부 사정을 알 필요가 있을까?

 

- 기능 욕심을 부리지 말자클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안 된다. 메서드가 다른 객체의 참조자(accessor)와 변경자(mutator)를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다. 자신이 그 클래스에 속해 그 클래스 변수를 직접 조작하고 싶다는 뜻이다.

- (내 생각) 올바른 '위임(delegation)' 에 대해서는 늘 고민해 볼 필요가 있다. 

 

- 선택자 인수는 지양해라. 선택자 인수는 큰 함수를 작은 함수 여럿으로 쪼개지 않으려는 게으름의 소산이다. 물론 부울 인수만이 문제라는 말이 아니다. enum, int 등 함수 동작을 제어하려는 인수는 하나 같이 바람직하지 않다. 일반적으로, 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.

 

- static 함수를 적절하게 사용하라. 간혹 우리는 static으로 정의하면 안 되는 함수를 static으로 정의한다. 일반적으로 static 함수보다 인스턴스 함수가 더 좋다. 조금이라도 의심스럽다면 인스턴스 함수로 정의한다. 반드시 static 함수로 정의해야겠다면 재정의할 가능성은 없는지 꼼꼼히 따져본다.

 

- 서술적인 변수명을 사용하자. 프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하는 방법이다.

- (내 생각) 일반적으로 변수의 갯수는 줄일 수 있다면 줄이는 것이 좋지만 사용하게 된다면 서술적인 변수 이름으로 사용하자

 

- 이름과 기능이 일치하는 함수를 작성하라. date 인스턴스에 5일을 더해 date 인스턴스를 변경하는 함수라면 addDaysTo 혹은 increaseByDays라는 이름이 좋다. 반면, date 인스턴스는 변경하지 않으면서 5일 뒤인 새 날짜를 반환한다면 daysLater나 daysSince라는 이름이 좋다.

const newDate: Date = date.add(5);

- (내 생각) 디테일이 돋보인다. 인스턴스를 바꿀거냐? 인스턴스는 수정하지 않으면서 값만 계산해서 줄거냐?

 

- 알고리즘을 제대로 이해하라. 대다수 괴상한 코드는 사람들이 알고리즘을 충분히 이해하지 않은 채 코드를 구현한 탓이다. 잠시 멈추고 실제 알고리즘을 고민하는 대신 여기저기 if 문과 플래그를 넣어보며 코드를 돌리는 탓이다. 알고리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.

 

- 논리적 의존성은 물리적으로 드러내라(클래스의 책임을 제대로 지게 하라). 클래스 A와 클래스 A2가 있다고 하자. 클래스 A는 클래스 A2가 가진 정보를 알 필요가 없고, 클래스 A2도 마찬가지로 클래스 A가 가진 정보를 알 필요가 없다. 이런 상황에서 클래스 A2의 책임인 상수값을 클래스 A가 가지고 있으면서 자신의 메서드에 사용하게 된다면 논리적인 의존관계가 생기게되고 클래스 A2는 클래스 A의 상수값을 본인의 책임과 관련이 있다라고 생각을 해야만 한다. 이런 경우 결국 A2에 해당 상수를 반환할 수 있는 메서드를 작성해주어 실질적인 책임을 A2에게 줄 수 있다.

 

- 표준 표기법을 따르라. 팀은 업계 표준에 기반한 구현 표준을 따라야 한다.

- (내 생각) '업계 표준' 이라는 단어가 굉장히 중요하다. 잘못된 표준을 팀의 표준으로 삼게 되면 팀원들이 바뀌고 수많은 변화가 있을 때마다 갈등 거리로 전락한다. 최대한 업계 표준을 맞추기 위해 노력하자.

 

- 매직 숫자는 명명된 상수로 교체하라. '매직 숫자'라는 용어는 단지 숫자만 의미하지 않는다. 의미가 분명하지 않은 토큰을 모두 가리킨다.

 

- 정확하라.

  • 검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다.
  • 부동 소수점으로 통화를 표현하는 행동은 거의 범죄에 가깝다.
  • 마찬가지로 갱신할 가능성이 희박하다고 잠금과 트랜잭션 관리를 건너뛰는 행동은 아무리 잘 봐줘도 게으름이다.
  • List로 선언할 변수를 ArrayList로 선언하는 행동은 지나친 제약이다.
  • 모든 변수를 protected로 선언한 코드는 무절제하다.
  • 코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충 결정해서는 안 된다. 호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다. 조회 결과가 하나뿐이라 짐작한다면 하나인지 확실히 확인한다. 통화를 다뤄야 한다면 정수를 사용하고 반올림을 올바로 처리한다. 병행(concurrent) 특성으로 인해 동시에 갱신할 가능성이 있다면 적절한 잠금 메커니즘을 구현한다.
  • 코드에서 모호성과 부정확은 의견차나 게으름의 결과다. 어느 쪽이든 제거해야 마땅하다.

- 숨겨진 시간적인 결합은 반드시 드러내도록 변경하라. 그 방법으로 일종의 연결 소자를 생성해 시간적인 결합을 노출한다. 각 함수가 내놓는 결과는 다음 함수에 필요하다. 그러므로 순서를 바꿔 호출할 수가 없다.

 

- 일관성을 유지하라. 일반적인 관례 안에서 일관성을 유지하라.

- (내 생각) 결국 다양한 책들을 공부하고 다양한 좋은 코드 레퍼런스들을 공부하는 이뉴는 '관례'를 제대로 알기 위해서이다.

 

- 경계 조건을 캡슐화하라. ...level + 1 이 두 번 나온다. 이런 경계 조건은 변수로 캡슐화하는 편이 좋다. 변수 이름은 nextLevel이 적합하겠다.

const nextLevel = level + 1;
if (nextLevel < tags.length) {
    parts = new Parse(body, tags, nextLevel, offset + endTag);
    body = null;
}

 

- 설정 정보는 최상위 단계에 둬라. 설정 관련 상수는 최상위 단계에 둔다. 그래야 변경하기도 쉽다. 설정 관련 변수는 나머지 코드에 인수로 넘긴다. 저차원 함수에 상수 값을 정의하면 안 된다.

 

- 긴 import 목록을 피하고 와일드 카드를 사용하라. import 문은 패키지를 단순히 검색 경로에 추가하므로 진정한 의존성이 생기지 않는다. 그러므로 모듈 간에 결합성이 낮아진다. 와일드카드 import 문은 때로 이름 충돌이나 모호성을 초래한다. 이름이 같으나 패키지가 다른 클래스는 명시적인 import 문을 사용하거나 아니면 코드에서 클래스를 사용할 때 전체 경로를 명시한다. 다소 번거롭지만 자주 발생하지 않으므로 여전히 와일드카드 import 문이 명시적인 import 문보다 좋다.

- (내 생각) 개인적으로 맨 마지막 줄은 고민이 된다. 최근 nestjs와 barrel파일 이용간의 문제 상황을 블로깅했었는데 이러한 물리적인 편안함이 의존성에 대한 충돌을 야기시킬 수 있기 때문이다.

 

- 적절한 추상화 수준에서 이름을 선택하라. 구현을 드러내는 이름은 피하라.

 

- 가능하다면 표준 명명법을 사용하라. 흔히 팀마자 특정 프로젝트에 적용할 표준을 나름대로 고안한다. 에릭 에반스는 이를 프로젝트의 유비쿼터스 언어(ubiquitous language)라 부른다. 코드는 이 언어에 속하는 용어를 열심히 써야 한다. 간단히 말해, 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

 

- 이름으로 부수 효과를 설명하라. 아래코드는 단순히 "oos"만 가져오지 않는다. 기존에 "oos"가 없으면 생성한다. 그러므로 createOrReturnOos라는 이름이 더 좋다.

public getOos(): ObjectOutputStream {
    if (m_oos === null) {
        m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
}

- (내 생각) 클라이언트 입장에서 create라는 부수효과를 알아야할까? 그냥 해당 객체를 받기만 하면 되지 않을까?

 

- 사소한 테스트를 건너뛰지 마라. 사소한 테스트는 짜기 쉽다. 사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.

 

- 경계 조건을 테스트하라. 경계 조건은 각별히 신경 써서 테스트한다. 알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.

 

- 버그 주변은 철저히 테스트하라. 버그는 서로 모이는 경향이 있다. 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다. 십중팔구 다른 버그도 발견하리라.