interface IPerson {
wakeUp(): void;
bathroomActivity(): void;
}
class Person implements IPerson {
private bathroom: IBathroom;
constructor(bathroom: IBathroom) {
this.bathroom = bathroom;
}
wakeUp() {
console.log('아침에 일어난다.');
}
bathroomActivity() {
this.bathroom.goToBathroom();
this.bathroom.washFace();
this.bathroom.brushTeeth();
this.bathroom.washHair();
this.bathroom.dryOff();
this.bathroom.leaveBathroom();
}
}
interface IBathroom {
goToBathroom(): void;
washFace(): void;
brushTeeth(): void;
washHair(): void;
dryOff(): void;
leaveBathroom(): void;
}
class Bathroom implements IBathroom {
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
}
class Morning {
private person: IPerson;
constructor(person: IPerson) {
this.person = person;
}
morningActivity(): void {
this.person.wakeUp();
this.person.bathroomActivity();
}
}
const morning = new Morning(new Person(new Bathroom()));
morning.morningActivity();
[객체지향] SOLID 예제(3) - 개방-폐쇄의 원칙(OCP)
SRP를 이용해 책임을 분리하고 DIP와 OCP를 이용해 결합도를 낮추고 확장성을 높였다.
추상화를 통해 코드의 수정을 최소화 하고 기능을 확장 할 수 있게 되었다.
만약 위의 코드에서 IPerson 또는 IBathroom을 제대로 구현하지 못하는 경우는 어떻게 되는가?
만약 화장실에서 나오는 메소드가 없는 경우는 어떻게 되는가?
위와 같은 문제점을 예방하기 위한 것이 LSP(리스코프 치환의 원칙)이다.
리스코프 치환의 원칙(LSP)
리스코프 치환의 원칙은 상속 관계로 연결한 두 클래스가 서브타이핑 관계를 만족시키기 위한 조건이다.
여기서 서브타이핑이란 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다.
A형의 객체 a와 B형의 객체 b가 있고 A에 의해 정의된 프로그램에서 A가 B로 치환될 때, 프로그램의 동작이 변하지 않으면 B는 A의 서브타입이다.
중요한 것은 서브타입을 만족하기 위해서는 그것의 기반 타입에 대해 대체 가능해야 한다는 것이다.
클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브 클래스를 사용할 수 있어야 함을 강조하고 있는 원칙이다.
또한, 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조한다.
예를 들어 위의 예제에서 Person은 Bathroom 클래스에 화장실에서 수행하는 다양한 활동이 있음을 가정하고 있다.
만약, Bathroom 클래스에 이러한 가정을 무시하는 식사하기와 같은 메소드가 추가되는 경우는 LSP의 위반이다.
서브 클래싱과 서브 타이핑
위에서 LSP는 서브타이핑 관계에 관한 조건이라는 것을 살펴봤다.
상속에는 두 가지 목적이 있으며 목적에 따라 서브 클래싱과 서브 타이핑으로 구분한다.
서브 클래싱은 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 의미한다.
서브 타이핑은 타입 계층을 구성하기 위해 상속을 사용하는 경우를 의미한다.
서브 클래싱과 달리 서브 타이핑은 부모 클래스와 자식 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
서브 클래싱은 다른 용어로 구현 상속 또는 클래스 상속이라 부르는 경우도 있으며, 서브 타이핑은 인터페이스 상속이라 부르는 경우도 있다.
LSP는 서브 타이핑에 관한 내용이다.
LSP에서 상속 관계가 올바른지 판단하는 것은 클라이언트이며 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때만 올바르다.
현재 예제에서는 인터페이스를 활용하여 각 클래스가 해당 인터페이스를 구현하도록 구성되어 있다.
이러한 경우 해당 인터페이스를 사용하는 클래스들은 모두 인터페이스를 만족하게 될 것이고 어떠한 자식 클래스 (클래스)라도 부모 클래스(인터페이스)를 대체할 수 있을 것이다.
클라이언트가 기대하는 모든 기능을 자식 클래스가 수행할 수 있다.
따라서 현재 예제는 LSP를 만족한다고 할 수 있다.
위의 예제를 약간 수정하여 아래와 같이 만들어보자.
interface IPerson {
wakeUp(): void;
bathroomActivity(): void;
}
class Person implements IPerson {
private bathroom: BasicBathroom;
constructor(bathroom: BasicBathroom) {
this.bathroom = bathroom;
}
wakeUp() {
console.log('아침에 일어난다.');
}
bathroomActivity() {
this.bathroom.goToBathroom();
this.bathroom.washFace();
this.bathroom.brushTeeth();
this.bathroom.washHair();
this.bathroom.dryOff();
this.bathroom.leaveBathroom();
}
}
class BasicBathroom {
goToBathroom() {
console.log('평범하게 화장실로 간다.');
}
washFace() {
console.log('평범하게 얼굴을 씻는다.');
}
brushTeeth() {
console.log('평범하게 양치를 한다.');
}
washHair() {
console.log('평범하게 머리를 감는다.');
}
dryOff() {
console.log('평범하게 물기를 닦는다.');
}
leaveBathroom() {
console.log('평범하게 화장실을 나온다.');
}
}
class LuxuryBathroom extends BasicBathroom {
goToBathroom() {
console.log('고급스럽게 화장실로 간다.');
}
washFace() {
console.log('고급스럽게 얼굴을 씻는다.');
}
brushTeeth() {
console.log('고급스럽게 양치를 한다.');
}
washHair() {
console.log('고급스럽게 머리를 감는다.');
}
dryOff() {
console.log('고급스럽게 물기를 닦는다.');
}
leaveBathroom() {
console.log('고급스럽게 화장실을 나온다.');
}
}
class Morning {
private person: IPerson;
constructor(person: IPerson) {
this.person = person;
}
morningActivity(): void {
this.person.wakeUp();
this.person.bathroomActivity();
}
}
const morning = new Morning(new Person(new LuxuryBathroom()));
morning.morningActivity();
기존 예제와 달리 Bathroom을 인터페이스에서 클래스로 수정했다.
고급 화장실이 추가되었으며 이는 기본 화장실을 상속했다.
Person 클래스는 BasicBathroom에 대한 내용만 알고 있지만 LuxuryBathroom이 BasicBathroom을 완벽히 대체할 수 있는 상황이므로 LSP를 만족했다고 볼 수 있다.
만약, 저렴한 화장실이 추가된다면 마찬가지로 BasicBathroom을 상속한 클래스를 사용하면 된다.
결론
리스코프 치환의 원칙은 OCP, DIP를 만족하기 위한 조건과도 같다.
상속 관계로 연결한 두 클래스가 서브 타이핑 관계를 만족시키기 위한 조건이다.
서브 타입은 기반 타입에 대해 대체 가능해야 하며, 대체 가능성을 결정하는 것은 클라이언트다.
클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다.
[객체지향] SOLID 예제(5) - 인터페이스 분리의 원칙(ISP)
[참고자료]
위키북스, 오브젝트, 2019
'개발 > 개념' 카테고리의 다른 글
Vite는 왜 빠를까? (0) | 2023.10.21 |
---|---|
[객체지향] SOLID 예제(5) - 인터페이스 분리의 원칙(ISP) (0) | 2023.10.03 |
[객체지향] SOLID 예제(3) - 개방-폐쇄의 원칙(OCP) (0) | 2023.09.26 |
[객체지향] SOLID 예제(2) - 의존성 역전의 원칙(DIP) (0) | 2023.09.25 |
[객체지향] SOLID 예제(1) - 단일 책임의 원칙(SRP) (0) | 2023.09.25 |