BackEnd/Refactoring

[Refactoring] 산탄총 수술 (Shotgun Surgery), 기능 편애 (Feature Envy)

샤아이인 2023. 1. 6.

백기선 님의 리팩터링 강의를 들으며 요약한 내용입니다.

8. 산탄총 수술

왜 그냥 Gun도 아닛고 Shotgun일까? 샷건은 여러 파편이 퍼져 나간다는점을 상기하면 납득이 간다.

 

이전에 살펴본 뒤엉킨 변경의 경우 여러 이유로 하나의 Class를 계속해서 손봐야 하는 경우이다.

이번 산탄총 수술의 경우 새로운 방식을 도입하려면 여러 곳을 수정해야 한다.

 

8 - 1) 필드 옮기기

처음에는 타당해 보였던 설계적인 의사 결정도 프로그램이 다루고 있는 도메인과 데이터 구조에 대해 더 많이 익혀나가면서, 틀린 의사 결정으로 바뀌는 경우도 있다.

 

필드 또한 처음 만들때는 적절해 보였을 수 있지만, 어느순간 부터 다른 데이터와 항상 함께 전달된다거나, 어떤 레코드를 변경할때 다른 곳에 있는 필드를 변경해야 하는 경우 등등... 필드를 이동시켜야 하는 조짐이 보이면 이동시켜주면 된다.

 

다음 코드의 discountRate의 위치가 적합할까?

Customer가 할인률을 알고있기 보다는 CustomerContract에 있는것이 더 적합하지 않을까?

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로 단계적으로 이동시켜보자!

 

이동해야 하는 필드(this.discountRate)에 직접 접근하는고 있는 부분을 전부 getter, setter로 변경한다.

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 setDiscountRate(double discountRate) { // setter 추가
        this.discountRate = discountRate;
    }

    public void becomePreferred() {
        this.setDiscountRate(this.getDiscountRate() + 0.03); // getter/setter 함수사용
        // 다른 작업들
    }

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

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

 

이후 CustomerContract에 필드를 옮기고, getter와 setter를 추가해준다.

public class CustomerContract {

    private LocalDate startDate;
    private double discountRate;

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

    // getter, setter 추가
}

 

마지막으로 원래의 Customer의 getDiscountRate, setDiscountRate에서 사용하던 this.discountRate = 값의 구조를

this.constract.setXxx(값)으로 변경시키고 필드를 제거한다.

 

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); // 변경
    }

    // 일부 생략...
}

 

8 - 2) 함수 인라인

함수로 추출하여 함수의 이름으로 의도를 표현하던 "함수 추출하기"와 정확히 반대의 기법이다.

오히려 함수의 본문이 메서드 이름보다 의도를 더 잘 표현할수도 있다! 다음 코드를 살펴보자.

public class Rating {

    public int rating(Driver driver) {
        return moreThanFiveLateDeliveries(driver) ? 2 : 1;
    }

    private boolean moreThanFiveLateDeliveries(Driver driver) {
        return driver.getNumberOfLateDeliveries() > 5;
    }
}

moreThanFiveLateDeliveries 보다는 그냥 driver.getNum~ () > 5만 봐도 한눈에 이해가 된다.

 

따라서 인라인을 제거해보자!

public int rating(Driver driver) {
    return driver.getNumberOfLateDeliveries() > 5 ? 2 : 1;
}

 

8 - 3) 클래스 인라인

클래스 추출하기 (Extract Class)”의 반대에 해당하는 리팩토링 기법.

리팩토링을 하는 중에 클래스의 책임을 옮기다보면 클래스의 존재 이유가 빈약해지는 경우가 발생할 수 있다.

 

두개의 클래스를 여러 클래스로 나누는 리팩토링을 하는 경우에, 우선 클래스 인라인을 적용해서 두 클래스의 코드를 한 곳으로 모으고

그런 다음에 클래스 추출하기를 적용해서 새롭게 분리하는 리팩토링을 적용할 수 있다.

 

9. 기능 편애

어떤 모듈에 있는 함수가 다른 모듈에 있는 데이터나 함수를 더 많이 참조하는 경우에 발생한다.

) 다른 객체의 getter를 여러개 사용하는 메소드

 

관련 리팩토링 기술

함수를 적절한 위치로 옮기거나, 함수 일부분만 다른 곳의 데이터와 함수를 많이 참조한다면 함수 추출하기 (Extract Function)”로 함수를 나눈 다음에 함수를 옮길 수 있다.

 

만약에 여러 모듈을 참조하고 있다면? 그 중에서 가장 많은 데이터를 참조하는 곳으로 옮기거나, 함수를 여러개로 쪼개서 각 모듈로 분산 시킬 수도 있다.

 

데이터와 해당 데이터를 참조하는 행동을 같은 곳에 두도록 하자.

예외적으로, 데이터와 행동을 분리한 디자인 패턴 (전략 패턴 또는 방문자 패턴)을 적용할 수도 있다.

댓글