가변 데이터(Mutable Data)

박상훈·2022년 8월 18일
0
데이터를 변경하다보면 예상치 못한 결과나 해결하기 어려운 버그 발생
함수형 프로그래밍 언어는 데이터를 변경하지 않고 복사본을 전달하며 그 밖의 프로그래밍 언어는 데이터 변경 허용
변경되는 데이터 사용시 발생할 수 있는 리스크를 관리할 수 있는 방법을 적용

변수 쪼개기(Split Variable)

  • 어떤 변수가 여러번 재할당 되어도 적절한 경우
    • 반복문 순회에 사용하느 변수, 인덱스
    • 값을 축적하는 용도의 변수
  • 그밖의 경우로 재할당 되는 변수는 여러 용도의 사용이며 분리해야 한다
    • 변수에는 하나의 책임만
    • 상수(final) 활용

double 타입의 acc 라는 임시 변수를 생성하여 두 번의 재할당으로 코드의 이해가 쉽지 않은 케이스
result 의 경우 두 번의 결과를 합쳐 리턴하므로 아래와 같이 사용하는게 맞음
acc 라는 임시 변수만 primaryAcceleration, secondaryAcceleration 라는 이름의
변수로 조깨 사용하여 코드의 이해를 도움

before

class Refactoring {
    public double distanceTravelled(int time) {
        double result;
        double acc = primaryForce / mass;
        int primaryTime = Math.min(time, delay);
        result = 0.5 * acc * primaryTime * primaryTime;

        int secondaryTime = time - delay;
        if (secondaryTime > 0) {
            double primaryVelocity = acc * delay;
            acc = (primaryForce + secondaryForce) / mass;
            result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime + secondaryTime;
        }

        return result;
    }
}

after

class Refactoring {
    public double distanceTravelled(int time) {
        double result;
        final double primaryAcceleration = primaryForce / mass;
        int primaryTime = Math.min(time, delay);
        result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
    
        int secondaryTime = time - delay;
        if (secondaryTime > 0) {
            final double primaryVelocity = primaryAcceleration * delay;
            final double secondaryAcceleration = (primaryForce + secondaryForce) / mass;
            result += primaryVelocity * secondaryTime + 0.5 * secondaryAcceleration * secondaryTime + secondaryTime;
        }
    
        return result;
    }
}

매개변수의 값을 재할당하여 리턴하는 케이스
double 타입의 임시 변수를 생성 및 초기화하고 분기에 따른 재할당과 리턴의 역할을
담당하도록 하여 코드의 이해를 돕는다

before

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

after

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

double 타입의 temp 임시 변수를 생성하고 메서드 안에서 2번의 할당이 발생하는 케이스
코드를 보면 각 할당에 대한 역할이 다르므로 각 역할에 해당하는 임시 변수를 생성한다

before

class Refactoring {
    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;
    }
}

after

class Refactoring {
    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;
    }
}

질의 함수와 변경 함수 분리하기(Separate Query from Modifier)

  • 사이드 이팩트 없이 값을 조회할 수 있는 메서드는 테스트와 이동도 쉽다
    • 사이드 이팩트: 변경시 다른곳에서 참조에 대해 문제가 생기는 경우
  • 명령 - 조회 분리(command-query separation) 규칙
    • 어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 함
  • 눈에 띌만한(observable) 사이드 이팩트
    • 캐시는 중요한 객체 상태 변화가 아니며 호출로 인한 캐시 데이터 변경은 분리가 필요 없음

하나의 메소드에서 조회를 하며 메일도 전송하고 있는 케이스
getTotalOutstandingAndSendBill 메서드에서 sendBill 메서드를 제거하여 역할 분리

before

class Refactoring {
    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));
    }
}

after

class Refactoring {
    public double getTotalOutstanding() {
        return customer.getInvoices().stream()
                .map(Invoice::getAmount)
                .reduce((double) 0, Double::sum);
    }
  
    private void sendBill() {
        emailGateway.send(formatBill(customer));
    }
}

메서드에서 Miscreant 을 찾아 알람을 울리도록 두 가지 역할을 담당하는 케이스
Miscreant 을 찾는 역할, 알람을 울리는 역할 2개의 메서드로 분리

before

class Refactoring {
    public String alertForMiscreant(List<Person> people) {
        for (Person p : people) {
            if (p.getName().equals("Don")) {
              setOffAlarms();
              return "Don";
            }
      
            if (p.getName().equals("John")) {
              setOffAlarms();
              return "John";
            }
        }
  
      return "";
    }
  
    private void setOffAlarms() {
        System.out.println("set off alarm");
    }
}

after

class Refactoring {
    public void alertForMiscreant(List<Person> people) {
        if (!findMiscreant(people).isBlank()) {
            setOffAlarms();
        }
    }
  
    public String findMiscreant(List<Person> people) {
        for (Person p : people) {
            if (p.getName().equals("Don")) {
                return "Don";
            }
      
            if (p.getName().equals("John")) {
                return "John";
            }
        }
        return "";
    }
  
    private void setOffAlarms() {
        System.out.println("set off alarm");
    }
}

세터 제거하기(Remove Setting Method)

  • 세터를 제공한다는 것은 필드의 변경이 가능하다는 의미
  • 객체 생성시 값을 설정하며 세터를 제거하여 변경은 할 수 없도록 한다

before

class Refactoring {
    private String name;
    private int id;
    
    // ... getter, setter 생성
}

after

class Refactoring {
    private String name;
    private int id;
    
    after(int id) {
        this.id = id;
    }
    
    // ... getter 생성, setter 는 name 만 있도록 수정
}

파생 변수를 질의 함수로 바꾸기(Replace Derived Variable with Query)

  • 변경할 수 있는 데이터는 최대한 줄여야 한다
  • 계산해서 알아낼 수 있는 변수는 제거 가능
    • 계산 자체가 데이터의 의미를 잘 표현하는 경우
    • 계산 값을 가지는 변수가 잘못된 값으로 수정될 가능성을 제거할 수 있다
  • 계산에 필요한 데이터가 변하지 않는 값의 경우 계산의 결과에 해당하는 데이터도 불변 데이터이며 그대로 유지 가능

discountedTotal 은 discount, baseTotal 의 계산에 의해 파생된 변수이며
setDiscount 메서드에서 discountedTotal 변수에 할당을 하지 않고 dicount 가 있다면
getDiscountedTotal 에서 직접 값을 할당하더라도 문제가 되지 않고
불필요한 discountedTotal 필드를 제거하고 메서드에서 바로 값을 리턴받을 수 있다

before

class Refactoring {
    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;
    }
}

after

class Refactoring {
    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;
    }
}

첫 번째 예시와 동일한 케이스
불필요한 production 필드를 제거하고 값을 직접 리턴하는 메서드를 생성
stream 을 이용하여 리스트를 순회하면서 값을 모두 더하여 리턴하는 2가지 방법 적용

before

class Refactoring {
    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;
    }
}

after

class Refactoring {
    private List<Double> adjustments = new ArrayList<>();
  
    public void applyAdjustment(double adjustment) {
        this.adjustments.add(adjustment);
    }
  
    public double getProduction() {
        //return this.adjustments.stream().reduce((double) 0, Double::sum);
        return this.adjustments.stream().mapToDouble(Double::valueOf).sum();
    }
}

여러 함수를 변환 함수로 묶기(Combine Functions into Transform)

  • 관련있는 여러 파생 변수를 만들어내는 함수가 여러곳에서 만들어지고 사용되면 변환 함수(transform function) 을 이용
  • 소스 데이터가 변결될 수 있는 경우 여러 함수를 클래스로 묶기 사용
  • 변경되지 않는 경우 변환 함수를 사용해 불변 데이터의 필드로 생성하고 재사용 가능

변경 전 A ~ C 까지의 클래스에서 동일한 파생 변수를 만들어내는 baseRate 메서드를 사용
record class, baseRate 결과 값을 리턴하는 새로운 record class 를 생성하여
각 각이 가지는 baseRate 메서드를 특정 record class 에서 관리하도록 하고
변경 후 A ~ C 클래스에서는 record 를 참조 하는 구조로 변경

before

class A {
    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;
    }
}

class B {
    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;
    }
}

class C {
    private double basicChargeAmount;
  
    public Client3(Reading reading) {
        this.basicChargeAmount = calculateBaseCharge(reading);
    }
  
    private double calculateBaseCharge(Reading reading) {
        return baseRate(reading.month(), reading.year()) * reading.quantity();
    }
  
    private double baseRate(Month month, Year year) {
        return 10;
    }
  
    public double getBasicChargeAmount() {
        return basicChargeAmount;
    }
}

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

after


class A {
    double baseCharge;
  
    public Client1(Reading reading) {
        this.baseCharge = enrichReading(reading).baseCharge();
    }
  
    public double getBaseCharge() {
        return baseCharge;
    }
}

class B {
    private double base;
    private double taxableCharge;
  
    public Client2(Reading reading) {
        this.base = enrichReading(reading).baseCharge();
        this.taxableCharge = Math.max(0, this.base - taxThreshold(reading.year()));
    }
  
    public double getBase() {
        return base;
    }
  
    public double getTaxableCharge() {
        return taxableCharge;
    }
}

class C {
    private double basicChargeAmount;
  
    public Client3(Reading reading) {
        this.basicChargeAmount = calculateBaseCharge(reading);
    }
  
    private double calculateBaseCharge(Reading reading) {
        return enrichReading(reading).baseCharge();
    }
  
    public double getBasicChargeAmount() {
        return basicChargeAmount;
    }
}

class parent {
    protected double taxThreshold(Year year) {
        return 5;
    }
  
    protected double baseRate(Month month, Year year) {
        return 10;
    }
  
    protected EnrichReading enrichReading(Reading reading) {
        return new EnrichReading(reading, calculatedBaseCharge(reading));
    }
  
    private double calculatedBaseCharge(Reading reading) {
        return baseRate(reading.month(), reading.year()) * reading.quantity();
    }
}

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

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

참조를 값으로 바꾸기(Change Reference to Value)

  • 참조 객체 vs 값 객체
  • 변경 사항을 최소화 하고 싶은 경우
  • 값 객체는 변하지 않음
  • 값 객체는 객체가 가진 필드의 값으로 동일성 체크

JDK 11 까지 JDK 14 ~ 17 두 가지 방법으로 나뉘며
14 ~ 17 버전의 경우 record 를 사용하는 방법 초기화시 값 변경이 불가하며 장점으로는
getter, setter, equals, hashcord 를 별도로 생성하지 않아도 된다
JDK 11 버전까지의 경우 필드를 final 로 생성하고 값의 변경이 필요할 때는 새로운 객체에 할당하여 리턴

before

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

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 ~ JDK 11

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

after JDK 14 ~

public record TelephoneNumber(String areaCode, String number) { }
profile
엔지니어

0개의 댓글