[자바와 객체 지향 그리고 스프링] 5. 객체지향 설계 5원칙

코린이서현이·2024년 1월 6일
0

😅들어가면서😅

스프링을 공부하면서, 객체 지향 설계 5원칙에 대해서 학습을 했었다.
그 글도 함께 보면서 공부를 해보자~!

🎯 목표

객체 지향 설계 5원칙을 알아보자.
SOLID를 잘 지키면 확장에는 열려있고 변경에는 닫혀있으며 유지보수가 쉬워진다.
👉 잘 지키지 않으면 유지보수가 어려워진다. 

🤔 그 전에! SOLID를 왜 지켜야할까?
📌 SRP: 단일 책임 원칙 
📌 OCP: 개방 폐쇄 원칙
📌 LSP: 리스코프 치환 원칙
📌 ISP: 인터페이스 분리 원칙
📌 DIP: 의존 역전 원칙

읽어보면 좋은 자료

📖 클린 소프트웨어 : Part 2 에자일 설계 부분
📖 UML 실전에서는 이것만 쓴다. : 6장 에자일 설계 부분
🖥️ 지다넷의 "객체 지향 SW 설계의 원칙"
이 글에 대해서는 다시 읽어보면 좋을 것 같다.

객체 지향 설계의 5원칙: SOLID

✏️ SOLID는 객체 지향적인 프로그램을 짜기 위한 원칙이다. 객체 지향의 4대 특성을 잘 활용하여 구현할 수 있다.

SOLID를 잘 적용하면, 논리적으로 정연하며 유지 관리 보수가 쉬워진다.

객체 지향 프로그래밍의 강점을 극대화하고 활용하기 위해서는 이런 SOLID 법칙을 따라야하고 따르게 된다.

📌 SRP: 단일 책임 원칙

SRP란

🤓 한 클래스는 하나의 책임만 가져야 한다.

관심사 분리를 통해 속성, 메서드, 클래스, 모듈, 패키지가 하나당 하나의 책임을 가진다.
따라서 👉 변경의 파급효과가 적다.

SRP를 지키지 않은 경우를 생각해보자.

  • 속성 : 하나의 속성이 경우에 따라 여러의미를 가진다.
  • 클래스 : 하나의 클래스가 여러 역할을 가진다.
  • 메서드 : 하나의 메서드가 여러 행위를 수행한다.

코드로 살펴보기

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: 개방-폐쇄 원칙

OCP란

🤓 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야한다.

즉, 외부의 변화로 인해 내부 코드를 변경해야하는 일이 없도록 한다.

다형성을 통한 OCP
클라이언트의 변경 없이 인터페이스를 구현한 객체 인스턴스를 유연하게 변경할 수 있다.

🚩 클라이언트 내부 코드의 변경이 없도록 한다.
운전자가 자동차를 운전할 때 차종에 의해 영향을 받지 않는다.
운전자는 차종이 아니라 인터페이스이거나 상위클래스인 자동차에 의존하기 때문이다.

🙅‍ 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면, 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성을 얻을 수 없다.

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: 리스코프 치환 원칙

LSP란

🤔 하위 클래스는 논리적으로 상위 클래스여야한다.

다형성을 이용하기 위해 인터페이스, 상위클래스 타입 객체 참조 변수를 사용해서 하위 클래스를 참조한다. 하위 클래스의 인스턴스는 상위 클래스나 인터페이스의 역할을 할 수 있어야한다. 신뢰성과 일관성을 위해서는 하위 클래스가 상위 클래스나 인터페이스의 역할 동일한 논리로 수행해야한다.

LSP가 지켜지지 않은 경우를 생각해보자.

자동차 인터페이스의 브레이크 기능은 어떤 구현에서든지 멈춰야하는데, 특정 자동차는 브레이크가 엑셀이다..!! -> 🙅‍ LSP 실패

지금은 개발이 아주 간단하지만 복잡한 개발을 하고 코드문을 짤 때는 구현 클래스의 실제 구현 메서드를 확인하기 힘들게 된다. 따라서 우리는 인터페이스의 구현을 잘 하고 있을거라는 믿음하에 코드를 짜게 된다.

📌 ISP: 인터페이스 분리 원칙

ISP란

특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.

자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
사용자 인터페이스 -> 운전자 클라이언트, 정비사 클라이언트로 분리

정비인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음.
인터페이스가 명확해지고, 대체 가능성이 높아진다.

🤔 인터페이스를 잘 만드는 방법?

  1. 할 수 있는 (is able to)라는 기준으로 만들자.
  2. 인터페이스를 통해 메서드를 외부에 제공할 때는 최소한의 메서드만 제공해야한다.

상위 클래스는 풍성할 수록 좋고, 인터페이스는 작을 수록 좋다.

풍성한 상위 클래스는 불필요한 다운캐스팅을 사용하지 않아도 되고, 상속의 장점을 최대로 활용할 수 있다.
인터페이스는 더 명확해진다.

📌 DIP: 의존 역전 원칙

DIP란

구체적인 것이 추상화된 것에 의존해야한다.
자주 변경되는 클래스에 의존하지마라; 자신보다 변하기 쉬운 것에 의존하지마라

의존관계역전이란 구체적인 것이 추상화 된것에 의존하게 하는 것이다.
👉 변화에 영향을 덜 받게 된다.

따라서, 자신보다 더 구체적인 것에 의존하고 있다면 인터페이스나 상위클래스를 사용해서 추상화 된 것에 의존하게 하는 것이다.

DIP적용해보기

  • 스노우 타이어에 의존하는 자동차 : 타이어는 더 자주 변경된다. 따라서 변경될 때마다 계속 자동차를 바꿔주어야한다.
    따라서, 타이어 인터페이스를 사용하여 추상화된 것에 의존하도록 변경한다.
  • 타이어 인터페이스에 의존하는 자동차 : 타이어변경에 자동차가 영향받지 않는다.

의존한다는 것은? 내가 알고 있다는 것?

MemberRepository m = new MemoryMemberRepository()
➡️ 구체화인 메모리멤버리포지토리에도 의존하고 있다.
즉, DIP를 위반한다.

➕ Soc : 관심사의 분리

관심사가 다르고 변화의 시기가 다르면 분리해야한다.

profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글