class Person {
private bathroom: Bathroom;
constructor(bathroom: Bathroom) {
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 Bathroom {
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
}
class Morning {
private person: Person;
constructor(person: Person) {
this.person = person;
}
morningActivity(): void {
this.person.wakeUp();
this.person.bathroomActivity();
}
}
const morning = new Morning(new Person(new Bathroom()));
morning.morningActivity();
[객체지향] SOLID 예제(2) - 의존성 역전의 원칙(DIP)
의존성 역전의 원칙 중 일부를 적용하여 두 모듈의 결합도를 낮출 수 있었다.
현재 Person은 어떤 종류의 Bathroom이라도 처리할 수 있다.
하지만, Bathroom이 변경되는 경우는 어떻게 하는가?
또는, Bathroom 이외의 다른 객체가 추가된다면 어떻게 해야 하는가?
또는, LuxuryBathroom 클래스가 추가되어 해당 객체에서 세수를 할 때 특별한 과정을 갖도록 한다면 어떻게 해야 하는가?
(아마 이 경우는 Bathroom 타입이 아니므로 생성자에 넣을 수 없을 것이다)
여전히 Person 객체에서 Bathroom 메서드를 호출하는 코드도 함께 변경되어야 할 것이다.
이를 해결하기 위해 사용하는 것이 추상화와 개방-폐쇄의 원칙이다.
개방-폐쇄의 원칙(OCP)
개방-폐쇄의 원칙이라는 이름을 들었을 때 서로 반대되는 것을 주장하고 있다.
개방과 폐쇄는 서로 상반되는 개념으로 처음 들었을 때 헷갈릴 수 있다.
무엇이 개방되고 무엇이 폐쇄된 것일까?
결론부터 얘기하면 "확장에는 개방되어 있고, 수정에는 폐쇄되어 있어야 한다"는 의미이다.
확장은 새로운 요구사항 또는 기존의 요구사항이 변경되는 경우 이 변경에 맞게 새로운 기능을 확장할 수 있다는 것을 의미하며, 수정은 기존의 코드를 수정하지 않고도 동작을 추가하거나 변경할 수 있다는 것이다.
쉽게 생각하면 기존의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 것이다.
기존의 코드를 수정하지 않으면서 새로운 기능을 추가할 수 있는 방법은 무엇이 있을까?
개방-폐쇄의 핵심적인 개념은 바로 "추상화"이다.
추상화를 사용하면 문맥에 따라 변하는 부분이 생략되고 문맥이 바뀌더라도 변하지 않는 부분만 남게 된다.
그렇다면 왜 추상화가 개방-폐쇄 원칙을 만족하는데 중요한 역할을 하는 것일까?
추상화 부분은 수정에 닫혀있게 된다.
반대로 추상화를 통해 생략된 부분은 확장의 여지를 남기게 되며 이것이 바로 추상화가 개방-폐쇄 원칙을 가능하게 만드는 이유라 할 수 있다.
그럼 OCP의 핵심 개념 중 하나인 "추상화"는 어떻게 달성할 수 있을까?
대표적인 방법으로 인터페이스로 분리하는 것이 있다.
인터페이스를 사용한다면 구체적인 구현에서 추상적인 행동을 분리할 수 있게 된다.
이렇게 된다면 새로운 기능이 필요할 때 기존 코드를 수정하지 않고도 새로운 클래스를 추가하거나 기존 클래스를 확장할 수 있게 된다.
결론적으로 개방-폐쇄 원칙을 달성하기 위한 방법 중 가장 중요한 것은 추상화이며 이를 달성하기 위한 방법으로 인터페이스가 있다는 것이다.
기존의 코드를 보자.
class Person {
private bathroom: Bathroom;
constructor(bathroom: Bathroom) {
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 Bathroom {
goToBathroom() {
console.log('화장실로 간다.');
}
washFace() {
console.log('얼굴을 씻는다.');
}
brushTeeth() {
console.log('양치를 한다.');
}
washHair() {
console.log('머리를 감는다.');
}
dryOff() {
console.log('물기를 닦는다.');
}
leaveBathroom() {
console.log('화장실을 나온다.');
}
}
class Morning {
private person: Person;
constructor(person: Person) {
this.person = person;
}
morningActivity(): void {
this.person.wakeUp();
this.person.bathroomActivity();
}
}
const morning = new Morning(new Person(new Bathroom()));
morning.morningActivity();
현재 상태는 Person이 Bathroom에 결합되어 있음을 알 수 있다.
인터페이스를 이용해 추상화를 한다면 아래 코드와 같이 수정할 수 있을 것이다.
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();
(예시는 인터페이스를 사용했지만 공통 부분이 있다면 추상 클래스로 정의해도 무방)
위와 같이 개선한다면 새로운 Person 객체가 추가되어도 Morning 객체는 수정할 필요가 없을 것이다.
또한, 새로운 Bathroom 객체가 추가되어도 Person 객체는 수정하지 않아도 될 것이다.
예를 들어, Person2라는 사람이 있고 이 사람은 양치 - 머리감기 - 세수를 순서대로 수행한다고 가정해보자. (Person2)
Morning은 IPerson에 대한 정보를 알고 있기 때문에 Person2는 IPerson의 인터페이스만 만족하면 된다.
그 결과 Morning은 IPerson에 대한 정보만 가지고 있어도 자신의 역할을 수행할 수 있다.
(코드 생략)
새로운 Person3이 나타나도 단순히 객체만 추가하고 Morning의 생성자에 넣으면 될 것이다.
결론적으로 Person을 추상화하여 (IPerson) 확장에는 개방되고, 확장에 대해 Morning의 수정이 요구되지 않으므로 수정에는 폐쇄되어 있게 된다.
또한, 이와 같이 추상화를 사용할 경우 상위 수준의 클래스와 하위 수준의 클래스가 모두 추상화에 의존하기 때문에 하위 수준의 변경으로 인해 상위 수준의 클래스가 영향을 받지 않도록 할 수 있으므로 DIP(의존성 역전의 원칙)도 만족하게 된다.
결론
개방-폐쇄의 원칙을 만족하기 위한 조건은 추상화이다.
추상화 부분은 수정에 닫혀있고 추상화를 통해 생략된 부분은 확장이 가능하다.
하지만, 어떤 개념을 단순히 추상화 했다고 해서 수정에 대해 닫혀있는 설계는 아니다.
개방-폐쇄의 원칙에서 폐쇄(수정에 닫혀있는)를 가능하게 하는 것은 "의존성의 방향"이다.
수정에 대한 영향을 최소화 하기 위해서는 위의 예시와 같이 모든 요소가 추상화에 의존해야 한다.
[객체지향] SOLID 예제(4) - 리스코프 치환의 원칙(LSP)
[참고자료]
위키북스, 오브젝트, 2019
'개발 > 개념' 카테고리의 다른 글
[객체지향] SOLID 예제(5) - 인터페이스 분리의 원칙(ISP) (0) | 2023.10.03 |
---|---|
[객체지향] SOLID 예제(4) - 리스코프 치환의 원칙(LSP) (0) | 2023.09.29 |
[객체지향] SOLID 예제(2) - 의존성 역전의 원칙(DIP) (0) | 2023.09.25 |
[객체지향] SOLID 예제(1) - 단일 책임의 원칙(SRP) (0) | 2023.09.25 |
[CS: 운영체제] 컴파일러 (0) | 2023.07.15 |