객체 지향 설계 5원칙: SOLID
객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계하는 방법과 원칙이 존재한다.
SOLID
객체 지향 설계(OOD)의 정수라고 할 수 있는 5원칙이다.
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
SOLID는 소프트웨어에 녹여 내야 하는 개념이다.
SOLID를 잘 녹여낸 소프트웨어는 상대적으로 이해하기 쉽고, 리팩터링과 유지보수가 수월할 뿐만 아니라, 논리적으로 정연하다.
또한 SOLID는 객체지향의 4대 특성을 발판으로 하고 있으며, 디자인 패턴의 뼈대이며 스프링 프레임워크의 근간이다.
결합도와 응집도
좋은 소프트웨어 설계를 위해서는 결합도는 낮추고 응집도는 높이는 것이 바람직하다.
- 결합도 : 모듈(클래스)간의 상호 의존 정도
- 결합도가 낮으면 모듈 간의 상호 의존정도가 줄어들어 객체의 재사용이나 수정, 유지보수가 용이하다.
- 응집도 : 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성
- 응집도가 높은 모듈은 하나의 책임에 집중하고, 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다.
SRP - 단일 책임 원칙
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C.마틴
예시 1 : 하나의 클래스에 역할과 책임이 너무 많은 경우
한 클래스에 역할과 책임이 너무 많아서 code smell이 난다..
클래스를 각각의 역할과 책임에 따라 분리하자!
- 클래스 뿐만 아니라 속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크도 대상이 될 수 있다.
class Pet {
int age;
void meow(){
System.out.println("야옹");
}
void bark(){
System.out.println("월월");
}
}
...
Pet cat = new Pet();
Pet dog = new Pet();
dog.meow(); // 잘못됨
Pet 클래스를 고양이 클래스와 강아지 클래스로 분리하고, 공통적인 부분은 Pet 클래스에 두고 차이점은 Pet 클래스를 상속하여 각자 구현한다.
class Pet {
int age;
}
class Cat extends Pet{
void meow(){
System.out.println("야옹");
}
}
class Dog extends Pet{
void bark(){
System.out.println("월월");
}
}
...
Pet cat = new Cat();
Pet dog = new Dog();
cat.age = 3;
cat.meow();
dog.age = 3;
dog.bark();
예시2 : if문이 자주 사용되는 경우
하나의 속성이 여러 의미를 갖지 않는 경우도 단일 책임 원칙을 지키지 못하는 경우이다.
DB에서 하나의 필드가 토지일 경우에는 면적으로, 건물일 경우 층수를 나타낼 경우는 역할에 따라 분리가 되지 않으므로 SRP를 지키지 못한다.
이 경우 if문이 자주 사용되어 code smell이 난다 .ㅠ
메서드에서 if문(분기문)이 자주 사용되는 경우 SRP가 지켜지지 않았는지 확인해보아야한다.
class Pet {
String kind;
int age;
void makeSound(){
if (this.kind.equals("고양이")){
System.out.println("야옹");
} else if (this.kind.equals("강아지")){
System.out.println("월월");
}
}
}
동물이 고양이인지 강아지인지에 따라 makeSound() 메서드에서 분기 처리가 진행된다.
동물의 makeSound() 메서드가 고양이와 강아지의 행위를 모두 구현하려고 하기 때문에 너무 많은 책임이 있어 SRP를 위배한다.
class Pet {
int age;
}
class Cat extends Pet{
void makeSound(){
System.out.println("야옹");
}
}
class Dog extends Pet{
void makeSound(){
System.out.println("월월");
}
}
if문으로 분기문을 작성하기보다는 각 클래스를 역할과 책임에 따라 분리하고 오버라이딩하자.
SRP와 추상화
단일 책임 원칙과 가장 관계가 깊은 것은 모델링 과정을 담당하는 추상화이다.
애플리케이션 경계를 정하고 추상화를 통해 클래스를 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하자.
또한 리팩토링을 할 때도 SRP를 적용할 곳이 있는지 살피자.
OCP - 개방 폐쇄 원칙
소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에 대해서는 열려있어야 하지만 변경에 대해서는 닫혀있어야한다. - 로버트 C.마틴
=> 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야한다.
예시 1
운전자는 자동차가 바뀔때마다 다른 메서드(운전 습관)을 사용해야한다.
상위 클래스와 인터페이스를 중간에 둠으로써 다양한 자동차를 몰아도 운전 습관에 영향을 받지 않게 된다. (호출하는 메서드를 변경하지 않아도 된다.)
=> 자동차 입장에서, 자신의 확장에는 개방돼 있고 운전자 입장에서는 주변(자동차)의 변화에 폐쇄돼있다.
예시 2
- JDBC : 데이터베이스를 바꾸더라도 연결 설정하는 부분 외에는 따로 수정할 필요가 없다.
=> 애플리케이션의 데이터베이스를 교체(확장)하더라도 주변의 변화에 닫혀있다. (코드 수정할 영역이 적다) - Java
- java 소스코드는 여러 운영체제에서 구동될 수 있다. (확장에 열려있다.)
- 자바 개발자는 작성하는 소스코드가 어떤 운영체제에서 구동될지 알 필요가 없다.(변화에 닫혀있다)
Java 소스코드는 Java 컴파일러에 의해 모든 운영체제에서 사용가능한 바이트코드(.class)로 변환되며 각 운영체제별 JVM은 JIT(Just In Time)를 통해 실행시점에 운영체제에 맞는 기계어로 번역한다.
예시 3
직원이 누구던(확장에 열려있다) 손님의 구매 행위는 영향을 받지 않는다(변화에 닫혀있다).
OCP 결론
OCP를 통해 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성을 얻을 수 있다.
LSP - 리스코프 치환 원칙
서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다. - 로버트 C.마틴
하위 클래스 is kind of 상위 클래스
> 하위 클래스는 상위 클래스의 한 종류다.
구현 클래스 is able to 인터페이스
> 구현 클래스는 인터페이스 할 수 있다.
객체지향의 상속을 잘 지켜 구현된 프로그램이라면 LSP를 잘 지킨다고 할 수 있다.
펭귄은 동물로 교체할 수 있어야 한다.
즉, 하위 클래스의 인스턴스는 상위 클래스 타입의 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
ISP - 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다. - 로버트 C.마틴
한 클래스에 역할과 책임이 너무 많을 경우
- SRP(단일 책임 원칙)을 적용해 여러 클래스로 분리하거나
- ISP(인터페이스 분리 원칙)을 적용하여 역할에 따라 작은 인터페이스로 분리하여 구현할 수 있다.
하지만 특별한 이유가 아니라면 SRP를 적용하는 것이 좋다.
클래스 설계는 책임의 분리에서 출발한다.
각 클래스가 SRP에 맞춰 잘 분리되어있으면, 인터페이스도 클래스에 맞춰 각 역할에 따라 분리된다.
남자 클래스의 역할을 여러개의 구체적이고 작은 인터페이스로 분리하여 여자친구에게는 남자친구 역할을, 어머니에게는 아들 역할을 수행하도록 한다.
큰 범용 인터페이스 (남자 인터페이스)를 두는 것보다 구체적이고 작은 인터페이스 여러개로 나누면 각 클라이언트는 필요한 인터페이스만 구현할 수 있어 불필요한 의존성을 줄이고 결합도를 낮출 수 있다.
굳이 클라이언트가 이용하지 않는 메서드에 의존할 필요가 없도록 인터페이스를 쪼개자.
인터페이스 최소 주의 원칙
인터페이스를 통해 메서드르 외부에 전달할 때는 최소한의 메서드만 제공하자.
직장상사 인터페이스에 굳이 아들 인터페이스의 효도하기() 메서드를 제공할 필요는 없다.
상위 클래스는 풍성할수록, 인터페이스는 작을 수록 좋다.
- 상위 클래스가 하위 클래스들이 공통으로 가질 수 있는 속성과 메서드를 가져 상속한다면, 굳이 하위 타입으로 캐스팅할 필요 없이 공통 속성을 상위 클래스에서 바로 접근할 수 있다.
- 인터페이스가 작고 특정 목적에 맞춰져 있으면 클라이언트는 필요한 메서드만을 의존하므로 의존성이 명확하다. 또한 작은 인터페이스로 분리해두기 때문에 다양한 방식으로 조합할 수 있어 유연성이 증가한다. 또한 테스트시에도 테스트 대상 메서드만 mocking하거나 stub 작성하면 되므로 용이하다.
DIP - 의존 역전 원칙
- 고차원 모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야한다.
- 추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야한다.
- 자주 변경되는 구체(concrete) 클래스에 의존하지 마라.
자동차가 자신보다 자주 바뀌는 스노우타이어에 의존하여 타이어를 바꿀 때마다 자동차 코드도 변경해야 할 수 있다.
추상화된 타이어 인터페이스에만 의존하게 함으로써 스노우타이어가 아닌 다른 타이어로 변경되어도 자동차는 그 영향을 받지 않는다.
위 설명은 OCP의 설명과도 같은데, 원칙들은 서로 연관되고 녹아들어간 경우가 많다.
- 스노우타이어는 원래 아무것도 의존하지 않았지만 타이어 인터페이스를 의존하게 되었다.
- 자동차는 스노우타이어에 의존하다가 스노우타이어 중간에 인터페이스를 두어 의존관계를 역전시켰다.
=> 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙(DIP)이다.
DIP 결론
즉 DIP는 자신보다 변하기 쉬운 것에 의존하지 마라 라는 뜻이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하자.
SOLID 결론
SOLID는 객체 지향 4대 특성(캡상추다)를 활용한 결과이다.
SoC(Separation Of Concerns) : 관심사의 분리
관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라.
=> 하나의 속성, 메서드, 클래스, 모듈 또는 패키지에는 하나의 관심사만 들어있어야한다.
SoC를 적용하면 SRP, ISP, OCP에 도달하게 된다.
스프링 또한 SoC를 통해 SOLID를 극한까지 적용하고 있다.
- SRP(단일 책임 원칙) : 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- OCP(개방 폐쇄 원칙) : 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
- LSP(리스코프 치환 원칙) : 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
- ISP(인터페이스 분리 원칙) : 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.
- DIP(의존 역전 원칙) : 자신보다 변하기 쉬운 것에 의존하지 마라.
SOLID 원칙을 적용하면 소스 파일의 개수는 더 많아질 수 있지만, 논리를 더 잘 분할하고 이해하기 쉬우며 유지보수하기 쉽다.
Reference
https://m.yes24.com/Goods/Detail/17350624
+ 책 내용 기반으로 추가적으로 내용을 작성했는데 오개념이 있다면 지적 부탁드립니다~!!