Skip to main content

3. 설계 원칙

7장. SRP: 단일 책임 원칙#

  • SRP는 헷갈리기 쉽습니다.
  • 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 합니다.
  • 이를 다르게 말하면 하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져야 합니다.
  • SRP의 최종적으로 정리는 다음과 같습니다.
    • 하나의 모듈은 하나의 오직 하나의 액터에 대해서만 책임져야 합니다.
  • '모듈'은 간단하게 이야기 하면 대부분은 소스 파일이고, 단순히 함수와 데이터 구조로 구성된 응집된 결합입니다.
  • SRP를 잘 이해하는 방법 중 하나는 원칙을 위반하는 징후를 확인하는 것입니다.

징후 1: 우발적 중복#

우발적 중복

  • SRP를 위반하며, 세가지 메서드가 세명의 액터를 책임지는 문제가 있습니다.
  • 이후 작업을 했을 때, 이러한 변경사항이 다른 문제를 만들게 됩니다.
    • 즉, CFO를 위해 수정을 했으나 변경을 희망하지 않는 COO가 변경될 수 있습니다.

징후 2: 병합#

  • 소스 파일에 다양하고 많은 메서드를 포함하면 병함이 자주 발생하리라고 짐작하기는 어려운 일은 아닙니다.
    • 특히 이들 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 확실히 더 높습니다.
  • 병합은 항상 위험이 뒤따르게 됩니다.
  • 이를 해결하는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것입니다.

해결책#

  • 이 문제의 해결책은 여러 방법이 있습니다.
  • 가장 확실한 해결책은 데이터와 메서드를 분리하는 방식입니다.

세 클래스는 서로의 존재를 알지 못합니다.

  • 다만 위 해결책은 세가지 클래스를 인스턴스화하고 추적해야 한다는 게 단점입니다.
  • 이를 해결하기 위한 방법으로 퍼사드(Facade) 패턴이 있습니다.

퍼사드 패턴

  • 가장 중요한 메서드를 기존 Employee에 넣고 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용할 수도 있습니다.

덜 중요한 메서드를 퍼사드 패턴

결론#

  • 단일 책임 원칙은 메서드와 클래스 수준의 원칙입니다.
  • 컴포넌트 수준에서는 공통 폐쇄 원칙(Common Closure Principle) 이 됩니다.
  • 아키텍처 수준에서는 아키텍처 경계(Architectural Boundary) 의 생성을 책임지는 변경의 축이 됩니다.

8장. OCP : 개방-폐쇄 원칙#

  • OCP의 뜻은 소프트웨어 개체(artifact)는 확장에는 열려 있어야하고, 변경에는 닫혀 있어야합니다.
  • 소프트웨어 아키텍처를 공부하는 가장 근본적인 이유이며, 확장성이 없다면 엄청난 수정이 필요합니다.

사고 실험#

  • 어떤 변경이 들어왔을 때 소프트웨어 아키텍처가 훌륭하다면 변경되는 코드의 양이 가능한 한 최소화가 될 것입니다.
  • 서로 다른 목적으로 변경되는 요소를 적절하게 분리(단일 책임 원칙, SRP)하고 이들 요소 사이의 의존성을 체계화함으로써(의존성 역전 원칙, DIP) 변경량을 최소화할 수 있습니다.

먼저 단일 책임 원칙을 적용하면 데이터 흐름을 그림으로 나타낼 수 있습니다.

SRP 적용

  • 위처럼 책임을 분리한 다음에는 클래스 단위로 분할하고, 컴포넌트 단위로 구분해야 합니다.

클래스, 컴포넌트 단위로 구분

<I> : 인터페이스, <DS> : 데이터 구조

  • 이를 한 후는 컴포넌트 관계를 그립니다.

컴포넌트 관계

  • 컴포넌트 관계는 단방향으로 이루어집니다.
  • 위의 예시에서 볼 수 있듯이 추가 설명을 하면 다음과 같습니다.
    • Presenter에서 발견한 변경으로부터 Controller를 보호하고자 합니다.
    • View에서 발생한 변경으로부터 Presenter를 보호하고자합니다.
    • Interactor은 다른 모든 것에서 발생한 변경으로 보호하고자 합니다.
    • Interactor은 업무 규칙을 포함하기 때문에 가낭 높은 수준의 정책을 포함하는 특별한 규칙을 가지고 있습니다.
    • 위의 예시처럼 계층구조가 '수준(level)'이라는 개념을 바탕으로 어떻게 생성되는지 확인하는 것이 중요합니다.

위는 아키텍처 수준에서 OCP가 동작하는 방식입니다. 아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화합니다. 컴포넌트 계층구조를 이와 같이 조직화 하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있습니다.

방향성 제어#

  • 위의 예시에서 FinancialDataGateway 인터페이스는 GeneratorDataMapper 사이에 존재하는데 이는 의존성을 역전시키기 위해서 입니다.
    • 이러한 인터페이스가 없었다면 의존성이 Interactor 컴포넌트에서 Database 컴포넌트로 바로 향하게 됩니다.

정보 은닉#

  • FinancialReportRequester 인터페이스는 방향성 제어와는 다른 목적을 가집니다. 즉, Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재합니다.
    • 인터페이스가 없는 경우, ControllerFinancialEntities에 대해 추이 종속성(transitive dependency)을 가지게 됩니다.
  • 추아 종속성을 가지게 되면, 소프트웨어 엔티티는 '자신이 직접 사용하지 않는 요소에는절대로 의존해서는 안 된다'는 소프트웨어 원칙을 위반하게 됩니다.
  • 이 원칙은 인터페이스 분리 원칙(ISP)와 공통 재사용 원칙(CRP)을 설명할 때 다시 한번 설명합니다.
  • 즉, Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 원합니다.

결론#

  • OCP는 시스템의 아키텍처를 떠받치는 원동력 중 하나입니다.
  • OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는데 있습니다.
  • 이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 합니다.

9장. LSP : 리스코프 치환 원칙#

  • 리스코프에서는 치환(substitution) 법칙에 대해 이야기 합니다.
  • S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입입니다.

상속을 사용하도록 가이드하기#

LSP 예시

  • 위는 LSP를 준수한 예시입니다.

정사각형/직사각옇 문제#

  • LSP를 위반하는 전형적인 문제로 정사각형/직사각형 문제가 있습니다.
  • Square는 Rectangle의 하위 타입으로 적합하지 않으며, Rectangle의 높이와 너비는 반드시 함께 변경되나 Square의 경우 높이와 너비가 반드시 함께 변경되기 때문입니다.
  • LSP 위반을 막기 위한 유일한 방법은 if로 찾는 것이지만 이는 행위가 사용하는 타입에 의존성을 가지기 때문에 타입을 치환할 수 없게 됩니다.

LSP와 아키텍처#

  • 객체 지향은 초칭기 상속을 사용하다록 가이드하는 방법으로 간주되었으나, 현재는 좀 더 광범위한 소프트웨어 설계원칙으로 변모해 왔습니다.
  • 인터페이스는 다양한 형태로 나타납니다.
    • 자바라면 인터페이스와 이를 구현하는 여러개의 클래스
    • 루비라면 동일한 메서드 시그니처를 공유하는 여러 개의 클래스로 구성
  • 모든 상황은 물론 더 많은 경우에 LSP를 적용할 수 있습니다.
  • 아키텍처 관점에서 LSP를 이해하는 최선의 방법은 이 원칙을 어겼을 때 시스템 아키텍처에서 무슨 일이 일어나는지 관찰하는 것입니다.

결론#

  • LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 합니다.
  • 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문입니다.

10장. ISP : 인터페이스 분리 원칙#

  • 인터페이스 분리 원칙(ISP)는 다음의 다이어그램에서 이름이 유래했습니다.

인터페이스 분리 원칙

  • 위의 예시에서 OPS가 정적 타입 언어로 작성된 클래스 인 경우, User1이 op2와 op3를 전혀 사용하지 않으나 User1의 소스 코드는 이 두 메서드에 의존하게 됩니다.
  • 이러한 문제는 오프레이션을 인터페이스 단위로 분리하여 해결할 수 있습니다.

분리된 오퍼레이션

ISP와 언어#

  • 위의 사례는 언어 타입에 의존합니다.
  • 정적 타입 언어는 사용자가 import, use 또는 include와 같은 타입 선언문을 사용하도록 강제합니다.
    • 이 구조에서는 포함된(included) 선언문으로 인해 소스 코드 의존성이 발생하므로, 재 컴파일 또는 재배포가 강제되는 상황이 무조건 초래됩니다.
  • 루비나 파이썬과 같은 동적 타입 언어에서는 소스 코드에 이러한 선언문이 존재하지 않습니다. 다만 런타임에 추론이 발생합니다.
    • 즉, 소스 코드 의존성이 아예 없으며, 재컴파일과 재배포가 필요없습니다.
  • 동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때보다 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유입니다.
  • 즉, ISP는 언어와 관련된 문제로 볼 수도 있습니다.

자바의 경우.

  • 자바는 정적 타입 언어이지만, 재 컴파일만 하면 되는 것은 자바의 독특한 바인딩 방식이 있어서 그렇습니다.
  • 자바는 비-final, 비-private 인스턴스 변쉥 대해서는 호출할 정확한 메서드를 런타임에 결정하는 늦은 바인딩 (late binding)을 수행합니다.
  • 컴파일타임에는 호환되는 시그니처의 메서드가 타입 계층구조 어딘가에 존재하는지까지만 확인합니다.

ISP와 아키텍처#

  • ISP를 사용하는 근본적인 동기는, 우려사항입니다.
  • 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 위험한 일입니다.
  • 소스 코드 의존성의 경우, 분명한 사실이나 불필요한 재컴파일과 재배포를 강제하기 때문입니다.
    • 고수준 아키텍처 수준에서도 상황이 발생하게 됩니다.

문제가 있는 아키텍처

  • F 프레임 워크가 D 데이터베이스를 반드시 사용하게 만들었고, S 시스템은 F를 사용할려고 합니다.
  • F에서 불필요한 기능, 따라서 S와 전혀 관계없는 기능이 D에 포함된다고 가정할 때 그 기능 때문에 D 내부가 변경되면 F를 재배포해야 할 수도 잇고, S까지도 재배포해야 할지도 모릅니다.
  • 더 심각한 문제는 D 내부의 기능 중 F와 S에서 불필요한 그 기능에 문제가 발생해도 F와 S에 영향을 준다는 사실입니다.

결론#

불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실입니다.


11장 DIP : 의존성 역전 원칙#

  • 의존성 역전 원칙(DIP)에서 말하는 '유연성이 극대화된 시스템'이란 소스 코드 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템입니다.
  • 자바와 같은 정적 타입 언어에서 이 말은 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻입니다.
  • 루비나 파이썬과 같은 동적 타입 언어에도 동일한 규칙이 적용됩니다.
    • 소스 코드 의존 관계에서 구체 모듈은 참조하면 안됩니다. (그러나 구체 모듈을 정의하기가 어렵습니다.)
  • 이러한 아이디어를 규칙으로 보기는 비현실적입니다. 왜냐하면 소프트웨어 시스템은 구체적인 많은 장치에 반드시 의존하기 때문입니다.
  • 이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편입니다. 이러한 환경에 대한 의존성은 용납하는데, 변경되지 않는다면 의존할 수 있다는 사실을 이미 알고 있기 때문입니다.
  • 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰(volatile) 구체적인 요소입니다.

안정된 추상화#

  • 추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라 수정해야합니다.
    • 반대로 구체적인 구현체에 변경이 생기더라도 구현체가 구현한 인터페이스는 대다수의 경우 변경될 필요가 없습니다.
  • 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기 위해 애씁니다. (소프트웨어 설계의 기본)
  • 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻입니다.
  • 이를 실천법으로 정리하면 다음과 같습니다.
    • 변동성이 큰 구체 클래스를 참조하지마라. 대신 추상 인터페이스를 참조하라.
      • 일반적으로 추상 팩토리(Abstract Factory)를 사용하도록 강제합니다.
    • 변동성이 큰 구체 클래스로부터 파생하지 말라.
      • 상속은 아주 신중하게 사용해야 합니다.
    • 구체 함수를 오버라이드 하지 말라.
    • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

팩토리#

  • 위 규칙들을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 합니다.
  • 자바 등 대다수의 객체 지향 언어에서는 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용합니다.

추상 팩토리 예시

  • 위 직선은 아키텍처 경계를 의미하며 구체적인 것과 추상적인 것들을 분리합니다.
    • 추상 컴포넌트는 애플리케이션의 모든 고수준 업무 규칙을 포함하며, 구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함합니다.
  • 제어흐름은 소스 코드 의존성과는 정반대 방향으로 곡선을 가로지른다는 점에 주목합니다.
    • 이 원칙을 의존성 역전(Dependency Inversion) 이라고 부릅니다.

구체 컴포넌트#

  • 구체 컴포넌트에는 구체적인 의존성이 하나 있고, 이는 DIP에 위반됩니다.
  • DIP 위배를 모두 없앨 수는 있지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고 이를 통해 시스템의 나머지 부분과 분리할 수 있습니다.
  • 대부분의 시스템은 이러한 구체 컴포넌트를 최소한 하나를 포함할 것이며, 일반적으로 이 컴포넌트를 메인(Main)이라고 부릅니다.

결론#

  • 고수준 아키텍처 원칙을 다루게 되면서 DIP는 몇번이고 등장합니다.
  • DIP는 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙이 됩니다.
  • 의존성은 더 추상적인 엔티티가 있는 쪽으로만 향합니다. 이 규칙을 의존성 규칙(Dependency Rule)이라 부릅니다.
Last updated on