[리팩토링] 악취 8 : 산탄총 수술

 

✍️ 악취 8 : 산탄총 수술

코드에 변경이 생겼을 때 관련 있는 여러 모듈 혹은 클래스 혹은 메서드를 수정해야 한다면 코드의 응집도는 낮고 결합도는 높은 상황일 가능성이 높다. 따라서 이런 현상이 발견된다면 관련 있는 데이터를 한 곳으로 옮겨서 응집도를 높이고 책임을 명확히 분리해서 결합도를 낮출 필요가 있다.

 

여기 낮은 응집도와 높은 결합도의 산탄총 악취를 해결하기 위한 세 가지 리팩토링 기법이 있다.

1. "함수 옮기기, 필드 옮기기" 필요한 변경 내역을 하나의 클래스로 모으기

2. "단계 쪼개기" 공통으로 사용되는 메서드의 결과물을 하나로 묶기

3. "함수 인라인, 클래스 인라인" 흩어진 로직을 한 곳으로

 

🍊 필드 옮기기

좋은 데이터 구조를 가지고 있다면, 해당 데이터를 기반한 행위를 메서드로 작성하는 것이 수월해진다. 이런 의미에서 관련 있는 필드를 한 곳에서 관리하도록 설계하는 것이 중요하다. 설령 현재 구조가 그렇지 않더라도 필드 옮기기를 통해 응집도를 높일 수 있다.

 

필드를 옮기는 단서

1. "어떤 데이터를 항상 어떤 객체와 함께 전달하는 경우" 데이터를 객체의 필드로 옮기기

2. "어떤 객체를 변경할 때 다른 객체에 있는 필드를 변경하는 경우" 변경의 주체로 필드를 옮기기

3. "여러 객체에 동일한 필드를 수정하는 경우" 필드를 한쪽으로 옮기기

 

고객의 이름과 할인율, 계약을 담는 클래스가 있다. 아래 구조에선 할인율과 계약이 별개의 필드로 존재하지만, 추후에 계약 클래스에 할인율이 포함돼야 한다고 판단이 되면 할인율 필드를 계약 객체로 옮길 수 있다.

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();
    }
}

public class CustomerContract {

    private LocalDate startDate;

    public CustomerContract(LocalDate startDate) {
        this.startDate = startDate;
    }
}

 

단순히 필드를 옮기는 것이 아니라, 필드와 관련된 메서드 가령 becomePreferred의 책임을 누가 지게 될 것인지 고민이 필요한 부분이다. 본인의 경우 CustomerContract가 그 책임을 지게 했고 판단에 따라 Customer의 책임으로 볼 수 도 있다.

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 contract.getDiscountRate();
    }

    public void becomePreferred() {
        contract.becomePreferred(0.03);
    }

    public double applyDiscount(double amount) {
        BigDecimal value = BigDecimal.valueOf(amount);
        return value.subtract(value.multiply(BigDecimal.valueOf(getDiscountRate()))).doubleValue();
    }

    private LocalDate dateToday() {
        return LocalDate.now();
    }
}

@Data
public class CustomerContract {

    private LocalDate startDate;

    private double discountRate;

    public CustomerContract(LocalDate startDate, double discountRate) {
        this.startDate = startDate;
        this.discountRate = discountRate;
    }

    public void becomePreferred(double discountRate) {
        this.discountRate += discountRate;
    }
}

 

🌱  함수 인라인

'함수 인라인'이란, 일련의 코드를 메서드로 추출해서 메서드의 이름으로 의도를 표현하는 '함수 추출하기'의 반대되는, 메서드 내부의 코드를 다른 메서드로 옮기는 리팩토링 기법을 말한다. 

 

일반적으로 '함수 추출하기' 기법이 더 유용하게 사용되지만 경우에 따라서 메서드의 이름이 의도를 잘 표현하지 못하거나 메서드의 본문이 그 의도를 충분히 잘 드러낸다면 '함수 인라인' 기법을 사용할 수 있다.

 

경우에 따라서 단순히 메서드 호출을 감싸는 우회형 메서드도 인라인 기법을 적용할 수 있다.

 

Student의 지각 횟수를 보고 페널티를 계산하는 StudentService에서 불필요하게 우회하는 메서드를 인라인화 시킬 수 있다.

@AllArgsConstructor
@Getter
public class Student {

    private String name;

    // 지각 횟수
    private int numberOfTardy;
}

public class StudentService {

    public int getTardyPenalty(Student student) {
        return isTardyMoreThanFive(student) ? 20 : 0;
    }

    private boolean isTardyMoreThanFive(Student student) {
        return student.getNumberOfTardy() > 5;
    }
}

 

 

// after
public class StudentService {

    public int getTardyPenalty(Student student) {
        return student.getNumberOfTardy() > 5 ? 20 : 0;
    }
}

 

🍃 클래스 인라인

클래스 인라인이란 클래스의 전체 필드와 메서드를 다른 클래스로 옮기는 기법이다. 리팩토링 과정에서 책임을 옮기다 보면 특정 클래스의 데이터 구조가 빈약한, 단순 위임만 된 형태로 변경될 수 있다. 이런 경우 클래스 인라인을 고려해 볼 수 있다.

 

아래 예제에서 TrackingInformation이 구조가 빈약한 형태의 클래스로, Shipment로 클래스 인라인을 적용해 볼 만하다.

@Data
public class Shipment {

    private TrackingInformation trackingInformation;

    public Shipment(TrackingInformation trackingInformation) {
        this.trackingInformation = trackingInformation;
    }

    public String getTrackingInfo() {
        return this.trackingInformation.display();
    }
}

public class TrackingInformation {

    private String shippingCompany;

    private String trackingNumber;

    public String display() {
        return this.shippingCompany + ": " + this.trackingNumber;
    }
}

 

Shipment로 TrackingInformation의 모든 필드와 메서드를 옮겼다.

@AllArgsConstructor
public class Shipment {

    private String shippingCompany;

    private String trackingNumber;

    public String getTrackingInfo() {
        return shippingCompany + ": " + trackingNumber;
    }
}