✏️ SOLID, 좋은 객체지향 설계의 5가지 원칙

박상민·2023년 8월 3일
2

개념 정리!

목록 보기
2/18
post-thumbnail

SOLID에 대한 설명에 들어가기 전에 이해에 도움을 줄 수 있는 이야기를 해보겠다.
"객체지향 프로그래밍의 특성과 장점을 최대한으로 끌어올리기 위해 프로그램을 어떻게 설계해야할까?"

📌 객체지향 설계과정

  1. 요구사항(제공해야 할 기능)을 찾고 세분화 한다. 그리고 그 기능을 알맞은 객체로 할당한다.
  2. 기능을 구현하는 데에 필요한 데이터를 객체에 추가한다.
  3. 해당 데이터를 이용하는 기능을 구현한다. 기능은 최대한 캡슐화를 적용한다.
  4. 객체 간에 어떻게 메소드 호출을 주고받을 지 결정한다.

⭐️ 객체지향 설계의 5가지 원칙, SOLID

SOLID라는 좋은 객체지향의 설계의 5가지 원칙이 존재한다. SOLIDSRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), ISP(인터페이스 분리 원칙), DIP(의존 역전 원칙)의 앞글자를 따서 만들어졌다. SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 된다.

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

  • 하나의 클래스는 하나의 책임만 가져야 한다.
  • 클래스를 변경하는 이유는 단 하나여야 한다. 변경이 있을 때 파급 효과가 적어야 한다.
    • 이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있다.
      결국, 유지보수가 매우 비효율적이게 된다.

책임?
SRP에서 이야기하는 책임이란 기능이라고 생각하면 된다. 예를 들어서, 하나의 클래스가 수행할 수 있는 기능(책임)이 여러 개라면, 클래스 내부의 함수끼리 강한 결합을 가질 가능성이 높아지면 코드의 효율 또한 떨어진다. 객체지향 설계의 핵심은 높은 응집도와 낮은 결합도인데 핵심을 지키지 못하는 것이다.
새로운 요구사항, 프로그램 변경에 의해 연쇄적으로 변경될 수가 있는데 이는 유지보수의 비효율을 동반한다. 때문에, 하나의 클래스는 하나의 책임을 가지도록 책임을 분리시켜야 한다.

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

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 즉, 기존의 코드를 변경하지 않고 기능을 수정, 추가할 수 있도록 설계해야한다.
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현

어떤 모듈의 기능을 수정할 때, 해당 모듈을 이용하는 모든 모듈 또한 수정한다면 유지보수가 복잡해진다. 따라서 OCP(개방-폐쇄 원칙)을 적용해서 기존 코드를 변경하지 않아도 기능을 수정, 추가할 수 있게 해야한다.

OCP를 지키지 않으면 객체지향 프로그래밍의 장점인 유연성, 재사용성, 유지보수성 등을 활용하지 못하게 된다.

기존의 코드를 변경하지 않고 어떻게 기능을 수정, 추가할 수 있을까?
상속(다형성), 추상화(인터페이스)를 활용하면 된다. 자주 변경하는 부분을 추상화해서 기존 코드를 수정하지 않고 기능을 확장할 수 있도록 해서 유연성을 살릴 수 있다.

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

  • 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 한다.
    • 즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작해야 한다.
  • 다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야 한다.
  • 상속 관계에서는 꼭 일반화 관계(IS-A)가 성립해야 한다.
  • 상속 관계가 아닌 클래스들을 상속관계로 설정하면, LSP 위반이다.

예를 들어서, 자동차 인터페이스가 있다고 하자. 자동차 인터페이스의 엑셀 기능은 자동차가 앞으로 가는 기능을 한다. 그런데 엑셀 기능을 실행했는데 자동차가 뒤로 간다고 생각해보자. 참으로 끔찍한 일이다. 이는 LSP를 위반하는 것이다.
설령 기능이 느리더라도 엑셀 기능을 실행했을 때 자동차는 앞으로 가야한다.

LSP를 위반하면 OCP 또한 위반한다.
따라서 상속 관계를 잘 정의하여 LSP가 위반되지 않도록 설계해야 한다.

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

  • 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다.
  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 한 개보다 낫다.
  • 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 한다.

예를 들어서, '자동차'라는 하나의 범용 인터페이스 보다는 운전, 정비, 타이어 등의 세부적인 인터페이스로 나누는 것이 더 낫다는 것이다. 이렇게 된다면, 타이어를 교체할 때는 타이어 인터페이스만 확인하고 변경하면 된다.
이렇게 세부적인 인터페이스로 나눈다면 인터페이스가 명확해지고, 대체 가능성이 높아진다.

각 클라이언트가 필요로 하는 인터페이스를 분리함해서 클라이언트가 사용하지 않는 인터페이스에 변경이 발생해도 다른 인터페이스는 영향을 받지 않도록 만드는 것이 ISP의 핵심이다.

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

프로그래머는 "추상회에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 말을 따르는 방법 중 하나다.

  • 의존 관계를 맺을 때, 변하기 쉬운 구체적인 것 보다는 변하기 어려운 추상적인 것에 의존해야 한다는 것이다.
    • 즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
  • 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존한다면 변경에 어려움이 생긴다.
  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.
    • 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다.

✔︎ 정리

SRP와 ISP는 객체가 커지는 것을 막는다. 객체가 단일 책임을 가지게 하고, 클라이언트마다 특화된 인터페이스를 구현하게 해서 일정 기능의 변경이 다른 곳까지 영향을 미치지 못하게 한다. 곧 이는 기능 추가 및 변경에 용이하도록 만든다.

LSP와 DIP는 OCP를 돕는다. OCP는 자주 변화되는 부분을 추상화하고, 다형성을 이용해서 기능 확정에는 유연하지만 기존 코드의 변화에는 보수적이도록 만들어준다. 이때, '변화되는 부분을 추상화'할 수 있도록 돕는 것이 DIP, 다형성 구현을 돕는 것이 LSP이다.

객체 지향의 핵심은 다형성이다. 그러나 다형성만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다. 다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되기 때문이다.
즉, 다형성 만으로는 OCP, DIP를 지킬 수 없다.

여기서 스프링을 사용하는 이유가 나타난다. 스프링은 DI(Dependency Injection)를 통해서 의존관계를 주입해주고 DI 컨테이너를 제공함으로써 다형성을 사용하면서 OCP, DIP를 지킬 수 있게 해준다.

물론 스프링이 없어도 가능은 하다. 그러나 코드의 양이 매우 많아질 것이고, 유지보수는 너무나 어려울 것이다. 나는 스프링이 좋다!


출처
SOLID 원칙, 어렵지 않다!
[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID
김영한님의 스프링 핵심 원리 - 기본편

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글