-
Notifications
You must be signed in to change notification settings - Fork 0
Description
2) 객체간의 기능 이동
1. Move Method
메서드가 자신이 정의된 클래스보다 다른 클래스의 기능을 더 많이 사용하고 있다면
이 메서드를 가장 많이 사용하고 있는 클래스에 비슷한 몸체를 가진 새로운 메서드를 만들어라.
그리고 이전 메서드는 간단한 위임으로 바꾸거나 완전히 제거하라.
// Before: 잘못된 위치에 있는 메서드
public class Account {
private AccountType accountType;
private double balance;
public double calculateOverdraftCharge() { // 이 메서드는 AccountType의 특성을 더 많이 사용
if (accountType.isPremium()) {
double baseCharge = 10;
if (daysOverdrawn() <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn() - 7) * 0.85;
}
} else {
return daysOverdrawn() * 1.75;
}
}
private int daysOverdrawn() {
// 초과 인출 일수 계산 로직
return 5; // 예시 값
}
}
public class AccountType {
private boolean premium;
public boolean isPremium() {
return premium;
}
}
// After: 메서드를 적절한 클래스로 이동
public class Account {
private AccountType accountType;
private double balance;
public double calculateOverdraftCharge() {
return accountType.calculateOverdraftCharge(daysOverdrawn());
}
private int daysOverdrawn() {
// 초과 인출 일수 계산 로직
return 5; // 예시 값
}
}
public class AccountType {
private final boolean premium;
public AccountType(boolean premium) {
this.premium = premium;
}
public boolean isPremium() {
return premium;
}
public double calculateOverdraftCharge(int daysOverdrawn) {
if (isPremium()) {
var baseCharge = 10.0;
if (daysOverdrawn <= 7) {
return baseCharge;
}
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
return daysOverdrawn * 1.75;
}
}🪄 동기
- 메서드를 옮기는 것은 리팩토링에서 가장 중요하고 기본이 되는 것이다.
- 클래스가 너무 많은 동작을 가지고 있거나, 다른 클래스와 공동으로 일하는 부분이 많아서 단단히 결합되어 있을 때 메서드를 옮긴다.
- 메서드를 옮김으로써 클래스를 더 간단하게 할 수 있고 클래스는 맡고 있는 책임에 대해 더욱 명확한 구현을 가질 수 있게 된다.
- 옮길만한 메서드를 발견하면, 이 메서드를 호출하는 메서드, 이 메서드가 호출하는 메서드, 그리고 상속 계층에서 이 메서드를 재정의하고 있는 메서드를 살펴본다.
- 그리고 옮기려고 하는 메서드와 상호작용을 더 많이 하고 있는 것처럼 보이는 클래스를 기초로 하여 계속 진행할지를 평가한다.
2. Move Field
필드가 자신이 정의된 클래스보다 다른 클래스의 기능을 더 많이 사용하고 있다면 타겟 클래스에 새로운 필드를 만들고 기존 필드를 사용하는 모든 부분을 변경하라.
🪄 동기
- 어떤 필드가 자신이 속한 클래스보다 다른 클래스의 메서드에서 더 많이 사용되고 있는 것을 보면 그 필드를 옮기는 것을 고려한다.
- 그러는 한편 다른 클래스가 get/set메서드를 통해서 이 필드를 간접적으로 많이 사용하고 있을지도 모른다는 생각도 한다.
// Before: 필드가 잘못된 클래스에 위치
public class Account {
private AccountType accountType;
private double interestRate; // 이 필드는 AccountType에 더 적합
public double getInterestRate() {
return interestRate;
}
public void setInterestRate(double rate) {
this.interestRate = rate;
}
public double calculateInterest() {
return balance * interestRate;
}
}
public class AccountType {
private String typeName;
public boolean isPremium() {
return "Premium".equals(typeName);
}
}
// After: 필드를 적절한 클래스로 이동
public class Account {
private final AccountType accountType;
private double balance;
public Account(AccountType accountType) {
this.accountType = accountType;
}
public double calculateInterest() {
return balance * accountType.getInterestRate();
}
}
public class AccountType {
private final String typeName;
private double interestRate; // 이자율은 계좌 타입의 특성이므로 여기에 더 적합
public AccountType(String typeName, double interestRate) {
this.typeName = typeName;
this.interestRate = interestRate;
}
public double getInterestRate() {
return interestRate;
}
public void setInterestRate(double rate) {
this.interestRate = rate;
}
public boolean isPremium() {
return "Premium".equals(typeName);
}
}3. Extract Class
두개의 클래스가 해야 할 일을 하나의 클래스가 하고 있는 경우, 새로운 클래스를 만들어서 관련 있는 필드와 메서드를 예전 클래스에서 새로운 클래스로 옮겨라.
🪄 동기
- 클래스는 분명하게 추상화되어야 하고, 몇 가지 명확한 책임을 가져야 한다는 말 또는 이와 비슷한 지침을 들었을 것이다.
- 실제로 클래스는 점점 커진다. 어떤 동작을 추가할 대도 있고 약간의 데이터를 추가할 때도 있다.
- 우리는 별도의 클래스로 만들만한 가치가 없다고 느끼는 책임을 기존 클래스에 추가한다.
- 클래스는 많은 메서드와 데이터를 가지고 있고 너무 커서 쉽게 이해할 수도 없다.
- 이제 우리는 그 클래스를 분리할 방법을 생각하고 클래스를 분리해야 한다.
- 데이터의 부분 집합과 메서드의 부분 집합이 같이 몰려다니는 것은 별도의 클래스로 분리할 수 있다는 좋은 신호이다.
- 보통 같이 변하거나 특별히 서로에게 의존적인 데이터의 부분 집합 또한 별도의 클래스로 분리할 수 있다는 좋은 신호이다.
- 만약 일부 데이터나 메서드를 제거한다면 다른 필드나 메서드가 의미없는 것이 될지를 자신에게 물어보는 것은 편리한 테스트 방법이다.
- 개발의 후반부에 종종 나타나는 신호중의 하나는 클래스가 서브타입이 되는 방법이다.
- 서브타이핑이 단지 몇몇 기능에만 영향에 미친다는 것을 알게 되거나 또는 어떤 부분은 이런 식으로 서브타입이 되어야 하고 다른 부분은 또 다른 방법으로 서브타입이 되어야 한다는 것을 알게 될 것이다
// Before: 너무 많은 책임을 가진 큰 클래스
public class Person {
private String name;
private String homePhone;
private String officePhone;
private String mobilePhone;
private String street;
private String city;
private String postalCode;
public String getName() {
return name;
}
public String getHomePhone() {
return homePhone;
}
public String getOfficePhone() {
return officePhone;
}
public String getFullAddress() {
return street + ", " + city + " " + postalCode;
}
// ... 더 많은 메서드들
}
// After: 책임에 따라 분리된 클래스들
public class Person {
private final String name;
private final PhoneNumbers phoneNumbers;
private final Address address;
public Person(String name, PhoneNumbers phoneNumbers, Address address) {
this.name = name;
this.phoneNumbers = phoneNumbers;
this.address = address;
}
public String getName() {
return name;
}
public PhoneNumbers getPhoneNumbers() {
return phoneNumbers;
}
public Address getAddress() {
return address;
}
}
public class PhoneNumbers {
private final String homePhone;
private final String officePhone;
private final String mobilePhone;
public PhoneNumbers(String homePhone, String officePhone, String mobilePhone) {
this.homePhone = homePhone;
this.officePhone = officePhone;
this.mobilePhone = mobilePhone;
}
public String getHomePhone() {
return homePhone;
}
public String getOfficePhone() {
return officePhone;
}
public String getMobilePhone() {
return mobilePhone;
}
}
public class Address {
private final String street;
private final String city;
private final String postalCode;
public Address(String street, String city, String postalCode) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
public String getFullAddress() {
return String.format("%s, %s %s", street, city, postalCode);
}
public String getCity() {
return city;
}
// ... 필요한 메서드들
}4. Inline Class
클래스가 하는 일이 많지 않은 경우에는 그 클래스에 있는 모든 변수와 메서드를 다른 클래스로 옮기고 그 클래스를 제거하라.
🪄 동기
Inline Class는Extract Class의 반대이다.- 클래스가 더 이상 제 몫을 하지 못하고 더 이상 존재할 필요가 없다면
Inline Class를 사용한다.
// Before: 너무 작은 책임을 가진 클래스들
public class Person {
private final PersonalDetails details;
private final Address address;
public String getName() {
return details.getName();
}
public String getPhoneNumber() {
return details.getPhoneNumber();
}
}
public class PersonalDetails {
private final String name;
private final String phoneNumber;
public PersonalDetails(String name, String phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
public String getName() {
return name;
}
public String getPhoneNumber() {
return phoneNumber;
}
}
// After: 불필요한 클래스를 인라인하여 단순화
public class Person {
private final String name;
private final String phoneNumber;
private final Address address;
public Person(String name, String phoneNumber, Address address) {
this.name = name;
this.phoneNumber = phoneNumber;
this.address = address;
}
public String getName() {
return name;
}
public String getPhoneNumber() {
return phoneNumber;
}
}✅ 절차
- 흡수하는 클래스에 소스 클래스의 public 필드와 메서드를 선언한다.
- 소스 클래스 메서드에 대한 인터페이스를 분리하는 것이 이치에 맞다면, 인라인화 하기 전에
Extract Interface를 사용하라. - 소스 클래스를 참조하고 있는 모든 부분을 흡수하는 클래스를 참조하도록 변경한다.
- 패키지 밖에서 참조하는 부분(out-of-package 참조)을 없애기 위해서 소스 클래스를
private으로 선언하라. 또한 컴파일러가 소스 클래스에 대한 모든 죽은 참조(dangling reference) 찾도록 소스 클래스의 이름을 변경한다. - 컴파일 & 테스트 한다.
Move Method와Move Field를 사용하여, 소스 클래스에 있는 모든 변수와 메서드를 흡수하는 클래스로 옮긴다.- 짧고 간단한 장례식을 거행한다.
5. Hide Delegate
클라이언트가 객체의 위임 클래스를 직접 호출하고 있는 경우 서버에 메서드를 만들어 대리 객체(delegate)를 숨겨라.
🪄 동기
- 캡슐화는 객체에서 가장 중요한 개념 가운데 하나이다.
- 캡슐화는 객체가 시스템의 다른 부분에 대해 적게 알아도 된다는 것을 의미한다.
- 캡슐화가 되어 있는 경우에는 어떤 것이 변경되었을 때 시스템의 다른 부분이 영향을 덜 받으므로 결과적으로 변경을 좀 더 쉽게 할 수 있게 한다.
- 자바는 필드가 public으로 선언되는 것을 허용하지만, 객체를 다루는 사람이라면 필드는 숨겨져야 한다는 것을 알고 있다.
- 여러분은 점점 세련되어 질수록 캡슐화 할 수 있다는 것이 더 많아진다는 것을 알게 된다.
- 클라이언트가 서버 객체의 필드에 들어있는 객체에 정의된 메서드를 호출한다면, 클라이언트는 대리객체(delegate)에 대해서 알아야 한다.
- 이와 같은 경우에 서버 객체에 간단한 위임 메서드를 두어 위임을 숨김으로서 이런 종속성을 제거할 수 있다.
- 서버의 일부 또는 모든 클라이언트에 대해서 Extract Class를 사용할 가치가 있다는 것을 발견할지도 모른다.
- 만약 모든 클라이언트에게 실제로 일을 처리하는 부분을 숨기고 있다면 서버의 인터페이스에서 위임과 관련된 모든 부분을 제거할 수 있다.
// Before: 클라이언트가 위임 객체를 직접 접근
public class Person {
private final Department department;
public Person(Department department) {
this.department = department;
}
public Department getDepartment() {
return department;
}
}
public class Department {
private final Employee manager;
public Department(Employee manager) {
this.manager = manager;
}
public Employee getManager() {
return manager;
}
}
// 클라이언트 코드
public class Client {
public void someMethod() {
Person person = new Person(new Department(new Employee("John")));
// 클라이언트가 위임 객체를 직접 탐색 (Law of Demeter 위반)
Employee manager = person.getDepartment().getManager();
}
}
// After: 위임을 숨기는 메서드 추가
public class Person {
private final Department department;
public Person(Department department) {
this.department = department;
}
// 위임을 숨기는 메서드 추가
public Employee getDepartmentManager() {
return department.getManager();
}
}
// 클라이언트 코드
public class Client {
public void someMethod() {
Person person = new Person(new Department(new Employee("John")));
// 단순화된 인터페이스를 통해 접근
Employee manager = person.getDepartmentManager();
}
}✅ 절차
- 대리 객체의 각각의 메서드에 대해, 서버에서 간단한 위임 메서드를 만든다.
- 클라이언트가 서버를 호출하도록 바꾼다.
- 클라이언트가 서버와 같은 패키지에 있지 않다면 실제로 일을 처리하는 메서드의 접근 권한을
package로 변경하는 것을 고려하라.
- 클라이언트가 서버와 같은 패키지에 있지 않다면 실제로 일을 처리하는 메서드의 접근 권한을
- 각각의 메서드를 알맞게 바꾸고 나서 컴파일 & 테스트를 한다.
- 어떤 클라이언트에서도 더 이상 대리객체에 접근할 필요가 없다면, 서버 클래스에서 대리객체에 대한 접근자를 제거한다.
The Law of Demeter (LoD)
"최소 지식 원칙"은 객체지향 설계의 중요한 원칙 중 하나이다.
각 객체는 자신과 직접적으로 관련된 객체와만 상호작용해야 한다는 원칙이다.
// Law of Demeter 위반 예시
public class Customer {
private Wallet wallet;
public Wallet getWallet() {
return wallet;
}
}
public class Store {
public void purchaseItem(Customer customer, double itemPrice) {
// 나쁜 예: 다른 객체의 내부 구조를 너무 많이 알고 있음
if (customer.getWallet().getMoney() >= itemPrice) {
customer.getWallet().deductMoney(itemPrice);
}
}
}
// Law of Demeter 준수 예시
public class Customer {
private Wallet wallet;
public boolean canAfford(double amount) {
return wallet.hasSufficientFunds(amount);
}
public void pay(double amount) {
wallet.deductMoney(amount);
}
}
public class Store {
public void purchaseItem(Customer customer, double itemPrice) {
// 좋은 예: 객체의 내부 구현에 대해 알 필요가 없음
if (customer.canAfford(itemPrice)) {
customer.pay(itemPrice);
}
}
}LoD를 준수하는 방법
- 객체는 다음과 직접 대화해야 한다.
- 자신의 필드
- 메서드의 파라미터
- 자신이 생성한 객체
- 직접적인 컴포넌트 객체
- '한 단계'만 호출하기
- 체이닝 피하기
6. Remove Middle Man
클래스가 간단한 위임을 너무 많이 하고 있는 경우에는 클라이언트가 대리객체(Delegate)를 직접 호출하도록 하라.
🪄 동기
Hide Delegate를 사용하는 동기를 이야기할 때 대리객체 사용을 캡술화 하는 것의 장점에 대해서 이야기 했다.- 그러나 여기에는 그만한 대가를 치러야 한다.
- 클라이언트 대리객체의 새로운 메서드를 사용하려 할 때 마다 서버 클래스는 간단한 위임 메서드를 추가해야하는 것이다.
- 새로운 메서드를 추가하려면 추가 비용이 들게 된다.
- 서버 클래스는 단지 미들맨(Middle Man)에 지나지 않게 되는데 아마도 이때가 클라이언트로 하여금 대리객체를 직접 호출하도록 해야할 때일 것이다.
- 어느 정도를 숨기는 것이 적절한지 판단하는 것은 어렵다.
- 다행이도 Hide Delegate와 Remove Middle Man에서는 이것이 별로 중요하지 않다.
- 시간이 지나고 시스템이 변할수록 얼마나 숨겨야 하는지에 대한 원칙 또한 변경된다.
// Before: 과도한 위임 메서드
public class Person {
private final Department department;
public Person(Department department) {
this.department = department;
}
// 단순 위임 메서드들이 너무 많음
public Employee getManager() {
return department.getManager();
}
public List<Employee> getTeamMembers() {
return department.getTeamMembers();
}
public String getDepartmentName() {
return department.getName();
}
public Location getDepartmentLocation() {
return department.getLocation();
}
public Budget getDepartmentBudget() {
return department.getBudget();
}
}
// After: 위임 객체를 직접 접근하도록 변경
public class Person {
private final Department department;
public Person(Department department) {
this.department = department;
}
// 필요한 경우 department 직접 접근 허용
public Department getDepartment() {
return department;
}
}
// 클라이언트 코드
public class Client {
public void someMethod(Person person) {
// 직접 department의 메서드 호출
Department dept = person.getDepartment();
Employee manager = dept.getManager();
List<Employee> team = dept.getTeamMembers();
String deptName = dept.getName();
Location location = dept.getLocation();
Budget budget = dept.getBudget();
}
}✅ 절차
- 대리객체에 대한 접근자를 만든다.
- 서버 클래스에 있는 위임 메서드를 사용하는 각각의 클라이언트에 대해 클라이언트가 대리객체의 메서드를 호출하도록 바꾸고 서버 클래스에 있는 메서드를 제거한다.
- 각각의 메서드에 대한 작업을 마칠 때 마다 컴파일 & 테스트 한다.
7) Introduce Foreign Method
사용하고 있는 서버 클래스에 부가적인 메서드가 필요하지만 클래스를 수정할 수 없는 경우에는 첫 번째 인자로 서버 클래스의 인스턴스를 받는 메서드를 클라이언트에 만들어라.
🪄 동기
- 모든 서비스를 제공하는 정말로 멋진 클래스를 사용하고 있다.
- 그러나 꼭 필요하지만 그 클래스가 제공하지 않는 서비스가 하나 있다.
- 소스 코드를 변경할 수 없다면 부족한 메서드를 클라이언트 쪽에 만들어야한다.
- 클라이언트 클래스에서 필요한 메서드를 단지 한 번만 사용한다면 추가 코딩은 큰 문제가 아니고, 이런 경우에는 아마도 서버 클래스에 메서드를 추가할 필요가 없을 것이다.
- 새로 만드는 메서드를 외래 메서드(
Foreign method)로 만들어서 이 메서드가 실제로는 서버 클래스에 있어야 하는 메서드라는 것을 명확하게 나타낼 수 있다. - 만약 서버 클래스의 외래 메서드를 많이 만들어야 한다는 것을 깨닫게 되거나 많은 클래스가 동일한 외래 메서드를 필요로 한다는 것을 알게 된다면
Introduce Local Extension대신 사용해야 한다. - 외래 메서드는 임시 방편이라는 것을 잊지마라!
- 만약 할 수 있다면, 외래 메서드를 그들이 원래 있어야 하는 위치로 옮기는 것을 시도해 봐라.
- 코드 소유권이 문제가 된다면 외래 메서드를 서버 클래스의 소유자에게 보내고 그 소유자에게 그 메서드를 구현해 달라고 요청하라.
// 수정할 수 없는 서버 클래스 (예: 라이브러리 클래스)
public final class Date {
// 자바의 Date 클래스라고 가정
// 수정 불가능한 클래스
}
// Before: 클라이언트 코드에서 반복적인 날짜 계산
public class DateReport {
public void someMethod() {
Date date = new Date();
// 다음날을 구하는 로직이 여러 곳에서 반복됨
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
Date nextDate = calendar.getTime();
}
public void anotherMethod() {
Date date = new Date();
// 같은 로직이 반복됨
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
Date nextDate = calendar.getTime();
}
}
// After: Foreign Method 도입
public class DateReport {
// Foreign Method - Date 클래스를 확장하는 것처럼 사용
public static Date nextDay(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
return calendar.getTime();
}
// 실제 사용
public void someMethod() {
Date date = new Date();
Date nextDate = nextDay(date);
}
public void anotherMethod() {
Date date = new Date();
Date nextDate = nextDay(date);
}
}
================================================================================================
// 더 현대적인 접근: 확장 함수 사용 (Kotlin)
fun Date.nextDay(): Date {
val calendar = Calendar.getInstance()
calendar.time = this
calendar.add(Calendar.DATE, 1)
return calendar.time
}
// 또는 Java의 유틸리티 클래스 사용
public class DateUtils {
private DateUtils() { } // 인스턴스화 방지
public static Date nextDay(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
return calendar.time;
}
}✅ 절차
- 필요한 작업을 하는 메서드를 클라이언트 클래스에 만든다.
- 그 메서드는 클라이언트 클래스의 어떤 부분에도 접근해서는 안 된다.
- 값이 필요하다면 값을 파라미터로 넘겨야 한다.
- 첫 번째 파라미터로 서버 클래스의 인스턴스를 받도록 한다.
- 메서드에
'외래 메서드, 원래는 서버 클래스에 있어야 한다.'와 같은 주석을 달아 놓는다.- 이렇게 해두면 나중에 이들 메서드를 옮길 기회가 생겼을 때 텍스트 검색을 이용하여 외래 메서드를 쉽게 찾을 수 있다.
8) Introduce Local Extension
사용하고 있는 서버 클래스에 여러 개의 메서드를 추가할 필요가 있지만 서버 클래스를 수정할 수 없는 경우, 필요한 추가 메서드를 포함하는 새로운 클래스를 만들어라.
이 확장 클래스를 원래 클래스의 서브 클래스 또한 래퍼(Wrapper) 클래스로 만들어라.
🪄 동기
- 때로는 소스 코드를 수정할 수 없는 경우가 있다.
- 한 두개의 메서드가 필요하다면
Introduce Foreign Method를 사용할 수 있다. - 객체지향 기술인 서브클래싱과 래핑(Wrapping)은 이런 작업을 하는 명확한 방법이다.
- 서브 클래스 또는 래퍼 클래스를
Local Extension이라 부른다. Local Extension을 사용함으로써 메서드와 데이터가 잘 정의된 단위로 묶어야 한다는 원칙을 지키는 것이다.- 서브 클래스와 래퍼중 하나를 선택해야 할 때 보통 할 일이 적은 서브클래스를 선택한다.
- 서브 클래스를 만들 때 가장 큰 장애물은 객체를 생성할 때에 적용해야 한다는 것이다.
- 서브 클래싱은 그 서브클래스의 새로운 객체를 만들도록 한다.
- 다른 객체가 예전 객체에 접근하고 있다면 원래의 데이터를 가진 두 개의 객체를 가지고 있는 것이 된다.
- 원래 객체가 불변성(immutable)이라면 문제가 없다. 안전하게 복사할 수 있다.
- 원래 객체가 가변성(mutable)이라면 문제가 있는데, 왜냐하면 한 객체에서의 변화가 다른 객체를 변경하지 않기 때문이다.
- 이런 경우 래퍼를 사용해야 한다.
- 래퍼를 사용하는 것은
Local Extension을 통해 변경된 사항이 원래 객체에 영향을 미칠 수 있게 하고 원래 객체를 통해 변경된 사항은 래퍼에 영향을 미치게 한다.
선택 기준
- 상속 사용:
- 기존 클래스가 final이 아닐 때
- 확장이 기존 클래스와 매우 밀접할 때
- 대부분의 기존 메서드를 그대로 사용할 때
- 래퍼 사용
- 기존 클래스가 final일 때
- 더 유연한 확장이 필요할 때
- 일부 메서드만 선택적으로 노출하고 싶을 때
// 수정할 수 없는 서버 클래스
public final class Date {
// Java의 레거시 Date 클래스라고 가정
}
// 방법 1: 상속을 통한 확장
public class ExtendedDate extends Date {
public ExtendedDate(Date date) {
super(date.getTime());
}
public ExtendedDate nextDay() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(this);
calendar.add(Calendar.DATE, 1);
return new ExtendedDate(calendar.getTime());
}
public ExtendedDate previousDay() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(this);
calendar.add(Calendar.DATE, -1);
return new ExtendedDate(calendar.getTime());
}
public boolean isWeekend() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(this);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
}
}
// 방법 2: 위임을 통한 확장 (Wrapper 클래스)
public class DateWrapper {
private final Date originalDate;
public DateWrapper(Date date) {
this.originalDate = date;
}
// 원본 메서드 위임
public long getTime() {
return originalDate.getTime();
}
// 확장 메서드들
public DateWrapper nextDay() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(originalDate);
calendar.add(Calendar.DATE, 1);
return new DateWrapper(calendar.getTime());
}
public DateWrapper previousDay() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(originalDate);
calendar.add(Calendar.DATE, -1);
return new DateWrapper(calendar.getTime());
}
public boolean isWeekend() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(originalDate);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
}
// 원본 객체 반환 메서드
public Date getOriginalDate() {
return originalDate;
}
}
// 사용 예시
public class Client {
public void useExtendedDate() {
// 상속을 통한 확장 사용
ExtendedDate date = new ExtendedDate(new Date());
ExtendedDate tomorrow = date.nextDay();
if (tomorrow.isWeekend()) {
// 주말 처리
}
// 래퍼 클래스 사용
DateWrapper wrapper = new DateWrapper(new Date());
DateWrapper nextDay = wrapper.nextDay();
if (nextDay.isWeekend()) {
// 주말 처리
}
}
}
// 현대적인 방식: 레코드와 static 메서드 활용
public record ModernDateWrapper(LocalDate date) {
public static ModernDateWrapper of(LocalDate date) {
return new ModernDateWrapper(date);
}
public ModernDateWrapper nextDay() {
return new ModernDateWrapper(date.plusDays(1));
}
public ModernDateWrapper previousDay() {
return new ModernDateWrapper(date.minusDays(1));
}
public boolean isWeekend() {
return date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY;
}
}✅ 절차
- 원래 클래스의 서브클래스나 래퍼 클래스로 확장 클래스를 만든다.
변환 생성자(converting constructor)를 확장 클래스에 추가한다.- 생성자는 원래 클래스를 인자로 받는다.
- 서브 클래스 버전은 적당한 수퍼클래스 생성자를 호출한다.
- 래퍼를 사용할 경우에는 대리객체에 대한 필드를 파라미터로 설정한다.
- 새로운 기능을 확장 클래스에 추가한다.
- 필요한 곳에서 원래 클래스를 확장 클래스로 대체한다.
- 이 클래스에 대해 정의된 외래 메소드를 모두 확장 클래스로 옮기라.