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