객체 지향의 5대 원칙으로 불리는 SOLID 원칙에 대해서 예제를 만들어서 정리해보려고 한다.
SOLID 를 처음 알게 된 것은 첫 회사 면접에서 질문을 통해서 알게 되었다.
"객체 지향의 5대 원칙에 대해서 아나요?" 라는 질문이다. 벌써 3년 전 일이다.
당시에는 "응? 그런게 있구나??" 속으로 생각했었다.
면접이 끝나고 찾아봤는데, 그 당시에는 이게 개발하는데 중요한 것일까? 라는 생각을 했었는데
1년 씩 연차가 쌓이고 경험을 하면서 음 그래서 중요하구나.. 로 바뀌게 되었다.
코드를 한 번 작성하면 끝나는 것이 아니라, 결국 유지 보수를 해야된다. Software 는 계속해서 변화하는 요구사항들을 만족시키기 위해서 진화해야 된다. 그래서 중요한 것은 재사용하기 쉽고, 확장하기 쉽고, 진화하기 쉽고, 테스트하기 쉬워야 한다.
따라서 좋은 코드를 작성하기 위해서 스스로 정리 한다.
1. Single Responsibility Principle - 단일 책임 원칙
A class should have a single responsibility.
클래스는 책임을 하나만 가져야 한다. 다시 말해, 클래스 변경의 이유는 오직 하나여야만 한다.
왜?
1. Testing - 책임이 적을수록 테스트 케이스도 적어진다.
2. Lower Coupling - 책임이 적으면, 클래스의 기능도 적어지고, 의존성도 낮아진다.
그렇다면 예제를 통해 확인해보자.
God 클래스를 만들었다. 이름에서 알 수 있듯이 많은 일을 하는 클래스이다. God 은 design 하고 develop 도 하고 plan 도 하고 customer service 도 한다. 흔히들 말하는 common 의 저주이다. God 은 모든일을 할 수 있다.
그럼 SRP 에 맞게 Class 를 나눠보자. God 클래스를 총 4개의 class 로 나누었다. Developer, Designer, Planner, CustomerServiceRepresentative.
준비한 예제가 실제 업무에서는 많이 이질적이지만, Class 를 분할해서 책임을 분산시키는 것이 포인트라고 보면 된다.
2. Open for Extension Close for Modification
Classes should be open for extension, but closed for modification.
Class 의 현재 행동의 변화는 해당 Class 를 사용하는 전체 시스템에 영향을 준다. 만약에 해당 Class 가 더 많은 기능을 수행하고자 원한다면, 기존의 기능을 변경하는 것이 아니라, 추가하는 것이 좋다.
Bug Fix 의 경우에만 변경을 하자.
간단한 예제를 통해 확인자.
먼저 BackendDeveloper 라는 Class 를 작성했다.
developBackEnd() 라는 행동 (메서드) 가 있다. 어느날 해당 Class 에 developFrontEnd() 라는 행동 (메서드) 를 추가하고 싶어졌다. 그런데 해당 Class 의 이름을 변경할 수는 없다. 혹은 BackendDeveloper 클래스에 developFrontEnd() 라는 행동 (메서드) 를 추가할 수도 있지만, 이미 클래스 이름과 맞지 않고, 위에서 언급한 단일 책임의 원칙도 어기게 된다.
( 현실 세계에서 백엔드 개발자라고 프론트 개발을 전혀 안하지는 않지만... )
따라서 아래와 같이, BackendDeveloper 를 상속하여 FullStackDeveloper 라는 클래스를 만들었다.
기존의 BackendDevloper 클래스의 변경은 일어나지 않았고, 상속을 통해서 기능을 확장했다.
( 현실 세계에서는 프론트 개발을 하다가 백엔드도 하게 되어 순서가 반대인 경우도 있지만... )
3. Liskov Substitution - 리스코프의 치환 원칙
If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
child class 가 parent class 와 같은 기능을 수행하지 못할 때, 버그를 야기할 수 있다.
만약 기존 클래스로 다른 클래스를 생성한다면, 기존 클래스는 parent 이고 새로 생성한 클래스는 child 이다. child class 는 parent class 가 할 수 있는 것을 모두 할 수 있어야 한다. 이를 Inheritance (상속) 이라 한다.
child class 는 parent class 와 같이 동일한 요청를 처리할 수 있어야 하며 동일한 결과(동일한 type)를 전달할 수 있어야 한다.
이번에는 스타크래프트 예제를 준비했다. 스타라.. 나는 옛날 사람이 되었나?
우선, StarCraftUnit 이라는 grand parent class 를 만들었다.
간단하게 name 을 가지고, attack() 이라는 행동을 할 수 있다.
그리고, MineralCollector class 라는 StarCraftUnit 을 상속받은 child class 를 만들었다.
MineralCollector 는 collectMineral() 이라는 행동을 할 수 있다.
또한, DarkTemplar 는 StarCraftUnit 을 상속받는 child class 를 만들었다.
마지막으로, Drone, SCV 는 MinerallCollector 를 상속받는 손자? class 를 만들었다.
Drone, SCV 는 MineralCollector 의 subtype 이다.
DarkTemplar 는 MineralCollector 의 subtype 이 아니다.
아래를 보면, MineralCollector 타입으로 probe 라는 변수로 객체를 생성했다.
probe 객체는 collectMineral() 이라는 행동을 할 수 있다.
또한 Drone, SCV 는 MineralCollector 의 subtype 으로 probe 객체의 collectMineral() 행동을 대체할 수 있다.
DarkTemplar 의 경우 MineralCollector 의 subtype 이 아니기 때문에, MineralCollector 를 대체할 수 없다.
4. Interface Segregation - 인터페이스 분리 원칙
Clients should not be forced to depend on methods that they do not use.
특정 Class 가 행할 능력이 되지 않은 행동을 수행하기를 요구받는 Class 가 있을 때, 이는 낭비이며 예기치 못한 많은 버그를 유발할 것이다.
Class 는 자신의 역할 수행에 필요한 행동만을 수행하기를 요구 받아야 한다. 수행할 수 없는 행동은 제거되거나 어딘가로 이동되어야 한다.
이번에도.. 스타크래프트 예제!
먼저 StarCraftUnit 이라는 interface 를 만들었다.
총 9가지의 행동이다.
그리고 Wraith Class 는 StarCraftUnit Interface 를 구현한다.
activeCloakingField() 행동은 스타를 해보신 분들은 다 알겠지만... 레이스의 주특기인 은신이다.
burrow() 는 레이스가 할 수 없는 행동이다.. 해당 행동을 호출하게 되면 예외를 던지고 종료된다.
마찬가지로 collectMineral(), collectGas(), moveOnGround() 는 레이스가 불가능한 행동이다.
따라서 Wraith 클래스는 행할 능력이 되지 않은 행동을 수행하기를 요구받고 있다.
마찬가지로, Drone 클래스는 StarCraftUnit Interface 를 구현했다.
드론은 클로킹을 할 수 없다. 드론은 하늘을 날 수 없다.
이처럼 Drone class 도 행할 수 없는 행동을 수행하기를 요구 받고 있다.
따라서 위의 Interface 를 나누어 보자.
AirUnit, BurrowAble, CloakingAble, GroundUnit, ResourceCollectable, Unit 6개의 Interface 로 분리했다.
이전과 다르게 새로운 Wraith 는 필요한 행동만 수행할 수 있도록 필요한 Interface 만 구현하도록 변경했다.
마찬가지로 Drone 클래스도 필요한 행동만 수행할 수 있도록 필요한 Interface 만 구현하도록 변경했다.
5. Dependency Inversion Principle - 의존 역전의 원칙
High-level modules should not depend on low-level modules. Both should depend on the abstraction. Abstraction should not depend on details. Details should depend on abstractions.
먼저, 간단하게 정리해보자.
High-level Module(or Class) : tool 을 사용하는 주체 Class
Low-level Module(or Class) : 사용되는 tool
Abstraction : 두 Class 를 연결하는 interface
Details : tool 이 어떻게 작동하는지.
Class 는 Interface 를 통해서 tool 을 사용해야 한다. Class 와 Interface 는 tool 이 어떻게 작동하는지 몰라야 한다. 하지만, tool 은 interface 의 명세를 충족해야 한다.
간단하게 정리하자면, M has a T 관계에서 M class 는 T 가 어떻게 작동하는지 모르는 체, Interface 의 시그니쳐를 통해서 일을 시키며, 따라서 T class 가 어떻게 작동하는지 M class 는 몰라야 한다. 이를 통해 M 과 T 의 의존성을 줄일 수 있다.
올해는 코로나로 시끄러우니, 코로나를 예방하기 위한 마스크 예제를 준비했다.
Human Class 는 High-level module 이다.
그리고 Low-level module 은 3가지로 HealthMask, DentalMask, CottonMask 세 개의 Class 이다.
그리고 각각의 마스크 class 들은 Mask Interface 를 구현했다.
Human Class 는 각각의 Mask 들이 어떻게 작동하는지 모른다. Mask 라는 Interface 를 통해서 연결되었다.
각각의 마스크들은 protectFace() 구현을 통해서 Details 이 다르다.
댓글
댓글 쓰기