주석C1 : 부적절한 정보C2 : 쓸모 없는 주석C3 : 중복된 주석C4 : 성의 없는 주석C5 : 주석 처리된 코드환경E1 : 여러 단계로 빌드해야 한다.E2 : 여러 단계로 테스트해야 한다.함수F1 : 너무 많은 인수F2 : 출력 인수F3 : 플래그 인수F4 : 죽은 함수일반G1 : 한 소스 파일에 여러 언어를 사용한다.G2 : 당연한 동작을 구현하지 않는다.G3 : 경계를 올바로 처리하지 않는다.G4: 안전 절차 무시G5 : 중복G6 : 추상화 수준이 올바르지 못하다.G7 : 기초 클래스가 파생 클래스에 의존한다.G8 : 과도한 정보G9 : 죽은 코드G10 : 수직 분리G11 : 일관성 부족G12 : 잡동사니G13 : 인위적 결합G14 : 기능 욕심G15 : 선택자 인수G16 : 모호한 의도G17 : 잘못 지운 책임G18 : 부적절한 static 함수G19 : 서술적 변수G20 : 이름과 기능이 일치하는 함수G21 : 알고리즘을 이해해라G22 : 논리적 의존성은 물리적으로 드러내라G23 : if/else 혹은 switch/case 보다는 다형성을 사용하라G24: 표준 표기법을 따르라G25 : 매직 숫자는 명명된 상수로 교체하라
리팩토링에서 마틴 파울러는 다양한 “코드 냄새”를 거론하고 있다.
아래 소개하는 목록에서 마틴이 맡은 냄새에 내가 맡은 냄새를 추가했다.
또한 저자가 코드를 작성하면서 사용하는 기교와 휴리스틱도 포함한다.
주석
C1 : 부적절한 정보
- 다른 시스템에 (소스 코드 관리 시스템, 버그 추적, 이슈 추적) 저장할 정보는 주석으로 적절하지 못하다.
- 예를들어, 변경 이력은 장황한 날짜와 따분한 내용으로 소스 코드만 번잡하게 만든다.
- 일반적으로 작성자, 최종 수정일, SPR 번호 등과 같은 메타 정보만 주석으로 넣는다.
- 주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다.
C2 : 쓸모 없는 주석
- 오래된 주석, 엉뚱한 주석, 잘못된 주석은 쓸모가 없다.
- 주석은 빨리 낡기 때문에 쓸모 없어질 주석은 아예 달지 않는 편이 가장 좋다.
- 즉 쓸모 없어진 주석은 빠르게 삭제하자.
C3 : 중복된 주석
- 코드만으로 충분한데 구구절절 설명하는 주석이 중복된 주석이다. 아래 예시를 보자
i++; // 1 증가 // 함수 서명만 달랑 기술하는 javadoc /** * @param sellRequest * @return * @throws ManagedComponentException * **/ public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException { }
C4 : 성의 없는 주석
- 작성할 가치가 있는 주석은 잘 작성할 가치도 있다. 주석을 달 참이라면 시간을 들여 최대한 멋지게 작성하자.
- 단어를 신중하게 선택하고 문법과 구두점을 올바르게 사용한다. (간단 명료하게)
C5 : 주석 처리된 코드
- 코드를 읽다가 주석으로 처리된 코드가 나오면 신경이 쓰이는 경우가 많다. 오래된 코드인지, 중요한 코드인지 알 길이 없다. 그럼에도 아무도 삭제하지 않는 문제가 있다. 누군가에게 필요할 것 이라고 생각하기 때문이다
- 따라서 코드는 그 자리에 남아 계속해서 낡아간다. 더 이상 존재하지 않는 함수를 호출하거나 이름이 바뀐 변수를 사용하게 될 것이다.
- 따라서 주석으로 처리된 코드를 발견하면 즉각 지워버리자. 걱정할 필요가 전혀 없다 우리는 형상 관리 도구가 있기 때문이다. 누군가 정말 필요했다면 알아서 이전 버전에서 가져올 것이다.
환경
E1 : 여러 단계로 빌드해야 한다.
- 빌드는 간단히 한 단계로 끝나야 한다.
- 소스 코드 관리 시스템에서 이것저것 따로 체크아웃 할 필요가 없어야 한다.
- 한 명령으로 전체를 체크아웃해서 한 명령으로 빌드할 수 있어야 한다.
E2 : 여러 단계로 테스트해야 한다.
- 모든 단위테스트는 한 명령으로 돌려야 한다.
- IDE에서 버튼 하나로 모든테스트를 돌린다면 가장 이상적이다.
- 모든 테스트를 한번에 실행하는 능력은 아주 근본적이고 아주 중요하므로 빠르고 쉽고 명백해야 한다.
함수
F1 : 너무 많은 인수
- 함수에서 인수 개수는 적을수록 좋다. 아예 없으면 가장 좋다.
- 넷 이상은 가치가 의심스러워 지기 때문에 최대한 피하자
F2 : 출력 인수
- 출력 인수는 직관을 정면으로 위배한다. 일반적으로 독자는 인수를 입력으로 간주한다.
- 함수에서 뭔가의 상태를 변경해야 한다면
F3 : 플래그 인수
- boolean 인수는 함수가 여러 기능을 수행해야 한다는 명백한 증거다.
- 플래그 인수는 혼란을 초래하므로 피해야 마땅하다.
F4 : 죽은 함수
- 아무도 호출하지 않는 함수는 삭제한다.
- 죽은 코드는 낭비다. 과감히 삭제하자.
일반
G1 : 한 소스 파일에 여러 언어를 사용한다.
- 좋게 말하자면 혼란스럽고 나쁘게 말하자면 조잡하다.
- 이상적으로는 소스 파일 하나에 언어 하나만 사용한 방식이 가장 좋다.
- 불가피한 경우라면 소스 파일에서 언어의 수와 범위를 최대한 줄이도록 애써야 한다.
G2 : 당연한 동작을 구현하지 않는다.
- 최소 놀람의 원칙에 의거해함수나 클래스는 프로그래머가 당연하게 여길만한 동작과 기능을 제공해야 한다.
- 당연한 동작을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵다.
- 저자를 신뢰하지 못하므로 코드를 일일이 살펴야 한다.
G3 : 경계를 올바로 처리하지 않는다.
- 코드는 올바르게 동작해야 한다. 당연한 말 !
- 우리는 올바른 동작이 아주 복잡하다는 사실을 간과한다.
- 스스로 직관에 의존하지 말고, 모든 경계 조건을 찾아내고 테스트화하는 테스트케이스를 작성하자
G4: 안전 절차 무시
- 체르노빌 원전 사고는 책임자가 안전 절차를 차례로 무시하는 바람에 일어났다.
- 안전 절차를 무시하면 위험하다.
- 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도가 신용카드가 공짜 돈이라고 생각만큼 위험하다
G5 : 중복
- 이 책에 나오는 가장 중요한 규칙 중 하나다.
- 소프트웨어 설계를 거론하는 저자라면 거의 모두가 이 규칙을 언급하고 있다.
- 데이비드 토머스 : Don’t Repeat Yourself!
- 켄트 백 : 한번, 단 한 번 만 !
- 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하자.
- 중복된 코드 > 하위 루틴이나 다른 클래스로 분리
- 중복 → 다형성으로 교체 템플릿 메서드 패턴 or 전략 패턴으로 중복을 제거하자.
G6 : 추상화 수준이 올바르지 못하다.
- 추상화는 저차원 상세 개념에서 고차원 일반 개념을 분리한다.
- 추상화로 개념을 분리할 때는 철저해야 한다.
- 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.
- 예를 들어, 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안된다.
- 기초 클래스는 구현 정보에 무지해야 마땅하다.
import java.util.EmptyStackException; public interface Stack { Object pop() throws EmptyStackException; void push(Object o) throws FullException; double percentFull(); ... }
G7 : 기초 클래스가 파생 클래스에 의존한다.
- 개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다.
- 그러므로 기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다.
- 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.
- 이렇게 구현하면 변경이 시스템에 미치는 영향이 아주 작아지므로 현장에서 시스템을 유지보수 하기가 한결 수월하게 된다.
G8 : 과도한 정보
- 잘 정의된 모듈은 인터페이스가 아주 작다. 하지만 작은 인터페이스로도 많은 동작이 가능하다.
- 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다.
- 자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하자.
G9 : 죽은 코드
- 죽은 코드 : 실행되지 않는 코드
- 죽은 코드는 시간이 지나면 악취를 풍기기 시작한다.
- 죽은 코드를 발견하면 적절한 장례식을 치뤄주고 시스템에서 제거해버리자
G10 : 수직 분리
- 변수와 함수는 사용되는 위치에 가깝게 정의한다.
- 자역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다.
- 비공개 함수는 처음으로 호출한 직후에 정의한다.
- 비공개 함수는 정의하는 위치와 호출하는 위치를 가깝게 유지한다.
- 비공개 함수는 처음으로 호출되는 위치를 찾은 후 조금만 아래로 내려가면 쉽게눈에 띄어야한다.
G11 : 일관성 부족
- 어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다.
- 착실하게 적용한다면 간단한 일관성만으로도 코드를 읽고 수정하기가 대단히 쉬워진다.
G12 : 잡동사니
- 소스파일은 언제나 깔끔하게 정리하도록 하자 !
G13 : 인위적 결합
- 서로 무관한 개념을 인위적으로 결합하지 않는다.
- 일반적인 enum은 특정 클래스에 속할 이유가 없다.
- enum이 클래스에 속한다면 enum을 사용하는 코드가 특정 클래스를 알아야만 한다.
- 범용 static 함수도 마찬가지로 특정 클래스에 속할 이유가 없다.
- 함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치를 고민한다.
- 그저 당장 편한곳에 선언하고 내버려두면 안 된다.
G14 : 기능 욕심
- 마틴 파울러가 말하는 코드 냄새 중 하나다.
- 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안된다.
public class HourlyPayCalculator { public Money calculateWeeklyPay(HourlyEmployee e) { final int tenthRate = e.getTenthRate().getPennies(); final int tenthsWorked = e.getTenthsWorked(); final int straightTime = Math.min(400, tenthsWorked); final int overTime = Math.max(0, tenthsWorked - straightTime); final int straightPay = straightTime * tenthRate; final int overTimePay = (int) Math.round(overTime * tenthRate * 1.5); return new Money(straightPay + overTimePay); } }
- 위 calculateWeeklyPay 메소드는 HourlyEmployee 클래스에서 온갖 정보를 가져온다.
- 즉, calculateWeeklyPay 메소드는 HourlyEmployee 클래스의 범위를 욕심낸다.
- 기능 욕심은 한 클래스의 속사정을 다른 클래스에 노출하기 때문에 별다른 문제가 없다면 제거하는 편이 좋다.
G15 : 선택자 인수
- 선택자 인수는 큰 함수를 작은 함수 여렷으로 쪼개지 않으려는 게으름의 소산이다.
- boolean 뿐만 아니라 enum, int 등 함수 동작을 제어하려는 인수는 하나 같이 바람직하지 않다.
- 일반적으로, 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.
G16 : 모호한 의도
- 코드를 짤 때는 의도를 최대한 분명히 밝히자.
- 행을 바꾸지 않고 표현한 수식, 매직 번호 등 저자의 의도를 흐리게 된다.
G17 : 잘못 지운 책임
- 코드는 독자가 자연스럽게 기대할 위치에 배치해야 한다. 놀람 최소화 원칙에 기반하여 적절히 코드를 배치해야 한다.
G18 : 부적절한 static 함수
Math.max(double a, double b)
는 좋은 static 메소드다.
- • 그러나 아래 함수는 static으로 정의하면 안되는 함수다. 왜?
HourlyPayCalculator.calculatePay(employee, overTimeRate)
- 함수를 재정의할 가능성이 존재하기 때문이다.
- 수당을 계산하는 알고리즘이 여러 개일지도 모른다.
- 일반적으로 static 함수보다 인스턴스 함수가 더 좋다.
- 조금이라도 의심스럽다면 인스턴스 함수로 정의한다.
- 반드시 static 함수로 정의해야겠다면 재정의할 가능성은 없는지 꼼꼼히 따져본다.
G19 : 서술적 변수
- 프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하는 것
Matcher match = headerPattern.matcher(line); if (match.find()) { String key = match.group(1); String value = match.group(2); }
- 서술적인 변수 이름을 사용한 탓에 첫 번째로 일치하는 그룹이 키고 두번째로 일치하는 그룹이 값이라는 사실이 명확하게 드러난다.
G20 : 이름과 기능이 일치하는 함수
Date newDate = date.add(5);
- 5일을 더하는 함수인가? 5주? 5시간? date 인스턴스를 변경하는 함수인가?
- date 인스턴스는 그대로 두고 새로운 Date를 반환하는 함수인지 코드만 봐서는 알 수 없다.
- 즉 이름만으로 분명하지 않기에 구현을 살피거나 문서를 뒤적여야 한다면 더 좋은 이름으로 바꾸거나 더 좋은 이름을 붙이기 쉽도록 기능을 정리하자.
G21 : 알고리즘을 이해해라
- 대다수 괴상한 코드는 사람들이 알고리즘을 충분히 이해하지 않은 채 코드를 구현한 탓이다.
- 잠시 멈추고 실제 알고리즘을 고민하는 대신 여기저기 if문과 플래그를 넣어보며 코드를 돌리는 탓이다.
- 프로그래밍은 흔히 탐험인데, 사실 코드가 '돌아갈' 때까지 이리저리 찔러보고 굴려본다.
- '돌아간다'는 사실이 중요한게 아닌, 작성자가 알고리즘이 올바르다는 사실을 알아야 한다.
- 가장 좋은 방법은 함수를 깔끔하고 명확하게 구성하는 것이다.
G22 : 논리적 의존성은 물리적으로 드러내라
- 한 모듈이 다른 모률에 의존한다면 물리적인 의존성도 있어야 한다.
- 논리적인 의존성만으로는 부족하다.
- 의존하는 모듈이 상대 모듈에 대해 뭔가를 가정하면(즉, 논리적으로 의존하면) 안 된다.
- 의존하는 모든 정보를 명시적으로 요청하는 편이좋다.
G23 : if/else 혹은 switch/case 보다는 다형성을 사용하라
- 선택 유형 하나에는 switch 문을 한 번만 사용한다.
- 같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성해 switch 문을 대신한다.
G24: 표준 표기법을 따르라
- 팀은 업계 표준에 기반한 구현 표준을 따라야 한다.
- 표준을 설명하는 문서는 코드 자체로 충분해야 하며 별도 문서를 만들 필요는 없어야 한다.
- 팀이 정한 표준은 팀원 모두가 따라야 한다.
- 실제 괄호를 넣는 위치는 중요 하지 않다.
- 모두가 동의한 위치에 넣는다는 사실이 중요하다.
- 이 사실을 이해할 정도로 팀원들이 성숙해야 한다.
G25 : 매직 숫자는 명명된 상수로 교체하라
- 매직숫자는 상수로 변경하라.
- 어떤 상수는 이해하기 쉬우므로, 코드 자체가 자명하다면, 상수 뒤로 숨길 필요가 없다.
- 하단 예제에 TWO (int 2)라는 상수가 반드시 필요할까? 어떤 공식은 그냥숫자를 쓰는 편이 훨씬 좋다.
double circumference = radius * Math.PI * 2;
- 매직 숫자라는 용어는 단지 숫자만 의미하지 않는다. 의미가 분명하지 않은 토큰을 모두가리킨다.