군만두의 IT 공부 일지

[스터디6] 05. 객체 지향 설계 5원칙 - SOLID 본문

프로그래밍/객체지향

[스터디6] 05. 객체 지향 설계 5원칙 - SOLID

mandus 2025. 3. 29. 15:46

목차

 

이번에는 SOLID에 대해서 책과는 다른 예시를 생각해보고 다이어그램을 그렸다.

05. 객체 지향 설계 5원칙 - SOLID

SOLID는 로버트 C. 마틴이 2000년대 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 마이클 페더스가 두문자어로 소개한 것이다.

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

응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 할 수 있다.
SoC(Separation Of Concerns: 관심사의 분리): 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하는 것. 하나의 속성, 하나의 메서드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어 있어야 한다는 것.

1. SRP - 단일 책임 원칙

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

  • SRP 위반:
    • User 클래스가 사용자 데이터 관리, 인증, 이메일 전송, 보고서 생성 등 여러 책임을 한꺼번에 담당한다.
    • 역할과 책임이 너무 많다.
  • SRP 준수:
    • User 클래스: 사용자 데이터 관리만 담당한다.
    • UserAuthentication 클래스: 사용자 인증만 담당한다.
    • NotificationService 클래스: 알림 전송만 담당한다.
    • ReportGenerator 클래스: 보고서 생성만 담당한다.

역할(책임)을 분리하는 것이 단일 책임 원칙이다.

  • 클래스, 속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있다.
  • 단일 책임 원칙과 관계가 깊은 것은 모델링 과정을 담당하는 추상화이다.

2. OCP - 개방 폐쇄 원칙

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

  • OCP 위반:
    • Shape 클래스는 모든 도형 유형을 하나의 클래스로 처리한다.
    • 새로운 도형(삼각형, 오각형 등)을 추가하려면 Shape 클래스 내부에 코드를 수정해야 한다.
  • OCP 준수
    • Shape 추상 클래스를 정의하고 각 도형 유형(Circle, Rectangle, Triangle)이 이를 상속받아 구현한다.
    • 새로운 도형 유형을 추가할 때 기존 코드를 수정할 필요 없이 새로운 클래스만 추가하면 된다.

상위 클래스 또는 인터페이스를 둠으로써 다양한 도형이 생긴다고 해도 사용자는 영향을 받지 않게 된다. → 다양한 도형이 생기는 것은 도형 입장에서는 자신의 확장에는 개방되어 있는 것이고, 사용자 입장에서는 주변의 변화에 폐쇄되어 있는 것이다.

  • OCP 예:
    • JDBC, iBatis, MyBatis, 하이버네이트 등 데이터베이스 프로그래밍을 지원하는 라이브러리와 프레임워크에 개방 폐쇄 원칙이 적용되어 있다. → 데이터베이스가 자신의 확장(데이터베이스 교체)에는 열려 있고, 자바 애플리케이션은 주변의 변화(데이터베이스)에 닫혀 있다.
    • 자바에도 개방 폐쇄 원칙이 적용되어 있다. → 각 운영체제별 JVM은 확장에 열려 있고, 개발자가 작성한 소스코드는 운영체제의 변화에 닫혀 있다.
    • 개방 폐쇄 원칙은 상속 구조를 통해서도 확인할 수 있다.
  • 개방 폐쇄 원칙을 준수하면 유연성, 재사용성, 유지보수성 등을 얻을 수 있다.

3. LSP - 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다. - 로버트 C. 마틴
1. 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다. (분류도)
2. 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다. (AutoColseable, Appendable, Cloneable, Runnable)

상속에 대해서 위 두 개의 문장대로 구현된 프로그램은 리스코프 치환 원칙을 잘 지킨다. 상속이 조직도나 계층도 형태로 구축된 경우에는 위 문장대로 구현되지 않는다.

  • LSP 위반:
    • Square가 Rectangle을 상속받았지만, 정사각형의 특성상 width와 height가 항상 같아야 한다.
    • setWidth() 또는 setHeight() 메서드 호출 시, Square는 두 속성을 모두 동일한 값으로 설정한다.
    • Rectangle의 예상 동작을 위반하므로, Square를 Rectangle 대신 사용할 경우 프로그램 동작에 오류가 발생한다.
  • LSP 준수:
    • Rectangle과 Square를 별도의 클래스로 정의하고 둘 다 Shape 인터페이스를 구현한다.
    • Square는 setSide() 메서드를 사용해 크기를 설정하고, Rectangle은 setWidth()와 setHeight()를 사용한다.

리스코프 치환 원칙에서 하위에 존재하는 것들은 상위에 있는 것들의 역할을 하는 데 전혀 문제가 없다.

4. ISP - 인터페이스 분리 원칙

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

  • ISP 위반:
    • Worker 인터페이스가 work(), eat(), sleep() 메서드를 모두 포함한다.
    • HumanWorker는 모든 메서드를 자연스럽게 구현할 수 있지만, RobotWorker는 eat()와 sleep()이 불필요한 구현을 해야 한다.
    • 결국 "뚱뚱한 인터페이스(fat interface)" 문제를 일으키게 된다.
  • ISP 준수:
    • 인터페이스를 Workable, Eatable, Sleepable로 분리한다.
    • Human은 모든 인터페이스를 구현하고, Robot은 Workable 인터페이스만 구현한다.
    • 클라이언트는 필요한 기능을 가진 인터페이스만 의존하게 되어 각 역할에 맞는 책임만 가지게 된다.

단일 책임 원칙에서 하나의 역할(책임)만 하는 다수의 클래스로 분할하는 것 외에도 인터페이스 분할 원칙을 선택할 수 있다. 결론적으로 단일 책임 원칙과 인터페이스 분할 원칙은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 특별한 경우가 아니면 단일 책임 원칙을 적용하는 것이 좋다.

  • 인터페이스 최소주의 원칙: 인터페이스를 통해 메서드를 외부에 제공할 때는 최소한의 메서드만 제공하라는 것
  • 상위 클래스는 풍성할수록 좋고, 인터페이스는 작을수록 좋다. 사용 불가능한 경우나 불필요한 형변환이 없기 때문이다.

5. DIP - 의존 역전 원칙

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

  • DIP 위반:
    • ReportGenerator 클래스가 MySQLDatabase 클래스에 직접 의존합니다.
    • 데이터베이스를 MySQL에서 Oracle로 변경하려면 ReportGenerator 클래스도 수정해야 한다.
    • 고수준 모듈(ReportGenerator)이 저수준 모듈(MySQL 구현)에 직접 의존하는 상향식 의존성이 생긴다.
  • DIP 준수:
    • ReportGenerator는 구체적인 데이터베이스 구현이 아닌 DatabaseInterface 추상화에 의존한다.
    • MySQLDatabaseImpl과 OracleDatabaseImpl은 이 인터페이스를 구현한다.
    • 데이터베이스 변경 시 ReportGenerator 코드를 수정할 필요가 없다.
    • 의존성 방향이 역전되어 고수준 모듈과 저수준 모듈 모두 추상화에 의존하게 된다.

자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.

  • 상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이다.

 

이 글은 『스프링 입문을 위한 자바 객체 지향의 원리와 이해』 책을 학습한 내용을 정리한 것입니다.
Comments