✍️ 악취 7 : 뒤엉킨 변경
좋은 코드는 응집도는 높고 결합도는 낮아야 한다.
응집도는 관련 있는 데이터 혹은 기능이 한 곳에 잘 밀집되어 있는가를 말하고,
결합도는 상호간 의존하는 정도를 의미한다.
가령 서로 연관이 없는 A, B 모듈이 수정됐을 때, 양쪽에서 모두 C 모듈의 수정을 요구한다면 응집도가 낮고 결합도는 높다고 할 수 있다. 이처럼 한 모듈이 여러 가지 이유로 수정되어야 한다면 역할이 제대로 나눠지지 않았을 수 있다. 서로 다른 문제는 서로 다른 모듈에서 해결해야 한다.
뒤엉킨 변경을 해결하기 위한 세 가지 리팩토링 기법이 있다.
1. "단계 쪼개기" 서로 다른 문맥의 코드를 분리
2. "함수 옮기기" 적절한 모듈로 함수를 옮기기
3. "클래스 추출하기" 모듈이 클래스 단위라면
🍊 단계 쪼개기
서로 다른 일을 하는 코드라면 각기 다른 모듈 혹은 클래스 혹은 메서드로 분리한다. 그래야만 수정사항이 생겼을 때 그것만 신경 쓸 수 있다.
여러 일을 하는 메서드의 경우 처리 과정을 각기 다른 단계로 구분할 수 있다. 가령 1.전처리 작업 2.메인 작업 3.후처리 작업이 그러하다. 만약 메서드내에서 서로 다른 데이터를 다루는 코드가 있다면 단계를 나누는 데 있어서 중요한 단서가 될 수 있다.
여기 상품의 가격을 계산하고 할인가를 계산하고 가격에 따른 운임료를 계산해서 최종 비용을 구하는 메서드가 있다. 아래 메서드를 크게 세 부분으로 나눌 수 있는데, 1. 수량에 따른 가격을 계산하고 수량에 따른 할인을 계산한다. 2. 가격에 따라 운임료를 결정한다. 3. 최종 비용을 계산한다.
public class PriceOrder {
public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
// 1. 가격, 할인 계산
final double basePrice = product.basePrice() * quantity;
final double discount = Math.max(quantity - product.discountThreshold(), 0)
* product.basePrice() * product.discountRate();
// 2. 운임료 계산
final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
final double shippingCost = quantity * shippingPerCase;
// 3. 가격 - 할인 + 운임료 계산
final double price = basePrice - discount + shippingCost;
return price;
}
}
수량에 따른 운임비 계산 코드를 별도의 메서드로 분리하면 메서드 자체의 가독성도 높아지고 기능 수정이 용이해진다.
public class PriceOrder {
public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
// 1. 가격과 할인율 계산
final double basePrice = product.basePrice() * quantity;
final double discount = Math.max(quantity - product.discountThreshold(), 0)
* product.basePrice() * product.discountRate();
// 2. 운임료 계산
final PriceData priceData = new PriceData(basePrice, quantity);
final double shippingCost = calcShippingCost(priceData, shippingMethod);
// 3. 가격 - 할인 + 운임료 계산
final double price = basePrice - discount + shippingCost;
return price;
}
private double calcShippingCost(PriceData priceData, ShippingMethod shippingMethod) {
final double shippingPerCase = (priceData.basePrice() > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
return priceData.quantity() * shippingPerCase;
}
}
🌱 함수 옮기기
모듈화가 잘 된 소프트웨어는 최소한의 정보만으로 프로그램을 변경할 수 있다. 다르게 말하면 모듈화가 잘 된 소프트웨어는 응집도가 높은 소프트웨어일 가능성이 높은데 관련 있는 함수나 필드가 한 곳에 잘 밀집되어 기능을 이해하기 쉽기 때문이다.
다만 관련 있는 메서드나 필드가 항상 고정적인 것은 아니기에, 필요에 따라 옮겨야 할 때가 있다.
한 메서드가 다른 클래스의 필드를 더 많이 참조하거나, 다른 클래스에서도 메서드를 필요로 하는 경우가 그러하다.
Subscriber 클래스는 구독 기간과 등급을 표현하는 필드와 기간과 등급에 따른 월간 구독료를 반환하는 메서드를 갖는다. 여기서 메서드 옮기기의 대상은 getDiscount로 등급에 따라 분기를 처리하고 있기에 그 책임을 Grade 내부로 옮겨볼 수 있다.
public enum Grade {
VIP, VVIP;
}
@AllArgsConstructor
@Getter
public class Subscriber {
private int subscriptionPeriod;
private Grade grade;
public int getMonthlyFee() {
int fee = 10000;
if (subscriptionPeriod >= 6)
fee -= getDiscount();
return fee;
}
private int getDiscount() {
if (grade == Grade.VVIP) {
if (subscriptionPeriod <= 10)
return 2000;
return 4000;
}
return 1000;
}
}
할인 금액 계산 코드를 Grade로 옮기긴 했지만 실습의 의미가 크고 Subscriber 클래스에 있어도 문제없다고 생각한다. 다만 getDiscount에서 Subscriber의 필드를 더 많이 참조해야 한다면 원래 위치로 옮기는 것이 좋을듯하다. 무엇보다 getDiscount에 Subscriber를 넘기면 쌍방 참조가 생기므로 그것만은 피하자.
public enum Grade {
VIP, VVIP;
public int getDiscount(int subscriptionPeriod) {
if (this == Grade.VVIP) {
if (subscriptionPeriod <= 10)
return 2000;
return 4000;
}
return 1000;
}
}
@AllArgsConstructor
@Getter
public class Subscriber {
private int subscriptionPeriod;
private Grade grade;
public int getMonthlyFee() {
int fee = 10000;
if (subscriptionPeriod >= 6)
fee -= grade.getDiscount(subscriptionPeriod);
return fee;
}
}
🍃 클래스 추출하기
클래스가 다루는 책임이 많아질수록 클래스가 점차 커짐에 따라 클래스를 쪼갤 필요가 있다.
데이터나 메서드 중 일부가 매우 밀접하게 관련이 있는 경우, 일부 데이터가 같이 변경되는 경우 클래스를 쪼개는 기준이 될 수 있다. 경우에 따라서 하위 클래스를 만들어 책임을 분산시킬 수 있다.
Person 클래스는 이름과 사무실 지역번호, 사무실 번호를 필드로 갖는다.
여기서 officeAreaCode와 officeNumber가 밀접한 관련이 있는 필드로 간주해서 별도의 클래스로 추출할 수 있다.
@AllArgsConstructor
@Data
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() {
return this.officeAreaCode + " " + this.officeNumber;
}
}
전화번호와 관련된 필드와 메서드를 한 곳으로 묶어서 관련된 데이터를 한 클래스에서 관리할 수 있다.
@AllArgsConstructor
@Getter
public class TelephoneNumber {
private String areaCode;
private String number;
@Override
public String toString() {
return areaCode + " " + number;
}
}
@AllArgsConstructor
@Getter
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public String getTelephoneNumber() {
return telephoneNumber.toString();
}
}
'리팩토링' 카테고리의 다른 글
[리팩토링] 악취 9 : 기능 편애 (0) | 2023.01.02 |
---|---|
[리팩토링] 악취 8 : 산탄총 수술 (0) | 2022.12.22 |
[리팩토링] 악취 6 : 가변 데이터 (0) | 2022.11.27 |
[리팩토링] 악취 5 : 전역 변수 (0) | 2022.11.13 |
[리팩토링] 악취 4. 긴 매개변수 목록 (0) | 2022.11.06 |