S.O.L.I.D 법칙

Clearn Architecture[1]에서는 좋은 소프트웨어 시스템은 깔끔한 코드(Clean Code)로 부터 시작한다고 이야기 한다. 이러한 코드들이 모여서 요소(Element)가 되는데 이를 건물에 보면 벽돌로 볼 수 있다. 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데 그게 바로 SOLID라고 이야기 한다. 즉, SOLID 원칙은 함수와 데이터구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명한다. 사실 SOLID 원칙은 "Design Pattern 첫 번째: Object Oriented Principles"[2]에서 Object Oriented Programming의 설계 원칙 중 하나로 다루었지만 여기서는 조금 더 다른 관점에서 다룬다. 

 

원칙의 목적은 "중간 수준"의 소프트웨어 구조가 아래와 같도록 만드는데 있다.

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

 

S.O.L.I.D는 아래 다섯가지 원칙의 약자를 모은 것이다.

  • Single Responsibility Principle
  • Open Close Principle
  • Liskov Substitution Principle
  • Interface Seggregation Principle
  • Dependency Inversion Principle

책에서는 2004년 무렵, 레거시 코드 활용 전략의 저자 마이클 페더스(Michael Feathers)가 기존에 있던 원칙을 재배열하여 각 원칙의 첫 번째 글자들로 SOLID라는 단어를 만들었다고 한다. 각 원칙을 하나씩 살펴 보자.

 

단일 책임 원칙 (Single Responsibility Principle)

객체 지향 프로그래밍(Object Oriented Programming)에서 하나의 객체는 하나의 책임을 가진다는 원칙이다.

 

하나의 책임이라는 부분은 사실 모호한다. 어떤 클래스가 가지는 책임이 하나라는 것은 한가지 일만 한다고 하기에는 1차원 적인 것이 있다. 이 책에서는 최종 버전으로 "하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다."라고 이야기 한다. 모듈이 하나의 객체 혹은 소스 파일로 볼 수 있다. 여기서 액터는 개발자/팀이라고 볼 수 있다. 그렇다면, 하나의 소스 파일은 개발자/팀이 책임 져야 한다는 것이다. 분리된 팀이 하나의 소스 파일을 건드린다면 팀을 합치거나 혹은 파일을 분리해야 한다는 이야기로 이해할 수 있다.

 

책[1]의 예를 살펴 보자. 

 

Employee Class [1]

 

  • calculatePay() 매서드는 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.
  • reportHours() 매서드는 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용된다.
  • save 매서드는 데이터베이스 관리자(DBA)가 기능을 정의하고, CTO 보고를 위해 사용된다.

즉, 서로 다른 액터가 의존하는 경우는 많이 발생한다. SRP에 따르자면, 이 코드는 분리하라고 하는 것이다. 어떻게 분리할 수 있을까? 최종적으로는 2가지가 가능해 보인다.

 

이 해결책은 개발자가 세가지클래스를 인스턴스화하고 추적해야 한다는게 단점이다.  이럴 때, 흔히 쓰는 기법으로 파사드(Facade) 패턴이 있다. 아래 그림과 같이, EmployeeFacade에 코드는 거의 없다. 이 클래스는 세 클래스의 객체를생성하고, 요청된 메서드를 가지고 객체로 위임하는 일을 책임진다. 이렇기 때문에,  Facade는 Architectural Composition이라고도 한다.

Facade  Pattern[1]

Employee Data를 CFO에서 담당한다고 하면 Employee Facade Class로 가져올 수 있다면 구조는 조금 더 단순화 될 수 있다. 아래 구조가 최종 구조라고 하면, CFO는 Employee Class를 담당하고, COO는 HourReporter Class를 담당하며, CTO는 EmployeeSaver Class를 각각 담당하여 개발을 진행하면 SRP가 잘 유지되며 개발될 수 있다고 볼 수 있다. 이처럼 조직의 구조도 Software의 구조에 영향을 미친다.

 

Facade Pattern 두 번째[2]

 

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

개방-폐쇄 원칙(OCP)이라는 용어는 1988년 버트란드 마이어(Bertrand Meyer)가 만들었는데, 소프트웨어 개체(Artifact)는 확장에는 열려 있어야 하고(Open to Extension) 변경에는 닫혀 있어야 한다(Close to Modification)라는 원칙이다. 즉, 객체의 행위는 확장할 수 있어야 하지만, 이 때 개체를 변경해서는 안된다. 어찌 보면 모순 같은 이야기는 매우 중요한 원칙이기도 하다.

 

책에서는 여러 번 이야기 하지만, 변경이 되지 많아야 할 곳과 변경이 되는 것을 분리하고 변경이 되는 곳이 변경되지 않는 것에 종속되도록(Dependent)하도록 해야 한다는 것이다. 그리고, 이 변경 되는 쪽을 확장하는 쪽으로 쓰고 변경되지 않도록 하는 쪽은 한 곳으로 모아 두어야 한다는 원칙이다.

 

어떻게 이를 획득할 수 있을까? 책에서의 제무제표 웹 시스템에서는 그러한 예를 많이 보유하고 있다.

재무 제표 웹 시스템[1]

 

 

일반적인 방향성 제어의 방법에서는 위의 예에서 FinancialDataGateway 인터페이스는 FinacialReportGenerator와 FinancialDataMapper 사이에 위치하는데, 이는 의존성을 역전시키기 위해서이다. 만일 이러한 DataGateway가 없다면 FinancailReportGenerator Class는 Database Component에 의존하게 된다. 즉, Database가 SQL이었다고 다른 Database로 변경된다면 영향을 받고 변경될 가능성이 생기게 된다. 하지만 Database들이 거꾸로 FinancialDataGateway가 존재함으로서 Database들은 여기에 의존하게 되면서 의존성이 역전되게 된다. FianancialReportPresenter 인터페이스와 2개의 View 인터페이스도같은 목적을 가진다.


정보은닉 차원에서 OCP도 위의 예에서 살펴 볼 수 있다. FainacialReportRequester 인터페이스는 방향성 제어와는 달리, FinancialReportController가 Ineractor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재한다. 즉, 의존 관계를 바꾸지는 않지만 의존하는 Class의 의존도를 제한하고 변경이 있을 때에도 인터페이스에 한정하여 의존성을 가지도록 하여 OCP를 유지할 수 있다. 즉, 여러 모듈의 의존도가 있는 모듈인 경우 각 Interafce를 분리하여 정보은닉을 하면서 OCP 의 효과를 획득하는 것이라 할 수 있다.

리스코프 치환 원칙(Liskov Substituion Priniciple)

1988년 바바라 리스코프(Barbara Liskov)는 하위 타입(Subtype)을 아래와 같이 정의했다.

 

S 타입의 객체 o1, T 타입의 객체 o2일 경우, T타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o2을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.

LSP 예제

이 원칙은 일반적으로 OOP에서 상속을 사용하도록 가이드 한다. 위의 예가 바로 그것인데, Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문이다. 이들 하위 타입은 모두 License 타입을 치환할 수 있다. 이를 Liskov Substitution Principle LSP라고 한다.

 

정사각형/직사각형 문제는 LSP에서 발생될 수 있는문제를 설명한다.  Square는 Rectangle의 하위 타입으로는 적합하지 않은데, Rectangle의 높이와 너비는 서로 독립적 변경이다. 하지만, Squarue이 높이와 너비는 반드시 함께 변경되기 때문이다. 이런 형태의 LSP 위반을 막기 위한 유일한 방법은 (if문 등을 이용해서) Rectangle이 실제로는 Square인지 검사하는 매커니즘을 User에 추가하는 것이다. 하지만 이렇게 하면 User의 행위가 사용하는 타입에 의존하게 되므로 결국 타입을 서로 치환할 수 없게 된다. 다시 말하자면,  개념적으로는 LSP라 생각하기 쉽지만 이렇게 LSP를 적용하는데는 적당하지 않다.

 

Square LSP 위반 사례[1]


객체 지향이 혁명처럼 등장한 초창기에는 앞서 본 것 처럼 LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주 되었다. 하지만, LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모하였다.

 

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

아래 왼쪽 그림 기술된 상황에서,다수의 사용자가 OPS 클래스의 오퍼레이션을 사용한다. User1은 오직 op1을 User2는 op2만을 User3는 op3만을 사용한다고 가정해 보자. 이러한 문제는 아래 그림 오른쪽에서 보는 것처럼 오프레이션을 인터페이스 단위로 분리하여 해결할 수 있다. 

Interface Segregation Principle [1]

 

정적타입 언어는 소스코드에 '포함된 included'선언문으로 인해 소스 코드 의존성이 발생한다. 루비나 파이썬과 같은 동적 타입 언어서는 소스코드에 이러한 선언문이 존재하지 않는다. 대신 런타임에 추론이 발생한다. 동적 타입 언어를 사용하면 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유는 바로 이때문이다. 이러한 사실로 인해 ISP를 아키텍처가 아니라, 언어와 관련된 문제라고 결론내릴 여지가 있다. 

일반적으로 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일이다. 고수준인 아키텍처 수준에서도 마찬가지 상황이다. 

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

의존성 역전 원칙(DIP)에서 말하는 '유연성이 극대화된 시스템'이란 소스 코드 의존성이 추상(Abstraction)에 의존하며 구체(Concretion)에는 의존하지 않는 시스템이다. use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안 된다.


우리가 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰(Volatile) 구체적인 요소다. 그리고 이 구체적인 요소는 우리가 열심히 개발하는 중이라 자주 변경될 수 밖에 없는 모듈들이다.

 

실제로 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기 위해 애쓴다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력한다. 다음과 같은 매우 구체적인 코딩 실천법이 있다. 사실 이것이 DIP를 다시 쓴것과 같다.

  • 변동성이 큰 구체 클래스(Concrete Class)를 참조하지 말라. (Refer)
  • 변동성이 큰 구체 클래스(Concrete Class)로 부터 파생하지 말라 (Derive)
  • 구체함수를 오버라이드하지 말라. 대체로 구체 함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다.
  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라 

 

아래 그림에서 곡선은 아키텍처 경계를 뜻한다. 이 곡선은 구체적인 것들로부터 추상적인 것들을 분리한다. 소스 코드 의존성은 해당 곡선과 교차할 때 모두 한 방향, 즉, 추상적인 쪽으로 향한다. 제어흐름은 소스 코드 의존성과는 정반대 방향으로 곡선을 가로지른다는 점에 주목하자. 다시 말해 소스 코드 의존성은 제어흐름과는 반대 방향으로 역전된다. 이러한 이류로 이 원칙을 의존성 역전(Dependency Inersion)이라고 부른다. 

Dependency Inversion Principle[1]

위 그림의 구체 컴포넌트에는 구체적인 의존성이 하나 있고, 따라서 DIP에 위배된다. 이는 일반적인 일이다. DIP 위배를 모두 없앨 수는 없다. 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있다.

 

결론

S.O.L.I.D는 Single Responsibility Principle, Open Close Principle, Liskov Substitution Principle, Interface Seggregation Principle, Dependency Inversion Principle의 앞글자를 딴 것이다. 원칙 하나씩 따져 보았지만, 서로 서로 관련되어 있는 것을 살펴 볼 수가 있다. 이러한 원칙은 컴포넌트 혹은 아키텍처로 확장될 수 있다. 또한, 이러한 원칙은 Design Pattern의 각 패턴의 기본 원칙으로 각 패턴을 상세히 설명하는데도 사용된다.

 

참고 도서

[1] 로버트 C. 마틴 저, "클린 아키텍처 소프트웨어 구조와 설계의 원칙" 인사이트(insight) 2019년 08월 20일, 송준이 역

[2] Design Pattern 첫 번째: Object Oriented Principles, https://technical-leader.tistory.com/7

 

+ Recent posts