[Clean Architecture] 3부 설계원칙

okstring·2022년 10월 31일
0
post-thumbnail

3부 설계원칙

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

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

여기서 중간수준이라 함은 모듈 수준에서 작업할 때 적용할 수 있다는 뜻이며 코드수준보다는 조금 상위에서 적용

7장 SRP: 단일 책임 원칙

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.

  • 헷갈리지 말아야 하는게 단 하나의 일만 해야한다는 것이 아니다. 단 하나만 일을 해야 하는 것은 함수다.
  • 모듈: 단순한 정의는 소스파일, 모듈은 단순히 함수와 데이터 구조로 구성된 응집된 집합

징후1: 우발적 중복

이 클래스는 SPR를 위반하는데, 이들 세 가지 메서드가 서로 매우 다른 세명의 액터를 책임지기 때문이다.

징후2: 병합

소스파일에 다양하고 많은 메서드를 포함하고 특히 이들 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 확실히 더 높다.

해결책

  • 가장 확실한 해결책은 데이터와 메소드를 분리하는 방식일 것이다.
  • 간단한 데이터 구조인 EmployeeData 클래스를 만들어, 세개의 클래스가 공유하도록 한다. 세 클래스는 서로의 존재를 몰라야 하며 따라서 우연한 중복을 피할 수 있다.

  • 한편 개발자가 세가지 클래스를 인스턴스화하고 추적해야 한다는게 단점이다. 이럴 때 Facade 패턴을 흔히 쓴다.
    • EmployeeFacade에 코드는 거의 없다. 세 클래스의 객체를 생성하고 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.

  • 어떤 개발자는 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호한다. 이 경우라면 가장 중요한 메서드는 기존의 Employee 클래스에 그대로 유지하되 Employee 클래스를 덜 중요한 나머지 메서드들에 대한 퍼사드로 사용한다.

결론

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

8장 OCP: 개방-폐쇄 원칙

소프트웨어 개체(artifact)는 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다.

OCP는 클래스와 모듈을 설계할때 도움되는 원칙이라고 알고있지만 아키텍처 컴포넌트 수준에서 OCP를 고려할 때 훨씬 중요한 의미를 가진다.

사고 실험

  • 소프트웨어 아키텍처가 훌륭하다면 변경되는 코드의 양이 가능한 한 최소화 될 것이다. 이상적인 변경량은 0이다.
  • 서로 다른 목적으로 변경되는 요소를 적적랗게 분리하고(단일 책임 원칙, SRP), 이들 요소 사이의 의존성을 체계화 함으로써(의존성 역전 원칙, DIP) 변경량을 최소화 할 수 있다.
  • 모든 컴포넌트 관계는 단방향으로 이루어진다
  • A컴포넌트에서 발생한 변경으로부터 B컴포넌트를 보호하려면 반드시 A컴포넌트가 B컴포넌트에 의존해야 한다.

방향성 제어

  • 의존성 역전 원칙

정보 은닉

  • 추이 종속성을 막기 위함

    • Controller에서 발생한 변경으로부터 Interactore를 보호하는 일의 우선순위가 가장 높지만 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바라고 이를 위해 Interactore 내부를 은닉한다
    • 추이 종속성: 클래스 A가 클래스 B에 의존하고 다시 클래스 B가 클래스 C에 의존한다면 클래스 A는 클래스 C에 의존하게 된다. 이를 추이 종속성이라 부른다.

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

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

명제가 길어 다른 정의를 가져온다면

부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙

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

  • 이 설계는 LSP를 준수하는데 Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문이다.

정사각형/직사각형 문제

  • Square는 Rectangle의 하위 타입으로 적합하지 않은데 Rectangle의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면, Square의 높이와 너비는 반드시 함께 변경되기 때문이다.
  • LSP 위반을 막기 위한 유일한 방법은 (if 문 등을 이용해서) Rectangle이 실제로는 Square인지 검사하는 매커니즘을 추가하면 되지만 User의 행위가 사용하는 타입에 의존하게 되므로 결국 타입을 서로 치환할 수 없게 된다.

LSP와 아키텍처

  • 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계원칙으로 변모해왔다.
  • 여기서 말하는 인터페이스는 다양한 형태로 나타난다. JAVA라면 인터페이스 하나와 이를 구현하는 여러개의 클래스로 구성되는 등

결론

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

+잘못된 객체를 상속하거나 올바르게 확장하지 못할 경우 위배 가능성이 크다.

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

  • 정적 언어타입의 경우, 분리된 오퍼레이션으로 새로 컴파일하고 다시 배포하는 상황이 초래되지 않는다

ISP와 언어

  • 정적 언어 같은 경우 included, use와 같은 타입선언문을 강제하게되고 소스코드 위존성이 발생한다
  • 동적 언어 같은 경우 선언문이 존재하지 않지만 런타임에 추론이 발생한다.
  • 이러한 사실로 인해 ISP를 아키텍처가 아니라 언어와 관련된 문제라고 결론내릴 여지가 있다.

ISP와 아키텍처

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

  • F에서는 불필요한 기능 즉, S와는 전혀 관계없는 기능이 D에 포함된다면 그 기능때문에 D 내부가 변경되면 F를 재배포해야 할 수도 있고 따라서 S까지 재배포해야 할지 모른다.

결론

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

11장 DIP: 의존성 역전 원칙

의존성 역전 원칙(DIP)에서 말하는 유연성이 극대화된 시스템이란 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.

  • 이 아이디어를 규칙으로 보기는 확실히 비현실적이다. 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다

    ex) Java에서 String 클래스는 구체 클래스며 반드시 의존하기 때문 하지만 String 클래스는 매우 안정적이다.

    • 이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편이다.
    • 우리가 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰 구체적인 요소다.

안정된 추상화

안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.

  • 변동성이 큰 구체 클래스를 참조하지 말라
    • 대신 추상 인터페이스를 참조하라
  • 변동성이 큰 구체 클래스로부터 파생하지 말라
    • 상속은 의존성을 가진다는 사실에 변함이 없기 때문에 신중에 신중을 거듭해야 한다.
  • 구체함수를 오버라이드 하지 말라
    • 구체함수는 소스코드 의존성을 필요로 한다. 따라서 구체함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되고 그 의존성을 상속하게 된다.
    • 이를 해결하려며 추상함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현
  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

팩토리

이처럼 바람직하지 못하게 된 의존성을 처리할 때 추상팩토리를 사용한다

  • Application에서 어떤식으로든 ConcreteImpl의 인스턴스를 생성해야 한다면

    • ConcreteImpl에 대해 소스코드 의존성을 만들지 않으면서 이 목적을 이루기 위해 Application은 serviceFactory인터페이스의 makeSvc메서드를 호출한다

    • ServiceFactory로부터 파생된 ServiceFactoryImpl에서 구현되고 ->

      그 구현체가 ConcreteImpl의 인스턴스 생성한 후 ->

      Service 타입으로 반환한다.

  • 곡선은 시스템을 두가지 컴포넌트로 분리한다. 추상 컴포넌트, 구체 컴포넌트

  • 제어흐름은 소스코드 의존성과는 정반대 방향으로 곡선을 가로지른다는 점에 주목하자. 소스코드 의존성은 제어 흐름과는 반대방향으로 역전되므로 의존성 역전(Dependency Inversion)이라고 부른다.

구체 컴포넌트

DIP위배를 모두 없앨 수는 없다. 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있다.


클린 아키텍처 소프트웨어 구조와 설계의 원칙
로버트 C. 마틴 저

http://www.yes24.com/Product/Goods/77283734

profile
step by step

0개의 댓글