자바/클린 코드

클린코드 17장 - 냄새와 휴리스틱

끄적끄적 2022. 7. 11. 22:25

마틴파울러의 리팩토링 책에서도 언급하는 것처럼, 우리가 소스를 보다보면 리팩토링이 필요한 나쁜 냄새나는 부분들을 찾을 수 있다.

주석
C1. 부적절한 정보 : 변경이력, 작성자, 최종수정일 등은 다른시스템(소스관리, 버그추적, 이슈추적 등)에 저장할 정보이다.
주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다.

C2. 쓸모 없는 주석 : 오래되거나 엉뚱하고 잘못된 주석은 더이상 쓸모다 없다. 코드와 무관하게 혼자서 따로 놀며 코드를 그릇된 방향으로 이끈다.

C3. 중복된 주석 : 코드에 이미 있는 내용을 구구절절 반복할 필요는 없다.

/**
 * @param sellRequest
 * @return
 * @throws ManagedComponentException
 */
public SellResponse beginSellItem(SellRequest sellRequest)
throws ManagedComponentException

C4. 성의 없는 주석 : 주석을 달 가치가 있다면, 단어를 신중하게 선택하고 문법과 구두점을 올바르게 사용하고, 간결하고 명료하게 작성한다.

C5. 주석 처리된 코드 : 소스코드 관리시스템이 보관하므로, 주석으로 처리된 코드는 모두 지우는게 좋다. 주석은 변경관리되지 않는다.

환경
E1. 여러 단계로 빌드해야 한다. : 빌드는 간단히 한 단계로 끝나야 한다. 이것저것 체크아웃할 필요가 있거나 여러 명령이나 스크립트를 잇달아 실행할 필요가 없어야 한다. 한 명령으로 체크아웃해서 한 명령으로 빌드할 수 있어야 한다.

E2. 여러 단계로 테스트해야 한다. : 모든 단위테스트는 한 명령으로 돌려야 한다. 테스트는 빠르고, 쉽고, 명백해야 하며 이는 아주 근본적이고 중요하다.

함수
F1. 너무 많은 인수 : 함수에서 인수개수는 작을수록 좋고 없는게 가장 이상적이다. 넷 이상은 최대한 피한다.

F2. 출력 인수 : 인수로 받은 값을 변경하는 것은 좋지 않다. 소스를 보는 사람은 인수는 입력으로 보지 출력으로 생각하지 않는다. 함수에서 뭔가를 변경해야 한다면 인수를 변경하지 말고 함수가 속한 객체의 상태를 변경해야 한다.

F3. 플래그 인수 : boolean을 인수로 받는다는 얘기는 함수가 여러 기능을 수행한다는 명백한 증거다.

F4. 죽은 함수 : 아무도 호출하지 않는 함수는 과감히 삭제하라.

일반
G1. 한 소스 파일에 여러 언어를 사용한다. : 소스파일 하나에는 언어 하나만 사용하는 방식이 좋다. XML, HTML, YAML, JavaScript 등이 한 파일안에 같이 포함되면 혼란스럽다.

G2. 당연한 동작을 구현하지 않는다. : 최소 놀람의 원칙(The Principle of Least Surpise)에 의거하여 함수나 클래스는 다른 프로그래머가 당연히 여길 동작과 기능을 제공해야 한다. 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵게 짜면, 일일이 코드를 다 살펴봐야 한다.

G3. 경계를 올바로 처리하지 않는다 : 스스로의 직관에 의존하지 말고, 모든 경계 조건을 찾아내고, 경계조건을 테스트하는 테스트 케이스를 작성하라.

G4. 안전 절차 무시 : 실험을 수행하기 번거롭다는 이유로 안전절차를 무시하면 사고가 발생한다. 컴파일러 경고, 실패하는 테스트 케이스는 나중으로 미루어서는 안된다.

G5. 중복 : 이책에 나오는 가장 중요한 규칙 중 하나이다. 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하고, 중복된 코드를 다른 함수나 클래스로 분리하라. 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어나며, 다른 프로그래머들이 그만큼 어휘를 사용하기 쉬워진다. 추상화수준을 높였으므로 구현이 빨라지고 오류가 적어진다.
switch/case나 if/else이 여러 모듈에서 반복되면 다형성으로 대체하라. 알고리즘이 유사하나 코드가 다르면 Template Method패턴이나 Strategy 패턴으로 중복을 제거하라.

G6. 추상화 수준이 올바르지 못하다 : (고차원 개념을 표현하는) 추상클래스와 (저차원 개념을 표현하는) 파생 클래스로 개념을 분리하여 추상화를 구현해야 한다.  구체적인 저차원 개념들은 파생 클래스에 넣고, 추상적인 고차원 개념은 기초 클래스에 넣어야 한다.

public interface Stack {
  Object pop() throws EmptyException;
  void push(Object o) throws FullException;
  double percentFull();
  class EmptyException extends Exception {}
  class FullException extends Exception {}
}

위의 소스에서 percentFull함수는 추상화 수준이 올바르지 못하다. Stack을 구현하는 방법은 다양하기 때문에 BoundedStack과 같이 고정길이 Stack에서나 구현할 함수이다. 

G7. 기초클래스가 파생클래스에 의존한다 : 기초 클래스와 파생 클래스를 나누는 이유는 독립성을 보장하기 위함이므로 기초 클래스는 파생 클래스를 몰라야 한다.

G8. 과도한 정보 : 잘 정의된 모듈은 인터페이스가 작고, 많은 함수를 제공하지 않아 낮은 결합도를 가짐

G9. 죽은 코드 : 죽은 코드란 실행되지 않는 코드를 가리킨다. 시간이 갈수록 제대로 관리되지 못해 악취를 풍기기 시작한다.    ex) 불가능한 조건을 확인하는 if 문, throw 문이 없는 try 문에서 catch 블록, 아무도 호출하지 않는 유틸리티 함수와 switch/case 문에서 불가능한 case 조건 등등

G10. 수직 분리 : 변수와 함수는 사용되는 위치에 가깝게 정의한다. 지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다.

G11. 일관성 부족 : 표기법은 신중하게 선택하며, 일단 선택한 표기법은 통일성있게 따라야 한다. 
ex) 한 메서드를 processVerificationRequest라고 정의했다면, processDeletionRequest라고 유사하게 이름을 사용한다.

G12. 잡동사니 : 비어 있는 기본 생성자, 아무도 사용하지 않고 호출하지 않는 함수, 정보를 제공하지 않는 주석 등 잡동사니이니 잡동사니를 없애라.

G13. 인위적 결합 : 서로 무관한 개념을 인위적으로 결합하지 않는다.
  ex) 일반적인 enum은 특정 클래스에 속할 이유가 없으며 범용 static 함수도 마찬가지로 특정 클래스에 속할 이유가 없다.
⇒ 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한 위치에 넣어버린 결과이므로 항상 시간을 들여 올바른 위치를 고민하라.

G14. 기능 욕심 : 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야만 하고 다른 클래스의 변수와 함수에 관심을 가져서는 안된다. 메서드가 다른 객체의 참조자(accessor)와 변경자(mutator)를 사용해 그 객체 내용을 조작한다면 범위를 욕심내는 탓이다. 기능 욕심은 한 클래스의 속사정을 다른 클래스에 노출하므로, 별다른 문제가 없다면 제거하는 편이 좋다.

G15. 선택자 인수 : 함수 호출 끝에 달리는 false 인수만큼이나 밉살스런 코드도 없다. 선택자(selector)인수는 목적을 기억하기 어려울 뿐 아니라 각 선택자 인수가 여러 함수를 하나로 조합한다. 이는 큰 함수를 여럿 작은 함수로 쪼개지 않으려는 게으름의 소산이다. 일반적으로 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.

G16. 모호한 의도 :  코드를 짤 때는 의도를 최대한 분명히 밝힌다. 행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 번호 등은 모두 저자의 의도를 흐린다. 독자에게 의도를 분명히 표현하도록 시간을 투자하자.

G17. 잘못 지운 책임 : 가장 중요한 결정 중 하나가 코드를 배치하는 위치다. 코드는 독자가 자연스럽게 기대할 위치에 배치한다. PI 상수는 삼각함수를 선언한 클래스에 넣어야 맞다. 때로는 개발자가 '영리하게' 기능을 배치한다. 독자에게 직관적인 위치가 아니라 개발자에게 편한 함수에 배치한다. 결정을 내리는 한 가지 방법으로 함수의 이름을 살펴본다. 이름을 보다보면 어디에 배치할 지 명백하게 드러난데. 이를 위해 사실을 반영해 함수의 이름을 제대로 지어야 한다.

G18 : 부적절한 static 함수 : Math.max(double a, double b)는 좋은 static 메서드다. 특정 인스턴스와 관련된 기능이 아니다. 사용하는 정보는 두 인수가 전부이며 메서드를 소유하는 객체에서 가져오는 정보가 아니다. 결정적으로 재정의(override)할 가능성이 거의 아니 전혀 없다. 하지만 우리는 간혹 static으로 정의하면 안 되는 함수를 static으로 정의한다. 함수를 재정의할 가능성이 존재하면 static 함수보다 인스턴스 함수로 구현하라.

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

G20 : 이름과 기능이 일치하는 함수 : 5일을 더하는 것인지 5시간인지 5주인지 아니면 인스턴스를 변경하는 함수인지 코드만 보아서는 알 방법이 없다. 5일을 더해 date 인스턴스를 변경하는 함수라면 addDaysTo 혹인 inclreaseByDays라는 이름이 좋다. 이름만으로 분명하지 않기에 구현을 살피거나 문서를 보아야 한다면 더 좋은 이름을 붙이기 쉽도록 기능을 정리해야 한다.

Date newDate = date.add(5);

 

G21 : 알고리즘을 이해하라 : 대다수 괴상한 코드는 알고리즘을 충분히 이해하지 않은 채 코드를 구현한 탓이다. 구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라. 테스트 케이스를 모두 통과했다는 사실만으로는 부족하다. 작성자가 알고리즘이 올바르다는 사실을 알아야 한다. 이를 위해서는 기능이 빤히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 것이 최고의 방법이다.

G22 : 논리적 의존성은 물리적으로 드러내라 : 한 모듈이 다른 모듈에 의존하다면 물리적인 의존성도 있어야 한다. 의존하는 모듈이 상대 모듈에 대해 뭔가를 가정하면 안된다. 의존하는 모든 정보를 명시적으로 요청하는 편이 좋다. 논리적인 의존성 만으로는 부족하다.

G23 : If/Else 혹은 Switch/Case 문보다 다형성을 사용하라 : 첫째로 대다수 개발자가 switch 문을 사용하는 이유는 올바르기보다는 손쉬운 선택이기 때문이다. 따라서 그 이전에 다형성을 먼저 고려하라는 의미다. 둘째로 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물기 때문에 switch 문을 의심해야 한다. 선택 유형 하나에는 switch 문을 한번만 사용하고, 같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성해 switch 문을 대신한다.

G24 : 표준 표기법을 따르라 : 팀은 업계 표준에 기반한 구현 표준을 따라야 한다. 인스턴스 변수 선언 위치, 이름을 정하는 방법, 괄호를 넣는 위치 등을 명시해야 한다. 이는 코드 자체로 충분해야 하며 별도의 문서로 설명할 필요가 없어야 한다. 그리고 이를 팀원들이 모두 따라야 하기에 이를 이해할 정도로 팀원들이 성숙해야 한다.

G25 : 매직 숫자는 명명된 상수로 교체하라 : 가장 오래된 규칙 중 하나로 일반적으로 코드에서 숫자를 직접 사용하지 말라는 규칙이다. 즉, 명명된 상수 뒤로 숨기라는 의미다. 매직 숫자는 단순히 숫자만 의미하지 않고 의미가 불분명한 토큰을 모두 가리킨다.

G26 : 정확하라 : 검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다. 부동소수점으로 통화를 표현하는 행동은 범죄에 가깝다. 코드에서 뭔가 결정할 때는 정확히 결정한다. 그 이유와 예외를 처리할 방법을 분명히 알고 대충 결정해서는 안된다. 코드에서 모호성과 부정확은 의견차나 게으름의 결과이기에 어느 쪽이든 제거해야 마땅하다.

G27 : 관례보다 구조를 사용하라 : 설계 결정을 강제할 때는 규칙보다 관례를 사용한다. 명명 관례도 좋지만 구조 자체로 강제하면 더 좋다. 예를 들어, enum 변수가 멋진 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다. switch/case 문을 매번 똑같이 구현하게 강제하기는 어렵지만, 파생 클래스는 추상 메서드를 모두 구현하지 않으면 안 되기 때문이다.

G28 : 조건을 캡슐화하라 : 부울 논리는 if나 while문에 넣어 생각하지 않아도 이해하기 어렵다. 예를 들어 아래 첫번째 줄의 코드가 두번째 줄 보다 좋다.

if (shouldBeDeleted(timer))

if (timer.haseExpired() && !timer.isRecurrent())

G29 : 부정 조건 피하라 : 부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건을 표현한다

G30 : 함수는 한 가지만 해야 한다 : 함수를 짜다보면 한 함수에 여러 단락을 이어, 일련의 작업을 수행하고픈 유혹에 빠진다. 이런 함수는 한 가지만 수행하는 함수가 아니기 때문에 좀 더 작은 함수 여럿을 나눠야 마땅하다

G31 : 숨겨진 시간적인 결합 : 때로는 시간적인 결합이 필요하지만 이를 숨겨서는 안 된다. 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다. 일종의 연결 소자를 생성해 시간적인 결합을 노출한다. 아래를 보면 각 함수가 내놓는 결과가 다음 함수에 필요하므로 순서를 바꾸어 호출할 수 없다.

public void dive(String reason) {
  Gradient gradient = saturateGradient();
  List<Spline> splines = reticulateSplines(gradient);
  divForMoog(splines, reason);
}

G32 : 일관성을 유지하라 : 구조에 일관성이 없어 보인다면 남들이 맘대로 바꾸어도 괜찮다고 생각한다. 시스템 전반에 걸쳐 구조가 일관성이 있다면 남들도 그 일관성을 따르고 보존한다.

G33 : 경계 조건을 캡슐화하라 : 경계 조건은 빼먹거나 놓치기 십상이기 때문에 코드 여기저기에서 처리하지 않고 한 곳에서 별도로 처리한다.

G34 : 함수는 추상화 수준을 한 단계만 내려가야 한다 : 함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다. 추상화 수준 분리는 리팩터링을 수행하는 가장 중요한 이유 중 하나다. 제대로 하기 어려운 작업 중 하나 이기도 하다. 함수에서 추상화 수준을 분리하면 앞서 드러나지 않았던 새로운 추상화 수준이 드러나는 경우가 빈번하다.

G35 : 설정 정보는 최상위 단계에 둬라 : 추상화 최상위 단계에 두어야 할 기본값 상수나 설정 관련 상수를 저차원 함수(구체적 함수)에 숨겨서는 안된다. 대신 고차원 함수에서 저차원 함수를 호출할때 인수로 넘긴다. 그래야 찾기도 편하고 변경하기도 쉽다.

G36 : 추이적 탐색을 피하라 : 일반적으로 한 모듈은 주변 모듈을 모를수록 좋다. 좀 더 구체적으로 A가 B를 사용하고 B가 C를 사용한다 하더라도 A가 C를 알아야 할 필요가 없다는 뜻이다. 이를 디미터의 법칙(Law of Demeter)이라 부른다. 예를 들면 a.getB().getC().doSomething();은 바람직하지 않다. 내가 사용하는 모듈이 내게 필요한 서비스를 모두 제공해야지 원하는 메서드를 찾아 시스템을 탐색할 필요가 없어야 한다 다시 말해 다음의 myCollaborator.doSomething(); 과 같이 간단한 코드로 충분해야 한다.

자바
J1 : 긴 import 목을 피하고 와일드카드를 사용하라 : 패키지에 클래스를 둘 이상 사용한다면 와일드 카드를 사용해 패키지 전체를 가져오라. 긴 목록은 읽기 부담스럽기 때문에 사용하는 패키지를 간단히 명시하면 충분하다. 명시적으로 클래스까지 import하면 강한 의존성을 생성하지만 와일드카드는 그렇지 않다. 물론 이름 충돌이나 모호성을 초래하기 때문에 명시적으로 사용하는 경우도 필요하지만 극히 드물다. 따라서 번거롭더라도 자주 발생하지 않으므로 와일드 카드문이 더 좋다.

J2 : 상수는 상속하지 않는다 : 상수를 상속 계층 맨 위에 숨겨놓는 것은 끔찍한 관행이다. 상속을 이렇게 사용하는 건 언어의 범위 규칙을 속이는 행위다. 대신 static import를 사용하라.

J3 : 상수 대 Enum : 자바 5는 enum을 제공하므로 마음껏 활용하라! public static final int라는 옛날 기교를 더 이상 사용할 필요가 없다. int는 코드에서 의미를 잃어버리기도 하지만 enum은 이름이 부여된 열거체이므로 그렇지 않다. 한 가지 덧붙이자면 enum은 메서드와 필드도 사용할 수 있는 유연하고 서술적인 강력한 도구이다.

이름
N1 : 서술적인 이름을 사용하라 : 이름은 성급하게 정하지 않는다. 서술적인 이름을 신중하게 고른다. 소프트웨어가 진화하면 의미도 변하므로 선택한 이름이 적합한지 자주 되돌아본다. 소프트웨어 가독성의 90%는 이름이 결정하기 때문에 너무나도 중요하다. 이름을 적절하게 지었다면 코드를 금방 이해하리라

N2 : 적절한 추상화 수준에서 이름을 선택하라 : 구현을 드러내는 이름을 피하라. 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라. 쉽지 않은 작업이다. 발견할 때마다 기회를 잡아 바꿔놓아야 한다. 이상적인 코드를 만들려면 지속적인 개선과 노력이 필요하다.

N3 : 가능하다면 표준 명명법을 사용하라 : 기존 명명법을 사용하는 이름은 이해하기 더 쉽다. 예를 들어 DECORATOR 패턴을 활용한다면 장식하는 클래스 이름에 Decorator라는 단어를 사용해야 한다. 패턴은 한가지 표준에 불과하다. 예를 들어 자바에서 객체를 문자열로 변환하는 함수는 toString이라는 이름을 많이 쓴다. 이런 것은 새로 만들어내기 보다는 따르는 편이 좋다. 간단히 말해, 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

N4 : 명확한 이름 : 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다. 다소 길더라도 함수의 내용을 명확히 서술하는 이름이 좋다.

N5 : 긴 범위는 긴 이름을 사용하라 : 이름 길이는 범위 길이에 비례해야 한다. 범위가 작으면 아주 짧은 이름을 사용해도 괜찮다. 하지만 범위가 길어지면 긴 이름을 사용한다. 범위가 5줄 안팎이라면 i나 j와 같은 변수 이름도 괜찮다. 반면 이름이 짧은 변수나 함수는 범위가 길어지면 의미를 잃기 때문에 범위가 길수록 정확하고 길게 짓는다

N6 : 인코딩을 피하라 : 이름에 유형 정보나 범위 정보를 넣어서는 안 된다. 오늘날 개발 환경에서는 이름 앞에 m_이나 f와 같은 접두어가 불필요하다. 이는 중복된 정보로 독자만 혼란스럽게 만든다. 헝가리안 표기법의 오염에서 이름을 보호하라.

N7 : 이름으로 부수 효과를 설명하라 : 함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다. 이름에 부수 효과를 숨기지 않는다. 실제 여러 작업을 수행하는 함수에다가 동사 하나만 사용하면 곤란하다.

테스트
T1 : 불충분한 테스트 : 이 정도로 충분하지 않을까 라는 척도는 충분하지 않다. 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다. 테스트 케이스가 확인하지 않는 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전하다.

T2 : 커버리지 도구를 사용하라! : 커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다. 대다수 IDE는 테스트 커버리지를 시각적으로 표현한다. 예를 들어 테스트되는 행은 녹색으로 아닌 것은 붉은색으로 표기한다.그러므로 전혀 실행되지 않는 if 혹은 case 문 블록이 금방 드러난다.

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

T4 : 무시한 테스트는 모호함을 뜻한다 : 때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확신하기 어렵다. 불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 @Ignore를 붙여 표현한다. 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달렸다.

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

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

T7 : 실패 패턴을 살펴라 : 때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 테스트 케이스를 최대한 꼼꼼히 짜라는 이유도 여기 있다. 합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.

T8 : 테스트 커버리지 패턴을 살펴라 : 통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 실패 원인이 드러난다

T9 : 테스트는 빨라야 한다 : 느린 테스트 케이스는 실행하지 않게 되므로 일정이 촉박하면 느린 테스트 케이스를 제일 먼저 건너뛴다. 그러므로 테스트 케이스가 빨리 돌아가게 최대한 노력한다.

결론
전문가 정신과 장인 정신은 가치에서 나온다. 그 가치에 기반한 규율과 절제가 필요하다.

반응형