Clean Code

Java 리팩토링 - 상속을 위임으로 바꾸기 (Replace Inherit with Delegate)

1. 알고자 하는 것

객체지향에서 상속은 기존 기능을 재사용할 수 있는 강력한 기능을 제공한다.

하지만 상속에도 단점이 존재하며, 상속이 적절치 않은 경우가 존재한다.

이럴 때 상속을 위임(Delegate) 으로 바꾸어 해결할 수 있다.

 

  • 상속을 위임으로 바꾸기 (Replace Inherit with Delegate)

 

 

 

 

 

2. 알게 된 것

  • 상속은 상위 클래스의 기능을 하위 클래스가 재사용 할 수 있는 좋은 방법이다.
  • 상속으로 불필요한 코드의 중복을 막을 수 있고, 공통 기능의 변경 시 변경점이 줄어든다.
  • 하지만 이러한 상속이 적절하지 않은 경우가 존재한다.
  • 상속 받는 하위 클래스의 경우 상위 클래스의 모든 기능을 지원해야 한다.
    • 자바의 Stack 클래스의 경우, Vector<E>를 상속 받는다.
    • 상위 클래스인 Vector의 add() 메서드를 지원해야 하므로 특정 위치에 element를 삽입 할 수 있는 add() 메서드가 구현되어 있다.
    • 상위 클래스의 모든 기능을 지원해야 하는 상속의 특성으로 인해 Stack의 LIFO 구조가 깨져버렸다.
    • * 이로 인해 Stack 클래스 내 주석을 보면 "더 완전하고 일관성 있는 LIFO 연산을 지원하는 Stack을 사용하고자 한다면 Deque 클래스 사용을 권장하고 있다. (Deque는 Queue<E>를 상속받음)
A more complete and consistent set of LIFO stack operations is
* provided by the {@link Deque} interface and its implementations, which
* should be used in preference to this class.

 

  • 또한 상속의 경우 상위 클래스의 기능이 변경되면 모든 하위 클래스가 영향을 받는다.
  • 즉, 상속은 상위 클래스와 하위 클래스의 결합도가 높음을 의미한다.
  • 상위 클래스의 모든 기능을 지원할 필요가 없거나, 상위 클래스의 기능이 자주 변경될 여지가 있다고 판단된다면 상속이 아닌 클래스를 인스턴스 변수로 가지는 위임(Delegate)을 통해 상속과 마찬가지로 코드를 재사용 할 수 있다.

 

  • 카테고리의 정보를 나타내는 CategoryItem과 이를 상속받은 Scroll이 있다.
public class CategoryItem {

    private Integer id;

    private String title;

    private List<String> tags;

    public CategoryItem(Integer id, String title, List<String> tags) {
        this.id = id;
        this.title = title;
        this.tags = tags;
    }

    public Integer getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean hasTag(String tag) {
        return this.tags.contains(tag);
    }
}

public class Scroll extends CategoryItem {

    private LocalDate dateLastCleaned;

    public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
        super(id, title, tags);
        this.dateLastCleaned = dateLastCleaned;
    }

    public long daysSinceLastCleaning(LocalDate targetDate) {
        return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}

 

  • 이를 상속 구조가 아닌 위임 구조로 변경하고자 한다면, Scroll의 상속을 제거하고 Scroll 클래스에 인스턴스 변수로 CategoryItem을 두면 된다.
public class Scroll {

    public static void main(String[] args) {
        Scroll scroll = new Scroll(1, "test", List.of("test1", "test2"), LocalDate.now());
        Integer id = scroll.getCategoryItem().getId(); // 기존 상위 클래스의 필드 재사용 가능
    }

    private LocalDate dateLastCleaned;
    private CategoryItem categoryItem; // 필드 선언

    public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
        this.dateLastCleaned = dateLastCleaned;
        this.categoryItem = new CategoryItem(id, title, tags);
    }

    public long daysSinceLastCleaning(LocalDate targetDate) {
        return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
    }

    public LocalDate getDateLastCleaned() {
        return dateLastCleaned;
    }

    public CategoryItem getCategoryItem() {
        return categoryItem;
    }
}

 

  • 상위 클래스의 상속을 제거하고 필드로 두어, 불필요한 기능 구현이 더 이상 필요하지 않아졌다. (구현 강제 X)
  • 만약 CategoryItem의 id를 조회하는 기능을 getId() 메서드로 감싸서 처리한다면, 추후 CategoryItem의 id 필드가 변경되었을 때 단순히 하단 주석의 categoryItem.getId() 만을 변경해주면 된다. (느슨한 결합도 유지)
public class Scroll {

    private LocalDate dateLastCleaned;
    private CategoryItem categoryItem; // 필드 선언

    public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
        this.dateLastCleaned = dateLastCleaned;
        this.categoryItem = new CategoryItem(id, title, tags);
    }

    public long daysSinceLastCleaning(LocalDate targetDate) {
        return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
    }

    public LocalDate getDateLastCleaned() {
        return dateLastCleaned;
    }

    public Integer getId() {
        return this.categoryItem.getId(); // 해당 코드만 수정하면 됨
    }
}

 

  • + CategoryItem을 인터페이스로 만든다면 Scroll 클래스를 생성할 때 조건에 맞는 CategoryItem을 유연하게 주입할 수 있다.
  • + 이를 통해 직접적인 상위 클래스와의 결합도를 더욱 낮출 수 있고, Scroll 클래스는 CategoryItem의 내부 구현은 신경쓰지 않고 단순히 위임자로서 CategoryItem의 메서드를 호출만 하면 된다.

 

 

3. 정리

  • 객체지향에서 상속은 기존 기능을 재사용할 수 있는 강력한 기능을 제공하지만, 상위 클래스의 기능 구현 강제 / 상위 클래스의 변경 시 영향을 받는 강한 결합도가 발생할 수 있다.
  • 하위 클래스가 상위 클래스의 기능을 모두 구현하기에 적합하지 않거나, 상위 클래스의 변경이 잦은 경우 위임(Delegate)을 사용할 수 있다.
  • 상속을 하지 않고 상위 클래스를 인스턴스 변수로 두어 메서드 호출을 위임한다.
  • 이를 통해 상위 클래스의 기능을 모두 구현 할 필요가 없고, 상위 클래스의 변경 시 상위 클래스의 메서드를 호출하는 코드만 변경할 수 있어 느슨한 결합도를 유지할 수 있다.
  • 이 때 상위 클래스를 인터페이스로 사용한다면, 클래스 생성 시 유연하게 클래스를 주입해 상위 클래스의 내부 구현에 상관 없이 유연하게 로직을 수행할 수 있어 결합도를 더욱 낮출 수 있다.

 


Reference

강의 - 코딩으로 학습하는 리팩토링 (백기선 강사님)