본문 바로가기

클린코드

[Refactoring] 6. 가변 데이터

 

 

코딩으로 학습하는 리팩토링 - 인프런 | 강의

리팩토링은 소프트웨어 엔지니어가 갖춰야 할 기본적인 소양 중 하나입니다. 이 강의는 인텔리J와 자바를 사용하여 보다 실용적인 방법으로 다양한 코드의 냄새와 리팩토링 기술을 설명하고 직

www.inflearn.com

| 인프런 - 백기선님의 코딩으로 학습하는 리팩토링 강의를 수강하며 정리한 글입니다.


가변 데이터
  • 데이터를 변경하다보면 예상치 못했던 결과해결하기 어려운 버그가 발생할 수 있다.
  • 함수형 프로그래밍 언어에서는 데이터를 변경하지 않고 복사본을 전달하지만, 자바 같은 경우 데이터 변경을 허용하기 때문에 변경되는 데이터 사용 시 발생할 수 있는 리스크를 관리할 수 있는 방법을 적용하는 것이 좋다.
  • 관련 리팩토링
    • 변수 캡슐화하기 -> 데이터를 변경할 수 있는 메서드를 제한하고 관리할 수 있다.
    • 변수 쪼개기 -> 여러 데이터를 저장하는 변수를 나눌 수 있다.
    • 코드 정리하기 -> 데이터를 변경하는 코드를 분리하고 피할 수 있다.
    • 함수 추출하기 -> 데이터를 변경하는 코드로부터 사이드 이팩트가 없는 코드를 분리할 수 있다.
    • 질의 함수와 변경 함수 변경하기 -> 클라이언트가 원하는 경우에만 사이드 이팩트가 발생할 수 있는 함수를 호출하도록 개선할 수 있다.
    • 세터 제거하기를 적용한다.
    • 파생 함수를 질의함수로 변경하기를 적용한다.
    • 여러 함수를 클래스로 묶기, 여러 함수를 변환 함수로 변경하기 -> 변수가 사용되는 범위를 제한한다.
    • 참조를 값으로 바꾸기 -> 데이터의 일부를 변경하기 보다는 데이터 전체를 교체할 수 있다.

Refactoring 1. 변수 쪼개기

  • 어떤 변수가 여러 번 재할당 되어도 적절한 경우
    • 반복문을 순회하는데 사용되는 변수 또는 인덱스
    • 값을 축적시키는데 사용하는 변수

  • 그 밖의 경우에 재할당되는 변수가 존재한다면 해당 변수는 여러 용도로 사용되는 것이며 변수를 분리해야 더 이해하기 좋은 코드가 될 수 있다.
    • 변수 하나 당 하나의 책임을 갖도록 한다.
    • 상수를 활용한다. ex) const, final...
Before
public class Rectangle {

    private double perimeter;
    private double area;

    public void updateGeometry(double height, double width) {
        double temp = 2 * (height + width);
        System.out.println("Perimeter: " + temp);
        perimeter = temp;

        temp = height * width;
        System.out.println("Area: " + temp);
        area = temp;
    }

    public double getPerimeter() {
        return perimeter;
    }

    public double getArea() {
        return area;
    }
}
After
public class Rectangle {

    private double perimeter;
    private double area;

    public void updateGeometry(double height, double width) {
        final double perimeter = 2 * (height + width);
        System.out.println("Perimeter: " + perimeter);
        this.perimeter = perimeter;

        final double area = height * width;
        System.out.println("Area: " + area);
        this.area = area;
    }

    public double getPerimeter() {
        return perimeter;
    }

    public double getArea() {
        return area;
    }
}

Refactoring 2. 질의 함수와 변경 함수 분리하기

  • 눈에 띌만한 사이드 이팩트 없이 값을 조회할 수 있는 메서드는 테스트 하기 쉽고, 메서드 이동도 편리하다.
  • 명령-조회 분리
    • 어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 한다.

  • 눈에 띌만한 사이드 이팩트
    • 캐시는 중요한 객체 상태 변화는 아니므로, 어떤 메서드 호출로 인해 캐시 데이터를 변경하더라도 분리할 필요는 없다.
Before
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();
    }
}
After
public class Billing {

    private Customer customer;

    private EmailGateway emailGateway;

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

    public double totalOutstanding() {
        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();
    }
}

=> 값 계산, Bill 전송 두 개의 역할을 분리


Refactoring 3. 세터 제거하기

  • 세터가 존재하는 것은 해당 필드가 변경될 수 있다는 것을 의미한다.
  • 객체 생성 시 처음 설정된 값이 변경될 필요가 없다면, 해당 값을 설정할 수 있는 생성자를 만들고 세터를 제거해서 필드가 변경될 가능성을 제거해야 한다.
Before
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;
    }
}
After
public class Person {

    private String name;

    private int id;

    public Person(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getId() {
        return id;
    }

}

=> 처음 Id 가 주어지고 이 후 변경되지 않을 시 적용할 수 있다.


Refactoring 4. 파생 변수를 질의 함수로 바꾸기다

  • 변경될 수 있는 데이터를 최대한 줄이도록 노력해야 한다.
  • 계산해서 알아낼 수 있는 변수는 제거할 수 있다.
    • 계산 자체가 데이터의 의미를 잘 표현한 것일 수 있다.
    • 해당 변수가 어디선가 잘못된 값으로 수정될 수 있는 가능성을 제거할 수 있다.

  • 계산에 필요한 데이터가 변하지 않는 값이라면, 계산의 결과에 해당하는 데이터 역시 불변하다.
Before
public class Discount {

    private double discountedTotal;
    private double discount;

    private double baseTotal;

    public Discount(double baseTotal) {
        this.baseTotal = baseTotal;
    }

    public double getDiscountedTotal() {
        return this.discountedTotal;
    }

    public void setDiscount(double number) {
        this.discount = number;
        this.discountedTotal = this.baseTotal - this.discount;
    }
}

=> setDiscount 호출 전 getDiscountedTotal() 호출 시 0이 호출 된다.

After
public class Discount {

    private double discount;

    private double baseTotal;

    public Discount(double baseTotal) {
        this.baseTotal = baseTotal;
    }

    public double getDiscountedTotal() {
        return this.baseTotal - this.discount;
    }

    public void setDiscount(double number) {
        this.discount = number;
    }
}

=> 수식 결과를 return 함으로써 불필요한 변수 제거


Refactoring 5. 여러 함수를 변환 함수로 묶기

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

    double baseCharge;

    public Client1(Reading reading) {
        this.baseCharge = baseRate(reading.month(), reading.year()) * reading.quantity();
    }

    private double baseRate(Month month, Year year) {
        return 10;
    }

    public double getBaseCharge() {
        return baseCharge;
    }
}

public class Client2 {

    private double base;
    private double taxableCharge;

    public Client2(Reading reading) {
        this.base = baseRate(reading.month(), reading.year()) * reading.quantity();
        this.taxableCharge = Math.max(0, this.base - taxThreshold(reading.year()));
    }

    private double taxThreshold(Year year) {
        return 5;
    }

    private double baseRate(Month month, Year year) {
        return 10;
    }

    public double getBase() {
        return base;
    }

    public double getTaxableCharge() {
        return taxableCharge;
    }
}

public record Reading(String customer, double quantity, Month month, Year year) {
}
After
public class Client {
    protected double baseRate(Month month, Year year) {
        return 10;
    }

    protected EnrichReading enrichReading(Reading reading) {
        return new EnrichReading(reading, calculateVaseCharge(reading));
    }

    private double calculateVaseCharge(Reading reading) {
        return baseRate(reading.month(), reading.year()) * reading.quantity();
    }
}

public class Client1 extends Client {

    double baseCharge;

    public Client1(Reading reading) {
        this.baseCharge = enrichReading(reading).baseCharge();
    }

    public double getBaseCharge() {
        return baseCharge;
    }
}

public class Client2 extends Client {

    private double base;
    private double taxableCharge;

    public Client2(Reading reading) {
        this.base = baseRate(reading.month(), reading.year()) * reading.quantity();
        this.taxableCharge = enrichReading(reading).baseCharge();
    }

    private double taxThreshold(Year year) {
        return 5;
    }

    public double getBase() {
        return base;
    }

    public double getTaxableCharge() {
        return taxableCharge;
    }
}

public record EnrichReading(Reading reading, double baseCharge) {
}

public record Reading(String customer, double quantity, Month month, Year year) {
}

=> 공통으로 계산되는 baseCharge를 Client Class에서 계산한다.


Refactoring 6. 참조를 값으로 바꾸기

  • 래퍼런스 객체 vs 값 객체
    • 값 객체는 객체가 가진 필드의 값으로 동일성을 확인한다.
    • 값 객체는 변하지 않는다.
    • 어떤 객체의 변경 내역을 다른 곳으로 전파시키고 싶다면 레퍼런스, 아니면 값 객체를 사용한다.

Before
public class Person {

    private TelephoneNumber officeTelephoneNumber;

    public String officeAreaCode() {
        return this.officeTelephoneNumber.areaCode();
    }

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

    public String officeNumber() {
        return this.officeTelephoneNumber.number();
    }

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

}

public class TelephoneNumber {

    private String areaCode;

    private String number;

    public String areaCode() {
        return areaCode;
    }

    public void areaCode(String areaCode) {
        this.areaCode = areaCode;
    }

    public String number() {
        return number;
    }

    public void number(String number) {
        this.number = number;
    }
}
After
public class Person {

    private TelephoneNumber officeTelephoneNumber;

    public String officeAreaCode() {
        return this.officeTelephoneNumber.areaCode();
    }

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

    public String officeNumber() {
        return this.officeTelephoneNumber.number();
    }

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

}

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


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

public record TelephoneNumber(String areaCode, String number) {
}

'클린코드' 카테고리의 다른 글

[Refactoring] 8. 산탄총 수술  (0) 2022.09.11
[Refactoring] 7. 뒤엉킨 변경  (0) 2022.09.10
[Refactoring] 5. 전역 데이터  (0) 2022.09.08
[Refactoring] 4. 긴 매개변수 목록  (0) 2022.09.07
[Refactoring] 3. 긴 함수  (0) 2022.09.06