리팩토링 - 냄새 6. 가변 데이터

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

리팩토링

목록 보기
7/17
post-thumbnail

들어가기

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



냄새 6. 가변 데이터

Mutable Data

  • 데이터를 변경하다보면 예상치 못했던 결과나 해결하기 어려운 버그가 발생하기도 한다.
  • 함수형 프로그래밍 언어는 데이터를 변경하지 않고 복사본을 전달한다. 하지만 그밖의 프로그래밍 언어는 데이터 변경을 허용하고 있다. 따라서 변경되는 데이터 사용 시 발생할 수 있는 리스크를 관리할 수 있는 방법을 적용하는 것이 좋다.
  • 관련 리팩토링
    • “변수 캡슐화하기 (Encapsulate Variable)”를 적용해 데이터를 변경할 수 있는 메소드를 제한하고 관리할 수 있다.
    • 변수 쪼개기 (Split Variable)”을 사용해 여러 데이터를 저장하는 변수를 나눌 수 있다.
    • “코드 정리하기 (Slide Statements)”를 사용해 데이터를 변경하는 코드를 분리하고 피할 수 있다.
    • “함수 추출하기 (Extract Function)”으로 데이터를 변경하는 코드로부터 사이드 이팩트가 없는 코드를 분리할 수 있다.
    • 질의 함수와 변경 함수 분리하기 (Separate Query from Modifier)”를 적용해서 클라이언트가 원하는 경우에만 사이드 이팩트가 있는 함수를 호출하도록 API를 개선할 수 있다.
    • 가능하다면 “세터 제거하기 (Remove Setting Method)”를 적용한다.
    • 계산해서 알아낼 수 있는 값에는 “파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)”를 적용할 수 있다.
    • 변수가 사용되는 범위를 제한하려면 “여러 함수를 클래스로 묶기 (Combine Functions into Class)”또는 “여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)”을 적용할 수 있다.
    • 참조를 값으로 바꾸기 (Change Reference to Value)”를 적용해서 데이터 일부를 변경하기 보다는 데이터 전체를 교체할 수 있다.


변수 쪼개기

Split Variable

  • 어떤 변수가 여러번 재할당 되어도 적절한 경우
    • 반복문에서 순회하는데 사용하는 변수 또는 인덱스
    • 값을 축적시키는데 사용하는 변수
  • 그밖에 경우에 재할당 되는 변수가 있다면 해당 변수는 여러 용도로 사용되는 것이며 변수를 분리해야 더 이해하기 좋은 코드를 만들 수 있다.
    • 변수 하나 당 하나의 책임(Responsibility)을 지도록 만든다.
    • 상수를 활용하자. (자바스크립트의 const, 자바의 final)

예제코드

public class Order {

    public double discount(double inputValue, int quantity) {
        if (inputValue > 50) inputValue = inputValue - 2;
        if (quantity > 100) inputValue = inputValue - 1;
        return inputValue;
    }
}

냄새

매개변수인 inputValue 의 값을 그대로 활용한다

해결

변수를 쪼개보자!

리팩토링 후

public class Order {

    public double discount(double inputValue, int quantity) {
        double result = inputValue;
        if (inputValue > 50) result -= 2;
        if (quantity > 100) result -= 1;
        return result;
    }
}

설명

inputValue 를 변수화 하여 사용한다.



질의 함수와 변경 함수 분리하기

Separate Query from Modifier

  • “눈에 띌만한” 사이드 이팩트 없이 값을 조회할 수 있는 메소드는 테스트 하기도 쉽고, 메소
    드를 이동하기도 편하다.
  • 명령-조회 분리 (command-query separation) 규칙:
    • 어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 한다.
  • “눈에 띌만한 (observable) 사이드 이팩트”
    • 가령, 캐시는 중요한 객체 상태 변화는 아니다. 따라서 어떤 메소드 호출로 인해, 캐시 데이터를 변경하더라도 분리할 필요는 없다

예제코드


public class Billing {

    private Customer customer;

    private EmailGateway emailGateway;

    public Billing(Customer customer, EmailGateway emailGateway) {
        this.customer = customer;
        this.emailGateway = emailGateway;
    }

    public double getTotalOutstandingAndSendBill() {
        double result = customer.getInvoices().stream()
                .map(Invoice::getAmount)
                .reduce((double) 0, Double::sum);
        sendBill();
        return result;
    }

    private void sendBill() {
        emailGateway.send(formatBill(customer));
    }

    private String formatBill(Customer customer) {
        return "sending bill for " + customer.getName();
    }
}

냄새

getTotalOutstandingAndSendBill 함수의 호출할 경우 sendBill() 불필요한 함수를 호출한다.

해결

조회 함수와 명렴 함수를 분리하자!

리팩토링 후

public class Billing {

    private Customer customer;

    private EmailGateway emailGateway;

    public Billing(Customer customer, EmailGateway emailGateway) {
        this.customer = customer;
        this.emailGateway = emailGateway;
    }

    public double getTotalOutstanding() {
        return customer.getInvoices().stream()
                .map(Invoice::getAmount)
                .reduce((double) 0, Double::sum);
    }

    public void sendBill() {
        emailGateway.send(formatBill(customer));
    }

    private String formatBill(Customer customer) {
        return "sending bill for " + customer.getName();
    }
}

설명

getTotalOutstanding() 함수는 조회만하고 senBill() 함수는 따로 호출하여 명령과 조회를 분리한다.



세터 제거하기

Remove Setting Method

세터를 제공한다는 것은 해당 필드가 변경될 수 있다는 것을 뜻한다.
• 객체 생성시 처음 설정된 값이 변경될 필요가 없다면 해당 값을 설정할 수 있는 생성자를 만들고 세터를 제거해서 변경될 수 있는 가능성을 제거해야 한다.

예제코드

public class Person {

    private String name;

    private int id;

    public String getName() {
        return name;
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

냄새

회원을 식별하는 필드인 id 는 변경을 하면 안되지만, setter 를 사용한다.

해결

setter 메서드를 없앤다.



파생 변수를 질의 함수로 바꾸기

Replace Derived Variable with Query

  • 변경할 수 있는 데이터를 최대한 줄이도록 노력해야 한다.
  • 계산해서 알아낼 수 있는 변수는 제거할 수 있다.
    • 계산 자체가 데이터의 의미를 잘 표현하는 경우도 있다.
    • 해당 변수가 어디선가 잘못된 값으로 수정될 수 있는 가능성을 제거할 수 있다.
  • 계산에 필요한 데이터가 변하지 않는 값이라면, 계산의 결과에 해당하는 데이터 역시 불변 데이터기 때문에 해당 변수는 그대로 유지할 수 있다.

예제코드

public class ProductionPlan {

    private double production;
    private List<Double> adjustments = new ArrayList<>();

    public void applyAdjustment(double adjustment) {
        this.adjustments.add(adjustment);
        this.production += adjustment;
    }

    public double getProduction() {
        return this.production;
    }
}

냄새

applyAdjustment() 함수는 매개변수의 값을 list에 넣어줌과 동시에 총 합을 profuction 필드에 저장을 하는데, 이는 list 에서 총합을 알 수 있는 불필요한 변수이다.

해결

production 필드를 제거 후 list 에 저장된 총합을 return 하도록 한다.

리팩토링 후

public class ProductionPlan {

    private List<Double> adjustments = new ArrayList<>();

    public void applyAdjustment(double adjustment) {
        this.adjustments.add(adjustment);
    }

    public double getProduction() {
        return adjustments.stream().mapToDouble(Double::valueOf).sum();
    }
}

설명

불필요한 production 필드 제거 후, getProduction 에서 총합을 계산한다.



여러 함수를 변환 함수로 묶기

Combine Functions into Transform

  • 관련있는 여러 파생 변수를 만들어내는 함수가 여러곳에서 만들어지고 사용된다면 그러한 파생 변수를 “변환 함수 (transform function)”를 통해 한 곳으로 모아둘 수 있다.
  • 소스 데이터가 변경될 수 있는 경우에는 “여러 함수를 클래스로 묶기 (Combine Functions into Class)”를 사용하는 것이 적절하다.
  • 소스 데이터가 변경되지 않는 경우에는 두 가지 방법을 모두 사용할 수 있지만, 변환 함수를 사용해서 불변 데이터의 필드로 생성해 두고 재사용할 수도 있다.

객체 통쨰로 넘기기(preserve whole method) 와 같은 방식을 사용한다.



참조를 값으로 바꾸기

-Change Reference to Value

  • 레퍼런스 (Reference) 객체 vs 값 (Value) 객체
    • https://martinfowler.com/bliki/ValueObject.html
    • “Objects that are equal due to the value of their properties, in this case their x and y coordinates, are called value objects.”
    • 값 객체는 객체가 가진 필드의 값으로 동일성을 확인한다.
    • 값 객체는 변하지 않는다.
  • 어떤 객체의 변경 내역을 다른 곳으로 전파시키고 싶다면 레퍼런스, 아니라면 값 객체를 사용한다.

예제코드

private TelephoneNumber officeTelephoneNumber;

    public String getOfficeAreaCode() {
        return this.officeTelephoneNumber.getAreaCode();
    }

    public void setOfficeAreaCode(String areaCode) {
        this.officeTelephoneNumber.setAreaCode(areaCode);
    }

    public String getOfficeNumber() {
        return this.officeTelephoneNumber.getNumber();
    }

    public void setOfficeNumber(String number) {
        this.officeTelephoneNumber.setNumber(number);
    }

냄새

TelephoneNumber 객체를 레퍼런스 객체가 아닌 값 객체로 사용할 경우 set

해결

setter 를 통해 수정하지 않고 새로운 값 객체를 만들어준다.

리팩토링 후

Person

public class Person {

    private TelephoneNumber officeTelephoneNumber;

    public String getOfficeAreaCode() {
        return this.officeTelephoneNumber.getAreaCode();
    }

    public String getOfficeNumber() {
        return this.officeTelephoneNumber.getNumber();
    }

    public void setOfficeAreaCode(String areaCode) {
        this.officeTelephoneNumber = new TelephoneNumber(areaCode, this.getOfficeNumber());
    }

    public void setOfficeNumber(String number) {
        this.officeTelephoneNumber = new TelephoneNumber(this.getOfficeAreaCode(), number);
    }

}

TelephoneNumber

public class TelephoneNumber {

    private final String areaCode;

    private final String number;

    public TelephoneNumber(String areaCode, String number) {
        this.areaCode = areaCode;
        this.number = number;
    }

    public String getAreaCode() {
        return areaCode;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TelephoneNumber that = (TelephoneNumber) o;
        return Objects.equals(areaCode, that.areaCode) && Objects.equals(number, that.number);
    }

    @Override
    public int hashCode() {
        return Objects.hash(areaCode, number);
    }
}

값 객체이기 때문에 setter 를 없애고 equals, hashCode 메서드를 각 구현해준다.

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

0개의 댓글