PostCover

SOLID를 가장 간단명료하게 풀어보자 - LSP편

SOLID 원칙 중 리스코프 치환 원칙(LSP)을 쉽게 풀어보기

Liskov Subsitution Principle

파생 클래스는 기본 클래스로 대체 가능해야 합니다. - Robert C.Martin

개인적으로 이 원칙은 명확하면서도, 생각하는 사람의 관점에 따라서 달라진다고 생각합니다.

이를 설명하기 위한 가장 효과적인 방법은 duck typing이라고 생각합니다.

관점에 다를 수도 있지만, 오리처럼 생겼고, 오리처럼 헤엄치고, 오리처럼 꽥꽥거린다면 아마도 오리일 것입니다. 물론 리스코프 치환 원칙을 어기지 않은 한에서 말입니다.

이 원칙에 따라 모든 객체를 기본 유형으로 치환할 수 있어야 합니다. 무슨 말인지 모르시겠죠?

오리에 대한 추상 클래스가 존재할 때, 청둥오리, 대리석 오리, 고무 오리를 상속받아 추가할 수 있습니다. 그런데 고무 오리는 날 수 없죠. 이런 경우 기본 클래스의 구현이 깨질 것입니다.

오리는 기본적으로 꽥꽥 하면서 울죠. 이를 기본 클래스의 메서드로 만들고 각 오리마다 울음 소리를 적용해볼게요.

public abstract class Duck {
    public abstract void quack();
}

public class MallardDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }
}

public class MarbledDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }
}

public class RubberDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Squeak!");
    }
}

그런데 오리는 수영도 할 수 있겠죠?

public abstract class Duck {
    public abstract void quack();
    public abstract void swim();
}

public class MallardDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }
}

public class MarbledDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }
}

public class RubberDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Squeak!");
    }

    @Override
    public void swim() {
        System.out.println("Floating!");
    }
}

좋아요. 수영까지는 완벽했습니다.

그런데 오리는 날 수도 있죠?

public abstract class Duck {
    public abstract void quack();
    public abstract void swim();
    public abstract void fly();
}

public class MallardDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }

    @Override
    public void fly() {
        System.out.println("Flying!");
    }
}

public class MarbledDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }

    @Override
    public void fly() {
        System.out.println("Flying!");
    }
}

public class RubberDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Squeak!");
    }

    @Override
    public void swim() {
        System.out.println("Floating!");
    }

    @Override
    public void fly() {
        throw new UnsupportedOperationException("Rubber ducks can't fly!");
    }
}

이때 문제가 발생했습니다. 고무로된 오리는 날 수 없다는 것을 깨달았습니다. 리스코프 치환 원칙을 사용하면 하위 유형은 기본 유형으로 대체 가능해야 합니다.

그런데 고무 오리 클래스가 기본 클래스로 대체할 수 있을까요? 절대로 안될겁니다. 고무로된 오리는 날 수 없으니까요.

그렇다면 이 코드를 어떻게 수정할 수 있을까요?

오리의 추상화를 제거하는 방법으로 수정할 수 있을겁니다. 각 행위에 대한 인터페이스를 만들고, 각 오리가 필요한 행위만 implement를 통해 구현할 수 있으면 될겁니다. 오리의 기본 클래스는 더 이상 없을 테지만, 그래도 원리는 지킬 수 있죠.

public interface IFly {
    void fly();
}

public interface ISwim {
    void swim();
}

public interface IQuack {
    void quack();
}

public class MallardDuck implements IFly, ISwim, IQuack {

    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }

    @Override
    public void fly() {
        System.out.println("Flying!");
    }
}

public class MarbledDuck implements IFly, ISwim, IQuack {

    @Override
    public void quack() {
        System.out.println("Quack!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming!");
    }

    @Override
    public void fly() {
        System.out.println("Flying!");
    }
}

public class RubberDuck implements ISwim, IQuack {

    @Override
    public void quack() {
        System.out.println("Squeak!");
    }

    @Override
    public void swim() {
        System.out.println("Floating!");
    }
}