Skip to content

리팩토링 방법 - 객체간의 기능 이동 #16

@hongxeob

Description

@hongxeob

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

🪄 동기

  1. 메서드를 옮기는 것은 리팩토링에서 가장 중요하고 기본이 되는 것이다.
  2. 클래스가 너무 많은 동작을 가지고 있거나, 다른 클래스와 공동으로 일하는 부분이 많아서 단단히 결합되어 있을 때 메서드를 옮긴다.
  3. 메서드를 옮김으로써 클래스를 더 간단하게 할 수 있고 클래스는 맡고 있는 책임에 대해 더욱 명확한 구현을 가질 수 있게 된다.
  4. 옮길만한 메서드를 발견하면, 이 메서드를 호출하는 메서드, 이 메서드가 호출하는 메서드, 그리고 상속 계층에서 이 메서드를 재정의하고 있는 메서드를 살펴본다.
  5. 그리고 옮기려고 하는 메서드와 상호작용을 더 많이 하고 있는 것처럼 보이는 클래스를 기초로 하여 계속 진행할지를 평가한다.

2. Move Field

필드가 자신이 정의된 클래스보다 다른 클래스의 기능을 더 많이 사용하고 있다면 타겟 클래스에 새로운 필드를 만들고 기존 필드를 사용하는 모든 부분을 변경하라.

🪄 동기

  1. 어떤 필드가 자신이 속한 클래스보다 다른 클래스의 메서드에서 더 많이 사용되고 있는 것을 보면 그 필드를 옮기는 것을 고려한다.
  2. 그러는 한편 다른 클래스가 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

두개의 클래스가 해야 할 일을 하나의 클래스가 하고 있는 경우, 새로운 클래스를 만들어서 관련 있는 필드와 메서드를 예전 클래스에서 새로운 클래스로 옮겨라.

🪄 동기

  1. 클래스는 분명하게 추상화되어야 하고, 몇 가지 명확한 책임을 가져야 한다는 말 또는 이와 비슷한 지침을 들었을 것이다.
  2. 실제로 클래스는 점점 커진다. 어떤 동작을 추가할 대도 있고 약간의 데이터를 추가할 때도 있다.
  3. 우리는 별도의 클래스로 만들만한 가치가 없다고 느끼는 책임을 기존 클래스에 추가한다.
  4. 클래스는 많은 메서드와 데이터를 가지고 있고 너무 커서 쉽게 이해할 수도 없다.
  5. 이제 우리는 그 클래스를 분리할 방법을 생각하고 클래스를 분리해야 한다.
  6. 데이터의 부분 집합과 메서드의 부분 집합이 같이 몰려다니는 것은 별도의 클래스로 분리할 수 있다는 좋은 신호이다.
  7. 보통 같이 변하거나 특별히 서로에게 의존적인 데이터의 부분 집합 또한 별도의 클래스로 분리할 수 있다는 좋은 신호이다.
  8. 만약 일부 데이터나 메서드를 제거한다면 다른 필드나 메서드가 의미없는 것이 될지를 자신에게 물어보는 것은 편리한 테스트 방법이다.
  9. 개발의 후반부에 종종 나타나는 신호중의 하나는 클래스가 서브타입이 되는 방법이다.
  10. 서브타이핑이 단지 몇몇 기능에만 영향에 미친다는 것을 알게 되거나 또는 어떤 부분은 이런 식으로 서브타입이 되어야 하고 다른 부분은 또 다른 방법으로 서브타입이 되어야 한다는 것을 알게 될 것이다
// 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

클래스가 하는 일이 많지 않은 경우에는 그 클래스에 있는 모든 변수와 메서드를 다른 클래스로 옮기고 그 클래스를 제거하라.

🪄 동기

  1. Inline ClassExtract Class의 반대이다.
  2. 클래스가 더 이상 제 몫을 하지 못하고 더 이상 존재할 필요가 없다면 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 MethodMove Field를 사용하여, 소스 클래스에 있는 모든 변수와 메서드를 흡수하는 클래스로 옮긴다.
  • 짧고 간단한 장례식을 거행한다.

5. Hide Delegate

클라이언트가 객체의 위임 클래스를 직접 호출하고 있는 경우 서버에 메서드를 만들어 대리 객체(delegate)를 숨겨라.

🪄 동기

  1. 캡슐화는 객체에서 가장 중요한 개념 가운데 하나이다.
    • 캡슐화는 객체가 시스템의 다른 부분에 대해 적게 알아도 된다는 것을 의미한다.
    • 캡슐화가 되어 있는 경우에는 어떤 것이 변경되었을 때 시스템의 다른 부분이 영향을 덜 받으므로 결과적으로 변경을 좀 더 쉽게 할 수 있게 한다.
  2. 자바는 필드가 public으로 선언되는 것을 허용하지만, 객체를 다루는 사람이라면 필드는 숨겨져야 한다는 것을 알고 있다.
  3. 여러분은 점점 세련되어 질수록 캡슐화 할 수 있다는 것이 더 많아진다는 것을 알게 된다.
  4. 클라이언트가 서버 객체의 필드에 들어있는 객체에 정의된 메서드를 호출한다면, 클라이언트는 대리객체(delegate)에 대해서 알아야 한다.
  5. 이와 같은 경우에 서버 객체에 간단한 위임 메서드를 두어 위임을 숨김으로서 이런 종속성을 제거할 수 있다.
  6. 서버의 일부 또는 모든 클라이언트에 대해서 Extract Class를 사용할 가치가 있다는 것을 발견할지도 모른다.
  7. 만약 모든 클라이언트에게 실제로 일을 처리하는 부분을 숨기고 있다면 서버의 인터페이스에서 위임과 관련된 모든 부분을 제거할 수 있다.
// 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)를 직접 호출하도록 하라.

🪄 동기

  1. Hide Delegate를 사용하는 동기를 이야기할 때 대리객체 사용을 캡술화 하는 것의 장점에 대해서 이야기 했다.
    • 그러나 여기에는 그만한 대가를 치러야 한다.
  2. 클라이언트 대리객체의 새로운 메서드를 사용하려 할 때 마다 서버 클래스는 간단한 위임 메서드를 추가해야하는 것이다.
    • 새로운 메서드를 추가하려면 추가 비용이 들게 된다.
  3. 서버 클래스는 단지 미들맨(Middle Man)에 지나지 않게 되는데 아마도 이때가 클라이언트로 하여금 대리객체를 직접 호출하도록 해야할 때일 것이다.
  4. 어느 정도를 숨기는 것이 적절한지 판단하는 것은 어렵다.
  5. 다행이도 Hide Delegate와 Remove Middle Man에서는 이것이 별로 중요하지 않다.
  6. 시간이 지나고 시스템이 변할수록 얼마나 숨겨야 하는지에 대한 원칙 또한 변경된다.
// 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

사용하고 있는 서버 클래스에 부가적인 메서드가 필요하지만 클래스를 수정할 수 없는 경우에는 첫 번째 인자로 서버 클래스의 인스턴스를 받는 메서드를 클라이언트에 만들어라.

🪄 동기

  1. 모든 서비스를 제공하는 정말로 멋진 클래스를 사용하고 있다.
  2. 그러나 꼭 필요하지만 그 클래스가 제공하지 않는 서비스가 하나 있다.
  3. 소스 코드를 변경할 수 없다면 부족한 메서드를 클라이언트 쪽에 만들어야한다.
  4. 클라이언트 클래스에서 필요한 메서드를 단지 한 번만 사용한다면 추가 코딩은 큰 문제가 아니고, 이런 경우에는 아마도 서버 클래스에 메서드를 추가할 필요가 없을 것이다.
  5. 새로 만드는 메서드를 외래 메서드(Foreign method)로 만들어서 이 메서드가 실제로는 서버 클래스에 있어야 하는 메서드라는 것을 명확하게 나타낼 수 있다.
  6. 만약 서버 클래스의 외래 메서드를 많이 만들어야 한다는 것을 깨닫게 되거나 많은 클래스가 동일한 외래 메서드를 필요로 한다는 것을 알게 된다면 Introduce Local Extension 대신 사용해야 한다.
  7. 외래 메서드는 임시 방편이라는 것을 잊지마라!
  8. 만약 할 수 있다면, 외래 메서드를 그들이 원래 있어야 하는 위치로 옮기는 것을 시도해 봐라.
  9. 코드 소유권이 문제가 된다면 외래 메서드를 서버 클래스의 소유자에게 보내고 그 소유자에게 그 메서드를 구현해 달라고 요청하라.
// 수정할 수 없는 서버 클래스 (예: 라이브러리 클래스)
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) 클래스로 만들어라.

🪄 동기

  1. 때로는 소스 코드를 수정할 수 없는 경우가 있다.
  2. 한 두개의 메서드가 필요하다면 Introduce Foreign Method를 사용할 수 있다.
  3. 객체지향 기술인 서브클래싱과 래핑(Wrapping)은 이런 작업을 하는 명확한 방법이다.
  4. 서브 클래스 또는 래퍼 클래스를 Local Extension이라 부른다.
  5. Local Extension을 사용함으로써 메서드와 데이터가 잘 정의된 단위로 묶어야 한다는 원칙을 지키는 것이다.
  6. 서브 클래스와 래퍼중 하나를 선택해야 할 때 보통 할 일이 적은 서브클래스를 선택한다.
  7. 서브 클래스를 만들 때 가장 큰 장애물은 객체를 생성할 때에 적용해야 한다는 것이다.
  8. 서브 클래싱은 그 서브클래스의 새로운 객체를 만들도록 한다.
    • 다른 객체가 예전 객체에 접근하고 있다면 원래의 데이터를 가진 두 개의 객체를 가지고 있는 것이 된다.
  9. 원래 객체가 불변성(immutable)이라면 문제가 없다. 안전하게 복사할 수 있다.
  10. 원래 객체가 가변성(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)를 확장 클래스에 추가한다.
  • 생성자는 원래 클래스를 인자로 받는다.
    • 서브 클래스 버전은 적당한 수퍼클래스 생성자를 호출한다.
    • 래퍼를 사용할 경우에는 대리객체에 대한 필드를 파라미터로 설정한다.
  • 새로운 기능을 확장 클래스에 추가한다.
  • 필요한 곳에서 원래 클래스를 확장 클래스로 대체한다.
  • 이 클래스에 대해 정의된 외래 메소드를 모두 확장 클래스로 옮기라.

Metadata

Metadata

Assignees

Labels

ETCThis will not be worked on

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions