SOLID
는 객체지향 프로그래밍(OOP)에서 좋은 설계를 위한 다섯 가지 원칙을 나타내는 약어다.
(솔직히 솔리드 들었을 때 가수 솔리드 밖에 떠오르지 않았다)
아무튼 이 원칙은 객체지향 설계에서 코드를 더 유지보수하기 쉽게 만들고,
변화에 더 유연하게 대응하도록 도와 준다고 하는데, SOLID는 객체지향 개념과 연관되어 있긴 하지만...
구체적으로는 "좋은 객체지향 설계"를 위한 지침서를 제공한다고 볼 수 있다.
SOLID
원칙을 따르는 코드는 일반적으로 다른 코드와 느슨하게 결합되어 있어서, 각 컴포넌트의 수정이나 테스트가 용이하다고 한다.
다른 코드와 느슨하게? 음
다른코드와 느슨하게 결합되어있다는 건 코드가 서로에 대해 갖는 의존성을 줄여,
독립적으로 작동하고나 변경할 수 있게 하기 위해서 라고 한다.
자세한 건 곧 아래에 의존역전원칙(DIP) 내용에서 배울 것이다.
다시 돌아와서 결론은 솔리드 원칙을 따를 경우,
수정하거나 테스트가 필요한 상황에서 특정 컴포넌트가 다른 컴포넌트에 미치는 영향을 최소화 할 수 있다는 것이다.
클래스는 하나의 책임만 가져야 한다.
클래스는 하나의 기능이나 역할만 수행하도록 설계되어야 한다는 건데,
이를 통해 만약 수정이 필요한 경우 문제가 한 부분에 한정되어 있을 수 있다보니
수정도 용이하고 유지보수성이 높아진다.
그럼 단일 책임 원칙을 지키다보니 class가 너무 많아지게 된다면 어떡하지?
오히려 관리가 어려워지는 거 아닌가? 싶을 수 있다.
적용을 하되 클래스의 수가 너무 많아지지 않도록 적절한 수준의 추상화와 관심사를 분리해야 하는 부분을 염두하며 만들어야 한다는 점을 기억해두자.
클래스는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 하는 원칙.
오 이게 무슨 뜻이지......
이 말은 기능을 추가하거나 변경할 수는 있지만, 기존 코드를 직접 수정하지 않아야 한다는 의미이다.
만약 새 기능이 필요할 때 기존 코드를 변경하기 않고도 프로그램을 확장할 수 있도록 설계를 하는 것이라고 하는데, 예시를 봐봅시다.
먼저 문제점이 있는 코드 먼저 보자.
이 코드는 할인 정책이 추가될 때매다 calcylate 메서드 안에 새로운 else if 조건문을 달아줘야한다. 이 경우에는 기존 코드를 계속 수정해야하니 개방-폐쇄 원칙을 위반하는 셈이다.
만약 저 코드를 개방-폐쇄 원칙으로 적용해본다면.
낯설지 않다.. 계산기 레벨 4를 만들었을 때 이 방식을 썼다..!!
아무튼 이렇게 무엇에 대한 할인 정책을 새로 추가할 경우 프로토콜에 준수하는 새로운 클래스만 만들면 되는 것이고 CalculatorPrice는 별도로 뭘 수정할 필요가 없이 새로운 할인 정책을 받아들일 수 있다는 것이다.
즉 이게 확장에는 열려있고, 수정에는 닫혀있는 구조가 된다.
다시 한번 정리하자면 개방-폐쇄 원칙의 장점은
한마디로 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다 라는 것이다.
부모 클래스 타입으로 자식 클래스를 사용해도 문제가 없도록 설계되어야 하고,
그렇게 된다면 클래스 간의 일관성을 유지하고 코드가 예상대로 동작하여 오류를 줄일 수 있다.
여기까지 보면 객체지향이 상속과 비슷한 느낌이다. 하지만 둘은 엄연히 다르다.
상속은 코드 재사용성과 확장성을 높이기 위해 부모 클래스 기능을 자식 클래스에게 물려주는 것. 자식은 아예 자신만의 새로운 것을 추가할 수 있다. 그러니까.. 필요에 따라 오버라이드해서 변경 가능하다는 것.
리스코프 치환 법칙의 경우 자식 클래스가 부모 클래스의 규칙을 따라야 하되, 부모 클래스의 원래 기능을 바꾸거나 무효화하지 않도록 하는 원칙이다. 부모 클래스의 기대 동작을 깨지 않고, 자식 클래스는 부모의 행동을 일관되게 확장하거나 유지해야 한다.
펭귄은 날지 못하는데 오버라이드해서 fly
라고 해놓고 날 수 없다고 설정은 할 수 있지만 이건 새 클래스가 기대하는 행동을 깨트린 것이라 원칙 위반이다. 삐-
상속개념으로서 자식클래스가 새로운 것으로 변경이 가능하긴 하지만! 그렇게 변경하게 되면 LSP 를 위반하는 것이기에 변경하더라도 부모 클래스가 원래 의도한 기능이나 행동을 유지해야하는걸 기억해야한다.
만약 굳이 굳이 나는 펭귄을 넣어야한다 .. 라고 한다면,
해당 방식으로 프로토콜로 비행가능 여부를 가린다면 날수 있는 새와 날수 없는 새를 구분할 수 있으니, LSP의 원칙을 만족할 수 있다.
인터페이스 분리 원칙은 클라이언트 (사용자) 가 본인이 사용하지 않는 메서드에 의존하지 않도록 하라는 원칙이다.
그러니까, 사용자를 작고 명확하게 나눠 꼭 필요한 기능만 포함해야 한다는 의미인데,
이 원칙을 따르면 클래스가 자신이 실제로 필요한 기능만 구현하게 되어 불필요한 의존성을 피할 수 있게 된다.
해당 예시의 경우 InkPrinter
는 scanDocument
메서드를 필요로 하지 않는데도 불구하고 인터페이스를 준수해야해서 구현하다보니, 불필요한 의존성이 생기게 된다.
불필요한 의존성을 풀어서 이야기한다면,
Printer
인터페이스가 scanDocument()
메서드를 포함하고 있을 때,
InkPrinter
는 실제로 스캔 기능이 필요 없지만 해당 메서드를 구현해야했다.
그러면 InkPrinter
는 scanDocument()
에 할 필요도 없는 의존을 해버리게 되고,
이런 과정은 오히려 낭비같은 것이다.
만약 scanDocument()
메서드의 구현 방식이 변경되거나 오류가 발생하게 된다면,
당연히 Inkprinter
에도 영향을 준다.
하지만 어차피 InkPrinter
는 스캔 기능이 필요하지 않으니 불필요한 의존성이라는 것.
이렇게 프로토콜을 각각 만들어주면 InkPrinter
는 printDocument()
만 필요로 하니 DocumentPrinter
프로토콜 인터페이스만 구현하고, 스캐너도 스캐너대로 프로토콜 준수하여 인터페이스 구현하면 되는 것이다.
불필요한 메서드 구현도 피했고, 보기에도 각 클래스가 필요로 하는 기능만 구현하기 때문에 깔끔하고 명확해보인다.
이렇게 불필요한 코드 의존성을 줄여 유지보수를 원활하게 할 수 있는 것이다.
이 원칙은 소프트웨어 디자인의 핵심 개념 중 하나인데,
좋은 설계와 유지보수성을 위해 매우매우매우매우 중요하다고 한다.
DIP란, 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 두 모듈간의 의존성은 추상화된 인터페이스에 의존해야 한다.
구체적인 구현이 아니라 인터페이스를 통해 의존해야한다고 하는데,
이렇게 한다면 인터페이스나 추상 클래스를 활용해 의존성을 줄일 수 있고, 변경에 더 유연하게 대처할 수 있도록 하기 때문이다.
또.. 또 어렵게 설명하지 또.
우선 DIP 의 안 좋은 예시를 한번 보자.
솔직히 나는 예시 쓰면서 이게 왜 안 좋은거지? 라고 생각했다.
그런데 의존 역전 원칙을 준수한 예시를 보면서 뭔가 알 것 같았다.
아래는 DIP의 원칙을 준수한 예시다.
이제 전환 클래스는 전환 가능 프로토콜 안의 인터페이스에 의존하게 되어 전구나 팬과 같은 장치들을 쉽게 제어 할 수 있다.
혹시라도 여기에 스피커라던지, TV 라던지 추가를 할 경우 전환가능을 구현하는 클래스만 추가하면 되기 때문에 굳이 전환 클래스를 손 댈 필요가 없다.
그리고 이렇게 공통된 인터페이스를 가진 여러 클래스가 동일한 방식으로 사용될 수 있어 재사용성 부분에서도 아주 좋다.
이러한 원칙은 사실 객체지향 개념에 기반하기에 비슷한 부분이 많다.
단순히 클래스와 객체를 사용하는 것 이상을 지향하기 때문에,
SOLID
는 코드의 가독성
, 재사용성
, 유지보수성
등을 높이기 위한 설계 지침을 따라
객체지향 프로그래밍을 더 구조적이고, 견고하게 만드는 데 많은 도움이 될 것이다.
SOLID 원칙을 무려 이틀동안 공부하고 배우면서 객체지향 설계의 중요성도 알게 되었고,
이런 원칙을 따르면 자연스럽게 만들고자하는 프로그램의 품질도 향상시켜준다는 걸 알게 됐다.
이런 각각의 원칙들이 상호작용하면서 프로그램 설계를 어떻게 개선할 수 있는지도 하나하나 실습을 통해 만들면서 배울 수 있었고,
이론적으로 적용해보면서 실행해 봤던 부분이 정말 많은 도움이 되었다.
앞으로 진행 될 프로젝트나 학습에도 이 원칙들을 염두하며 적용해볼 수 있도록 신경을 써야겠다.
정말 많은 도움이 된 것 같다.
어제 새벽에 이걸 읽으면서 과제에 참고했습니다 아주 유익한 포스팅.