보통 좋은 코드라고 하면 응집도는 높고 결합도는 낮아야 한다.
응집도는 얼마나 관련 있는 데이터나 함수들이 한 곳에 잘 밀집되어 있는가를 말하고
결합도는 얼마나 느슨한 의존성을 가지고 있는지 말한다.
이렇지 않은 코드는 다음과 같은 상황을 만날 수 있다.
여러 문제로 인해 하나의 클래스를 지속적으로 고치게 된다면 해당 클래스가 좋은 코드인지 의심해봐야 한다.
또 한 변경사항을 만들려면 여러 클래스를 돌아다니면서 고쳐야 한다는 것이다.
Divergent Change는 어떤 한 모듈이 여러가지 이유로 다양하게 변경되어야 하는 상황을 말한다.
서로 다른 문제는 서로 다른 모듈에서 해결해야 한다.
이는 모듈의 책임이 분리되어 있을수록 해당 문맥을 더 잘 이해할 수 있으며 다른 문제는 신경 쓰지 않아도 되기 때문이다.
관련 리팩토링 기술로
Split Phase를 사용해 서로 다른 문맥의 코드를 분리할 수 있다.
Move Function을 사용해 적절한 모듈로 함수를 옮길 수 있다.
이 과정에서 함수 추출 및 클래스 추출을 사용할 수 있다.
Split Phase
서로 다른 일을 하는 코드를 각기 다른 모듈로 분리하는 행위를 말한다. 그래야 어떤 변경 사항이 발생했을 때 해당 관련된 것만 신경 쓸 수 있다.
여러 일을 하는 함수의 처리과정을 각기 다른 단계로 구분할 수 있다.
예를 들면 전처리 -> 주요 작업 -> 후처리를 들 수 있다.
서로 다른 데이터를 사용한다면 단계를 나누는 데 있어 중요한 단서가 될 수 있다.
중간 데이터(intermediate Data)를 만들어 단계를 구분하고 매개변수를 줄이는데 활용할 수 있다.
public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
final double basePrice = product.basePrice() * quantity;
final double discount = Math.max(quantity - product.discountThreshold(), 0)
* product.basePrice() * product.discountRate();
final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
final double shippingCost = quantity * shippingPerCase;
final double price = basePrice - discount + shippingCost;
return price;
}
이 함수는 주문의 가격을 계산하는 기능을 합니다.
인자로는 제품, 제품 수량, 배송 방법이 전달됩니다.
함수 내부에서는 제품의 기본 가격을 계산한 다음, 수량에 따른 할인을 계산하고,
배송 비용을 계산한 다음 전체 가격을 계산하여 반환합니다.
말 그대로 우리는 3단계를 나눌 수 있습니다.
1. 기본가격을 구한다.
2. 할인을 계산한다.
3. 배송 비용을 계산한다.
4. 기본 가격에 할인 , 배송 비용을 더한 총가격을 구해 리턴한다.
여기서 2, 3에 대해서 우리는 별도의 split Phase를 적용할 수 있으며 파라미터 수가 많기 때문에 중간에 intermediate Data를 통해 파라미터 대신 객체로 넘기는 작업을 진행할 수 있습니다. ( 이때 객체는 VO가 좋습니다.)
public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
final PriceData priceData = calculatePriceData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
private PriceData calculatePriceData(Product product, int quantity) {
final double basePrice = product.basePrice() * quantity;
final double discount = Math.max(quantity - product.discountThreshold(), 0) * product.basePrice() * product.discountRate();
final PriceData priceData = new PriceData(basePrice, discount, quantity);
return priceData;
}
private double applyShipping(PriceData priceData, ShippingMethod shippingMethod) {
double basePrice = priceData.basePrice();
final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
final double shippingCost = priceData.quantity() * shippingPerCase;
final double price = basePrice - priceData.discount() + shippingCost;
return price;
}
다음과 같이 기본 가격에 할인가를 적용한 intermediate Data를 생성하고 이를 applyShipping에 넘겨 최종 가격을 계산하도록 했습니다.
매개 변수의 수도 줄고 함수의 의도를 조금 더 표현할 수 있도록 리팩터링 되었습니다.
이는 함수 추출하기를 사용함을 알 수 있고 추가적으로 관련 있는 매개변수를 묶어 객체로 생성하여 넘기는 리팩터링도 동시에 진행한 것을 알 수 있습니다.
Move Function
모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
하지만 관련있는 함수나 필드가 항상 고정적인 것은 아니기 때문에 때에 따라 옮겨야 한다.
1. 해당 함수가 다른 문맥에 있는 데이터를 더 많이 참조하는 경우
2. 해당 함수를 다른 클라이언트에서도 필요로 하는 경우
함수를 옮길 적당한 위치를 찾기가 어렵다면, 그대로 두어도 괜찮다. 언제든 나중에 옮길 수 있기 때문이다.
private int daysOverdrawn;
private AccountType type;
public Account(int daysOverdrawn, AccountType type) {
this.daysOverdrawn = daysOverdrawn;
this.type = type;
}
public double getBankCharge() {
double result = 4.5;
if (this.daysOverdrawn() > 0) {
result += this.overdraftCharge();
}
return result;
}
private int daysOverdrawn() {
return this.daysOverdrawn;
}
private double overdraftCharge() {
if (this.type.isPremium()) {
final int baseCharge = 10;
if (this.daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
getBankCharge() 메서드를 살펴보자. 기본 값은 4.5이지만 남기일을 넘기면 추가적인 비율이 붙어 결과를 리턴하는 함수이다.
public class Account {
private int daysOverdrawn;
private AccountType type;
public Account(int daysOverdrawn, AccountType type) {
this.daysOverdrawn = daysOverdrawn;
this.type = type;
}
}
해당 함수의 위치도 나쁘지 않다.
리팩터링을 위해 type의 필드를 사용하는 overdraftChage()를 해당 클래스로 옮겨보자.
private double overdraftCharge() {
if (this.type.isPremium()) {
final int baseCharge = 10;
if (this.daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
매개변수로 this.daysOverdrawn 필드를 파라미터로 넘겨주어야 한다.
public class AccountType {
private boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return this.premium;
}
double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
final int baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return daysOverdrawn * 1.75;
}
}
}
만약 해당 함수에서 Account의 필드를 많이 사용하고 있다면 Account 객체를 파라미터로 넘기는 것도 고려해볼 수 있다.
다만 Account를 넘기는 것은 의존성을 퍼뜨리는 것으로 보인다. 따라서 그럴 바에는 함수를 이동하지 않는 것이 맞지 않을까 생각해보아야 한다.
Extract Class
클래스가 다루는 책임이 많아질수록 클래스가 점진적으로 커진다.
그러다 보면 다양한 이유로 클래스가 계속 바뀌는 일이 발생할 수 있다.
클래스를 추출하는 기준은 무엇일까?
- 데이터나 메서드 중 일부가 매우 밀접한 관련이 있는 경우
- 일부 데이터가 대부분 같이 변경되는 경우
- 데이터 또는 메소드 중 일부를 삭제한다면 어떻게 될 것인가?
혹은 하위 클래스를 만들어 책임을 분산시킬 수도 있다.
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String telephoneNumber() {
return this.officeAreaCode + " " + this.officeNumber;
}
public String name() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String officeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String officeAreaCode) {
this.officeAreaCode = officeAreaCode;
}
public String officeNumber() {
return officeNumber;
}
public void setOfficeNumber(String officeNumber) {
this.officeNumber = officeNumber;
}
}
작은 예제로 확인해보자. 특정 필드를 제거해보자. 그럼 컴파일 오류가 발생하는 메서드 혹은 관련된 데이터들이 눈에 들어온다.
그럼 본능적으로 연관성이 있는 데이터 혹은 메서드이기 때문에 같이 추출되어야 할 대상이라는 것이 명확해진다.

public class TelephoneNumber {
String officeAreaCode;
String officeNumber;
public TelephoneNumber() {
}
public String officeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String officeAreaCode) {
this.officeAreaCode = officeAreaCode;
}
public String officeNumber() {
return officeNumber;
}
public void setOfficeNumber(String officeNumber) {
this.officeNumber = officeNumber;
}
}
인텔리제이의 도움을 받아 Delegate으로 진행하면 위와 같이 코드가 만들어진다. 이후에 사용되지 않는 메서드를 지우는 작업과 기존 코드의 모양을 확인해서 의도한 대로 변경됐는지 확인해야 한다.
추가적으로 클래스로 추출한다면 해당 클래스에서는 해당 필드의 이름이 적절하지 않을 수 있다. 따라서 리네임까지 고려해봐야 한다.
'독서에서 한걸음' 카테고리의 다른 글
Primitive Obsession (0) | 2022.12.19 |
---|---|
Shotgun Surgery (0) | 2022.12.18 |
Change Reference to Value (0) | 2022.12.11 |
Replace Derived Variable with Query, Combine Functions into Transform (0) | 2022.12.11 |
Separate Query from Modifier && Remove Setting Method (0) | 2022.12.10 |
댓글