8. 산탄총 수술(Shotgun Surgery)
- 어떤 한 변경 사항이 생겼을 때, 여러 모듈을 (여러 함수 또는 여러 클래스를) 수정해야 하는 상황
- "뒤엉킨 변경" 냄새와 유사하지만 반대의 상황임
- 원인은 동일 -> 낮은 응집도 & 높은 결합도
- 결과는 반대 -> 뒤엉킨 변경은 여러 가지 이유로 하나의 클래스를 계속해서 손보는 것 / 산탄총 수술은 하나의 일로 여러 곳을 손보는 것
- ex) 새로운 결제 방식을 도입하려면 여러 클래스의 코드를 수정해야 함
- "뒤엉킨 변경" 냄새와 유사하지만 반대의 상황임
- 변경 사항이 여러 곳에 흩어진다면 찾아서 고치기도 어렵고 중요한 변경사항을 놓칠 가능성도 생김
- 관련 리팩토링 기술 -> 대체로 묶는 기술을 사용
- "함수 옮기기(Move Function)" 또는 "필드 옮기기(Move Field)" -> 필요한 변경 내역을 하나의 클래스로 모을 수 있음
- "여러 함수를 클래스로 묶기(Combine Functions into Class)" -> 비슷한 데이터를 사용하는 여러 함수가 있을 때 사용
- "단계 쪼개기(Split Phase)" -> 공통으로 사용되는 함수의 결과물들을 하나로 묶을 수 있음
- "함수 인라인(Inline Function)" & "클래스 인라인(Inline Class)" -> 흩어진 로직을 한 곳으로 모을 수 있음
필드 옮기기(Move Field)
- 좋은 데이터 구조를 가지고 있다면, 해당 데이터에 기반한 어떤 행위를 코드로 (메서드나 함수) 옮기는 것도 간편하고 단순해짐
- 처음에는 타당해보였던 설계도 프로그램이 다루고 있는 도메인과 데이터 구조에 대해 더 많이 익혀나가다보면 잘못된 설계로 변할 수 있음
- 필드를 옮기는 단서
- 어떤 데이터를 항상 어떤 데이터와 함께 전달할 경우
- 어떤 데이터를 변경할 때 다른 데이터에 있는 필드를 변경해야 하는 경우
- 여러 데이터에 동일한 필드를 수정해야 하는 경우
여기서 언급한 '데이터'는 레코드, 클래스 또는 객체로 대체 가능
예제 코드
public class Customer {
private String name;
private double discountRate;
private CustomerContract contract;
public Customer(String name, double discountRate) {
this.name = name;
this.discountRate = discountRate;
this.contract = new CustomerContract(dateToday());
}
public double getDiscountRate() {
return discountRate;
}
public void becomePreferred() {
this.discountRate += 0.03;
// 다른 작업들
}
public double applyDiscount(double amount) {
BigDecimal value = BigDecimal.valueOf(amount);
return value.subtract(value.multiply(BigDecimal.valueOf(this.discountRate))).doubleValue();
}
private LocalDate dateToday() {
return LocalDate.now();
}
}
위와 같은 코드에서, 특정 변경 사항에 의해 discountRate(할인률)이 CustomerContract와 더 관련있는 데이터로 변경되었다고 가정하자
=> discountRate를 CustomerContract로 옮겨주자
public class CustomerContract {
private LocalDate startDate;
private double discountRate;
public CustomerContract(LocalDate startDate, double discountRate) {
this.startDate = startDate;
this.discountRate = discountRate;
}
public double getDiscountRate() {
return discountRate;
}
public void setDiscountRate(double discountRate) {
this.discountRate = discountRate;
}
}
public class Customer {
private String name;
private CustomerContract contract;
public Customer(String name, double discountRate) {
this.name = name;
this.contract = new CustomerContract(dateToday(), discountRate);
}
public double getDiscountRate() {
return this.contract.getDiscountRate();
}
public void setDiscountRate(double discountRate) {
this.contract.setDiscountRate(discountRate);
}
// discountRate에 직접 수정하는 부분을 setter를 사용하도록 변경
public void becomePreferred() {
this.setDiscountRate(this.getDiscountRate() + 0.03);
// this.discountRate += 0.03;
// 다른 작업들
}
public double applyDiscount(double amount) {
BigDecimal value = BigDecimal.valueOf(amount);
// discountRate에 직접 접근하는 부분을 getter를 사용하도록 변경
return value.subtract(value.multiply(BigDecimal.valueOf(this.getDiscountRate()))).doubleValue();
}
private LocalDate dateToday() {
return LocalDate.now();
}
}
여기서 더 나아가면 Customer 클래스의 getDiscountRate, setDiscountRate, becomePreferred와 같은 메서드는 CustomerContract의 필드만을 사용하는 메서드이다. 이들은 CustomerContract로 옮겨주는 것이 책임 관점에서 적절해보인다.
public class CustomerContract {
private LocalDate startDate;
private double discountRate;
public CustomerContract(LocalDate startDate, double discountRate) {
this.startDate = startDate;
this.discountRate = discountRate;
}
public double getDiscountRate() {
return discountRate;
}
public void setDiscountRate(double discountRate) {
this.discountRate = discountRate;
}
public void becomePreferred() {
this.setDiscountRate(this.getDiscountRate() + 0.03);
// 다른 작업들
}
}
public class Customer {
private String name;
private CustomerContract contract;
public Customer(String name, double discountRate) {
this.name = name;
this.contract = new CustomerContract(dateToday(), discountRate);
}
public double applyDiscount(double amount) {
BigDecimal value = BigDecimal.valueOf(amount);
// discountRate에 직접 접근하는 부분을 getter를 사용하도록 변경
return value.subtract(value.multiply(BigDecimal.valueOf(this.contract.getDiscountRate()))).doubleValue();
}
private LocalDate dateToday() {
return LocalDate.now();
}
public CustomerContract getContract() {
return contract;
}
}
함수 인라인(Inline Function)
- "함수 추출하기(Extract Function)"의 반대에 해당하는 리팩토링
- 함수 추출하기(Extract Function): 함수로 추출하여 함수 이름으로 의도를 표현하는 방법
- 함수 인라인을 사용하는 경우
- 대부분의 경우는 '함수 추추하기'가 더 적절하지만.. 간혹, 함수 본문이 함수 이름만큼 또는 그보다 더 잘 의도를 표현하는 경우도 있음 => 함수 인라인 사용
- 함수 추출하기 리팩토링이 잘못된 경우 => 여러 함수를 다시 인라인하여 커다란 함수를 만든 다음에 다시 함수 추출하기를 시도 가능 (함수 인라인을 과정으로 사용)
- 단순히 메서드 호출을 감싸는 우회형(indirection) 메서드인 경우 => 인라인으로 없앨 수 있음
- 상속 구조에서 오버라이딩 하고 있는 메서드는 인라인할 수 없음
public class Rating {
public int rating(Driver driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
private boolean moreThanFiveLateDeliveries(Driver driver) {
return driver.getNumberOfLateDeliveries() > 5;
}
}
Rating의 moreThanFiveLateDeliveries 메서드는 메서드를 통해 이름을 주어 의미를 부여하긴 했지만.. 구현부를 읽는 것과 메서드 이름을 읽는 것이 동일한 수준의 이해를 가져옴. 어쩌면 구현부가 더 잘 의도를 표현한다고 볼 수도 있음
=> 단순 우회형 메서드
=> 인라인 적용
public class Rating {
public int rating(Driver driver) {
return driver.getNumberOfLateDeliveries() > 5 ? 2 : 1;
}
}
클래스 인라인(Inline Class)
- "클래스 추출하기(Extract Class)"의 반대에 해당하는 리팩토링
- 클래스 인라인을 사용하는 경우
- 리팩토링을 하는 중에 클래스의 책임을 옮기다보면 클래스의 존재 이유가 빈약해지는 경우가 발생 가능 => 클래스 인라인 적용
- ex) 어떤 클래스가 단순히 위임만 하고 아무 기능을 하지 않는 경우
- 2개의 클래스를 여러 클래스로 나누는 리팩토링을 하는 경우 (클래스 인라인을 과정으로 사용)
- 우선 "클래스 인라인"을 적용해서 두 클래스의 코드를 한 곳으로 모으기
- 이후, "클래스 추출하기"를 적용해서 새롭게 분리하여 리팩토링
- 리팩토링을 하는 중에 클래스의 책임을 옮기다보면 클래스의 존재 이유가 빈약해지는 경우가 발생 가능 => 클래스 인라인 적용
예제 코드
public class Shipment {
private TrackingInformation trackingInformation;
public Shipment(TrackingInformation trackingInformation) {
this.trackingInformation = trackingInformation;
}
public TrackingInformation getTrackingInformation() {
return trackingInformation;
}
public void setTrackingInformation(TrackingInformation trackingInformation) {
this.trackingInformation = trackingInformation;
}
public String getTrackingInfo() {
return this.trackingInformation.display();
}
}
public class TrackingInformation {
private String shippingCompany;
private String trackingNumber;
public TrackingInformation(String shippingCompany, String trackingNumber) {
this.shippingCompany = shippingCompany;
this.trackingNumber = trackingNumber;
}
public String display() {
return this.shippingCompany + ": " + this.trackingNumber;
}
public String getShippingCompany() {
return shippingCompany;
}
public void setShippingCompany(String shippingCompany) {
this.shippingCompany = shippingCompany;
}
public String getTrackingNumber() {
return trackingNumber;
}
public void setTrackingNumber(String trackingNumber) {
this.trackingNumber = trackingNumber;
}
}
Shipment의 getTrackingInfo은 TrackingInformation의 display 메서드를 단순히 호출하는 메서드이다. TrackingInformation을 Shipment에 단순히 인라인 시켜도 될 것 같다고 보여진다. 클래스 인라인을 적용해보자!
- 필드를 먼저 가져오기
- 이후 Shipment에서 TrackingInformation 합성 부분 삭제
- 이를 사용하던 메서드들 삭제
public class Shipment {
private String shippingCompany;
private String trackingNumber;
public Shipment(String shippingCompany, String trackingNumber) {
this.shippingCompany = shippingCompany;
this.trackingNumber = trackingNumber;
}
public String getTrackingInfo() { // display 메서드에 함수 인라인 리팩토링 적용
return this.shippingCompany + ": " + this.trackingNumber;
}
public String getShippingCompany() {
return shippingCompany;
}
public void setShippingCompany(String shippingCompany) {
this.shippingCompany = shippingCompany;
}
public String getTrackingNumber() {
return trackingNumber;
}
public void setTrackingNumber(String trackingNumber) {
this.trackingNumber = trackingNumber;
}
}
9. 기능 편애(Feature Envy)
- 어떤 모듈에 있는 함수가 다른 모듈에 있는 데이터나 함수를 더 많이 참조하는 경우에 발생
- ex) 다른 객체의 getter를 여러개 사용하는 메서드
- 관련 리팩토링 기술
- "함수 옮기기(Move Function)" -> 함수를 적절한 위치로 옮기기
- "함수 추출하기(Extract Function)" -> 함수 일부분만 다른 곳의 데이터와 함수를 많이 참조한다면, 이를 추출한 후 '함수 옮기기' 적용
- 만약 여러 모듈에 참조하고 있다면?
- => 그 중 가장 많은 데이터를 참조하는 곳으로 옮기거나, 함수를 여러 개로 쪼개서 각 모듈로 분산시킬 수 있음
- 데이터와 해당 데이터를 참조하는 행동을 같은 곳에 두자 => 높은 응집도를 위해
- 예외적으로, 데이터와 행동을 분리한 디자인 패턴(전략 패턴 or 방문자 패턴)을 적용할 수 있음
- 행동이 여러가지 조건에 따라 바뀌어야 하는 경우에 해당
예제 코드
public class Bill {
private ElectricityUsage electricityUsage;
private GasUsage gasUsage;
public double calculateBill() {
var electicityBill = electricityUsage.getAmount() * electricityUsage.getPricePerUnit();
var gasBill = gasUsage.getAmount() * gasUsage.getPricePerUnit();
return electicityBill + gasBill;
}
}
위 코드는 electicityBill과 gasBill을 굳이 Bill에서 계산하기 위해 ElectricityUsage와 GasUsage을 모두 참조하고 있다. 이는 너무 욕심을 낸 것
=> 먼저 함수 추출하기를 적용하고, 이후 함수 옮기기를 적용하여 각각의 클래스로 책임을 이동시켜주자
public class Bill {
private ElectricityUsage electricityUsage;
private GasUsage gasUsage;
public double calculateBill() {
var electicityBill = getElecticityBill();
var gasBill = getGasBill();
return electicityBill + gasBill;
}
private double getGasBill() {
var gasBill = gasUsage.getAmount() * gasUsage.getPricePerUnit();
return gasBill;
}
private double getElecticityBill() {
var electicityBill = electricityUsage.getAmount() * electricityUsage.getPricePerUnit();
return electicityBill;
}
}
이후 책임 이동 시키기
public class GasUsage {
private double amount;
private double pricePerUnit;
...
public double getGasBill() {
return this.amount * this.pricePerUnit;
}
}
public class ElectricityUsage {
private double amount;
private double pricePerUnit;
...
public double getElecticityBill() {
return this.amount * this.pricePerUnit;
}
}
public class Bill {
private ElectricityUsage electricityUsage;
private GasUsage gasUsage;
public double calculateBill() {
return electricityUsage.getElecticityBill() + gasUsage.getGasBill();
}
}
10. 데이터 뭉치(Data Clumps)
- 항상 뭉쳐 다니는 데이터는 한 곳으로 모아두는 것이 좋음
- 여러 클래스에 존재하는 비슷한 필드 목록
- 여러 함수에 전달하는 매개변수 목록
- 관련 리팩토링 기술
- "클래스 추출하기(Extract Class)" -> 여러 필드를 하나의 객체나 클래스로 모을 수 있음
- "매개변수 객체 만들기(Introduce Parameter Object)" 또는 "객체 통째로 넘기기(Preserve Whole Object)" -> 메서드 매개변수 개선 가능
예제 코드
public class Employee {
private String name;
private String personalAreaCode;
private String personalNumber;
public Employee(String name, String personalAreaCode, String personalNumber) {
this.name = name;
this.personalAreaCode = personalAreaCode;
this.personalNumber = personalNumber;
}
public String personalPhoneNumber() {
return personalAreaCode + "-" + personalNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPersonalAreaCode() {
return personalAreaCode;
}
public void setPersonalAreaCode(String personalAreaCode) {
this.personalAreaCode = personalAreaCode;
}
public String getPersonalNumber() {
return personalNumber;
}
public void setPersonalNumber(String personalNumber) {
this.personalNumber = personalNumber;
}
}
public class Office {
private String location;
private String officeAreCode;
private String officeNumber;
public Office(String location, String officeAreCode, String officeNumber) {
this.location = location;
this.officeAreCode = officeAreCode;
this.officeNumber = officeNumber;
}
public String officePhoneNumber() {
return officeAreCode + "-" + officeNumber;
}
public String getOfficeAreCode() {
return officeAreCode;
}
public void setOfficeAreCode(String officeAreCode) {
this.officeAreCode = officeAreCode;
}
public String getOfficeNumber() {
return officeNumber;
}
public void setOfficeNumber(String officeNumber) {
this.officeNumber = officeNumber;
}
}
두 클래스 모두에 areaCode와 number가 중복으로 필드가 사용되고 있음을 볼 수 있다. 이를 별도의 클래스로 만들자
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 areaCode + " " + number;
}
}
public class Employee {
private final TelephoneNumber personalPhoneNumber;
private String name;
public Employee(String name, TelephoneNumber personalPhoneNumber) {
this.name = name;
this.personalPhoneNumber = personalPhoneNumber;
}
public String personalPhoneNumber() {
return this.personalPhoneNumber.toString();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TelephoneNumber getPersonalPhoneNumber() {
return personalPhoneNumber;
}
}
public class Office {
private String location;
private TelephoneNumber officePhoneNumber;
public Office(String location, TelephoneNumber officePhoneNumber) {
this.location = location;
this.officePhoneNumber = officePhoneNumber;
}
public String officePhoneNumber() {
return this.officePhoneNumber.toString();
}
public TelephoneNumber getOfficePhoneNumber() {
return officePhoneNumber;
}
}
'ETC' 카테고리의 다른 글
[Refactoring] 12 ~ 14. 반복되는 switch 문 / 반복문 / 성의없는 요소 (0) | 2025.01.04 |
---|---|
[Refactoring] 11. 기본형 집착 (0) | 2025.01.04 |
[Refactoring] 7. 뒤엉킨 변경 (0) | 2025.01.03 |
[Refactoring] 6. 가변 데이터 (0) | 2025.01.03 |
[Refactoring] 5. 전역 데이터 (2) | 2025.01.02 |