728x90

1. 주석

- 부적절한 정보

변경 이력 등의 주석은 적절하지 못하다

소스 코드 관리 시스템, 버그 추적 시스템, 이슈 추적 시스템 등 다른 시스템에서 저장하도록 맡기자

 

- 쓸모 없는 주석

주석은 빨리 낡는다

쓸모 없어질 주석은 달지 않는 편이 가장 좋으며

쓸모 없어진 주석은 재빨리 삭제하는 편이 좋다

 

- 중복된 주석

코드만으로 충분한 경우 중복된 주석을 달지 않는다

i++; // i 증가

 

- 성의 없는 주석

문법과 구두점을 올바로 사용하고

간결하고 명료하게 작성한다

 

- 주석 처리된 코드

얼마나 오래된 코드인지 중요한 코드인지 아닌지 알 길이 없어진다

정말 필요하다면 소스 코드 관리 시스템(형상 관리 시스템)에서 이전 버전을 가져와 사용하면된다


2. 환경

- 여러 단계로 빌드하는 경우

빌드는 간단히 한 단계로 끝나야 한다

불가해한 명령이나 스크립트를 잇달아 실행해 각 요소를 따로 빌드할 필요가 없어야 한다

 

- 여러 단계로 테스트하는 경우

모든 단위 테스트는 한 명령으로 돌려야 한다

IDE에서 버튼 하나로 모든 테스트를 돌린다면 가장 이상적이다

아무리 열악한 환경이라도 셸에서 명령 하나로 가능해야 한다


3. 함수

- 너무 많은 인수

함수에서 인수 개수는 작을수록 좋다

아예 없는 것이 가장 좋고 넷 이상은 최대한 피하자

 

- 출력 인수

출력 인수는 직관을 정면으로 위배한다

함수에서 뭔가의 상태를 변경해야 한다면

출력 인수를 사용하지 말고 함수가 속한 객체의 상태를 변경한다

 

- 플래그 인수

boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다

플래그 인수는 혼란을 초래한다

사용하지 말자

 

- 죽은 함수

사용하지 않는 함수는 삭제하라


4. 일반

- 한 소스 파일에 여러 언어를 사용할 경우

이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다

현실적으로는 불가피하지만, 각별한 노력을 기울여 소스 파일에서 언어 수와 범위를 최대한 줄이도록 애써야 한다

 

- 당연한 동작을 구현하지 않는 경우

최소 놀람의 원칙(The Principle of Least Surprise)에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다

 

요일 문자열에서 요일을 나타내는 enum으로 변환하는 함수를 예로 들어보자

Day day = DayDate.StringToDay(String dayName);

해당 함수를 사용할 경우 Monday를 Day.MONDAY로 변환할 것을 기대한다

또한 일반적으로 쓰는 요일 약어도 사용할 수 있고, 대소문자 역시 구분하지 않고 사용 가능하리라 기대한다

당연한 동작을 구현하지 않으면 더 이상 이름만으로 함수 기능을 예상하기 어렵고

신뢰를 잃어 결국 코드를 일일히 살펴야 한다

 

- 경계를 올바로 처리하지 않는 경우

코드는 올바로 동작해야 한다

너무나도 당연한 말이지만 올바른 동작이 아주 복잡하다는 사실을 자주 간과한다

머릿속에서 코드를 돌려보고 끝내면 안된다

스스로의 직관에 의존하지 마라

모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성하라

 

- 안전 절차 무시

안전 절차를 무시하면 위험하다

컴파일러 경고 일부를(혹은 전체를) 꺼버리면 빌드가 쉬워질지 모르지만

자칫하면 끝없는 디버깅에 시달린다

실패하는 테스트 케이스를 미루지 말자

 

- 중복

이 책에 나오는 가장 중요한 규칙 중 하나이다

 

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라

중복된 코드를 하위 루틴이나 다른 클래스로 분리하라

 

이렇듯 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어난다

그만큼 다른 프로그래머들이 어휘를 사용하기 쉬워지며,

추상화 수준을 높혓으므로 구현이 빨라지고 오류가 적어진다

 

여기저기 복사한 듯한 코드들이 보인다면 함수로 교체하자

 

- 추상화 수준이 올바르지 못할 경우

추상화는 저차원 상세 개념에서 고차원 일반 개념을 분리한다

때로 우리는 추상 클래스(고차원 개념)와 파생 클래스(저차원 개념)를 생성해 추상화를 수행한다

추상화로 개념을 분리할 때는 철저해야 한다

모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다

 

예를 들어 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안 된다

기초 클래스는 구현 정보에 무지해야 한다

소스 파일, 컴포넌트, 모듈도 마찬가지다

 

우수한 소프트웨어 설계자는 개념을 다양한 차원으로 분리해 다른 컨테이너에 넣는다

때로는 기초 클래스와 파생 클래스로 분리하고, 때로는 소스 파일과 모듈과 컴포넌트로 분리한다

고차원 개념과 저차원 개념을 섞어서는 안 된다

 

- 기초 클래스가 파생 클래스에 의존할 경우

개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는

고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다

즉, 기초 클래스가 파생 클래스를 사용한다면 문제가 있다는 말이다

일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 한다

 

물론 예외는 있다

간혹 파생 클래스 개수가 확실히 고정되었다면 기초클래스에 파생 클래스를 선택하는 코드가 들어간다

일반적으로는 기초 클래스와 파생 클래스를 다른 JAR 파일로 배포하는 편이 좋다

 

- 과도한 정보

잘 정의된 모듈은 인터페이스가 아주 작다

작은 인터페이스로도 많은 동작이 가능하며, 부실하게 정의된 모듈은 인터페이스가 구질구질하다

그래서 간단한 동작 하나에도 온갖 인터페이스가 필요하다

잘 정의된 인터페이스는 많은 함수를 제공하지 않기 때문에 결합도가 낮다

부실하게 정의된 인터페이스는 반드시 호출해야 하는 온갖 함수를 제공하기 때문에 결합도가 높다

 

  • 클래스가 제공하는 메서드 수는 작을수록 좋다
  • 함수가 아는 변수 수도 작을수록 좋다
  • 클래스에 들어있는 인스턴스 변수 수도 작을수록 좋다

 

  • 자료를 숨겨라
  • 유틸리티 함수를 숨겨라
  • 상수와 임시 변수를 숨겨라
  • 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피해라
  • 하위 클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 마라

 

- 수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다

지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다

선언한 위치로 부터 몇백 줄 아래에서 사용하면 안 된다

 

비공개 함수는 처음으로 호출한 직후에 정의한다

비공개 함수는 전체 클래스 범위에 속하지만 그래도 정의하는 위치와 호출하는 위치를 가깝게 유지한다

비공개 함수는 처음으로 호출되는 위치를 찾은 후 조금만 아래로 내려가면 쉽게 눈에 띄어야 한다

 

- 일관성이 부족한 경우

어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다

이 역시 최소 놀람의 원칙에 부합한다

변수 이름이나 메서드 명을 일관성있게 작성해라

일관성 하나만으로도 코드를 읽고 수정하기 대단히 쉬워진다

 

- 잡동사니

아무도 사용하지 않는 변수, 함수, 주석 등 모두 제거해라

소스 파일은 언제나 깔끔하게 정리하고 잡동사니를 없애라

 

- 인위적 결합

서로 무관한 개념을 인위적으로 결합하지 않는다

예를 들어 일반적인 enum은 특정 클래스에 속할 이유가 없다

enum이 클래스에 속한다면 enum을 사용하는 코드가 특정 클래스를 알아야만 한다

범용 static 함수도 마찬가지로 특정 클래스에 속할 이유가 없다

 

함수, 상수, 변수를 선언할 때는 신간을 들여 올바른 위치를 고민하자

당장 편한 곳에 선언하고 내버려두면 안 된다

 

- 기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야한다

다른 클래스의 변수와 함수에 관심을 가져서는 안된다

메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작한다면,

메서드가 그 객체 클래스의 범위를 욕심내는 탓이다

해당 내용은 객체 지향 설계의 여러 원칙을 위반하는 행위이다

 

- 부적절한 static 함수

일반적으로 static 함수보다 인스턴스 함수가 더 좋다

조금이라도 의심스럽다면 인스턴스 함수로 정의한다

반드시 static 함수로 정의해야한다면 재정의할 가능성은 없는지 꼼꼼히 따져본다

 

- 서술적 변수

프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가

계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하는 방법이다

Matcher match = headerPattern.matcher(line);
if(match.find()) {
    String key = match.group(1);
    String value = match.group(2);
    headers.put(key.toLowerCase(), value);
}

서술적인 변수 이름을 사용하여 첫 번째로 일치하는 그룹이 key이고,

두번째로 일치하는 그룹이 value 라는 사실이 명확히 드러난다

 

- 이름과 기능이 일치하는 함수

Date newDate = date.add(5);

5일을 더해 date 인스턴스를 변경하는 함수라면

addDaysTo 혹은 inccreaseByDays라는 이름이 더 좋다

이름이 불분명하면 5일을 더하는지 5달을 더하는지 5시간을 더하는지 알 수가 없다

 

- 논리적 의존성은 물리적으로 드러내라

한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다

논리적인 의존성만으로는 부족하다

의존하는 모든 정보를 명시적으로 요청하는 편이 좋다

 

상수를 선언할 때 책임을 따져보고 함수를 통해 해당 상수를 호출 할 수 있도록 하자

 

- if/else 혹은 swich/case 문보다 다형성을 사용하라

  • swich 문을 사용하기전에 다형성을 고려해라
    당장 손쉬운 선택이기 때문에 swich를 사용해서는 안된다
  • 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다
    그러므로 모든 swich 문을 의심해야 한다
  • 'swich 문 하나' 규칙을 따른다
    선택 유형 하나에는 swich 문을 한번만 사용한다
    같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성해 swich 문을 대신하라

 

- 표준 표기법을 따르라

팀은 업계 표준에 기반한 구현 표준을 따르는 것이 좋고

팀이 정한 표준은 팀원들 모두가 따라야 한다

 

- 매직 숫자는 명명된 상수로 교체하라

일반적으로 코드에서 숫자를 사용하지 말라는 규칙이다

숫자는 명명된 상수 뒤로 숨기라는 의미

 

- 정확하라

코드에서 뭔가를 결정할 때는 정확히 결정한다

결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다

호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다

조회 결과가 하나뿐이라 짐작한다면 하나인지 확실히 확인한다

통화를 다뤄야 한다면 정수를 사용하고 반올림을 올바로 처리한다

병행 특성으로 인해 동시에 갱신할 가능성이 있다면 적절한 잠금 매커니즘을 구현한다

 

- 관례보다 구조를 사용하라

설계 결정을 강제할 때는 규칙보다 관례를 사용한다

명명 관례도 좋지만 구조자체로 강제하면 더 좋다

 

예를 들어 enum 변수가 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다

switch/case 문을 매번 똑같이 구현하게 강제하기는 어렵지만,

파생 클래스는 추상 메서드를 모두 구현하지 않으면 안 되기 때문이다

 

- 조건을 캡슐화하라

조건을 함수로 표현하자

if(shouldBeDeleted(timer)) {
    ...
}

 

- 부정 조건은 피하라

부정 조건은 긍정 조건보다 이해하기 어렵다

가능하면 긍정 조건으로 표현하자

 

- 경계 조건을 캡슐화하라

코드 여기저기에 +1이나 -1을 흩어놓지 않는다

/* before */
if(level + 1 < tags.length) {
    ....
}

/* after */
int nextLevel = level + 1;
if(nextLevel < tags.length) {
    ....
}

 

- 함수는 추상화 수준을 한 단계만 내려가야 한다

함수 내 모든 문장은 추상화 수준이 동일해야 한다

그리고 그 추상화 수준은 함수 이름을 의미하는 작업보다 한 단계만 낮아야 한다

 

- 설정 정보는 최상위 단계에 둬라

추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안된다

대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다

 

- 추이적 탐색을 피하라

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다

좀 더 구체적으로 A가 B를 사용하고 B가 C를 사용한다면

A가 C를 알 필요가 없다는 뜻이다

이를 디미터의 법칙이라 부른다


5. 자바

- 긴 import 목록을 피하고 와일드카드를 사용하라

패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져오라

긴 import 목록은 읽기에 부담스럽다

 

- 상수는 상속하지 않는다

상수를 상속 계층 위에 숨겨 놓아서는 안된다

언어의 범위 규칙을 속위는 행위다

대신 import static 문을 사용해라

 

- 상수 vs Enum

enum 문법을 사용하면 메서드와 필드도 사용할 수 있다

훨씬 더 유연하고 서술적인 강력한 도구다


6. 이름

- 서술적인 이름을 사용하라

무엇을 의미하는지 서술적인 이름을 신중하게 고르자

 

- 적절한 추상화 수준에서 이름을 선택하라

구현을 드러내는 이름은 피하라

작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라

 

예를 들어 모뎀을 사용하는 애플리케이션이 있을 때

이전에는 전화선을 통해 모뎀을 연결했다

그렇기 때문에 매개 변수를 전화번호를 표현하는 phoneNumber를 변수명으로 사용하여 구현했을 경우

연결 대상이 더 이상 전화번호로 제한 되지 않을 수 있다

phoneNumber 라는 추상화 개념보다는

connectionLocator 라는 변수명을 사용하면 전화번호는 물론이고 다른 연결 방식에도 사용이 가능하다

 

- 긴 범위는 김 이름을 사용하라

이름 길이는 범위 길이에 비례해야 한다

범위가 작으면 아주 짧은 이름을 사용해도 괜찮다

하지만 범위가 길어지면 긴 이름을 사용한다

범위가 5줄 안팎이라면 i 나 j와 같은 변수 이름을 사용해도 괜찮다

오히려 변수 i(j)를 길게 지으면 더 헷갈리는 경우가 발생한다

 

- 인코딩을 피하라

이름에 유형 정보나 범위 정보를 넣어서는 안된다

오늘날 개발 환경에서는 이름 앞에 m_ 이나 f와 같은 접두어가 불필요하다

 

- 이름으로 부수 효과를 설명하라

함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다

이름에 부수 효과를 숨기지 않는다

실제로 여러 작업을 수행하는 함수에다 동사 하나만 달랑 사용하면 곤란하다

 

예를 들어 기존에 'oos'라는 데이터가 없으면 생성하고 가져오는 함수가 있다면

getOos() 라는 이름보다는

createOrReturnOos() 라는 이름이 더 좋다


7. 테스트

- 불충분한 테스트

테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다

테스트 케이스가 확인하지 않는 조건이나 검증하지 않은 계산이 있다면 그 테스트는 불완전하다

 

- 커버리지 도구를 사용하라

커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다

 

- 사소한 테스트를 건너뛰지 마라

사소한 테스트는 짜기 쉽다

사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다

 

- 무시한 테스트는 모호함을 뜻한다

때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확신하기 어렵다

불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 @ignore를 붙여 표현한다

선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달려있다

 

- 경계 조건을 테스트하라

경계 조건은 각별히 신경 써서 테스트하라

 

- 버그 주변은 철저히 테스트하라

버그는 서로 모이는 경향이 있다

한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다

 

- 실패 패턴을 살펴라

때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다

테스트 케이스를 최대한 꼼꼼히 짜야하는 이유가 여기에 있다

합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다

 

- 테스트 커버리지 패턴을 살펴라

통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 실패 원인이 드러난다

 

- 테스트는 빨라야 한다

느린 테스트 케이스는 실행하지 않게 된다


8. 결론

이 장에서 소개한 휴리스틱과 냄새 목록은 완전하다 말하기는 어렵다

하지만 완전한 목록이 목표가 아니며, 완전한 목록이 가능하다고도 생각하지 않는다

일군의 규칙만 따른다고 깨끗한 코드가 얻어지지 않는다

전문가 정신과 장인 정신은 가치에서 나온다

그 가치에 기반한 규율과 절제가 필요하다


마무리 느낀점

이번 마지막 장은 정리하는 느낌이 더 강했다

해당 챕터에서도 중복되는 내용이 많아서 그 부분들을 제외하고는 모두 정리했다

지금까지 읽었던 내용들을 정리해주는 느낌이라 도움이 많이 되었다

728x90
복사했습니다!