ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Refactoring] 6. 가변 데이터
    클린코드 2022. 9. 9. 18:03

     

     

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

    리팩토링은 소프트웨어 엔지니어가 갖춰야 할 기본적인 소양 중 하나입니다. 이 강의는 인텔리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

    댓글

Designed by Tistory.