728x90
반응형
뒤엉킨 변경(Divergent Change)
- 소프트웨어는 변경에 유연하게(soft) 대처할 수 있어야 함
- 어떤 한 모듈이(함수 또는 클래스가) 여러가지 이유로 다양하게 변경되어야 하는 경우, (= 낮은 응집도, 너무 많은 책임)
- ex) 새로운 결제 방식 도입하거나 DB 변경할 때 동일한 클래스에 여러 메서드를 수정해야 하는 경우
- 서로 다른 문제는 서로 다른 모듈에서 해결해야 함
- 모듈의 책임이 분리되어 있을수록 해당 문맥을 더 잘 이해할 수 있으며 다른 문제는 신경쓰지 않아도 됨
- => 단일 책임 원칙(SRP), 모듈의 높은 응집도 & 낮은 결합도
- 관련 리팩토링 기술
- "단계 쪼개기(Split Phase)" -> 서로 다른 문맥의 코드를 분리 가능
- "함수 옮기기(Move Functions)" -> 적절한 모듈로 함수를 옮기기
- "함수 추출하기(Extract Function)" -> 여러가지 일이 하나의 함수에 모여있다면 사용
- "클래스 추출하기(Extract Class)" -> 모듈이 클래스 단위라면 별도의 클래스로 분리
단계 쪼개기(Split Phase)
- 서로 다른 일을 하는 코드를 각기 다른 모듈로 분리하자
- => 어떤 것을 변경할 때, 그것과 관련있는 것만 신경쓸 수 있음 & 테스트에도 용이
- 여러 일을 하는 함수의 처리 과정을 각기 다른 단계로 구분하기
- ex) 전처리 -> 주요 작업 -> 후처리
- ex) 컴파일러: 텍스트 읽어오기 -> 실행 가능한 형태로 변경
- 단계를 나누는 기준은?
- => 서로 다른 데이터를 사용한다면 서로 다른 단계일 확률이 높음
- "중간 데이터(intermediate data)"를 만들어 단계를 구분하고 매개변수를 줄이는데 활용 가능
예제 코드
public class PriceOrder {
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;
}
}
priceOrder라는 메서드가 너무 많은 책임을 가지고 있는 것을 볼 수 있다. 기본 단가 계산 부분과 운반비를 계산하는 부분을 별도의 메서드로 분리하자
public class PriceOrder {
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 price = applyShipping(quantity, shippingMethod, basePrice, discount);
return price;
}
// 운반비 가격을 포함하는 메서드
private static double applyShipping(int quantity, ShippingMethod shippingMethod, double basePrice, double discount) {
final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
final double shippingCost = quantity * shippingPerCase;
final double price = basePrice - discount + shippingCost;
return price;
}
}
여기까지만 하면 applyShipping이 너무 많은 파라미터를 갖게 된다.. 중간 데이터를 넘겨주어 파라미터 개수를 줄여보자.
// 중간 데이터
public record PriceData(double basePrice, double discount, int quantity) {
}
public class PriceOrder {
public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
final PriceData priceData = calculatePriceData(product, quantity);
return applyShipping(priceData, shippingMethod); // 인라인 리팩토링 적용
}
// PriceData를 구하는 부분을 따로 메서드로 추출
private static 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 static double applyShipping(PriceData priceData, ShippingMethod shippingMethod) {
final double shippingPerCase = (priceData.basePrice() > shippingMethod.discountThreshold()) ?
shippingMethod.discountedFee() : shippingMethod.feePerCase();
final double shippingCost = priceData.quantity() * shippingPerCase;
final double price = priceData.basePrice() - priceData.discount() + shippingCost;
return price;
}
}
중간데이터를 추가하여 메서드 파라미터 수를 줄여주었고, priceData를 구하는 부분도 따로 별도의 메서드로 분리하여 의미를 부여해주었다
함수 옮기기(Move Functions)
- 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경 가능함
- 관련있는 함수나 필드가 모여있어야 더 쉽게 찾고 이해 가능 (= 높은 응집도)
- 관련있는 함수나 필드가 항상 고정적인 것은 아니기 때문에 때에 따라 옮겨야 할 필요가 있음
- 함수를 옮겨야 하는 경우
- 해당 함수가 현재 문맥(클래스)보다 다른 문맥(클래스)에 있는 데이터(필드)를 더 많이 참조하는 경우 -> 해당 클래스로 옮기기
- 해당 함수를 현재 문맥뿐만 아니라 다른 클라이언트(클래스)에서도 필요로 하는 경우 -> 이를 공통적으로 사용할 수 있는 외부로 옮기기
- 함수를 옮겨갈 새로운 문맥(클래스)이 필요한 경우에는 "여러 함수를 클래스로 묶기(Combine Functions info Class)" 또는 "클래스 추출하기(Extract Class)"를 사용
- 함수를 옮길 적당한 위치를 찾기 어렵다면?
- => 그대로 두어도 ok. 언제든 나주에 옮길 수 있다.
- 정답은 없음. 언젠가 적절한 위치가 보이는 때가 오면 그떄 옮겨도 됨
예제 코드
public class AccountType {
private boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return this.premium;
}
}
public class AccountType {
private boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return this.premium;
}
}
public class Account {
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;
}
}
}
overdraftCharge의 메서드는 내부에서 AccountType의 데이터를 사용하여 분기를 타는 로직을 갖고 있다. 이는 절차적인 설계에서 나타나는 특징으로, 이를 개선하기 위해서 데이터를 사용하는 프로세스 로직을 데이터 내부로 이동시킬 수 있다. 이를 '책임의 이동'이라고 한다.
관련된 내용은 '[오브젝트 - 기초편] 영화 예매 도메인 - 절차적인 방식 개선해보기'를 참조하자
public class AccountType {
private boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return this.premium;
}
public double overdraftCharge(int daysOverdrawn) {
if (this.isPremium()) {
final int baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return daysOverdrawn * 1.75;
}
}
}
public class Account {
...
public double getBankCharge() {
double result = 4.5;
if (this.daysOverdrawn() > 0) {
result += this.type.overdraftCharge(this.daysOverdrawn());
}
return result;
}
...
}
이렇게 책임을 AccountType으로 이동시켜줌으로써, AccountType의 premium이라는 데이터를 외부에 노출시키지 않고, 오직 overdraftCharge를 계산하는 로직만을 노출시켰다
=> AccountType은 자기 자신을 책임지게 된다
클래스 추출하기(Extract Class)
- 클래스가 다루는 책임(Responsibility)이 많아질수록 클래스가 점차 커지게 된다 => 클래스를 쪼개보자!
- 클래스를 쪼개는 기준 => 응집도
- 데이터나 메서드 중 일부가 매우 밀접한 관련이 있는 경우
- 일부 데이터가 대부분 같이 바뀌는 경우
- "데이터 또는 메서드 중 일부를 삭제한다면 어떻게 될 것인가?"를 생각해보기
- 하위 클래스를 만들어 책임을 분산시킬 수도 있다
예제 코드
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;
}
}
위 클래스를 보면 딱 봐도 officeAreaCode와 officeNumber가 밀접한 관련이 있는 필드라는 것을 볼 수 있다. 이를 하나의 하위 클래스로 추출하여 책임을 분산시켜보자. 그리고 이를 빼다보면 연관된 부분을 찾을 수 있게 된다.
public class TelephoneNumber {
private String areaCode;
private String number;
public TelephoneNumber(String areaCode, String number) {
this.areaCode = areaCode;
this.number = number;
}
public String getAreaCode() {
return areaCode;
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@Override
public String toString() {
return this.areaCode + " " + this.number;
}
}
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public Person(String name, TelephoneNumber telephoneNumber) {
this.name = name;
this.telephoneNumber = telephoneNumber;
}
public String name() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String telephoneNumber() {
return this.telephoneNumber.toString();
}
}
728x90
반응형
'ETC' 카테고리의 다른 글
[Refactoring] 11. 기본형 집착 (0) | 2025.01.04 |
---|---|
[Refactoring] 8 ~ 10. 산탄총 수술 / 기능 편애 / 데이터 뭉치 (1) | 2025.01.03 |
[Refactoring] 6. 가변 데이터 (0) | 2025.01.03 |
[Refactoring] 5. 전역 데이터 (2) | 2025.01.02 |
[Refactoring] 4. 긴 매개변수 목록 (2) | 2025.01.02 |