리팩토링 - 냄새 8. 산탄총 수술

김상운(개발둥이)·2022년 4월 5일
0

리팩토링

목록 보기
9/17
post-thumbnail

들어가기

해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.



냄새 8. 산탄총 수술

Shotgun Surgery

  • 어떤 한 변경 사항이 생겼을 때 여러 모듈을 (여러 함수 또는 여러 클래스를) 수정해야 하는 상황.
    • “뒤엉킨 변경” 냄새와 유사하지만 반대의 상황이다.
    • 예) 새로운 결제 방식을 도입하려면 여러 클래스의 코드를 수정해야 한다.
  • 변경 사항이 여러곳에 흩어진다면 찾아서 고치기도 어렵고 중요한 변경 사항을 놓칠 수 있는 가능성도 생긴다.
  • 관련 리팩토링 기술
    • “함수 옮기기 (Move Function)” 또는 “필드 옮기기 (Move Field)”를 사용해서 필요한 변경 내역을 하나의 클래스로 모을 수 있
      다,
    • 비슷한 데이터를 사용하는 여러 함수가 있다면 “여러 함수를 클래스로 묶기 (Combine Functions into Class)”를 사용할 수 있다.
    • “단계 쪼개기 (Split Phase)”를 사용해 공통으로 사용되는 함수의 결과물들을 하나로 묶을 수 있다.
    • 함수 인라인 (Inline Function)”과 “클래스 인라인 (Inline Class)”로 흩어진 로직을 한 곳으로 모을 수도 있다.


필드 옮기기

Move Field

  • 좋은 데이터 구조를 가지고 있다면, 해당 데이터에 기반한 어떤 행위를 코드로 (메소드나 함수) 옮기는 것도 간편하고 단순해진다.
  • 처음에는 타당해 보였던 설계적인 의사 결정도 프로그램이 다루고 있는 도메인과 데이터 구조에 대해 더 많이 익혀나가면서, 틀린 의사 결정으로 바뀌는 경우도 있다.
  • 필드를 옮기는 단서:
    • 어떤 데이터를 항상 어떤 레코드와 함께 전달하는 경우.
    • 어떤 레코드를 변경할 때 다른 레코드에 있는 필드를 변경해야 하는 경우.
    • 여러 레코드에 동일한 필드를 수정해야 하는 경우
    • (여기서 언급한 ‘레코드’는 클래스 또는 객체로 대체할 수도 있음)

예제 코드

Customer

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

CustomerContract

public class CustomerContract {

    private LocalDate startDate;

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

냄새

discountRate 필드를 CustomerContract 클래스에 있는것이 응집도를 높일 수 있는것 같다.

해결

discountRate 필드를 CustomerContract 클래스로 옮겼다.

리팩토링 후

Customer

import java.math.BigDecimal;
import java.time.LocalDate;

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 becomePreferred() {
        this.setDiscountRate(this.getDiscountRate() + 0.03);
        // 다른 작업들
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setDiscountRate(double discountRate) {
        this.contract.setDiscountRate(discountRate);
    }

    public CustomerContract getContract() {
        return contract;
    }

    public void setContract(CustomerContract contract) {
        this.contract = contract;
    }
}

CustomerContract

import java.time.LocalDate;

public class CustomerContract {

    private LocalDate startDate;

    private double discountRate;

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

    public LocalDate getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDate startDate) {
        this.startDate = startDate;
    }

    public double getDiscountRate() {
        return discountRate;
    }

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

설명

필드를 옮기는데 있어 IDE 의 도움을 받기 힘들어, 안전하게 CustomerContract 생성자에 discountRate 를 넣어 IDE 도움을 받아 옮겼다.



함수 인라인

Inline Function

  • “함수 추출하기 (Extract Function)”의 반대에 해당하는 리팩토링
    • 함수로 추출하여 함수 이름으로 의도를 표현하는 방법.
  • 간혹, 함수 본문이 함수 이름 만큼 또는 그보다 더 잘 의도를 표현하는 경우도 있다.
  • 함수 리팩토링이 잘못된 경우에 여러 함수를 인라인하여 커다란 함수를 만든 다음에 다시 함수 추출하기를 시도할 수 있다.
  • 단순히 메소드 호출을 감싸는 우회형 (indirection) 메소드라면 인라인으로 없앨 수 있다.
  • 상속 구조에서 오버라이딩 하고 있는 메소드는 인라인 할 수 없다. (해당 메소드는 일종의 규약
    이니까)

예제 코드

Driver

public class Driver {

    private int numberOfLateDeliveries;

    public Driver(int numberOfLateDeliveries) {
        this.numberOfLateDeliveries = numberOfLateDeliveries;
    }

    public int getNumberOfLateDeliveries() {
        return this.numberOfLateDeliveries;
    }
}

Rating

public class Rating {

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

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

설명

Rating 클래스의 rating() 함수는 배달기사가 5회 이상 늦게 배달한 경우가 있는지 확인 후 결과 값에 따라 rating 을 반환해주는 함수다.

냄새

moreThanFiveLateDeliveries() 함수는 한줄짜리 코드를 단순 위임 해주는 함수이며, 의미를 크게 변환해주지는 않는다.

해결

함수를 인라인하자!

리팩토링

수정된 Rating 클래스

public class Rating {

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

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

설명

위임의 역할을 하는 함수를 inline 을 해주기 위해 IDE 에서 ctrl + N 을 눌러 함수를 inline 하였다.



클래스 인라인

Inline Class

  • 클래스 추출하기 (Extract Class)”의 반대에 해당하는 리팩토링
  • 리팩토링을 하는 중에 클래스의 책임을 옮기다보면 클래스의 존재 이유가 빈약해지는 경우가 발생할 수 있다.
  • 두개의 클래스를 여러 클래스로 나누는 리팩토링을 하는 경우에, 우선 “클래스 인라인”을 적용해서 두 클래스의 코드를 한 곳으로 모으고 그런 다음에 “클래스 추출하기”를 적용해서 새롭게 분리하는 리팩토링을 적용할 수 있다.

예제 코드

Shipment

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

TrackingInformation

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

냄새

TrackingInformation 클래스의 역할이 단순 위임으로써 응집도를 높이기 위해 Shipment 클래스에 TrackingInformation 필드와 메소드를 옮기는게 좋을것 같다.

해결

class 를 inline 하자!

리팩토링

Shipment

public class Shipment {

    private String shippingCompany;

    private String trackingNumber;

    public Shipment(String shippingCompany, String trackingNumber) {
        this.shippingCompany = shippingCompany;
        this.trackingNumber = trackingNumber;
    }

    public String getTrackingInfo() {
        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;
    }

}

설명

TrackingInformation 클래스를 Shipment 클래스로 inline 시켰으며, 필요없어진 TrackingInformation 클래스를 삭제하였다.

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글