3부, 설계 원칙

2021.10.19

  • 좋은 소프트웨어 시스템은 클린 코드로부터 시작한다.
    • 좋은 벽돌을 사용하지 않으면 아키텍처가 좋고 나쁨은 큰 의미가 없는 것 과 같다.
    • 반대로 좋은 벽돌을 사용하더라도 아키텍처를 엉망으로 만들 수 있다.
  • SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는데 있다.
    • 변경에 유연하다.
    • 이해하기 쉽다.
    • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
  • SOLID원칙의 탄생은 아래와 같다.
    • 1980년대부터 사람들이 소프트웨어 설계 원칙을 토론하였는데, 이 과정에서 나온 원칙들은 점차적으로 정리가 되어갔고, 2004년대에 정리된 원칙들을 재배열하면서 탄생했다.

7장 Single Responsibility Principle : 단일 책임의 원칙

  • 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다. ( 단 하나의 일만 해야 한다는 것이 아니다 )
    • 모듈(소스파일 or 단순히 함수와 데이터구조로 이루어진 코드)은 하나의 집단(사용자 or 이해관계자)에 대해서만 책임진다.
  • 응집된이라는 단어가 SRP를 암시한다.

징후 1 : 우발적 중복

  • Employee 클래스
    • calculatePlay -> 회계팀에서 기능을 정의, 사용
    • reportHours -> 인사팀에서 기능을 정의, 사용
    • save -> 데이터베이스 관리자가 기능을 정의, 사용
    • 각 메서드내에서 편의를 위해 다른 메서드를 호출하는 코드가 생성된다면, 우연하게 중복이 발생한다.
      • 데이터가 꼬이거나 추적이 힘들어지거나 스파게티가 될 수 있다.

징후 2: 병합

  • 위와 같은 패턴이 많다면, 병렬로 개발되어지는 상황에서 코드 병합시 문제가 발생할 수 있다

해결책

  • PayCalculator 클래스
  • HourReporter 클래스
  • EmployeeSaver 클래스
  • 각 클래스 인스턴스를 생성하고 추적하는 비용이 피로하면 퍼사드(Facade)패턴을 사용할 수 있다.

결론

  • 단일 책임 원칙은 메서드와 클래스 수준의 원칙이다.
  • 이보다 상위 수준인 컴포넌트에서는 공통 폐쇄 원칙(Common Closure Principle)이 된다.
  • 아키텍처 수준에서는 아키텍처 경계의 생성을 책임지는 변경의 축(Axis of Change)이 된다.

8장 Open-Closed Principle : 개방-폐쇄 원칙

  • 소프트웨어 개체는 확장에는 열려 있어야하고, 변경에는 닫혀 있어야 한다.
    • 개체의 행위는 확장할 수 있어야 하지만, 산출물을 변경해서는 안 된다.
  • View, Presenter, Controller, Interactor 등의 컴포넌트 관계는 모두 단방향으로만 이루어져야 한다.
    • 각 컴포넌트의 수준(레벨)은 View < Presenter < Controller < Interactor
  • 아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 컴포넌트의 계층구조로 조직화한다.
  • 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.

결론

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

9장 Liskov Substitution Principle : 리스코프 치환 원칙

  • 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야한다는 원칙이다.
  • 초창기에는 상속을 사용하도록 가이드하는 방법 정도로 간주되었다.
  • 시간이 지나면서 인터페이스와 구현체에도 적용되는 광범위한 소프트웨어 설계 원칙으로 변모되었다.

결론

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

10장 Interface Segregation Principle : 인터페이스 분리 원칙

  • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.
  • 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다.
  • 정적 타입의 언어가 아닌 동적 타입 언어에서는 (ex. 파이썬 루비 ) import, include, 문과 같은 선언문이 필요가 없고 런타임에 추론이 발생한다.
  • 위와 같은 특징으로 미루어보아 ISP를 아키텍처가 아니라 언어와 관련된 문제라고 결론내릴 여지가 있다.
  • 불필요한(사용하지 않는) 코드 의존성을 가지고 있으면 재배포를 해야할 수 있다.

결론

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

11장 Dependency Inversion Principle : 의존 역전 원칙

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다는 원칙이다. ( 자신보다 변하기 쉬운 것에 의존하지 말아라. )
  • 의존성을 추상에 의존하고 구현체에는 의존하지 않도록 한다.
  • 이 아이디어를 규칙으로 보기에는 굉장히 비현실적이다.
    • String, FileSystem 과 같은 빌트인되어 있는 모듈들은 구현체를 참조하기 때문이다.
  • 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기 위해 애쓴다.
  • 변동성이 큰 구현체에는 의존성을 두지 말자.
    • 객체 생성 방식을 강하게 제약, 추상 팩토리 를 사용하도록 강제.
  • 변동성이 큰 구현체 모듈로부터 상속받지 말라.
    • 정적 타입 언어에서 상속은 가장 강력하나 동시에 뻣뻣하다.
  • 구현체 함수를 오버라이드하지 말라.
    • 구현체 함수는 소스 코드 의존성을 필요로 하다. 이를 오버라이드하면 의존성 제거가 불가하고, 의존성을 상속하게 된다.
    • 차라리 추상 함수로 선언하고 구현체 각각 용도에 맞게 구현하자.

결론

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