1. 알고자 하는 것
프로그램을 작성할 때 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
- 즉, A 도메인의 기능을 변경하기 위해 최소한의 코드만을 살펴볼 수 있도록 되어 있다면 모듈화가 잘 되어있는 것이다.
- 반대로, A 도메인의 기능을 변경하기 위해 여러 클래스, 여러 코드를 봐야 이해할 수 있다면 모듈화가 잘 되어있지 않은 것이다.
- 비슷한 문맥의 코드가 응집되어 있지 않고 여러 곳에 흩어져 있고 (응집도가 낮음), 불필요하게 여러 문맥 간 서로 의존하고 있어 하나의 변경점에 대해서 의존성이 묶여있는 여러 코드가 함께 변경되어야 한다면 (의존성 높음) 모듈화가 잘 되어있지 않은 것이다.
- 높은 모듈화를 통해 하나의 변경사항에 대해 최소한의 코드 파악과 최소한의 코드 수정으로 효율을 높일 수 있어야 한다.
이러한 모듈화를 위한 하나의 리팩토링 기법을 알아본다.
- 함수 옮기기 (Move Function)
2. 알게 된 것
- 모듈화를 위해서는 관련있는 메서드나 필드를 한 곳으로 모아서 살펴보아야 할 코드의 범위를 최소화해야 한다.
- 하지만 우리가 작성하는 소프트웨어는 관련있는 메서드와 필드가 언제나 고정되어 있지 않다.
- 요구사항이 변경되고, 도메인이 확장될 수록 변경되고 이동한다.
- 이러한 경우, 기존에는 모듈화가 잘 되어있다고 판단되었던 코드가 더 이상은 모듈화가 잘 되어있지 않게 될 수도 있다.
- 해당 메서드가 다른 클래스에 있는 데이터를 더 많이 참조하게 되는 경우
- 해당 메서드를 다른 클래스에서도 필요로 하는 경우
- 이럴 때, 함수 옮기기 (Move Function)를 통해 끊임없이 변화하는 요구사항에 유연하게 대처하고 모듈화를 지속할 수 있다.
public class Account {
private int daysOverdrawn;
private AccountType type;
public Account(int daysOverdrawn, AccountType type) {
this.daysOverdrawn = daysOverdrawn;
this.type = type;
}
public double getBankCharge() {
double result = 4.5;
if (this.daysOverdrawn() > 0) {
result += this.overdraftCharge(); // 납부일이 지난 경우 추가요금
}
return result;
}
private int daysOverdrawn() {
return this.daysOverdrawn;
}
// 납부일이 지난 경우 추가요금 계산
private double overdraftCharge() {
if (this.type.isPremium()) { // AccountType에 속한 필드
final int baseCharge = 10;
if (this.daysOverdrawn <= 7) { // Account에 속한 필드
return baseCharge;
} else {
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
}
- Account 클래스 내 메서드 중 납부일이 지난 경우 추가요금을 계산하는 overdraftCharge() 메서드가 있다.
- 해당 메서드는 자신이 가진 필드인 daysOverdrawn이라는 필드를 참조하기도 하지만, AccountType이 가진 필드인 isPremium 필드 역시 참조한다.
- 자신이 가진 필드와 AccountType이 가진 필드를 비슷하게 참조하므로 Account에 있어도 될 메서드이지만, AccountType에 있는 필드를 참조하기도 하고, 추후 AccountType에 있는 필드를 추가로 참조할 여지가 있을 수 있으므로 AccountType으로 메서드를 옮기는 함수 옮기기 (Move Function)를 사용하도록 한다.
public class AccountType {
private boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return this.premium;
}
// Account 필드의 daysOverdrawn을 매개변수로 받아온다
public double overdraftCharge(int daysOverdrawn) {
if (this.isPremium()) {
final int baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return daysOverdrawn * 1.75;
}
}
}
- AccountType으로 해당 메서드를 옮겨오면서 Account에 있는 daysOverdrawn 필드를 필요로 하게 된다.
- 이 때, Account 객체를 매개변수로 넘겨주는 경우 AccountType에 불필요한 Account 의존성이 생겨 변경점이 많아진다.
- 또한, Account 객체는 필드로 AccountType을 가지고 있어 순환 참조(Circular Reference)가 발생한다.
- 따라서 daysOverdrawn 필드만을 매개변수로 넘겨받아 함수를 옮긴다.
public class Account {
private int daysOverdrawn;
private AccountType type;
public Account(int daysOverdrawn, AccountType type) {
this.daysOverdrawn = daysOverdrawn;
this.type = type;
}
public double getBankCharge() {
double result = 4.5;
if (this.daysOverdrawn() > 0) {
// AccountType의 overdraftCharge 메서드 호출
result += this.type.overdraftCharge(this.daysOverdrawn);
}
return result;
}
private int daysOverdrawn() {
return this.daysOverdrawn;
}
}
- Account에서는 AccountType의 overdraftCharge 메서드를 호출하는 형태로 변경한다.
- 이렇게 된다면 추후 overdraftCharge 메서드를 사용하는 또 다른 클래스가 있을 경우 AccountType에서 해당 메서드를 호출하면 된다.
- 만약 overdraftCharge 메서드에 Account 필드를 더 많이 참조하게 된다면 이 때에는 매개변수에 Account 자체를 넘겨주어 순환 참조 / 불필요 의존성을 만들어내기보다는 다시 Account 클래스에 overdraftCharge 메서드를 옮겨주도록 한다.
- 이렇게 기능이 변경되면서 함수는 계속 옮겨가게 된다.
- 이러한 변경 사항을 주기적으로 판단하고 리팩토링을 꾸준히 진행해야 한다.
3. 정리
- 모듈화가 잘 된 소프트웨어는 최소한의 지식만으로 프로그램을 변경할 수 있다.
- 응집도가 높은 코드는 변경 사항이 있을 때 한 곳의 코드만 보아도 이해할 수 있다.
- 의존성이 낮은 코드는 변경 사항이 있을 때 한 곳의 코드만 변경해도 side effect가 없다.
- 하지만 소프트웨어는 언제나 끊임없이 변경사항에 대한 요구가 있으므로 관련있는 메서드나 필드 역시 고정되어 있지 않다.
- 즉, 기존에는 모듈화가 잘 되어있다고 판단되었던 코드가 변경 후에는 모듈화가 잘 되어있지 않을 수 있다.
- 해당 메서드가 다른 클래스에 있는 데이터를 더 많이 참조하게 되는 경우
- 해당 메서드를 다른 클래스에서도 필요로 하는 경우
- 이럴 때, 메서드를 다른 클래스로 옮기는 함수 옮기기 (Move Function)를 통해 주기적인 변경사항에 대해 모듈화를 유지한다.
- 함수를 옮길 때 의존성과 순환참조 등, 변경되는 참조 관계를 고려하여 적절하게 판단한다.
- 추후 또 다른 변경사항에 대해서 다시 함수를 옮기는 것을 주기적으로 판단하고 꾸준히 모듈화를 위해 고민해야 한다.
Reference
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 기본형을 객체로 바꾸기 (Replace Primitive with Object) (0) | 2024.01.15 |
---|---|
Java 리팩토링 - 함수 인라인, 클래스 인라인 (Function Inline, Class Inline) (0) | 2024.01.14 |
Java 리팩토링 - 변수 캡슐화하기 (with. Getter/Setter) (0) | 2023.12.17 |
Java 리팩토링 - 플래그 인수 제거하기 (0) | 2023.11.12 |
Java 리팩토링 - 조건문을 다형성으로 바꾸기 (0) | 2023.11.05 |