객체지향 프로그래밍의 5가지 설계 원칙, SOLID
SOLID란 객체 지향 프로그래밍을 하면서 지켜야 하는 5대 원칙으로 각각 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)의 앞글자를 따서 만들어졌다. SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 되는 것으로 알려져 있다.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Priciple): 개방 폐쇄 원칙
- LSP(Listov Substitution Priciple): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 역전 원칙
즉, SOLID 란 응집도는 높이고, 결합도는 낮추는 고전 원칙을 객체 지향의 관점에서 5가지 원칙으로 재정립한 것이라고 할 수 있다.
SRP - 단일 책임 원칙
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C. 마틴
class Person {
void cook(); //요리하기 - 요리사
void order(); //주문하기 - 손님
void pickup(); //픽업하기 - 손님
void eat(); //먹기 - 손님
...
}
Person 클래스는 요리사, 손님의 역할을 모두 가진 클래스이다. 따라서 하나의 책임만을 가지고 있지 않다.
만약 요리사의 역할과 관련된 기능이 추가된다면 코드의 변경이 불가피해진다.
class Chef {
void cook();
}
class Customer {
void order();
void pickup();
void eat();
}
Person 클래스를 하나의 책임을 갖도록 Chef , Customer클래스로 분리하였다.
이제 요리사의 역할과 관련된 기능이 추가될 때마다 Customer클래스는 영향을 받지 않는다.
이로 인해 클래스 변경으로 인한 다른 클래스에 영향을 줄일 수 있기 때문에 응집도가 높아지고 결합도가 낮아지는 장점을 갖는다.
OCP - 개방/폐쇄 원칙
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다. - 로버트 C. 마틴
기존 소프트웨어 엔티티 기능에 대한 변경이 있더라도 변경 없이 새롭게 확장하여 기능을 추가할 수 있어야 한다는 것을 의미한다.
객체지향의 특징인 "추상화"와 "다형성"을 사용하여 OCP를 지킬 수 있다.
interface Car {
void accel();
void brake();
}
class Bus implements Car {
void accel() { //속도 7 증가 };
void brake() { //속도 5 감소 };
}
class Truck implements Car {
void accel() { //속도 5 증가 };
void brake() { //속도 3 감소 };
}
운전자는 자동차가 변경되어도 엑셀을 밟거나 브레이크를 밟는 것에 대한 영향을 받지 않는다.
이는 운전자가 변경에는 닫혀있다는 것을 의미한다.
반대로 자동차는 Bus나 Truck 이외에도 다른 자동차 종류로 클래스를 인터페이스를 통해 확장할 수 있고, 운전자의 역할에는 영향을 끼치지 않는다. 이는 확장에는 열려있는 것을 의미한다.
데이터베이스가 MySQL에서 오라클로 바뀌더라도 Connection을 설정하는 부분만 변경해 주면 된다.
즉, 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀 있는 것이다. 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것이다.
개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.
따라서 객체지향 프로그래밍에서 개방 폐쇄 원칙은 반드시 지켜야 할 원칙이다.
LSP - 리스코프 치환 원칙
서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다. - 로버트 C. 마틴
- 하위 클래스 is a kind of 상위 클래스 - 하위분류는 상위 분류의 한 종류다.
- 구현 클래스 is able to 인터페이스: 구현 분류는 인터페이스 할 수 있어야 한다.
위의 두 문장을 잘 지키고 있다면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다.
리스코프 치환 원칙 위배
리스코프 치환 원칙 만족
즉, 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
ISP - 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다. - 로버트 C. 마틴
단일 책임 원칙(SRP)을 이용해서 여러 책임을 가진 클래스를 여러 단일 책임 클래스로 분리할 수도 있지만 인터페이스 분할 원칙(ISP)을 이용해서 여러 인터페이스로 여러 책임을 가진 클래스를 단일 책임을 가진 클래스로 만들 수 있다.
이와 같은 구조를 사용해서 여자친구를 만날 때는 남자친구의 역할만 할 수 있게 인터페이스로 제한하고, 어머니와 있을 때는 아들 인터페이스로 제한하고, 직장 상사 앞에서는 사원 인터페이스로 제한하고, 소대장 앞에서 소대원 인터페이스로 제한하는 것이 바로 인터페이스 분할 원칙의 핵심이다.
interface Phone {
void call(String number); // 통화 기능
void message(String number, String text); // 문제 메세지 전송 기능
}
interface AR {
void AR(); // 증강 현실(AR) 기능
}
interface Biometrics {
void biometrics(); // 생체 인식 기능
}
class NewPhone implements Phone, AR, Biometrics{
@Override
public void AR() {
//구현
}
@Override
public void biometrics() {
//구현
}
@Override
public void call(String number) {
//구현
}
@Override
public void message(String number, String text) {
//구현
}
}
class OldPhone implements Phone {
@Override
public void call(String number) {
//구현
}
@Override
public void message(String number, String text) {
//구현
}
}
최신 폰과 구형 폰의 경우 필요한 기능이 다르다. 따라서 각각의 기능에 맞게 인터페이스를 잘게 분리하여 구성해야 한다.
ISP를 적용할 때 상위 클래스는 풍성할수록 좋고, 인터페이스는 작을수록 좋다는 것을 기억하자
인터페이스는 “~할 수 있는(is able to)”이라는 기준으로 만드는 것이 정석이다.
결론적으로는 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다
SRP와 ISP 원칙 사이의 관계
SRP가 클래스의 단일 책임 원칙이라면, ISP는 인터페이스의 단일 책임 원칙이다.
즉, 인터페이스에 기능에 대한 책임에 맞게 추상 메소드를 구성하면 된다는 말이다.
하지만 책임을 준수하더라도 ISP가 만족되지 않을 수 있는 케이스가 존재한다.
예를 들어 위와 같이 게시판 인터페이스엔 글쓰기, 읽기, 삭제 추상 메서드가 정의되어 있다. 이들은 모두 게시판에 필요한 기능들이며 게시판만을 이용하는 단일 책임에 위배되지 않는다.
하지만 이를 구현하는 일반 사용자 입장에선 게시글 강제 삭제 기능은 사용할 수 없기 때문에 결국 ISP 위반으로 이어진다.
따라서 책임을 잘 구성해 놓은 것 같지만 실제 적용되는 객체에겐 부합되지 않을 수 있기 때문에 책임을 더 분리해야 한다.
정리하자면, ISP는 SRP를 만족하면 성립되는가라고 질문한다면 반드시 그렇다고는 볼 수 없다고 답변하는 게 맞을지도 모른다.
DIP - 의존관계 역전 원칙
고차원 모듈은 저 차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다. 추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다. 자주 변경되는 구체(Concrete) 클래스에 의존하지 마라 - 로버트 C. 마틴 -
의존 역전 원칙 : 자신보다 변하기 쉬운 것에 의존하지 마라.
자동차와 스노우타이어 사이에는 다음 그림과 같은 의존관계가 있다. 자동차가 스노우타이어에 의존한다.
자동차는 한 번 사면 몇 년은 타야 하는데 스노우타이어는 계절이 바뀌면 일반 타이어로 교체해야 한다. 이런 경우 스노우타이어를 일반타이어로 교체할 때 자동차는 그 영향에 노출돼 있음을 알 수 있다.
자동차 자신보다 더 자주 변하는 스노우타이어에 의존하는 것은 DIP원칙을 지키지 못한 것이다.
이러한 상황에 DIP 원칙을 적용하면 다음과 같은 구조를 설계할 수 있다.
이와 같이 설계를 바꾼다면 자동차가 구체적인 타이어들(스노우타이어, 일반타이어, 광폭타이어)이 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 스노우 타이어에서 일반타이어로, 또는 다른 구체적인 타이어로 변경돼도 자동차는 이제 그 영향을 받지 않는 형태로 구성된다.
즉, 이러한 상황을 보면 기존에는 스노우타이어가 그 무엇에도 의존하지 않는 클래스였는데, DIP설계 원칙을 적용 후 추상적인 것인 타이어 인터페이스에 의존하게 됐다. 즉, 의존의 방향이 역전된 것이다.
이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존 역전 원칙이다.
'좋은 개발자가 되기' 카테고리의 다른 글
객체지향의 사실과 오해(7장) (0) | 2023.05.16 |
---|---|
객체지향의 사실과 오해(6장) (0) | 2023.05.09 |
단위테스트? 통합테스트? 런던파 vs 고전파 (1) | 2023.05.08 |
객체지향의 사실과 오해(5장) (0) | 2023.05.01 |
객체지향의 사실과 오해(4장) (0) | 2023.04.25 |