Code Smells
코드에 악취가 날 수 있는 상황들과 해결 방법들에 대해서 알아보자.
비대해진 것들 (Bloaters)
비대해진 것들은 너무 거대한 비율로 증가하여 작업하기 어려운 코드, 메서드 및 클래스이다. 일반적으로 이러한 악취는 바로 나타나지 않고, 프로그램이 발전함에 따라 시간이 지나면서 축적된다.
- 긴 메서드 (Long Method)
- 거대한 클래스 (Large Class)
- 기본형 집착 (Primitive Obsession)
- 긴 매개변수 목록 (Long Parameter List)
- 데이터 덩어리 (Data Clumps)
긴 메서드 (Long Method)
징후와 증상
메서드가 10줄 이상의 코드를 포함한다.
문제의 원인
기존 메서드에 추가하는 것보다 새 메서드를 만드는 것이 더 어려우므로 기존 메서드에 코드가 계속해서 추가되어 스파게티 코드가 된다.
해결 방법
메서드에 대해 설명이 필요해질 경우 새 메서드로 분리해야 한다. 그리고 메서드에 설명적인 이름이 있으면, 해당 메서드가 무엇을 하는지 코드를 보지 않아도 된다.
메서드 길이를 줄이려면 메서드 추출을 사용해라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 문제
void printOwing() {
printBanner();
// Print details.
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}
// 해결
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
지역 변수나 매개변수 때문에 메서드를 추출하기 어렵다면, 임시 변수를 질의로 바꾸기, 매개변수 객체 만들기, 객체 통째로 넘기기를 시도해라.
임시 변수를 질의로 바꾸기 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 문제
double calculateTotal() {
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}
// 해결
double calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quantity * itemPrice;
}
매개변수 객체 만들기 예시
1
2
3
4
5
// 문제
Int amountInvoicedIn(start: Date, end: Date)
// 해결
Int amountInvoicedIn(date: DateRange)
객체 통째로 넘기기 예시
1
2
3
4
5
6
7
// 문제
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
// 해결
boolean withinPlan = plan.withinRange(daysTempRange);
위 방법들이 모두 통하지 않는다면, 메서드를 메서드 객체로 바꾸기로 메서드 전체를 별도 객체로 옮기는 것도 방법이다.
메서드를 메서드 객체로 바꾸기 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 문제
class Order {
// ...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform logn computation.
}
}
// 해결
class Order {
// ...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// Copy relevant information from the
// order object.
}
public double compute() {
// Perform long computation.
}
}
조건문이나 반복문은 분리할 수 있다는 좋은 신호이다. 조건문에는 조건문 분해하기를, 반복문에는 메서드 추출을 적용해보자.
조건문 분해하기 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 문제
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
// 해결
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}
장점
코드 이해와 유지보수가 쉬워지고, 숨어있던 중복 코드를 찾기 쉽다.
거대한 클래스 (Large Class)
징후와 증상
클래스에 많은 필드/메서드/코드 줄이 포함되어 있다.
문제의 원인
클래스는 보통 작게 시작하지만, 시간이 지남에 따라 프로그램이 성장하면서 비대해진다.
긴 메서드의 경우와 마찬가지로, 프로그래머들은 일반적으로 기능을 휘한 새 클래스를 만드는 것 보다 기존 클래스에 새 기능을 배치하는 것이 정신적으로 덜 부담스럽다고 느낀다.
해결 방법
클래스가 너무 많은 역할을 담당하고 있다면 분할을 고려해라.
- 거대한 클래스 발견
- 클래스 동작의 일부를 별도 컴포넌트로 분리할 수 있는가?
- 예: 클래스 추출 적용
- 클래스 동작의 일부가 다른 방식으로 구현되거나 드물게 사용되는가?
- 예: 서브 클래스 추출 적용
- 클라이언트가 사용할 수 있는 작업 및 동작 목록이 필요한가?
- 예: 인터페이스 추출 적용
- 거대한 클래스가 GUI를 담당하는가?
- 예: 중복 관측 데이터 적용
긴 매개변수 목록 (Long Parameter List)
징후와 증상
메서드에 3~4개 이상의 매개변수가 있다.
문제의 원인
긴 매개변수 목록은 여러 유형의 알고리즘이 단일 메서드에 존재하는 경우에 발생할 수 있다.
이러한 목록은 이해하기 어렵고, 길어질수록 모순되고 사용하기 어려워집니다. 긴 매개변수 목록 대신 메서드는 자체 객체의 데이터를 사용할 수 있습니다.
해결 방법
- 매개변수를 메서드 호출로 대체
- 매개변수에 전달되는 값이 다른 객체의 메서드 호출 결과인 경우, 이 객체를 자체 클래스의 필드에 배치하거나 메서드 매개변수로 전달
- 전체 객체 보존
- 다른 객체에서 받은 데이터 그룹을 매개변수로 전달하는 대신 객체 자체를 메서드에 전달
- 매개변수 객체 도입
- 이러한 매개변수가 서로 다른 소스에서 오는 경우 단일 매개변수 객체로 전달
객체 지향 남용 (Object-Orientation Abusers)
이 모든 악취는 객체 지향 프로그래밍 원칙의 불완전하거나 잘못된 적용이다.
- 서로 다른 인터페이스를 가진 대안 클래스 (Alternative Classes with Different Interfaces)
- 거부된 유산 (Refused Bequest)
- Switch 문 (Switch Statements)
- 임시 필드 (Temporary Field)
변경 방해자 (Change Preventers)
이러한 악취는 코드의 한 곳에서 무언가를 변경해야 하면 다른 여러 곳에서도 많은 변경을 해야 한다는 것을 의미한다. 그 결과 프로그램 개발이 훨씬 더 복잡하고 비용이 많이 든다.
불필요한 것들 (Dispensables)
불필요한 것은 무의미하고 불필요한 것으로, 그것이 없으면 코드가 더 깨끗하고 효율적이며 이해하기 쉬워진다.
- 주석 (Comments)
- 중복 코드 (Duplicate Code)
- 데이터 클래스 (Data Class)
- 죽은 코드 (Dead Code)
- 게으른 클래스 (Lazy Class)
- 추측성 일반화 (Speculative Generality)
중복 코드 (Duplicate Code)
징후와 증상
두 코드 조각이 거의 동일하게 작성된다.
문제의 원인
- 동시 개발
- 여러 프로그래머가 동일한 프로그램의 다른 부분에서 동시에 작업할 때 발생
- 미묘한 중복
- 코드의 특정 부분이 다르게 보이지만 실제로는 동일한 작업을 수행하는 경우
- 의도적 중복
- 마감일을 맞추기 위해 서두르고 기존 코드가 작업에 “거의 맞을” 때 복사 붙여넣기의 유혹을 이기지 못할 수 있음
- 게으름
- 일부 경우 프로그래머가 단순히 정리하기에 너무 게으름
해결 방법
- 같은 클래스 내 두 개 이상의 메서드 발견
- 메서드 추출로 해결
- 같은 수준의 두 서브 클래스에서 발견
- 일반 중복 코드일 경우 메서드 추출, 상위 클래스로 이동
- 생성자 내 중복일 경우 상위 클래스 생성자 활용
- 유사하지만 완전히 동일하지 않은 중복일 경우 템플릿 메서드 패턴 활용
- 같은 작업을 다른 알고리즘 수행인 경우 최선의 알고리즘 선택 후 변경
- 서로 다른 두 클래스에서 발견
- 계층 구조에 속하지 않는 클래스일 경우 슈퍼클래스로 추출하여 이전의 모든 기능을 유지하는 단일 상위 클래스 생성
- 상위 클래스 생성이 어렵거나 불가능한 경우 한 클래스에서 클래스 추출을 사용하고 다른 클래스에서 새 컴포넌트를 사용
- 조건부 표현식에서 발견
- 많은 조건 표현식이 같은 코드를 수행하는 경우 조건식 통합을 통해 연산자를 단일 조건으로 병합하거나 메서드 추출로 이해하기 쉬운 이름의 별도 메서드에 조건 배치
- 조건 표현식의 모든 분기에서 같은 코드 수행할 경우 조건부 조각 통합을 통해 동일 코드를 조건 트리 외부에 배치
결합자 (Couplers)
이 그룹의 모든 악취는 클래스 간의 과도한 결합에 기여하거나 결합이 과도한 위임으로 대체될 때 어떤 일이 발생하는지 보여준다.
- 기능 욕심 (Feature Envy)
- 부적절한 친밀함 (Inappropriate Intimacy)
- 불완전한 라이프러리 클래스 (Incomplete Library Class)
- 메시지 체인 (Message Chains)
- 중개자 (Middle Man)
Reference
https://refactoring.guru/refactoring/smells