[설계] 객체지향 설계 5원칙(SOLID)
안녕하세요. 개발자 Jindory입니다.
오늘은 "객체지향 설계 5원칙"이란 무엇인지 알아보고 객체지항 설계 5원칙의 사례를 설명하며 글을 작성해보려고 합니다.
[ 글 작성 이유 ]
소스코드를 작성하면서, 유지보수성을 높이고 재사용성을 높이려면 객체지향 설계 5원칙을 준수해야 한다고 하는데, 각각의 개념에 대해서 머릿속으로 잘 정리가 안 되어 있는것 같아, 이번 기회에 개념을 정리하여 두고두고 다시 보기 위해 글을 작성하게 되었습니다.
1. 객체지향 설계 원칙이란?
객체지향적으로 프로그래밍 하기 위해 지켜야하는 원칙으로 SRP(Single Responsibility Principle), OCP(Open Closed Principle), LSP(Listov Substitution Principle), ISP(Interface Segregation Principle), DIP(Dependency Inversion Principle) 이렇게 5가지가 있습니다.
그럼 S(Single Resposibility Principle)부터 DIP(Dependency Inversion Principle)까지 차근차근 알아가 보도록 하겠습니다.
1-1 객체지향 설계 원칙 배경
우선 객체지향 설계 5원칙에 대해서 공부하기 이전에, 객체지향 설계 5원칙이 왜 생겼는지 잠깐 알아보고 가도록 하겠습니다.
1. 소프트웨어 복잡성 증가
- 요구사항이 다양해 짐에 따라, 소프트웨어 시스템이 점점 더 복잡해지면서 유지보수와 확장이 어려워졌습니다.
- 기존 설계 방식으로는 이러한 복잡성을 효과적으로 다루기 어려웠습니다.
작은 프로젝트나 요구사항이 단순했을때는 해당 요구사항에서 필요로하는 기능만 구현하면 됐지만, 계속된 요구사항으로 인해 기존 기능을 변경해야하거나 새로 추가함에 따라 복잡해진 구조를 효과적으로 다루기 위해 객체지향 설계 원칙을 만들게 되었다고 합니다.
2. 객체지향 프로그래밍의 한계 인식
- 객체지향 프로그래밍이 널리 사용되면서 설계 원칙의 필요성이 대두되었습니다.
- 단순히 클래스와 객체를 사용하는 것만으로는 충분하지 않다는 것이 드러났습니다.
객체지향 프로그래밍(OOP)은 클래스와 객체를 사용하여 프로그래밍 하는 방식인데, 초기에는 OOP로 복잡한 문제를 해결하는데 효과적이라고 여겼으나, 시간이 지나면서 한계성(클래스간 의존성 증가, 코드의 유지보수 어려움)을 드러났습니다. 그래서 OOP의 한계를 극복하기 위한 새로운 원칙이 필요하게 되었던 것입니다.
3. 소프트웨어 개발 생산성 향상 필요
- 소프트웨어 개발 생산성을 높이기 위해서는 설계 원칙이 필요했습니다.
- 이를 통해 코드의 가독성, 유지보수성, 확장성을 향상시킬 수 있습니다.
우리가 일을 하면서 느끼겠지만, 항상 시간은 충분히 주어지지 않습니다. 제한된 시간 안에 기존의 소스를 빠르게 파악하고 , 요구사항에 맞춰 기존의 코드를 변경하거나 추가하고, 코드 생산성은 늘려야할 필요성을 느껴 개발 생산성을 향상시키기 위한 방법이 고안된것 입니다.
4. 객체지향 설계 모범 사례 정립
- 객체지향 설계에 대한 모범 사례를 정립하여 개발자들에게 제공할 필요가 있었습니다.
- 이를 통해 일관된 설계 원칙을 적용할 수 있게 되었습니다.
객체지향 설계 모범사례는 OOP의 한계를 극복하고 더 나은 소프트웨어 설계를 위한 SOLID원칙, 디자인 패턴, 코드 리팩토링 기법 등이 있습니다. 이러한 기법을 사용하며 개발의 가독성과, 유지보수성, 확장성 등이 향상되게 되었습니다.
5. 테스트 및 디버깅 용이성 향상
- 객체지향 설계 원칙을 적용하면 코드의 테스트와 디버깅이 용이해집니다.
- 이는 소프트웨어 품질 향상에 기여합니다.
객체지향적으로 설계한 코드에 대해서 테스트와 디버깅이 용이하여, 로직을 검증하거나, 에러발생에 대한 디버깅이 훨씬 수월하다고 합니다. 테스트와 디버깅은 필수적이지만 시간을 많이 소비하는 부분이라, 이 두가지를 편하고 쉽게 할 수 있다면 개발 생산성이 올라갈것 같습니다.
2. 객체지향 설계 원칙(SOLID)
2-1 Sinple Resposibility Principle(단일 책임 원칙)
단일 책임 원칙은 "클래스는 하나의 책임만 가져야한다"라는 의미인데요, 저는 책임이라는 단어를 이해하는데 어려움을 겪었던것 같습니다. 책임이란 클래스에는 하나의 목적만 있어야 함을 의미합니다. 이해를 위해 아래의 예시를 들어 설명해 보도록 하겠습니다.
30대 기혼인 남성의 책임을 갖는 Man 클래스가 있다고 가정하겠습니다.
이 남성은 가정에서는 남편이자, 직장에서는 선생님이며, 운동 동호회에서는 운영진을 맡고 있으며, 종교집단에서 한명의 신자로 위 그림과 같은 책임을 갖고 있습니다.
이랬던 Man이 학교에서 착하게 혹은 좋게좋게 말했더니, 학생들이 얕잡아 보는것 같아 엄하게, 무섭게 대화하기로 책임을 바꿨습니다. 이때 대화하기는 학교에서만 사용하는 기능이 아니라 가정에서, 동호회에서, 종교집단에서도 사용하는 기능이었기에 주변 사람들이 바뀐 Man의 대화 방식에 대해 불쾌감과 두려움을 느낍니다.
이러한 문제를 클래스는 하나의 책임을 가져야 한다는 단일책임원칙(Sinple Principle Resposibility)를 통해 해결할 수 있습니다.
위 그림처럼 30대 기혼 남성 Man 클래스가 하는 책임을 남편 클래스, 선생님 클래스, 운영진 클래스, 신자 클래스로 나누어 클래스가 하나의 책임만을 가지도록하여, 선생님 때문에 바뀐 책임이 다른 책임들이 영향을 받지 않도록 할 수 있습니다.
이렇게 단일책임원칙(SPR)을 준수하여 설계 및 개발하면 아래와 같은 장점을 얻을 수 있습니다.
- 코드의 가독성 및 유지보수성이 높아집니다.
- 하나의 책임을 변경하더라도 다른 부분에 영향을 미치지 않습니다.
- 테스트와 디버깅이 용이해지며, 코드 재사용성이 높아집니다.
2-2 Open Closed Principle(개방 폐쇄 원칙)
개방 폐쇄 원칙은 변경에는 닫혀있고, 확장에는 열려있는 원칙입니다. 이 또한 개념만 들었을때는 이해가 잘 가지 않았기에, 좀 더 쉬운 예제로 같이 이해해 보도록 하겠습니다.
게임회사 JavaGames는 HelloWorld라는 게임을 만들기로 했고, 우선 캐릭터 직업으로 전사,마법사,도적을 만들기로 했습니다. 캐릭터와 기능이 아직 많지 않으므로 Newbie라는 클래스를 만들고, 그 안에 직업별로 행동을 다르게 하도록 개발을 했습니다.
그런데 게임 기획자가 와서는 무도가 직업을 추가해야하고, 공격하기를 했을때 주먹 날리기, 방어하기를 했을 때는 손으로 가드 올리기, 음식 먹기에는 방어강화 음식 먹기를 해야한다고 합니다.
게임 개발자는 Newbie라는 클래스를 수정하려고 하다가, 또 다시 등장한 기획자가 추가로 "궁수랑 힐러, 소환사 직업을 추가하고 각 각의 직업에 맞는......"라는 말을 듣고는 퇴사를 하기로 결정합니다......
위 상황처럼 하나의 클래스에서 각 각의 조건에 따라 행위를 구분하게 되면, 새로운 요구사항이 왔을때마다 Newbie라는 클래스를 수정해야하고, 그 때마다 다른 조건들에는 영향이 없는지 변화가 없는지 체크해야합니다.
이런 상황에서 개방폐쇄원칙을 사용하여, 새로운 기능이 요구되더라도 기존 기능을 변경하지 않고, 추가할 수 있습니다.
위 그림처럼 Newbie라는 클래스에 각 캐릭터가 해야할 행위만 정의해 놓고, 각 각의 캐릭터에서 행위의 구체적으로 구현하게 만드는것입니다. 이렇게 구현하면 기존의 캐릭터의 변경이 필요하면, 변경이 필요한 캐릭터의 행위를 수정하고, 새로운 캐릭터가 필요하면 Newbie의 기능을 그대로 전달받아 새로운 캐릭터의 기능만 구현하면 다른 캐릭터의 기능에 영향을 끼치지 않습니다. 이렇게 변경에는 닫혀있고, 확장에는 열려있는 개발 원칙을 개방폐쇄원칙(OCP)라고 합니다.
개방폐쇄원칙을 사용하면 아래와 같은 장점이 있습니다.
- 기존 코드를 변경하지 않고도, 새로운 기능을 추가할 수 있습니다.
- 변경에 따른 영향 범위가 최소화 됩니다.
- 재사용성이 증가합니다
- 코드의 안정성이 높아집니다.
- 각각의 클래스에 대해 테스트가 용이해집니다.
2-3 Liskov Substitution Principle(리스코프 치환 원칙)
리스코프 치환 원칙이란...... 상위 타입의 객체를 하위 타입의 객체로 치환할 수 있어야한다는 원칙입니다.(이름부터 어렵습니다.). 그러니까 상위객체에서 상속 받은 하위객체 기능은 상위 객체로 대체하더라도 문제없이 사용할 수 있어야 한다라는 의미입니다. 이해를 돕기 위해, 예제로 설명해 보도록 하겠습니다.
중소기업 peaceWorld에서 자신들이 가진 안마 기술을 가지고, 전신 마사지기, 발 마사지기, 손 마사지기, 얼굴 마사지기를 출시했습니다. peaceWorld에서 생산한 모든 제품은 peaceCon 리모컨으로 컨트롤이 가능했습니다.
각 각의 제품에 맞게 안마 기능을 개선했고, 주무르기, 두드리기, 당기기와 같은 기능은 초기 모델의 기술을 그대로 가져왔습니다.
어느날 고객사가 방문하여, 마사지기를 체험을 요청했고, 다른 제품들이 모두 판매 혹은 생산중이라 원천 기술인 초기 모델만 체험이 가능했습니다. 그래서 고객사가 초기모델을 사용하려고, peaceCon의 주무르기를 눌렀는데 동작하지 않았습니다.
잘못 눌렸나 싶어, 이번에는 두드리기를 눌렀는데도 동작하지 않았습니다. 각 각의 제품에 맞게 개선하다보니 peaceCon의 주무르기와 두드리기가 초기모델에는 동작하지 않았던것입니다. 이렇게 peaceWorld는 신규 고객사와의 계약을 실패를 하게됩니다.
만일 리스코프 치환 원칙을 준수 했다면, 초기 모델의 주무르기, 두드리기 기능이 잘 동작하여 계약이 성사 될 수 있었을텐데 말입니다........
리스코프 치환 원칙은 상위 객체에서 상속 받은 하위 객체의 기능은 상위 객체로 대체되더라도 문제없이 사용할 수 있어야 한다는 것입니다. 이를 통해 프로그램의 고유 속성은 변경하지 않고도 상위 객체와 하위 객체를 자유롭게 교체할 수 있게 됩니다. 개발에서는 ArrayList나 Stack, LinkedList를 List 객체로 받아서 사용해도 add()와 clear()의 기능을 그대로 사용할 수 있는것이 대표적인 예입니다.
리스코프 치환 원칙을 준수하면 아래와 같은 장점을 얻을 수 있습니다.
- 상위 클래스와 하위 클래스간의 호환성이 보장되므로 코드의 일관성이 유지됩니다.
- 상위 클래스의 기능을 그대로 사용할 수 있기 때문에, 프로그램을 쉽게 확장할 수 있습니다.
- 상위 클래스의 코드를 하위 클래스에서 재사용할 수 있습니다.
- 이를 통해 중복 코드를 줄이고, 개발 생산성을 높일 수 있습니다.
2-4 Interface Segregation Principle(인터페이스 분리 원칙)
인터페이스 분리 원칙은 클래스가 사용하지 않는 메서드에 의존하고 있을때 분리해야한다라는 원칙입니다. 다른 원칙들에 비해 이름으로 원칙을 유추할 수 있는데, 일단 예시와 함께 설명해 보도록 하겠습니다.
조물주가 HelloWorld에 동물을 만드려고 합니다. 동물을 만들때 필요한 행위가 뭐가 있을지 고민하다가, 먹기,자기,배변하기와 같은 기초 행위를 넣어서 만들었습니다.
그러다 옆에 있던 다른 조물주가 "지구가 저렇게 넓은데, 돌아다닐 수 있도록 그럼 육지에서 달리거나, 바다에서 헤엄도 칠 수 있어야 하는거 아니야? 하늘을 날 수 있으면 더 좋구!!!". 이 말을 들은 조물주는 일리가 있다고 생각하여, 달리기, 헤엄치기, 날기도 넣었습니다.
이제 이 모든 행위들을 실체화 한 육지동물 "기린"과 해양동물 "고래", 비행동물 "두루미"을 만들었습니다. 조물주는 뿌듯해 했지만, 동물들은 불만이 많았습니다.
- 기린 : 나는 헤엄도 못 치고, 날지도 못 하는데 이런건 왜 준거야!!!
- 고래 : 나는 무거워서 달리지도, 날지도 못 하는데.... 가볍게나 만들어 주던가.....
- 두루미 : 헤엄치기.... 나 대충 만들어진건가....?
동물들의 불만 없이 조물주가 각 각의 동물을 만들 방법은 없었을까요? 만일 조물주가 인터페이스 분리 원칙을 알았다면 ,사용 할 행위만 받아서 불평을 없앨 수 있었을 것입니다. 아래의 그림은 인터페이스 분리 원칙을 따랐을때의 그림입니다.
각각의 행위를 인터페이스로 만들어서 각각의 동물에게 필요한 행위만 구현할 수 있게 만든다면, 모든 동물들이 사용하지 못하는 기능 때문에 불만 없이 사용할것입니다. 여기서 새로운 동물을 창조한다고 했을때도, 그 동물에 맞는 행위 인터페이스를 만들어 새로운 동물에서 구현하면 되기 때문에 모두가 만족하는 HelloWorld가 만들어 질것입니다.
인터페이스 분리 원칙을 준수하면 아래와 같은 장점이 있습니다.
- 각각의 인터페이스가 단일 책임을 가지므로 높은 응집도를 가집니다.
- 클래스에서는 자신이 필요한 기능만 구현하면 되므로 결합도가 낮아집니다
- 새로운 클래스 생성식 필요한 기능만 구현하면 되므로 유연성이 높아집니다.
- 꼭 필요한 클래스에서만 구현하므로 테스트 범위가 좁아져 테스트 비용이 감소합니다.
2-5 Dependency Inversion Principle(의존성 주입 원칙)
의존성 주입 원칙은 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다라는 원칙입니다. 의존이라는것이 다른 모듈을 가져와서 사용한다는 의미이고, 이때 일반적이고 추상적인것에 의존해야한다는 의미입니다. 이해를 돕기 위해 예시와 함께 설명해 보도록 하겠습니다.
HelloWorld라는 여행 회사를 다니는 가이드 김자바씨가 여행자 패키지를 기획하고 있습니다. 요즘 강아지를 키우는 사람들이 많아진것을 고려하여 " 반려견과 함께하는 제주도여행"이라는 여행 패키지 상품을 출시했습니다. 패키지 여행 출시 후 한껏 부푼 기대감을 안고 출근한 김자바씨는 빗발치는 문의전화를 받게 됩니다.
고양이를 키우는 나파이썬씨가 "저는 고양이랑 제주도 가고 싶은데 안 되나요?"라고 문의를 했고, 거북이를 키우는 단노드씨는 "저는 거북이와 제주도 앞바다를 보고 싶은데 안 되나요?"라고 문의를 했으며, 앵무새를 키우는 라루비씨는 "저는 앵무새와 같이 한라산 가고 싶은데, 어떻게 안 될까요?"라는 문의를 했다. 결국 김자바씨는 며칠밤을 세워 고양이와 함께하는, 거북이와 함께하는, 앵무새와 함께하는 제주도 여행 상품을 출시하고 기절했다고 합니다.
위 문제를 의존성 주입 원칙으로 해결한다면 어떻게 해결할 수 있을까요? "반려견과 함께하는 제주도 여행"을 "반려동물과 함께하는 제주도 여행"으로 바꾼다면, 모든 여행자의 니즈를 충족하며 김자바씨는 야근을 하지 않아도 되지 않았을까요?
의존성 주입 원칙을 준수하면 아래와 같은 장점을 얻을 수 있습니다.
- 의존성 주입을 통해 모듈간의 결합도를 낮출 수 있어 시스템의 유연성이 높아집니다.
- 의존성 주입을 사용하면 모듈을 독립적으로 테스트 할 수 있습니다.
- 의존성 주입을 통해 모듈간의 결합도가 낮아지므로, 시스템의 유지보수성이 향상됩니다.
- 새로운 기능을 추가할 때 기존 코드를 크게 수정하지 않고도 확장할 수 있습니다.
3. 객체지향 설계 원칙의 이점과 한계점
그럼 지금까지 배운 객체지향 설계 원칙을 정리하며 각 원칙의 장점과 단점에 대해서 정리해 보도록 하겠습니다.
- SRP(Single Responsibility Principle) - 단일 책임 원칙
- 장점:
- 코드 구조와 유지보수성 향상
- 코드 가독성과 이해도 향상
- 디버깅과 테스트 용이성 증대
- 단점:
- 클래스 수가 증가하여 복잡도가 높아질 수 있음
- 지나친 분리로 인한 과도한 복잡성 발생 가능
- 장점:
- OCP(Open/Closed Principle) - 개방 폐쇄 원칙
- 장점:
- 유지보수성 향상: 기존 코드 수정 없이 새로운 기능 추가 가능
- 확장성 증대: 새로운 요구사항에 대한 대응이 용이
- 재사용성 향상: 확장 가능한 모듈 설계
- 단점:
- 설계 복잡도 증가: 확장 가능한 구조를 설계하는 것이 어려울 수 있음
- 과도한 추상화로 인한 복잡성 증가 가능
- 장점:
- LSP(Liskov Substitution Principle) - 리스코프 치환 원칙
- 장점:
- 코드 재사용성 향상: 상위 클래스와 하위 클래스의 상호 교체 가능
- 유연성 증대: 상속 계층 구조 설계 시 유연성 확보
- 단점:
- 상속 계층 구조 설계의 어려움: 하위 클래스가 상위 클래스를 완전히 대체할 수 있도록 설계해야 함
- 상속 계층 구조 유지보수의 어려움: 상위 클래스 변경 시 하위 클래스에 미치는 영향 고려 필요
- 장점:
- ISP(Interface Segregation Principle) - 인터페이스 분리 원칙
- 장점:
- 클라이언트 코드의 유연성 향상: 클라이언트가 필요한 기능만 사용할 수 있음
- 유지보수성 향상: 인터페이스 변경 시 클라이언트 코드에 미치는 영향 최소화
- 단점:
- 인터페이스 수 증가로 인한 복잡성 증가 가능
- 인터페이스 설계의 어려움: 적절한 인터페이스 분리가 중요
- 장점:
- DIP(Dependency Inversion Principle) - 의존성 주입 원칙
- 장점:
- 높은 유연성: 모듈 간 결합도 감소로 독립적인 변경, 교체, 확장 가능
- 테스트 용이성: 모듈을 독립적으로 테스트할 수 있음
- 유지보수성 향상: 모듈 간 의존관계 명확화로 유지보수 용이
- 단점:
- 설계 복잡도 증가: 의존성 주입 메커니즘 구현이 복잡할 수 있음
- 초기 개발 비용 증가: 의존성 주입 구조 구축에 시간이 더 소요될 수 있음
- 장점:
이처럼 객체지향 설계 원칙이 각 각 장점과 단점을 가지고 있으므로, 요구사항을 분석하여 객체지향 설계 원칙을 사용하여 설계시 장/단점을 고려하여 이익이 극대화 될 때 사용해야합니다.
이렇게 객체지향 설계 5원칙에 대해서 알아봤습니다.
혹시라도 정정할 내용이나 추가적으로 필요하신 정보가 있다면 댓글 남겨주시면 감사하겠습니다.
오늘도 Jindory 블로그에 방문해주셔서 감사합니다.
[참고]
https://www.youtube.com/watch?v=KO2xdqOZSAs
< 이미지 참조 >
https://blog.naver.com/crowdpic/222050779927