The SOLID Principles
SOLID 원칙에 대해서 알아보자.
Last updated
SOLID 원칙에 대해서 알아보자.
Last updated
객체 지향을 올바르게 프로그램에 녹여내기 위한 원칙.
객체 지향 4대 특성을 제대로 활용한 결과로 당연히 나타나는 것이다.
SOLID를 적용하면 소스 파일의 개수는 더 많아지는 경향이 있으나, 이렇게 많아진 파일이 오히려 논리를 잘 분할하고, 잘 표현하기에 이해, 개발, 유지/관리 보수하기 쉬운 소스를 만든다.
SoC (Separation of Concerns, 관심사의 분리)도 SOLID를 언급할 때 빼놓을 수 없는 개념이다.
관심사가 같고 변경의 시기가 같은 것끼리 하나의 객체 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것이다.
SoC를 적용하면 자연스레 SRP, ISP, OCP에 도달하게 된다.
스프링 또한 SoC를 통해 SOLID를 적용하고 있다.
- <스프링 입문을 위한 자바 객체 지향의 원리와 이해>
어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다.
여자친구에게는 남자친구 역할, 부모님에게는 아들의 역할, 상사에게는 직원의 역할, 선생님에겐 학생 역할인 남자 클래스가 있다. 하지만 이렇게 많은 역할과 책임을 갖게 되면 피곤할 것이다. 객체지향에서 이런 경우 나쁜 냄새가 난다고 한다.
SRP를 적용하여 남자 클래스를 역할과 책임에 따라 분리해 남자친구, 아들, 직원, 학생 클래스로 분리하였다.
자신의 확장에는 열려 있고, 주변의 변화에는 닫혀 있어야 한다.
운전자 클래스와 마티즈 클래스가 있다고 하자. 마티즈를 타는 운전자는 마티즈 클래스에 의존한다. 열심히 마티즈에 적응하다가 어느 날 쏘나타가 생겼다. 창문, 기어가 수동이던 마티즈에서 창문, 기어가 자동인 쏘나카 클래스로 차종을 바꾸니 운전자의 행동에 변화가 온다. 기어수동조작()
메서드를 사용할 수 없고 기어자동조작()
메서드를 사용해야 하는 것이다.
OCP를 적용하여 운전자를 상위 클래스 또는 인터페이스인 자동차에 의존하게 하고, 그를 상속 또는 구현하는 마티즈와 쏘나타 클래스를 두고, 창문개방()
, 기어조작()
퍼블릭 인터페이스를 갖게 하도록 하자. 그렇게 하면 다양한 자동차가 생기더라도 운전자는 습관을 바꾸지 않아도 되며, 자동차 입장에서 자신의 확장에 열려있는 것이고, 주변의 변화에 닫혀 있는 것이다.
OCP의 예시로는 JDBC 인터페이스가 있다. JDBC 인터페이스를 구현하는 Oracle, MySQL, MS-SQL JDBC 드라이버 클래스 등이 있다. 이렇게 하면 DB가 바뀌더라도 connection을 설정하는 부분 말고는 바꿀 필요가 없다.
서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
위의 예시는 이상하다. 딸을 낳아 춘향이라고 한 것은 좋은데 아빠의 역할을 맡기고 있다. 다음 예시를 보자.
논리적인 흠이 없다. 펭귄 한마리가 태어나 이름을 뽀로로라 짓고, 동물의 행위(메서드)를 하는 데 전혀 이상함이 없다.
이런 식으로, 아버지 - 딸 구조(계층도/조직도)는 LSP를 위반하고 있는 것이며, 동물 - 펭귄 (분류도)는 LSP를 만족하는 것이다.
즉, LSP를 설명하자면 다음과 같다.
하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
잘 모르겠다면 할아버지부터 아들/딸 까지 내려가는 계층도/조직도와 동물부터 펭귄으로 내려가는 분류도를 떠올려 보자.
클라이언트와 서버 사이의 협력을 의무(obligation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계라고 부른다.
계약에 의한 설계는 다음 세 가지 요소로 구성된다.
사전조건(precondition): 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건.
사후조건(postcondition): 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 조건.
클래스 불변식(class invariant): 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 식.
LSP는 어떤 타입이 서브타입이 되기 위해서는 슈퍼타입의 인스턴스와 협력하는 '클라이언트'의 관점에서 서브타입의 인스턴스가 슈퍼타입을 대체하더라도 협력에 지장이 없어야 한다는 것을 의미한다. 따라서 계약에 의한 설계를 사용하면 LSP가 강제하는 조건을 계약의 개념을 이용해 좀 더 명확하게 설명할 수 있다.
서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 '계약'을 준수해야 한다.
서브타입에 더 강력한 사전조건을 정의할 수 없다. (e.g., 직사각형과 정사각형)
서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다.
서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다.
서브타입에 더 약한 사후조건을 정의할 수 없다.
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
위의 SRP 예시를 생각해보자. 남자 클래스를 분리하는 것이 아니라 다중인격화(?)시켜 인터페이스로 각 역할마다 행위를 제한시키는 것이다.
SRP와 ISP는 같은 문제에 대한 두 가지 다른 해법이라고 볼 수 있다. 프로젝트 요구사항과 설계자의 취향에 따라 SRP 와 ISP 중 하나를 선택해 설계할 수 있다. 하지만 특별한 경우가 아니라면 SRP를 선택하는 것이 더 좋은 해결책이라고 할 수 있다.
자신보다 변경하기 쉬운 것에 의존하지 마라.
자신보다 변하기 쉬운 것에 의존하던 것을, 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것.
예) 자동차가 스노우 타이어에 의존한다고 가정하자.
자동차는 한 번 사면 몇 년은 타야 하는데 스노우 타이어는 계절이 바뀌면 일반 타이어로 변경해야 한다.
자동차 자신보다 자주 변하는 스노우 타이어에 의존하면 부서지기 쉬운 설계가 된다.
자동차를 <<interface>>인 타이어에 의존하도록 하고, 타이어를 구현하는 스노우타이어, 일반타이어, 광폭타이어, ... 등을 만들자.
이제 타이어가 변경되어도 자동차는 영향을 받지 않게 된다.