SOLID 원칙

도두맨·2025년 3월 27일

공부

목록 보기
8/23
post-thumbnail

객체 지향 지향 설계(OOD: Object Oriented Design)의 정수라고 할 수 있는 5원칙이 집대성 됐는데, 바로 SOLID다.
SOLID는 아래 5가지 원칙의 앞 머리 알파벳을 따서 부르는 이름이다.

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Principle): 개방 폐쇄 원칙
  • LSP(Liskov Substitution Principle): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 법칙
  • DIP(Dependency Inversion Principle):의존 역전 원칙

이 원칙들도 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 할 수 있다.


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

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다" - 로버트 C.마틴

  • 하나의 클래스는 하나의 책임만 가져야한다.
    클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 한다.
  • 예시
public class 남자{
  // 아들, 사원, 소대원 역할을 같이 함
  효도하기();
  안마하기();
  출근하기();
  아부하기();
  사격하기();
  구보하기();
}
  • 여기서 남자는 너무 많은 역할을 하기 때문에 피곤하다
  • 이걸 나눠줘야 한다.
public class 아들{
  효도하기();
  안마하기();
}

public class 사원{
  출근하기();
  아부하기();
}

public class 소대원{
  사격하기();
  구보하기();
}
  • 남자라는 하나의 클래스가 역할과 책임에 따라 세 개의 클래스로 쪼개졌다.
  • 단일 책임 원칙은 속성, 메소드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있다.

강아지를 예시로 들어보자.

  • 단일 책임 원칙 X
class 강아지 {
  final static boolean 수컷 = true;
  final static boolean 암컷 = false;
  boolean 성별;
  
  void 소변() {
    if(this.성별 == 수컷) {
      // 다리들고 소변
    } else {
      // 앉아서 소변
    }
  }
}
  • 단일 책임 원칙 적용
abstract class 강아지 {
  abstract void 소변보기()
}

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

class 암컷강아지 extends 강아지 {
  void 소변() {
    // 앉아서 소변
  }
}
  • 단일 책임 원칙과 가장 관계가 깊은 것은 모델링 과정을 담당하는 추상화 임을 알 수 있다.
  • 애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메소드를 성계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자.

개방 폐쇄 원칙 OCP(Open Closed Principle)

"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야한다." 로버트 C.마틴

  • 위 문장을 조근 더 의역해 보면 아래와 같은 문장을 이끌어 낼 수 있다.

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

  • 조금 억지스럽지만 몇 가지 예제를 통해 개방 폐쇄 원칙을 이해해보자.

  • 어느 날, 한 운전자가 마티즈를 구입했다. 그리고 마티즈에 적응했다. 그리고 훗날 소나타가 생겼다.
  • 창문과 기어가 수동이던 마티즈에서 창문과 기어가 자동인 소나타로 차종을 바꾸니 운전자의 행동에도 변화가 온다.
  • 스틱 차량에서 오토 차량으로 바꿨다고 해서 운전자가 운전에 영향을 받아야만 하는가를 생각해보자. 현실 세계라면 당연히 어느정도 변화가 있어야 하겠지만 객체 지향 세계에는 다른 해법이 있다.

  • 상위 클래스 또는 인터페이스를 중간에 둠으로써 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 운전 습관에 영향을 받지 않게 된다.

  • 편의점에서는 직원이 바뀐다고 해서 손님이 구매라는 행위를 하는 데는 영향이 없다.
  • 직원은 교대라고하는 확장 행위에 열려있는 것이다.
  • 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지 보수성 등을 얻을 수 없다.
  • 개방 폐쇄 원칙에 대한 좋은 예로 스프링 프레임워크도 있다. 스프링 프레임워크를 공부하다 보면 개방 폐쇄 원칙의 김연아라고 할 정도로 개방 폐쇄 원칙을 교과서적으로 활용하고 있음을 확인할 수 있다.

리스코프 치환 원칙 LSP(Liskov Substitution Principle)

"서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다." - 로버트 C.마틴

  • 객체 지향의 상속은 다음 조건을 만족해야 한다.
    • 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.
    • 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
  • 위 두 개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다. 하지만 위 문장대로 구현되지 않은 코드가 존재할 수 있는데 바로 상속이 조직도나 계층도 형태로 구축된 경우다.

참고 위에서 두 번째 문장에서 "인터페이스할 수 있어야 한다"라는 표현은 각 인터페이스 명에 따리 읽으면 쉽게 이해할 수 있다.

  • AutoCloseable - 자동으로 닫힐 수 있어야 한다.
  • Appendable - 덧붙일 수 있어야 한다.
  • Cloneable - 복제할 수 있어야 한다.
  • Runnable - 실행할 수 있어야 한다.
  • 아버지를 상위 클래스(기반 타입)로 하는 딸이라는 하위 클래스(서브 타입)가 있다고 하자. 상위 클래스의 객체 참조 변수에는 하위 클래스의 인스턴스를 할당할 수 있다.
아버지 춘향이 = new 딸()
  • 춘향이는 아버지형 객체 참조 변수이기에 아버지 객체가 가진 행위(메소드)를 할 수 있어야 하는데 춘향이에게 아버지의 어떤 역할을 시킬 수 있을까?
동물 뽀로로 = new 펭귄()
  • 논리적인 흠이 없다. 아버지 - 딸 구조(계층도/조직도)는 리스코프 치환 원칙을 위배하고 있는 것이며, 동물 - 펭귄 구조(분류도)는 리스코프 치환 원칙을 만족하는 것이다. 로버트 C.마틴으 ㅣ말을 다시 의역하고 결론을 내보자.

"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다."

  • 이 그림은 리스코프 치환 원칙을 위반한 사례이다. (계층도/조직도)
  • 계층도/조직도인 경우를 보면 딸이 아버지, 할아버지의 역할을 하는 것이 논리에 맞지 않음을 알 수 있다.

  • 이 그림은 리스코프 치환 원칙을 적용한 사례이다. (분류도)
  • 분류도인 경우 하위에 존재하는 것들은 상위에 있는 것들의 역할을 하는 데 전혀 문제가 없다.
  • 결국 리스코프 치환 원칙은 객체 지향의 상속이라는 특성을 올바르게 활용하면 자연스럽게 얻게 되는 것이다.

인터페이스 분리 법칙 ISP(Interface Segregation Principle)

"클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다." - 로버트 C.마틴

  • 단일 책임 원칙을 적용한 후에는 이렇게 클래스로 나뉘었다.

  • 남자를 토막 내는 것이 너무 잔인하다는 생각이 든다면 그 때 선택할 수 있는 방법이 바로 ISP 즉, 인터페이스 분할 원칙이다.

  • 남자 클래스를 토막 내는 것이 아니라 자아 붕괴(?) 또는 다중 인격화(?) 시켜서 각자의 역할 인터페이스로 제한하는 것이 인터페이스 분할 원칙의 핵심이다.

  • 결론적으로 단일 책임 원칙과 인터페이스 분할 원칙은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다.

  • 인터페이스 분할 원칙을 이야기할 때 함께 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙이라는 것이 있다. 인터페이스를 통해 메소드를 외부에 제공할 때는 최소한의 메서드만 제공하라는 것이다.

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


의존 역전 원칙 DIP(Dependency Inversion Principle)

"고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화 된 것에 의존해야 한다.".
"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라"

  • 로버트 C.마틴
  • 자동차와 스노우타이어 사이에는 의존 관계가 있다. 자동차가 스노우타이어에 의존한다.

  • 의존 역전 원칙 적용 전(자주 변경되는 구체 클래스에 의존)

  • 자동차는 한 번 사면 몇 년은 타야하는데 스노우타이어는 계절이 바뀌면 일반 타이어로 교체해야 한다. 이런 경우 스노우타이어를 일반타이어로 교체할 때 자동차는 그 영향에 노출되어 있음을 알 수 있다.

  • 자동차가 구체적인 타이어가 아닌 추상회된 타이어 인터페이스에만 의존하게 함으로써 타이어의 종류를 변경해도 자동차가 영향을 받지 않는다. 의존 역전 원칙을 의역해 보면 다음과 같다.

"자신보다 변하기 쉬운 것에 의존하지 마라."


정리

  • SRP(단일 책임 원칙): 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
  • OCP(개방 폐쇄 원칙): 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
  • LSP(리스코프 치환 원칙): 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
  • ISP(인터페이스 분리 법칙): 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.
  • DIP(의존 역전 원칙): 자신보다 변하기 쉬운 것에 의존하지 마라.

0개의 댓글