원칙(Principle)은 중요합니다. 물론 저마다 우선하는 가치들이 다르기 때문에 사람에 따라 원칙보다는 융통성을 중요시하는 경우도 있지요.
소프트웨어 개발에서도 원칙은 존재합니다. 주로 이런 원칙은 학부 수업이나 전문 자료들을 통해 익힌 경우가 많은데요. 하지만 실제 서비스를 만들면서 바쁘게 기능을 추가하고 버그를 수정하느라 어느새 기억에서 잊히곤 합니다.
그렇게 한참을 정신없이 구현하다가 문득 코드를 돌아봤을 때 “이게 왜 여기에 있지?” 하는 생각이 들 때가 있는데요. 이런 의문이 든다면 한 번쯤은 원칙을 되새겨 보라는 신호가 아닐까요?
이 글에서는 Clean Architecture와 Clean Code 등의 저자로 유명한 Uncle Bob(Robert C. Martin)이 얘기하는 S.O.L.I.D Principles에 대해 얘기해 보려고 합니다.
SOLID원칙은 밥 아저씨가 2000년도 자신의 논문 Design Principle and Design Patterns에서 OOD(Object-Oriented Design)를 위해서 제안한 5가지 원칙의 앞 글자만 떼서 붙여졌습니다. Object-Oriented Design을 대상으로 제안된 원칙이지만 Agile 개발 등의 개발 방법론 핵심 철학에도 적용될 수 있는 개념들입니다.
S.O.L.I.D Principles
Single Responsibility Principle
Class 는 오직 한 가지의 책임이 주어져야 하고, 오직 한 가지 이유에서만 변경되어야 합니다. 보고서를 편집하고 출력하는 모듈을 예로 든다면 해당 모듈은 두 가지의 이유로 변경될 가능성이 있습니다. 바로 보고서 내용이 바뀌었거나 형식이 달라졌을 때 변경되어야 하죠.
편집 과정 때문에 모듈을 변경하다 보면 출력할 때에도 영향을 줄 가능성이 상당히 높기 때문에 이런 경우는 내용을 편집하는 모듈(i.e 내용을 담당하는 모듈)과 출력하는 모듈(i.e 형식을 담당하는 모듈) 두 가지로 나뉘어야 합니다.
“할 수 있다고 해서 해야 한다는 뜻은 아닙니다.”
Open / Closed Principle
Class, Module, Function 등의 소프트웨어 구성 요소는 확장(extension)에 대해 열려 있어야 하며, 변경(modification)일 경우 닫혀 있어야 합니다. 어떤 모듈이 Data Structure에 필드를 추가하거나 함수를 추가하는 등의 확장이 가능할 경우, 그 모듈은 ‘확장에 대해 열려있다’라고 표현합니다. 모듈이 별도의 수정 없이 다른 모듈에 의해 사용할 수 있다면 이는 닫혀 있다고 표현하죠.
public class CreditCard { private int cardType; public int getCardType() { return cardType; } public void setCardType(int cardType) { this.cardType = cardType; } public double getDiscount(double monthlyCost){ if (cardType == 1) { return monthlyCost * 0.02; } else { return monthlyCost * 0.01; } } }
위 CreditCard class 에 새로운 카드 타입을 추가하려고 하면 getDiscount 함수를 변경할 수밖에 없습니다. 이 경우 Open/Closed Principle 을 위반된다고 볼 수 있습니다.
“코트를 입기 위해서 개복 수술을 할 필요는 없으니까요.”
Liskov Substitution Principle
프로그램 상의 Object 들은 프로그램의 정확성을 해치지 않으면서 하위 타입의 Instance로 변경 가능해야 합니다. 하위 타입 함수 인자의 반공 변성(Contravariance), 하위 타입 함수 반환 타입의 공변성(Covariance), 상위 타입의 예외를 상속하지 않는 추가적인 예외 발생 금지 등의 요구 사항이 있습니다.
OOP(객체 지향 프로그래밍)에서 상속 개념을 학습할 때 이해를 위한 몇 가지 예시들이 있었을 텐데요. 재밌게도 우리가 타당하다고 생각하는 상속 예시들 중에 원칙에 어긋나는 경우가 더러 있었습니다. Liskov Substitution Principle을 위반하는 대표적인 예로 정사각형과 직사각형이 있죠. 얼핏 봤을 때 정사각형은 직사각형의 일종이니 Square가 Rectangle을 상속하는 것이 타당해 보이기도 합니다. 그런데 정말 그럴까요? Rectangle의 넓이를 구하는 함수의 테스트를 구성해 봅시다.
Rectangle rect = new Rectangle(); rect.setWidth(10); rect.setHeight(20); assertEquals(200, rect.getArea());
여기에 new Rectangle() 대신에 new Square()가 rect 에 할당되면 어떻게 될까요? 넓이는 400을 반환하기 때문에 테스트는 실패하겠죠. 정사각형이 직사각형을 상속받으면 Liskov Subsitution Principle을 위반한다고 볼 수 있습니다.
상속은 문제 해결에 있어서 상당히 매력적이지만 오용될 가능성도 매우 높습니다.
“오리처럼 생기고 오리처럼 꽥꽥 거리더라도, 배터리가 필요하다면 오리가 아닙니다.”
Interface Segregation Principle
여러 방면에서 두루 사용 가능한 하나의 interface 보다 특정 클라이언트를 위한 여러 개의 interface 가 더 나을 때도 있습니다.
Xerox는 Stapling, Fax 등 여러 기능이 포함된 최신형 프린터 소프트웨어를 개발하는 과정에서 프로그램이 번잡해져 더 이상 프로그램 개발이 어려운 상황에 직면하게 됩니다. 결국 밥 아저씨에게 도움을 청하는데요. 알아보니 문제는 Job Class 하나가 모든 기능을 다 구현하는 데 있었습니다. 이 비대한 Class 는 Client 입장에서 사용하지 않을 함수들도 모두 알 수 있게 구성되어 있었죠.
밥 아저씨는 이 문제를 Interface Segregation Principle을 적용해서 각 Client가 꼭 사용해야 하는 함수들만을 가지고 각각의 interface를 따로 만들었습니다. 그리고 아래에서 소개할 Dependency Inversion Principle을 통한 기능 구현으로 문제를 해결하게 됐죠.
Dependency Inversion Principle
“추상화에 의존해야지, 구체화에 의존하면 안됩니다.”
상위 계층의 모듈은 하위 계층을 구현하는 것이 아닌 추상화에 의존해야 합니다. 무슨 말이냐 하면 상위 계층이 하위 계층 구현에 의존하던 전통적인 관계를 역전 시킴으로써 구현 방법을 독립되게 할 수 있다는 것이죠. Dependency Injection이 바로 이 원칙을 따르는 방법 중 하나입니다.
Conclusion
세상에 나쁜 프로그램은 있습니다. 눈에 보이는 기능이 똑같다고 같은 프로그램은 아니라는 말이죠. 생각보다 많은 코드들이 “그곳에 넣을 수 있기 때문에”, “그곳에 넣어도 돌아가기 때문에”라는 이유로 아무 위치에나 정착해 있습니다. 기능을 좀 더 빨리 추가해 당장은 주변 사람들의 박수를 받을 순 있어도, 나중에는 더 이상 손댈 수 없을 만큼 문제가 생길 수 있습니다. 결국엔 모두 엎은 다음에 또다시 같은 코드들을 만들게 되겠죠.
쉬운 코드가 가장 만들기 어려운 코드이고 좋은 코드는 원칙으로부터 나옵니다. 변화에 적응하는 프로그램, 의도가 쉽게 읽히는 프로그램, 문제 발생 가능성이 적은 프로그램, 확장하기 쉬운 프로그램 등 좋은 프로그램을 만드는 것은 우리의 최종 목표를 이루는데 정말 중요한 과정 중 하나입니다. 이는 그저 경험이나 Tweak만으로 이뤄지지 않습니다.
여태까지 새로운 기술들과 Framework를 섭렵하고 경험을 쌓는 시간들만 가져왔다면, 가끔씩은 기본으로 돌아가 원칙을 생각하는 시간을 가져보는 것은 어떨까요?
*버즈빌에서 활기찬 개발자를 채용 중입니다. (전문연구요원 포함) : 링크 참고
[fbcomments url=”http://ec2-13-125-22-250.ap-northeast-2.compute.amazonaws.com/2018/12/07/buzzvil-principles/” width=”100%” count=”off” num=”5″ countmsg=”wonderful comments!”]