스프링을 공부하면서, 객체 지향 설계 5원칙에 대해서 학습을 했었다.
그 글도 함께 보면서 공부를 해보자~!
객체 지향 설계 5원칙을 알아보자.
SOLID를 잘 지키면 확장에는 열려있고 변경에는 닫혀있으며 유지보수가 쉬워진다.
👉 잘 지키지 않으면 유지보수가 어려워진다.
🤔 그 전에! SOLID를 왜 지켜야할까?
📌 SRP: 단일 책임 원칙
📌 OCP: 개방 폐쇄 원칙
📌 LSP: 리스코프 치환 원칙
📌 ISP: 인터페이스 분리 원칙
📌 DIP: 의존 역전 원칙
📖 클린 소프트웨어 : Part 2 에자일 설계 부분
📖 UML 실전에서는 이것만 쓴다. : 6장 에자일 설계 부분
🖥️ 지다넷의 "객체 지향 SW 설계의 원칙"
이 글에 대해서는 다시 읽어보면 좋을 것 같다.
✏️ SOLID는 객체 지향적인 프로그램을 짜기 위한 원칙이다. 객체 지향의 4대 특성을 잘 활용하여 구현할 수 있다.
SOLID를 잘 적용하면, 논리적으로 정연하며 유지 관리 보수가 쉬워진다.
객체 지향 프로그래밍의 강점을 극대화하고 활용하기 위해서는 이런 SOLID 법칙을 따라야하고 따르게 된다.
🤓 한 클래스는 하나의 책임만 가져야 한다.
관심사 분리를 통해 속성, 메서드, 클래스, 모듈, 패키지가 하나당 하나의 책임을 가진다.
따라서 👉 변경의 파급효과가 적다.
속성
: 하나의 속성이 경우에 따라 여러의미를 가진다.클래스
: 하나의 클래스가 여러 역할을 가진다.메서드
: 하나의 메서드가 여러 행위를 수행한다. public class SchoolMembers_NotSPR {
final Role role;
private String schoolClass;
public SchoolMembers_NotSPR(Role role,String schoolClass) {
this.role = role;
this.schoolClass = schoolClass;
}
void toBeActive() {
if (this.role == Role.STUDENT){
System.out.println("학습하다.");
}else {
System.out.println("가르치다.");
}
}
}
toBeActive()
: 분기처리를 통해 역할을 구분하고, 행위를 하는 두가지의 역할을 담당하고 있다.schoolClass
: 역할에 따라, 선생님의 경우 관리하는 반과 학생은 소속된 반이라는 여러 의미를 가진다. 🤓 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야한다.
즉, 외부의 변화로 인해 내부 코드를 변경해야하는 일이 없도록 한다.
다형성을 통한 OCP
클라이언트의 변경 없이 인터페이스를 구현한 객체 인스턴스를 유연하게 변경할 수 있다.
🚩 클라이언트 내부 코드의 변경이 없도록 한다.
운전자가 자동차를 운전할 때 차종에 의해 영향을 받지 않는다.
운전자는 차종이 아니라 인터페이스이거나 상위클래스인 자동차에 의존하기 때문이다.
🙅 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면, 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성을 얻을 수 없다.
운전자인 클라이언트가 마티즈, K7 클래스를 직접 선택하거나 의존한다면 차종을 변경할 때 클라이언트의 내부 코드를 변경해야한다.
public class Client {
private K7 k7;
public Client(K7 k7) {
this.k7 = k7;
}
void stepOnTheBrakes (){
this.k7.brake();
}
}
public class K7 {
void brake(){
System.out.println("K7 브레이크 밟음");
System.out.println("15초 후");
System.out.println("완전히 멈춤");
}
}
public class Matiz {
void brake(){
System.out.println("Matiz 브레이크 밟음");
System.out.println("30초 후");
System.out.println("완전히 멈춤");
}
}
public class Driver {
K7 seohyunCar = new K7();
Client seohyun = new Client(seohyunCar);
// 마티즈로 변경하고 싶은데?
Matiz matiz = new Matiz();
// Client client = new Client(matiz);
// 클라이언트의 내부 코드에 가서 직접 변경을 해야한다.
}
👉 car이라는 인터페이스를 통해서 OCP를 지킬 수 있다.
🤔 하위 클래스는 논리적으로 상위 클래스여야한다.
다형성을 이용하기 위해 인터페이스, 상위클래스 타입 객체 참조 변수를 사용해서 하위 클래스를 참조한다. 하위 클래스의 인스턴스는 상위 클래스나 인터페이스의 역할을 할 수 있어야한다. 신뢰성과 일관성을 위해서는 하위 클래스가 상위 클래스나 인터페이스의 역할 동일한 논리로 수행해야한다.
자동차 인터페이스의 브레이크 기능은 어떤 구현에서든지 멈춰야하는데, 특정 자동차는 브레이크가 엑셀이다..!! -> 🙅 LSP 실패
지금은 개발이 아주 간단하지만 복잡한 개발을 하고 코드문을 짤 때는 구현 클래스의 실제 구현 메서드를 확인하기 힘들게 된다. 따라서 우리는 인터페이스의 구현을 잘 하고 있을거라는 믿음하에 코드를 짜게 된다.
특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
사용자 인터페이스 -> 운전자 클라이언트, 정비사 클라이언트로 분리
정비인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음.
인터페이스가 명확해지고, 대체 가능성이 높아진다.
할 수 있는 (is able to)
라는 기준으로 만들자.상위 클래스는 풍성할 수록 좋고, 인터페이스는 작을 수록 좋다.
풍성한 상위 클래스는 불필요한 다운캐스팅을 사용하지 않아도 되고, 상속의 장점을 최대로 활용할 수 있다.
인터페이스는 더 명확해진다.
구체적인 것이 추상화된 것에 의존해야한다.
자주 변경되는 클래스에 의존하지마라; 자신보다 변하기 쉬운 것에 의존하지마라
의존관계역전이란 구체적인 것이 추상화 된것에 의존하게 하는 것이다.
👉 변화에 영향을 덜 받게 된다.
따라서, 자신보다 더 구체적인 것에 의존하고 있다면 인터페이스나 상위클래스를 사용해서 추상화 된 것에 의존하게 하는 것이다.
MemberRepository m = new MemoryMemberRepository()
➡️ 구체화인 메모리멤버리포지토리에도 의존하고 있다.
즉, DIP를 위반한다.
관심사가 다르고 변화의 시기가 다르면 분리해야한다.