상속과 합성

bp.chys·2020년 4월 9일
0

OOP & Design Pattern

목록 보기
7/17

상속

  • DRY(Don't Repeat Yourself) 원칙
  • 상속을 사용하면 중복되는 내용을 상당수 제거할 수 있다.
  • 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 구현 방법에 대한 정확한 지식을 가져야 한다.
  • 이는 부모 객체의 캡슐화를 약화시키는 문제를 가져온다.
  • 결과적으로 상속은 부모 클래스와 자식 클래스 간의 결합도를 높이고 이 높은 결합도는 코드 수정을 어렵게 만든다.
  • 자식 클래스는 부모 클래스 구현에 의존하고 있기 때문에 부모 클래스의 변경에 영향을 많이 받는다. 이를 취약한 기반 클래스 문제라고 부른다.
  • 이 문제를 해결하는 방법은 자식클래스와 부모클래스가 동시에 추상 클래스에 의존하도록 만드는 것이다.

구현 상속과 인터페이스 상속

  • 내부구현을 공유하는 목적만(코드 재사용)으로 상속을 쓰지말아야 한다.
  • 이는 변경에 취약한 코드를 낳게 될 확률이 높다.
  • 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용해야 한다. 즉, 부모 클래스는 추상 클래스가 되어야한다는 말이다.
    👉 인터페이스만 공유할 목적이라면 자바의 인터페이스를 사용하고, 인터페이스와 함께 공통된 내부 구현을 공유하고 싶다면 추상 클래스를 사용한다.

상속을 남용할 경우

  1. 상위 클래스 변경의 어려움
    • 상속 계층을 따라 상위 클래스의 변경이 하위 클래스에 영향을 주기 때문에, 최악의 경우 상위 클래스의 변화가 모든 하위 클래스에 영향을 줄 수 있다.
    • 클래스들을 한 개의 거대한 단일 구조처럼 만들어 주는 결과를 초래한다.
    • 이런 이유 때문에, 클래스 계층도가 커질수록, 상위 클래스를 변경하는 것은 점점 어려워진다.
  2. 상속을 남용하게 되면 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는데, 이를 클래스 폭발 또는 조합의 폭발문제라고 부른다.

합성 (Composition)

  • 상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은 방법이다.
  • 상속 관계는 is-a 관계라고 부르고 합성관계는 has-a 관계라고 부른다.
  • 합성은 구현에 의존하지 않는다. 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에만 의존한다. (캡슐화가 지켜진다.)
  • 따라서 객체 내부가 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있다.
  • 합성은 컴파일 타임 관계를 런타임 관계로 변경함으로써 문제를 해결한다.
  • 따라서 합성을 사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며 실행 시점에 정책들의 관계를 유연하게 변경할 수 있게 된다. 이것이 합성의 본질이다.

예제 코드

상속보다 재사용을 사용했을 때 어떤 장점들이 있는지 예제 코드를 통해서 알아보자.
인터페이스에 대고 프로그래밍하기 부터 시작해보겠다.

step01. 인터페이스에 대고 프로그래밍하기

  • 음식을 먹는 책임 또는 기능을 제공하는 Eatable 인터페이스와 이를 구현하는 FastFood 클래스를 만들어보자.
  • 패스트푸드 추상 클래스는 이를 상속받는 구체 클래스가 drink와 empty 그리고 isDoublePatty를 직접 구현하도록 추상메서드로 남겨두었다.
public interface Eatable {
    void buy();
    void eat();
    void clean();
}
public abstract class FastFood implements Eatable {
    private String name;

    public FastFood(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println(name);
    }

    @Override
    public void buy() {
        System.out.println("음식을 구매했습니다.");
    }

    @Override
    public void eat() {
        System.out.println("음식을 먹습니다.");
    }

    @Override
    public void clean() {
        System.out.println("쓰레기를 정리합니다.");
    }

    abstract public boolean isDoublePatty();

    abstract public void drink();

    abstract public void empty();
}

Step02. 조합만큼 늘어나는 클래스 개수

  • 치즈버거 + 사이다 세트를 만들어보자.
public class CheeseBurgerSprite extends FastFood {
    public CheeseBurgerSprite(String name) {
        super(name);
    }

    @Override
    public boolean isDoublePatty() {
        return false;
    }

    @Override
    public void drink() {
        System.out.println("사이다를 마십니다.");
    }

    @Override
    public void empty() {
        System.out.println("사이다를 다 마셨습니다.");
    }
}
  • 치즈버거 + 콜라 클래스도 만들어보자.
public class CheeseBurgerCola extends FastFood {
    public CheeseBurgerCola(String name) {
        super(name);
    }

    @Override
    public boolean isDoublePatty() {
        return false;
    }

    @Override
    public void drink() {
        System.out.println("콜라를 마십니다.");
    }

    @Override
    public void empty() {
        System.out.println("콜라를 다 마셨습니다.");
    }
}
  • 이처럼 버거나 음료 중 하나만 변해도 클래스를 새로 만들어야한다.
  • 여기에 사이드메뉴나 소스 까지 추가된다면?
  • 경우의 수는 계속 늘어날 것이고 클래스 수도 그만큼 늘어나게 될 것이다. (조합의 폭발 문제!)

step03. 응집도 감소

  • FastFood 추상 클래스를 보면 버거의 패티가 2장인지, 음료를 마시는 행위, 음료를 다 비운 행위가 제공된다.
  • 패스트 푸드 자체는 버거만도 아니고 음료만도 아닌 것은 분명하다.
  • 패스트 푸드를 구현한 구체 클래스도 마찬가지다.
  • 단일 책임 원칙을 고수한다고 했을 때, 한 클래스가 둘 이상의 변경이유를 갖고 있기 때문에 응집도가 감소한다고 볼 수 있다.

step04. 런타임에 상위 클래스 교체 불가

  • sugar가 100(default)인 CheeseBurgerCola 를 상속받는 CheeseBurgerZeroCola를 만들었다가 sugar를 60으로 줄이고 싶어서 CheeseBurgerSprite로 변경하고자 한다.
  • 런타임 시점에 변경할 수 있을까?
public class Application {
    public static void main(String[] args) {
        CheeseBurgerCola fastFood = new CheeseBurgerCola("치즈버거 + 콜라");
        fastFood.printName();
        System.out.println(fastFood.getSugar());

        fastFood = new CheeseBurgerZeroCola("치즈버거 + 제로콜라");
        fastFood.printName();
        System.out.println(fastFood.getSugar());
        
        fastFood = new CheeseBurgerSprite("치즈버거 + 사이다");
        // 컴파일 에러 발생
        fastFood.printName();
        System.out.println(fastFood.getSugar());

    }
}
  • 아쉽게도 CheeseBurgerZeroCola는 CheeseBurgerCola를 상속 받고 있기 때문에 불가능하다. 이를 해결하기 위해서는
    CheeseBurgerZeroCola가 상속 받고있는 객체를 CheeseBurgerSprite로 변경하고 다시 컴파일을 시켜야 한다.

Step05. 조합을 통한 리팩토링

조합은 어렵지 않다. 조합의 대상이 되는 객체들을 분리해서 인스턴스 변수로 참조하는 것이 전부이다. 아래 Burger와 Beverage처럼 말이다.

public class FastFood implements Eatable {
    private Burger burger;
    private Beverage beverage;

    public FastFood(Burger burger, Beverage beverage) {
        this.burger = burger;
        this.beverage = beverage;
    }

    public void setBurger(Burger burger) {
        this.burger = burger;
    }

    public void setBeverage(Beverage targetBeverage) {
        this.beverage = targetBeverage;
    }

    public void printName() {
        System.out.println(burger.getName() + "+" + beverage.getName());
    }

    @Override
    public void buy() {
        System.out.println("음식을 구매했습니다.");
    }

    @Override
    public void eat() {
        System.out.println("음식을 먹습니다.");
    }

    @Override
    public void clean() {
        System.out.println("쓰레기를 정리합니다.");
    }
}
  • Beverage와 Burger 클래스를 추상클래스로 구현해보자.
// Beverage.java
public abstract class Beverage {
    private String name;
    private int sugar;

    public Beverage(String name, int sugar) {
        this.name = name;
        this.sugar = sugar;
    }

    public String getName() {
        return name;
    }

    public int getSugar() {
        return sugar;
    }

    abstract void drink();

    abstract void empty();
}
  • 버거 객체
// Burger.java
public abstract class Burger {
    private String name;

    public Burger(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract boolean isDoublePatty();

}
// CheeseBureger.java
public class CheeseBurger extends Burger{
    public CheeseBurger(String name) {
        super(name);
    }

    @Override
    boolean isDoublePatty() {
        return false;
    }
}
// BigMacBurger.java
public class BigMacBurger extends Burger {
    public BigMacBurger(String name) {
        super(name);
    }

    @Override
    boolean isDoublePatty() {
        return true;
    }
}
// Cola.java
public class Cola extends Beverage {
    public Cola(String name, int sugar) {
        super(name, sugar);
    }

    @Override
    public void drink() {
        System.out.println("코카 콜라를 마십니다.");
    }

    @Override
    public void empty() {
        System.out.println("코카 콜라를 다 마셨습니다.");
    }
}
//Sprite.java
public class Sprite extends Beverage {
    public Sprite(String name, int sugar) {
        super(name, sugar);
    }

    @Override
    public void drink() {
        System.out.println("사이다를 마십니다.");
    }

    @Override
    public void empty() {
        System.out.println("사이다를 다 마셨습니다.");
    }
}
// Application 실행
public class Application {
    public static void main(String[] args) {
        FastFood fastFood = new FastFood(new CheeseBurger("치즈버거"), new Cola("콜라", 100));
        fastFood.printName();
        System.out.println(fastFood.getBeverageSugar()); // 100

        fastFood.setBurger(new BigMacBurger("빅맥"));
        fastFood.setBeverage(new Sprite("사이다", 60));
        fastFood.printName();
        System.out.println(fastFood.getBeverageSugar()); // 60
    }
}

참고자료

  • NHN 기술세미나, 객체지향 입문 - 최범균
  • 오브젝트 - 조영호
profile
하루에 한걸음씩, 꾸준히

0개의 댓글