[java] 스프링 입문을 위한 자바 객체지향의 원리와 이해 정리 5

객체지향 설계 5원칙 - SOLID

위 원칙들은 결합도는 낮게 응집도는 높게라는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 볼 수 있다..

** 결합도: 서로 다른 모듈간의 상호 의존 정도

** 응집도: 하나의 모듈 내부에 존재하는 구성 요소들의 기능 적 관련성

SRP(Single Responsibility Principle): 단일책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다.

위 그림의 남자 클래스는 역할과 책임이많다 => 코드에서 냄새가 난다.

위와 같이 역할을 분리한다면 다른 역할에서 어떤 불편함도 느끼지 않을 수 있다.

** 속성이 SRP를 지키지 않은 경우

class 사람 {
  String 군번;
}

사람 로미오 = new 사람();
사람 줄리엣 = new 사람();

줄리엣.군번 = 123??????

위의 코드에 단일 책임 원칙을 적용해본다면 제일 상위 클래스로 사람을 두고 이를 상속하는 여자사람과 남자사람 클래스로 나누어 각자의 차이점만 구현할 것이다.

하나의 속성이 여러 의미를 갖는 경우는 단일 책임을 지키지 못하는 경우다.

예를들어 어떤 DB 테이블에 존재하는 하나의 필드가 토지인 경우에는 면적을 건물인 경우에는 층수를 나타내는 경우가 있어서 클라이언트 코드에서 if문을 여기저기 사용할 수 밖에 없던 일이 있었다.

이 같은 경우를 방지하기 위해서라도 테이블을 설계할 때도 단일 책임 원칙을 고려해야 한다. 이 같은 과정을 보통 정규화라고 하는데, 정규화를 조금 더 확장해서 생각해보면 결국 테이블과 필드에 대한 단일 책임 원칙의 적용이라고 할 수 있다.

** 메서드가 SRP를 지키지 않은 경우

class 강아지 {
  final static Boolean 수컷 = true;
  final static Boolean 암컷 = false;
  Boolean 성별;

  void 소변보다() {
    if(this.성별 == 수컷) {
      //한쪽 다리 들고 소변
    } else {
      //뒷다리 두개 굽혀 소변
    }
  }
}

위 코드는 소변보다 코드가 암컷 강아지와 수컷 강아지의 소변보다 메서드를 둘다 이용하려고 하는 메소드다. 이는 단일 책임 원칙을 위배하는 부분인데, 메서드가 단일 책임 원칙을 지키지 않을 경우 나타나는 대표적이 냄새가 바로 분기처리를 위한 if문이다.

abstract class 강아지 {
  abstract void 소변보다()
}

class 수컷 강아지 extends 강아지 {
  void 소변보다() {
    // 한쪽다리 들고 소변
  }
}

class 암컷 강아지 extends 강아지 {
  void 소변보다() {
    // 두 다리 굽혀 소변
  }
}


위 코드와 같이 각 책임을 가진 클래스로 고쳐서 작성할 수 있다.

OCP(open closed principle): 개방폐쇄 원칙

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

<개방 폐쇄="" 원칙을="" 사용하지="" 않은="" 경우=""> 위와 같은 경우는 운전자라는 클래스가 소나타와 마티즈의 변화(창문 자동,수동 개방)에 따라 자신도 변화에야 하는 개방폐쇄 원칙을 어기고 있는 상황이다. <개방 폐쇄="" 원칙을="" 사용한="" 경우=""> 위와 같은 경우는 자동차라는 마티즈와 소나타의 위에 인터페이스, 혹은 상위 클래스를 두어 두 객체의 공통행동을 추상화하고 운전자는 추상화된 인터페이스, 혹은 클래스를 활용하여 다른 부분의 변화를 몰라도 된다. \*\* 개방 폐쇄 원칙의 가장 좋은 예는 JDBC와 JVM이다. JDBC 인터페이스를 통해서 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌어도 커넥션을 설정하는 부분외에는 영향을 받지 않는다. 개발자가 작성한 소스코드는 운영체제의 변화에 닫혀 있고 각 운영체제별 JVM은 확장에 열려 있는 구조이다. 소스코드와 운영체제별 JVM 사이에는 각 운영체제 별 목적 파일이라고 하는 완충 장치가 있기 때문이다. `개방 폐쇄 원칙은 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻게 해준다.` ## LSP(Liskov Substitution Principle): 리스코프 치환 원칙 **서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.** 상속은 다음 두가지 조건을 만족해야 한다. - 하위 클래스 is kind of 상위 클래스 :하위 분류는 상위 분류의 한 종류다. - 구현 클래스 is able to 인터페이스: 구현 분류는 인터페이스 할 수 있어야 한다. 위 두개의 문장대로 구현된 프로그램은 리스코프 치환 원칙을 잘 지키고 있다. 하지만 위 문장대로 구현되지 않은 코드가 존재할 수 있는데 이는 상속이 `조직도` 혹은 `계층도` 형태로 이루어졌을 때이다. ### 위반사례 ### 적용사례 조직도와 계층도 형태의 상속은 하위 객체가 상위 객체의 역할을 할 수 없게 만든다. (딸이 할아버지의 역할을..?) 결국 리스코프치환 원칙은 상속이라는 특성을 올바르게 활용하면 자연스럽게 얻게 된다. ## ISP(Interface Segregation Principle): 인터페이스 분리 원칙 **클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.** 위 그림처럼 SRP는 공통된 클래스가 여러 책임을 지지 않게 각 클래스가 하나의 책임만 지도록 설정하여 여러 책임을 갖는 클래스 문제를 해결했다. 하지만 한가지 클래스를 여러개로 분할하는 것이 아닌 한가지 클래스에서 여러 인터페이스를 분할하는 방법도 있다. 위와 같이 여러 인터페이스를 활용하여 분리한다. ``` 남자친구 홍길동 = new 남자(); 아들 홍길동 = new 남자(); 사원 홍길동 = new 남자(); 소대원 홍길동 = new 남자(); ``` 위의 코드로 표현이 가능하다. 결국 SRP와 ISP는 같은 문제에 대한 두가지 다른 해결책이라고 볼 수 있지만 대부분의 경우는 단일 책임 원칙을 적용하는 것이 더 좋은 해결책으로 본다. ISP를 얘기할 때 가장 먼저 따라오는 이슈는 인터페이스 최소주의 원칙이다. 인터페이스를 통해 메서드를 외부에 제공할 때 최소한의 메서드만 제공하라는 원칙이다. ### 빈약한 상위 클래스 vs 풍성한 상위 클래스 ``` 사람 김학생 = new 학생(****); 사람 김군인 = new 군인(****); 김학생.이름 -> 가능 김군인.이름 -> 가능 김학생.생일 -> 불가능 김군인.생일 -> 불가능 ((학생)김학생)).생일 -> 형변환을 통해 가능 ((군인)김군인).생일 -> 형변환을 통해 가능 ``` 빈약한 상위 클래스는 위 코드와 같이 여기저기 형변환을 발생시켜야 하므로 상속의 혜택을 제대로 누리지 못하고 지저분한 코드의 작성을 자아낸다. ## DIP(Dependency Inversion Principle): 의존성 역전 원칙 **고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화 된 것에 의존해야 한다.** **추상화된 것은 구체적인 것에 의존하며 안 된다. 구체적이 ㄴ것이 추상화된 것에 의존해야 한다.** **자주 변경되는 구체 클래스에 의존하지 마라.** => 자신보다 변하기 쉬운 것에 의존하지마라 - 자동차는 스노우 타이어에 의존한다. - 해당 의존 관계를 타이어 인터페이스를 사용하여 역전시켰다: 구체적인 스노우 타이어에 의존하던 것을 추상적인 타이어에 의존하는 것으려 변경 ``` // 기존 코드 class Car { SnowTire tire = new SnowTire(); } // DIP 적용 class Car { Tire tire; void setTire(Tire tire) { this.tire = tire; } } ``` 상위클래스, 인터페이스, 추상클래스 일 수 록 변하지 않을 가능성이 크다. 하위 클래스, 구체 클래스에 의존하지말고 더 추상적인 변하지 않는 것에 의존하라는것이 의존 역전 원칙이다. ``` 참조: https://sehun-kim.github.io/sehun/solid/ https://devcraft.tistory.com/28?category=749976 https://rira9817.blogspot.com/2019/03/5-solid.html ```