YD_S 개발로그
article thumbnail
Published 2023. 6. 29. 12:23
SOLID 원칙 Architecure

SOLID 원칙이란?

  • 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 의미한다.
  • SRP, OCP, LSP, ISP, DIP 의 앞글자를 따서 만들어졌다.

SOLID 원칙의 목적

  • 변경에 유연하다.
  • 이해하기 쉽다.
  • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트 기반이 된다.

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

  • 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.
  • 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축(Axis of Change)이 된다.

SRP 위배 사례

  • 아래 Employee 클래스 SRP를 위반하고 있다.
  • 한 클래스 내 세 가지 메서드가 서로 다른 세 명의 엑터(회계 팀, 인사 팀, DBA)를  전부 책임지고 있기 때문이다.

 

초과 근무를 제외한 업무 시간을 계산하는 알고리즘인 새로 생성한 메서드 regularHours()을 calculatePay() 메서드와 reportHours() 메서드에서도 사용한다고 가정하고 아래와 같은 상황이 생겼다고 해보자.

  1. CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 수정하길 원해서 개발자가 regularHours() 메서드를 수정하였다.
  2. COO 팀에선 CFO팀과는 다른 목적으로 사용하기에 변경을 원치 않는다.
  3. 개발자가 뒤늦게 COO 팀에서 사용하는reportHours()에서도 regularHours()메서드를 사용하는 것을 발견하였다.
  4. COO 팀에선 개발자가 발견하기까지 엉터리 보고서로 인한 잘못된 데이터로 회사에 수백만 달러의 예산이 지출되었다.

위 상황은 한 모듈 내 여러 엑터의 책임을 지고 있기 때문에 발생한 일이다. 

 

해결책

  • 엑터 단위로 책임을 분리한다.
  • 퍼스드(Facade) 패턴을 사용해 개발자가 클래스를 인스턴스해 추적하는 단점을 해결한다. 

 

  • 아래 EmployeeFacade에 코드는 거의 없다. 
  • EmployeeFacade는 어떠한 엑터도 담당하지 않는다.
  • 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.

위와 같이 변경 후 CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 변경한다면 PayCalulator 클래스의 calculatePay()  메서드만 수정해주면 되므로 HourReporter 클래스의 reportHours() 메서드는 어떠한 영향도 받지 않는다.


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

  • 기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변결할 수 있도록 설계해야만 한다.
  • 소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야한다.

만약 요구사항을 살짝 확장하는 데 소프트웨어를 엄청나게 수정해야 한다면, 그 소프트웨어 시스템을 설계한 아키텍트는 엄청난 실패에 맞닥뜨린 것이다.

 

OCP 위배 사례

위 소스에서 소, 돼지를 추가한다면 Animal 클래스의 speak() 메서드에서 type을 분류되지 않아 원하는 결과를 얻지 못한다.

만일 speak() 메서드에서 type을 추가한다면 원하는 결과는 나오지만 아래와 같은 원칙이 지켜지지 않는다.

  • 기존 speak() 메서드 변경 (변경에는 닫혀 있어야한다. X)
  • speak() 메서드의 변경 없이는 추가가 제약된다. (확장에는 열려 있어야한다. X)

해결책

  • 객체 지향에서 기본적으로 제공되는 추상화다형성을 사용한다. (Abstract, Interface)
  • 변경되는 부분과 변경되지 않는 부분을 분리한다.

주의점

  • 추이 종속성이 될 경우 '자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다'는 원칙을 위반하게된다.
추이 종속성이란?
  • 클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존한다면, 클래스 A는 클래스 C에 의존하게 되는 경우를 의미한다.
  • 클래스 의존성이 순환적(cyclic)이라면 모든 클래스가 서로 의존하게 되는 문제가 있다.

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

  • 다형성을 지원하기 위한 원칙
  • 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다.
  • 상속을 통한 재사용은 기반(부모) 클래스와 서브 클래스 사이에IS-A관계가 있을 경우로만 제한, 그 외는 합성(composition)을 사용해야 한다.

LSP 위배 사례

  • Square(정사각형)은 Rectangle(직사각형)의 하위 타입으로 적합하지 않다.
    • Rectangle은 높이와 너비가 서로 독립적으로 변경될 수 있는 반면, Square는 반드시 높이와 너비가 함께 변경되기 때문이다. 
    • LSP 위반을 막기 위한 방법은 비교문을 통해 타입 검사를 하는 것 뿐이다. 하지만 그럴 경우 사용하는 타입에 의존하게 되므로 결국 타입을 치환할 수 없다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있다.

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

  • 사용하지 않는 것에 의존하면 안된다.
  • 큰 인터페이스를 작은 단위로 나누는 것이 좋다. 
  • SRP는 클래스의 단일책임을 강조한다면, ISP는 인터페이스의 단일책임을 강조한다.

ISP 위배 사례

  • 인터페이스로 차를 추상화하였다.
  • 휘발류, 경유, 전기를 주유/충전하는 기능이 있다. 하지만 차량 별 연료가 다르다.
  • Avente는 휘발유, Tucson은 경유를 연료만을 사용하지만 지원하지 않는 연료 충전 기능도 재정의 해야한다.

 

해결책

  • 각 기능에 맞게 인터페이스를 분리한다.
  • 사용하는 기능만을 인터페이스만 implements 하여 구현한다.
  • 만약 연료가 하이브리드라면 Car, ElectroRefuelAble, GasolineRefuelAble 인터페이스를 implements 한다.

주의점
  • 인터페이스는 구성된 이후 나중에 수정사항이 생긴다면 인터페이스 분리, 추상 메서드의 추가 등의 행위를 하지 말아야한다.
  • ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 경우를 인정한다.

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

  • 고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안된다. 대신 세부사항이 정책에 의존해야 한다.
  • 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템 즉, 구현 클래스가 아니라 인터페이스 혹은 추상 클래스에 의존해야한다.

 

DIP 위배 사례

  • Cafe 클래스(고수준)이 Americano, CafeMocha, VanillaLatte 클래스(저수준)에 의존되어 있다.
  • 만약 Coffee 종류가 추가 되면 고수준의 의존성이 많아져 추후 수정, 관리가 힘들어진다. 

해결책

  • 새로 생성한 추상 클래스 혹은 인터페이스에 의존하도록 한다. 
  • Cafe 클래스(고수준)이 Coffee 인터페이스에 의존


Reference

Clean Architecture - 로버트C.마틴

https://www.youtube.com/@nullnull_not_eq_null - 널널한 개발자 TV

https://www.youtube.com/@user-pw9fm4gc7e - 코드없는 프로그래밍

profile

YD_S 개발로그

@YD_S

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!