Goal
- 객체지향 설계의 5원칙이 무엇인지 이해한다.
객체지향 설계 5 원칙 - SOLID
객체 지향 설계의 정수라고 할 수 있는 5원칙이 있는데, 바로 SOLID다. 응집도는 높이고, 결합도는 낮추라는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 할 수 있다.
- SRP - Single Responsibility Principle 단일 책임 원칙
- OCP - Open Closed Principle 개방 폐쇄 원칙
- LSP - Liscov Subsititution Principle 리스코프 치환 원칙
- ISP - Interface Segregation Principle 인터페이스 분리 법칙
- DIP - Dependency Inversion Principle 의존 역전 원칙
왜 객체지향 설계 5원칙을 사용해야 할까?
- SOLID를 잘 녹여낸 소프트웨어는 그렇지 않은 소프트웨어에 비해 상대적으로 이해하기 쉽다
- 리팩터링과 유지보수가 용이 => 유연하며 확장이 쉽다
- 논리정연하다
- SOLID는 객체 지향 4대 특성을 발판으로 하며 디자인 패턴의 뼈대이자, 스프링 프레임워크의 근간이다
SRP - Single Responsibility Principle 단일 책임 원칙
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다 - 로버트C.마틴
작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어야 한다는 원칙이다.
SRP를 적용하면 책임 영역이 확실해지기 때문에, 한책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있다.
뿐만 아니라 책임을 적절히 분배하여 코드의 가독성 향상, 유지보수성을 높이는 이점까지 누려 OCP 원칙 뿐아니라 다른 원리들을 적용하는 기초가 된다.
적용 방법
1. 여러 원인에 의한 변경 : Extract Class를 통해 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 하는 것이다. 관건은 책임만 분리하는 것이 아닌, 분리된 두 클래스 간의 관계 복잡도를 줄이도록 설계하는 것이다. 만약 Extract Class된 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 가지고 있다면 Extract Superclass를 사용할 수 있다. 이것은 Extract된 각각의 클래스들의 공유되는 요소를 상위 클래스로 정의하여 상위 클래스에 위임하는 기법이다. 따라서 각각의 Extract Class들의 유사한 책임들은 부모에게 명백히 위임하고 다른 책임들은 각자에게 정의할 수 있다.
2. 산탄총 수술 : Move Field와 Move Method를 통해 책임을 기존의 어떤 클래스로 모으거나, 이럴만한 클래스가 없다면 새로운 클래스를 만들어 해결할 수 있다. 즉 산발적으로 여러 곳에 분포된 책임들을 한 곳에 모으면서 설계을 깨끗하게 한다. 즉, 응집성을 높이는 작업이다.
적용 사례
=> 그림1에서 보는 것과 같이 serialNumber는 변화요소라 할 수 없고 단지 고유정보이다. 동종의 다른 클래스와 구분되는 정보!
그외의 price, maker, type 등은 모두 특성 정보군으로 변경이 발생할 수 있는 부분이며, 변화요소로 예상이된다.
따라서 특정 정보군에 변화 발생시 해달 클래스를 수정해야하는 부담이 되므로, 이 부분이 SRP의 대상이 된다
=> 그림2에서 보는 것과 같이 변화가 예상되는 특성 정보군을 분리한 것이 확인된다. 따라서 특성 정보에 변경이 일어나면 GuitarSpec 클래스만 변경하면 된다. 보기에도 좋고, 변화에 의해 변경되는 부분을 한 곳에서 관리할 수 있게 되었다.
public class Dog {
final static Boolean male = true;
final static Boolean female = false;
Boolean sex;
void poo(){
if(this.sex == male) {
//한쪽 다리를 들고 소변을 본다
}else{
// 뒷다리 두개를 굽혀 낮은 자세로 수변을 본다
}
}
}
참고
단일 책임 원칙을 위배하는 경우, 대표적으로 분기 처리를 하기위한 If문을 볼 수 있다.
OCP - Open Closed Principle 개방 폐쇄 원칙
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려있지만 변경에 대해서는 닫혀있어야 한다. -로버트 C.마틴
=> 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
=> 객체의 확장에는 개방적, 객체의 수정에는 폐쇄적 -> 다른 클래스 전이, 전파는 열려 있다.
=> 기능이 변하거나 확장되는 것은 가능하지만, 그 과정 자체에서 기존의 코드가 수정되지 않아야 하고 기존 요소를 쉽게 확장하여 재사용
=> 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야한다
- ex) 라이브러리, jdbc, jvm
OCP는 관리가능하고 재사용 가능한 코드를 만드는 기반이자, OCP를 가능케 하는 중요 메커니즘은 추상화와 다형성이다. 이는 객체지향의 장점을 극대화하는 중요한 원리라고 할 수 있다.
적용 방법
- 변경(확장)될 것과 변하지 않을 것을 엄격히 구분한다.
- 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.
- 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성 한다.
적용 사례
기타 외에 바이올린, 첼로와 같은 다른 악기가 다루어야 한다면, OCP를 적용하지 않느다면 매번 새로운 악기와 요소를 만들어야 한다.
기타와 추가 될 다른 악기들을 추상화하는 작업이 필요하고, 여기서 추가될 악기들의 공통 속성을 모두 담을 수 있는 StringInstrument라는 인터페이스를 생성한다. 이는 새로운 악기가 추가되면서 변경이 발생되는 부분을 추상화하여 분리 하였음을 확인할 수 있다.
이렇게 해서 코드의 수정을 최소화하여 결합도는 줄이고 응집도는 높이는 효과를 볼 수 있다.
참고
확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 복잡해질 수 있다. 인터페이스는 가능하면 변경되어서는 안되고, 정의시 여러 경우 수에대한 고려와 예측이 필요하다. 인터페이스 설계에서 적당한 레벨을 선택해야 한다.
'추상화란 다른 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징'이라고 정의한다. 즉, 이 '행위'에 대한 본질적인 정의를 통해 인터페이스를 식별해야한다.
LSP - Liscov Subsititution Principle 리스코프 치환 원칙
서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다 -로버트 C.마틴
하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
달리 말하면 서브 타입은 기반 타입이 약속한 규약을 지켜야한다. 상속은 구현상속이든 인터페이스 상속이든 궁극적으로는 다형성을 통한 확장성 획득을 목표로한다. 다형성과 확장성을 극대화하기 위해서는 하위 클래스의 사용보다 상위 클래스(인터페이스)를 사용하는 것이 더 좋다. 상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 is a (is a kind of)관계가 있을 경우만 제한되어야하고, 그 외의 경우는 composition(합성)을 이용한 재사용을 해야한다. 상속은 다형성과 따로 생각 할 수 없으며, 다형성으로 인한 확장 효과를 얻기 위해서는 서브 클래스가 기반 클래스와 클라이언트 간의 규약(인터페이스)를 어겨서는 안된다. 결국 이 구조는 다형성을 통한 확장의 원리인 OCP를 제공하게 되며, LSP는 OCP를 구성하는 구조가 된다.
- 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스를 할 수 있어야한다.
- LSP를 만족시키는 것은 재정의하지 않는 것이다 -> 상위 클래스에서 상속받은 메서드들이 하위클래스에서 오버라이딩 되지 않도록한다.
적용 방법
- 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다
- 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현 한다(인터페이스 상속)
- 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만든다
- 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현 상속을 사용한다
ISP - Interface Segregation Principle 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다. 로버트 C.마틴
ISP원리는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리다. 즉, 어떤 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 한다. '하나의 일반적인 인터페이스보다는, 여러개의 구체적인 인터페이스가 낫다'라고 정의할 수 있다.
SRP가 클래스의 단일 책임을 강조한다면 ISP는 인터페이스의 단일 책임을 강조한다. 하지만 ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 것을 인정한다. 이러한 경우 ISP가 사용되는데, SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달한다.
적용 방법
- 클래스 인터페이스를 통한 분리
- 클래스의 상속을 이용하여 인터페이스를 나눌 수 있다. 이 같은 구조는 클라이언트에게 변화를 주지 않고, 인터페이스를 분리하는 효과를 갖는다. 하지만 모든 객체지향 언어에서는 상속을 이용한 확장은 상속받는 클래스의 성격을 디자인 시점에 규정해버린다.따라서 인터페이스를 상속받는 순간 인터페이스에 예속되어 서비스 성격이 제한된다
- 객체 인터페이스를 통한 분리
- 위임을 이용하여 인터페이스를 나눌 수 있다. 위임이란, 특정 일의 책임을 다른 클래스나 메소드에 맡기는 것이다. 만약 다른 클래스의 기능을 사용해야 하지만 그 기능을 변경하고 싶지 않다면, 상속 대신 위임을 사용한다.
DIP - Dependency Inversion Principle 의존 역전 원칙
고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.
추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.
자주 변경되는 구체(Concrete)클래스에 의존하지 말라 - 로버트 C.마틴
의존 관계의 역전이란 구조적 디자인에서 발생하던 하위 레벨 모듀의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의역전입니다. 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존 역전의 원칙이다.
적용 방법
Layering 즉, 잘 구조화된 객체지향 아키텍처들은 각 레이어마다 잘 정의되고 통제되는 인터페이스를 통한 긴밀한 서비스들의 집합을 제공하는 레이어들로 구성되어 있다. 이것은 단순히 레이어를 통한 구조화만을 뜻하는 것이 아닌 Transitive Dependency가 발생했을 때 상위 레벨의 레이어가 하위 레벨의 레이어를 바로 의존하게 하는 것이 아니라 이 둘 사이에 존재하는 추상레벨을 통해의존해야 할 것으로 말하고 있다. 이를 통해 상위레벨의 모듈은 하위레벨의 모듈로의 의존성에서 벗어나 그 자체로 재사용 되고 확장성도 보장받을수 있다.
적용 사례
참고
스프링 입문을 위한 자바 객체지향의 원리와 이해
https://www.nextree.co.kr/p6960/