객체 지향 설계 5원칙이란 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 작성해 나가는 방법이나 원칙을 말한다.
객체 지향 설계 5원칙은 다음과 같다.
이 원칙들은 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Couplig)는 고전 원책을 객체 지향의 관점에서 재정립한 것이다.
결합도와 응집도
좋은 소프트웨어 설계를 위해서는 결합도(coupling)은 낮추고 응집도(cohesion)은 높이는 것이 바람직하다.
결합도는 모듈(클래스)간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이하다.
응집도는 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다.
"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다" - 로버트 C. 마틴
클래스를 역할에 따라 분리해서 각각 하나의 역할과 책임만 갖게 하는 것이 단일 책임 원책이다.
클래스 뿐만 아니라 단일 책임 원칙은 속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있다.
사람클래스를 상속하는 남자클래스와 여자클래스가 있다고 가정하자. 남자클래스와 여자클래스에 공통점이 없다면 사람 클래스는 제거하고, 공통점이 많다면 사람클래스를 상위 클래스로 해서 공통점을 상위 클래스에 두고 남자클래스와 여자클래스는 사람클래스를 상속받고 차이점만 각자 구현하면 된다.
하나의 속성이 여러 의미를 갖는 경우도 단일 책임 원칙을 지키지 못하는 경우가 될 수 있고, 메서드가 단일 책임 원칙을 지지 않았을 때 나타나는 대표적인 현상인 분기 처리를 위한 if문이 나타날 수도 있다.
단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화
이다. 애플리케이션 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들여야 한다. 또한 리팩터링을 통해 코드를 개선할 때도 단일 책임 원칙을 적용할 곳이 있는지 살펴보아야 한다.
"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다." - 로버트 C.마틴
위 문장을 조금 더 의역해보면 아래와 같은 문장을 이끌어 낼 수 있다.
"자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다."
만약 운전자가 차를 바꾼다고 하면 운전자는 자신이 운전하던 습관을 차에 맞추어 바꾸어야 하는가?
개방 폐쇄 원칙에 의하면 객체 지향 세계에는 다음과 같은 해결 방법이 있다.
위의 그림과 같이 상위 클래스 또는 인터페이스를 중간에 둠으로써
다양한 자동차가 생긴다고 하더라도 운전자는 운전 습관을 바꾸지 않아도 된다. 다양한 자동차가 생긴다고 하는 것은 자동차 입장에서는 자신의 확장에는 개방돼 있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄돼 있는 것이다.
데이터베이스 프로그래밍을 할 때 JDBC 인터페이스를 사용하더라도 Connection 설정 부분을 별도의 파일로 분리해두면 Connection을 설정하는 부분외에는 클라이언트 코드는 변경할 필요가 없다. 오라클을 MySQL이나 MS-SQL로 교체할 때 자바 애플리케이션은 JDBC 인터페이스라고 하는 완충장치로 인해 변화에 영향을 받지 않는다. 바로 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에는 닫혀 있는 것이다. 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것이다.
자바에서도 개방 폐쇄 원칙이 적용돼 있다. 자바 개발자는 작성하고 있는 소스코드가 어떤 운영체제에서 구동될지 걱정하지 않아도 된다. 각 운영체제별 JVM과 목적 파일(.class)가 있기 때문에 자신이 작성하고 있는 코드에만 신경쓰면 된다. 개발자가 작성한 소스코드는 운영체제의 변화에 닫혀있고, 각 운영체제별 JVM은 확장에 열려있는 구조가 되는 것이다. 개발자의 소스코드와 운영체제의 JVM 사이에는 목적파일이라고 하는 완충 장치가 있는 것이다.
"서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다." - 로버트 C. 마틴
객체 지향의 상속은 다음의 조건을 만족해야 한다.
즉, 객체 지향에서의 상속은 조직도가 계층도가 아닌 분류도가 되어야 한다.
위 두 문장대로 구현한 프로그램은 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다. 그러나 위 문장대로 구현되지 않은 코드가 존재할 수 있는데 바로 상속이 조직도나 계층도 형태로 구현된 경우다.
다음은 상속이 조직도나 계층도 형태로 구현된 경우이다. 이 경우에는 상위 클래스의 객체 참조 변수에는 하위 클래스의 인스턴스를 할당할 수 있다.
아버지 춘향이 = new 딸();
춘향이는 아버지형 객체 참조 변수이기 때문에 아버지 객체가 가진 행위(메서드)를 할 수 있어야 한다. 그러나 분류도 형태인 경우를 살펴보자.
동물 뽀로로 = new 펭귄();
뽀로로는 동물형 객체 참조 변수에 펭귄 객체를 할당해서 생성되었다. 이 경우에는 뽀로로가 동물의 행위(메서드)를 하게 되는데 전혀 이상함이 없다. 아버지 - 딸 구조(계층도/조직도)는 리스코프 치환 원칙을 위배하고 있는 것이며, 동물 - 펭귄 구조(분류도)는 리스코프 치환 원칙을 만족하는 것이다. 로빈 C. 마틴의 말을 의역하고 결론을 내리면 다음과 같다.
"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스 인스턴스의 역할을 하는데 문제가 없어야 한다."
분류도 형태의 상속의 경우 하위에 존재하는 것들은 상위에 있는 것들의 역할을 하는데 전혀 문제가 없다. 결국 리스코프 치환 원칙은 객체 지향의 상속
이라는 특성을 올바르게 사용하면 자연스럽게 얻게 되는 것이다.
"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다." - 로버트 C.마틴
단일 책임 원칙에 의해 남자 클래스를 남자친구, 사원, 아들, 소대원으로 다수의 클래스로 바꿀 수 있다.
인터페이스 분리 원칙을 사용하면 다음과 같이 남자 클래스의 역할을 제한할 수 있다.
결론적으로 단일 책임 원칙(SRP)와 인터페이스 분리 원칙(ISP)는 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 프로젝트 요구사항과 설계자의 취향에 따라 단일 책임 원칙이나 인터페이스 분할 원칙 중 하나를 선택해서 설계할 수 있다. 하지만 특별한 경우가 아니라면 단일 책임 원칙을 사용하는 것이 더 좋은 해결책이라고 할 수 있다.
인터페이스 분할 원칙을 이야기할 때 항상 함께 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙
이 있다. 인터페이스를 통해 메서드를 외부에 공개할 때는 최소한의 메서드만 제공하라는 것이다. 남자친구 인터페이스에 사격하기() 메서드를 제공할 필요도 없고 제공해서도 안된다는 것이다.
또한 상위 클래스는 풍성할수록 좋고 인터페이스는 작을수록 좋다고 했다. 상위 클래스가 풍성할수록 좋은 이유는 리스코프 치환원칙에 의해 하위클래스가 상위클래스의 역할을 할 수 있는데, 상위 클래스가 풍성하면 상위 클래스형의 참조 변수와 추상메서드를 이용했을 때 불필요한 형변환이 발생하지 않는다. 또한 인터페이스가 작을수록 좋은 이유는 인터페이스 분리 원칙에 의해 인터페이스는 그 역할에 충실한 최소한의 기능만 공개해야 하기 때문이다.
"고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라." - 로버트 C.마틴
자동차와 스노우타이어 사이에는 아래 그림처럼 의존 관계가 있다. 자동차가 스노우타이어에 의존한다.
자동차는 한 번 사면 몇 년은 타야하는데 스노우타이어는 계절이 바뀌면 일반타이어로 교체해 주어야 한다. 이런 경우 스노우타이어를 일반타이어로 교체할 때 자동차는 그 영향에 노출되어 있음을 알 수 있다. 자동차 자신보다 더 자주 변하는 스노우타이어에 의존하는 것이다.
의존 역전 원칙을 적용하면 다음과 같이 바꿀 수 있다.
위의 그림과 같이 자동차가 구체적인 타이어들(스노우타이어, 일반타이어, 광폭타이어)가 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 스노우타이어에서 일반 타이어로, 또는 다른 구체적인 타이어로 변경돼도 자동차는 이제 그 영향을 받지 않는 형태로 구성된다.
개방 폐쇄 원칙에서도 동일한 해결책으로 상위 클래스나 인터페이스를 두고 그에 의존하게 했었다.
처음의 그림에서는 스노우타이어가 그 무엇에도 의존하지 않는 클래스였는데 두번째 그림에서는 추상적인 것인 타이어 인터페이스에 의존하게 되었다. 바로 의존의 방향이 역전된 것이다. 그리고 자동차는 자신보다 변하기 쉬운 스노우타이어에 의존하던 관계를 중간에 추상화된 타이어 인터페이스를 추가해 두고 의존 관계를 역전시키고 있다. 이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.
의존 역전 원칙을 의역해 보면 다음과 같다.
"자신보다 변하기 쉬운 것에 의존하지 마라."
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스에 의존하라는 것이 바로 의존성 역전 원칙이다.
SoC는 관심사의 분리(Separation of Concerns)의 약자이다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것이다. 하나의 속성, 하나의 메서드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어 있어야 한다는 것이 SoC이다. 관심사가 다르고 변화의 시기가 다르면 분리해야 한다는 것이다. SoC를 적용하면 자연스럽게 단일 책임 원칙(SRP), 인터페이스 분리 원칙(ISP), 개방 폐쇄 원칙(OCP)에 도달하게 된다.
참고