[객체지향] 2. 객체지향 설계 5원칙 (SOLID)
0. 압도적 감사...!
SOLID 원칙
이라고도 불리는, 객체지향 설계 5원칙
은 선배 개발자분들의 시행착오와 고민들로 만들어진 객체지향 설계 가이드라인 입니다. 후배 개발자들이 자신들과 같은 고민을 하여 시간을 낭비하지 않도록 열심히 고민해주신 선배 개발자 분들에게 감사하며, 객체지향 설계 5원칙
에 대해 알아봅시다.
1. S - 단일책임원칙
SRP (Single Responsibility Principle)
모든 클래스
는 하나의 책임만 가져야 하고, 클래스
는 그 책임을 완전히 캡슐화 해야한다는 원칙입니다.
자동차 클래스 (다중 책임)
class 자동차 { public void 시동을건다(type) { if (type=="저가형") { this.is엔진동작 = true; } else if (type="고급형") { this.is엔진동작 = true; this.네비게이션켜기(); this.드라이브모드_변경('기본_드라이빙'); } } }
위의 다중 책임
예제에서는 시동을건다() 라는 메소드
에 저가형 자동차
, 고급형 자동차
등 복수의 책임
이 부여되어 있습니다. 만약 전기 자동차
, 수소전기 자동차
등 종류가 늘어난다면, 계속해서 else if 를 추가해야하고 이 때문에 하나의 클래스
에 여러 책임
이 부여됩니다. 만약 시동을 건다() 메소드
에 오류가 발생한다면, 모든 종류의 자동차
의 시동을건다() 메소드
는 동작하지 않게 되는 문제가 발생하겠죠.
자동차 클래스들 (단일 책임)
class 저가형자동차 { public void 시동을건다() { this.is엔진동작 = true; } } class 고급형자동차 { public void 시동을건다(type) { this.is엔진동작 = true; this.네비게이션켜기(); this.드라이브모드_변경('기본_드라이빙'); } }
단일 책임
으로 분리한 예제를 보면, 책임
별로 클래스
를 나누어서 각 클래스
는 하나의 책임
만 가집니다. 그러면 전기 자동차
, 수소전기 자동차
가 추가되더라도 기존 클래스
들을 수정하지 않아도 됩니다. 그리고 저가형 자동차
가 문제가 생기더라도 다른 자동차
들은 영향을 받지 않죠.
이렇게 각 클래스
에 하나의 책임
만 부여함으로서, 변경과 확장에 유연한 프로그램 구조를 만들 수 있습니다.
2. O - 개방 폐쇄 원칙
OCP : (Open Closed Principle)
자신의 확장에는 열려있고, 주변의 변화에는 닫혀있어야 한다는 원칙입니다.
최근 전기 자동차가 많이 상용화 되었습니다. 그러면서 자동차의 구조에도 많은 변화가 생겼는데요. 그 중 주된 변화의 한가지로 엔진 vs 모터
를 생각해볼 수 있습니다. 여기서 운전자(사용자)가 자동차를 사용하는 방식에 중점을 두고 생각해 봅시다. 자동차를 움직이는 기술의 변화 즉, 주변의 변화는 사용자가 자동차를 사용하는데 영향을 끼쳐서는 안됩니다.
- 엑셀 = 속도 증가
- 브레이크 = 속도 감소
위와 같은 자동차
의 특성이 주변의 변화에 영향을 받아선 안되죠. 이것이 주변의 변화에 닫혀있는 상태인 것입니다.
이 처럼 운전자는 자동차를 사용할 때, 내가 엑셀을 밟으면 엔진 RPM이 올라가며 바퀴에 연결된 축이 빠르게 돌아가서 속도가 빨라지는지, 아니면 모터가 더 빠른속도로 돌아서 빨라지는지 몰라도 됩니다. 그저 엑셀 = 속도 증가, 브레이크 = 속도 감소 만 알고 자동차를 사용할 수 있어야 하죠.
그리고 위와 같은 특성을 유지한 채로, 엔진
으로 움직이는 자동차
가 아닌 모터
로 움직이는 자동차
가 추가되는, 즉 자신의 확장에는 열려있죠.
마찬가지로, 상위 클래스
혹은 인터페이스
를 하위 클래스
에 상속시킴으로서 기본적으로 갖춰야 할 속성
과 메소드
형식을 유지시킬 수 있습니다. 그리고 그 내부에서 어떤 작업이 실행되는지 몰라도 사용자가 기대한 기능이 수행되도록 합니다. 이는 객체지향의 캡슐화
와 상속
이라는 특성을 잘 이용한 원칙입니다.
3. L - 리스코프 치환 원칙
LSP : (Liskov Substitution Principle)
하위 클래스
는 언제나 자신의 상위 클래스
로 교체할 수 있어야 한다.
다시 자동차로 예를들어볼까요? 😄
옛날에는 말
은 주 이동수단이었죠. 그러다 자동차
가 개발되어 우리는 휘발유 자동차
도 타고 전기 자동차
도 탑니다. 이 처럼 말은 자동차
라는 이동수단의 조상님 격이 될 수 있습니다.
이제 그림을 살펴보면, 왼쪽 그림처럼 휘발유 자동차
와 전기 자동차
는 자동차
로 치환할 수 있습니다.
하지만 오른쪽 그림처럼 자동차
는 말
로 치환할 수 없죠.
이 경우, 리스코프 치환 원칙에 위배된다고 할 수 있습니다.
이 처럼, 하위 클래스
가 상위 클래스
로 치환된 수 없는 관계(상속
등)를 맺고 있다면, 이는 잘못된 관계일 확률이 매우 크겠죠?
4. I - 인터페이스 분리 원칙
ISP : (Interface Segregation Principle)
인터페이스 분리 원칙이란, 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원칙입니다.
위 그림에서는 자동차
를 상속받아 저가형 자동차
, 고급형 자동차
라는 클래스를 정의하였습니다.저가형 자동차
에서는 드라이브모드_변경() 이라는 메소드
는 사용되지 않지만, 상속을 하였기 때문에 어쩔 수 없이 구현이 된 상태입니다. 이를 인터페이스 분리 원칙에 맞게 변경하면 다음과 같습니다.
이렇게 되면, 저가형 자동차
, 고급형 자동차
모두 자신이 필요한 인터페이스만 구현하게 되었습니다!
...
뭔가 이상한 점을 발견하셨나요? 🤔
이대로 설계를 진행한다면, 1번의 단일책임원칙에 위배됩니다. 저가형 자동차
는 {시동, 속도} 2가지 책임, 고급형 자동차
는 {시동, 속도, 드라이브모드} 3가지 책임을 갖게 됩니다. 그래서 인터페이스 분리 원칙에서는 복수의 책임을 갖는것을 허용하게 되어 단일책임원칙과 함께 적용될 수 없습니다.
그래서 프로젝트의 상황에 따라, 단일책임원칙과, 인터페이스 분리 원칙을 선택하여 설계해야 합니다. 이 원칙에 따라 설계한다면, 불필요한 인터페이스가 줄어들어 효율적으로 인터페이스를 사용할 수 있습니다.
5. D - 의존의 역전 원칙
DIP : (Dependency Inversion principle)
의존의 역전 원칙이란, 의존 관계를 맺을 때 자신보다 변하기 쉬운 것에 의존하지 말아야 한다는 원칙입니다.
휘발유 자동차
가 아반테
를 상속하여 만든다고 가정해 봅시다.아반테
는 매년 새로운 모델을 출시하고, 그러면 매년 휘발유 자동차
의 상위 클래스
인 아반테
를 추가/변경 해주어야 합니다. 이러한 구조가 의존의 역전 원칙을 위배한 예시라 할 수 있습니다.
6. 마무리하며..
객체지향 설계 5원칙
의 핵심은 좋은 객체지향 설계를 위해서는 결합도
를 낮추고 응집도
를 높혀야 한다 인것 같습니다.
- 결합도
객체
간의 상호 의존 정도를 나타내는 지표
결합도가 낮다는 것은,객체
간의 상호 의존성이 낮다는 의미이고객체
의 재사용성이 증가하고, 유지보수가 용이함 - 응집도
하나의객체
내부에 존재하는속성
과메소드
의 기능적 관련성
응집도가 높은객체
는 하나의 책임(역할)에 집중되고 독립성이 높아져서 재사용성이 증가하고, 유지보수가 용이함
하지만
예외 앞에 장사 없다(?)
우리가 항상 명심할 것은, 아무리 좋은 방법론이라 하더라도 프로젝트 상황에 따라 예외적인 상황이 있을 수 있다는 것이고, 이러한 방법들은 최고의 방법이 아니라 최선의 방법 이라는 것을 생각해야합니다.
모든것을 의심하고 검증하며 이게 과연 최선인가? 를 항상 고민하는 개발자가 됩시다!
(저도 포함ㅎ)