Post

[독서] Clean Architecture - 3부

설계 원칙의 경우 이전에 디자인 패턴을 공부했던 페이지를 참고하면 더 좋을 것 같다.

3부_ 설계 원칙

좋은 아키텍처를 정의하는 원칙, SOLID원칙

  • 함수와 데이터 구조를 클래스로 배치하는 방법
  • 클래스를 서로 결합하는 방법
    (클래스 : 함수와 데이터를 결합한 집합을 의미)

SOLID원칙의 목적

  • 변경에 유연하도록
  • 이해하기 쉽도록
  • 컴포트의 기반

‘중간 수준’, 모듈 수준에서 작업할 때 적용할 수 있음.
코드 수준보다는 조금 상위에서 적용
모듈-컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의할 때 사용
모듈 수준의 아키텍쳐를 잘 만들어도 전체적인 아키텍쳐는 망가질 수 있다.
컴포넌트 세계에서 SOLID원칙에 대응하는 원칙 / 고수준의 아키텍처 원칙을 함께 공부해야 함
SOLID원칙 요약

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

각 소프트웨어 모듈은 변경의 이유가 단 하나여야 한다.

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

기존 코드를 수정하기 보다 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계
시스템을 쉽게 변경할 수 있도록 하는 원칙.

LSP : 리스코프 치환 원칙(Liskov Subsitution Principle)

상호 대체 가능한 구성요소를 이용해 소프트웨어를 만들기 위해서는 구성요소가 서로 치환 가능해야 한다는 계약을 지켜야 한다.

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

소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.

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

고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 의존하면 안된다-세부사항이 정책에 의존해야 한다.

7장 : SRP - 단일 책임 원칙

단일 모듈은 변경의 이유가 하나여야 한다.
여기서, ‘변경의 이유’란 사용자와 이해관계자를 가리킨다. 이러한 시스템의 변경을 원하는 사용자와 이해 관계자 집단을 액터(Actor)라고 부른다. 따라서 위의 문장은 다음과 같은 뜻을 가진다.
하나의 모듈은 하나의 액터만 책임진다.
모듈이란 함수와 데이터 구조로 응집된(cohesive) 집합이다.

징후 1 : 우발적 중복

각기 다른 액터(회사로 비유하자면, 부서-회계부서,인사부서,개발부서)가 관여하는 세 메소드를 단일 클래스에 배치해 모두가 함께 사용한다고 가정하자. 회계부서에서 메소드를 변경할 때 그 메소드가 다른 곳에서도 사용되었지만 고려하지 못했을 경우, 다른 부서에서는 문제점을 인식하지 못해 오류를 발생시킬 수 있다.

서로 다른 액터가 의존하는 코드를 서로 분리해야 한다.

징후 2 : 병합

메서드가 많고 각 메서드는 다른 액터를 책임진다면 병합이 발생할 가능성은 매우 높다.
이러한 경우는 많은 사람이 서로 다른 목적으로 동일한 소스 파일을 변경하는 경우에 발생한다. (conflict가 발생하는 경우를 생각하면 될까?)
역시 서로 다른 액터가 의존하는 코드를 서로 분리해야 한다.

가장 확실한 해결책은 데이터와 메서드를 분리하는 방식이다.
아무런 메서드가 없는 간단한 데이터 구조 클래스를 만들고 각 액터가 책임지는 세 클래스가 데이터 클래스를 공유하도록 한다.
각 클래스는 반드시 필요한 소스 코드만을 포함하며, 서로의 존재를 모르도록 한다. 이를 통해 ‘우연한 중복’을 막을 수 있다.
사족) 말은 정말 쉽지만, 이 ‘서로의 존재를 모르도록’ 클래스를 설계하는 것은 정말 어려운 일이다. 그저 편한 방법으로 코드를 짜다 보면, 서로의 존재와 구조를 알지 않고서는 도저히 작동하지 못하는 코드가 완성되어있다..
이러한 방식의 해결책은 개발자가 세 클래스를 인스턴스화 하고 추적해야 한다는 단점이 있다. 이것은 흔히 퍼사드(Facade) 패턴을 통해 해결한다.
거의 코드를 갖지 않는 클래스를 만들고, 이 클래스가 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.
상태 변경과 트랜잭션 메모리
각 클래스는 하나의 유효범위가 되고, 그 유효범위 바깥에서는 어떤 멤버가 존재하는지 전혀 알 수 없다!

결론

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

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

소프트웨어 개체(artifact)는 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 한다.
즉, 소프트웨어 개체의 행위는 언제든 확장이 가능하지만 개체를 변경해서는 안된다.
OCP는 클래스와 모듈을 설계할 때 뿐만아니라 컴포넌트 수준에서 OCP를 고려하는 것이 중요하다.

이상적인 소프트웨어 아키텍쳐에서 기능의 추가를 위해 변경되는 코드의 양은 0이라는 것이다.
이를 위해 서로 다른 목적으로 변경되는 요소를 적절히 분리하고(SRP-단일 책임 원칙), 요소 사이의 의존성을 체계화한다.(DIP-의존성 역전 원칙)
책임을 분리하면 책임 중 어느 하나가 변경되어도 다른 책임은 변경이 일어나지 않도록 의존성을 조직화해야 하며, 조직화한 구조는 행위가 확장될 때 변경이 발생하지 않음을 보장해야 한다.
이를 위해서는 처리 과정을 클래스 단위로 분할하고, 클래스를 컴포넌트 단위로 구분해야 한다.
처리 과정을 클래스 단위로 분할하고, 클래스는 컴포넌트 단위로 분리한다.
컴포넌트 관계는 단방향으로 이루어진다. 즉, 의존성을 역전하는 방향으로는 결코 호출이 일어나지 않아야 한다. 즉, A클래스에서 B클래스로 화살표가 향한다면, A클래스는 B클래스를 호출할 수 있지만 B클래스는 A클래스의 존재를 전혀 알지 못한다.
또한 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
컴포넌트 관계는 단방향으로만 이루어진다.
즉, A컴포넌트에서 발생한 변경으로부터 B컴포넌트를 보호하기 위해서는 A컴포넌트가 B컴포넌트에 의존해야 한다. B컴포넌트가 B컴포넌트의 존재를 몰라야 하기 때문!!!
이 예제에서 Interactor는 다른 어떤 변경에도 영향을 받지 않는다. Interactor는 업무규칙을 포함하므로 변경에 영향을 받지 않도록 구성된 것.
다른 부수적인 문제는 다른 컴포넌트가 다루게 하고, 중요한 문제를 다루는 것은 Interactor가 한다.
또 Controller는 Interactor보다 부수적이지만, Presenter와 View에 비해서는 중요한 문제를 다룬다.
이렇 듯이 보호의 계층구조는 수준(level)을 바탕으로 생성된다. 높은 level은 가장 보호받고, 낮은 level의 컴포넌트일수록 보호받지 못한다.
OCP는 아키텍트가 기능이 어떻게(how), 왜(why), 언제(when)발생하는지에 따라 기능을 분리하고 기능을 컴포넌트의 계층 구조로 조직화한다.

방향성 제어

그림 8.2의 클래스 설계도에서는 의존성을 올바른 방향으로 향하게 하기 위해 인터페이스가 위치해있다. 예를 들어 FinacialDataGateway 인터페이스가 FinancialReportGenerator와 FinancialDataMapper 사이에 위치함으로서 의존성이 interactor컴포넌트에서 Database컴포넌트로 바로 향하는 것을 막는다.

정보 은닉

FinancialReportRequester 인터페이스는 FinancialReportController로부터 Interactor 내부에 대한 정보를 숨긴다. 즉, Controller가 추이종속성(transitive dependency)를 갖는 것을 막는다.
추이종속성이란, 클래스 A가 클래스 B에 의존하고 클래스 B가 클래스 C에 의존해 클래스 A가 클래스 C에 의존하게 되는 것이다. 클래스 의존성이 cyclic하다면 모든 클래스가 서로 의존하게 된다.
추이 종속성을 갖는다면 소프트웨어 엔티티는 ‘자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안된다’는 소프트웨어 원칙을 위반한다.
참고) 인터페이스 분리 원칙(ISP), 공통 재사용 원칙(CRP) 이는 Controller에서 발생한 변경으로부터 Interactor를 보호하는 것이 우선순위가 높으나 Interactor에서 발생한 변경으로부터 Controller도 보호하고 싶기 때문이다. 이를 위해 Interactor 내부를 은닉한다.

결론

OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경에 시스템이 너무 많이 영향을 받지 않도록 하는 것이다.
이를 위해 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 의존성 계층구조를 만들어야 한다.

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

다음은 바바라 리스코프가 정의한 하위타입이다.

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

궁금증 : 그렇다면 S가 T의 하위 타입이면서, T가 S의 하위타입일 수도 있을까?

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

Licencse 클래스는 두 가지 하위 타입이 존재한다. 각 하위타입은 서로 다른 알고리즘으로 라이센스 비용을 계산하지만, Billing 애플리케이션의 행위는 License 하위 타입 중 무엇을 사용하는지에 대해서 전혀 의존하지 않는다. 따라서 각 하위타입은 License를 치환할 수 있으므로 이 설계는 LSP를 준수한다고 할 수 있다.
LSP를 준수하는 License와 파생 클래스

정사각형/직사각형 문제

이제 LSP를 위반하는 전형적인 문제를 알아보자.
정사각형/직사각형 문제
Square는 높이와 너비가 반드시 함께 변경되어야 하지만, Rectangle은 서로 독립적으로 변경이 가능하다. 따라서 Square는 Rectangle의 하위 타입으로는 적합하지 않다.
따라서 User가 대상을 Rectangle이라고 생각해 높이와 너비를 각각 다른 값으로 변경하려 한다면, 에러가 발생한다.
이런 형태의 LSP 위반을 막기 위해서는 if문 등을 통해 Rectangle이 실제로는 Square인지를 검사하는 메커니즘을 User에 추가해야 한다.
이는 User의 행위가 사용하는 타입에 의존하게 되어 타입을 치환할 수 없게 된다.

LSP와 아키텍처

LSP는 인터페이스와 구현체에 적용되는 소프트웨어 설계 원칙이다.
인터페이스란, JAVA와 같은 언어에서는 인터페이스 하나와 이를 구현하는 여러 개의 클래스로, 루비에서는 동일한 메서드 시그니처를 공유하는 여러 개의 클래스로, 또 동일한 REST 인터페이스에 응답하는 서비스 집단일 수도 있다.
인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자가 존재하는 모든 경우에 LSP를 적용할 수 있다.
LSP원칙을 어겼을 때 시스템 아키텍처에서는 무슨 일이 일어나는가?

LSP 위배 사례

기존 시스템에서 같은 동작을 하지만 다른 필드명을 사용하는 코드가 작성되었다고 하자. 이 url을 처리하기 위해서는 if문을 통한 매커니즘이 추가되어야 하고, 이 추가된 매커니즘은 예기치 못한 수많은 문제를 야기할 수 있다. 그리고 추가되는 매커니즘의 양은 아주 방대해질 수 있다.

결론

LSP는 반드시 아키텍처 수준까지 확장해야만 한다. 시스템 아키텍처의 오염으로 인해 상당한 양의 별도 매커니즘을 추가해야 할 수 있기 때문이다.

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

‘유연성이 극대화된 시스템’ 이란 소스 코드 의존성이 추상(abstraction)에 의존하며, 구체적인 것(concretion)에는 의존하지 않는 시스템이다.
즉, 자바 등의 정적 타입 언어에서 use, import, include문은 인터페이스, 추상 클래스 등의 추상적인 선언 만을 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안된다.
파이썬과 같은 동적타입 언어에서도 동일하게, 소스 코드 의존 관계에서 구체 모듈은 참조해서는 안된다.
그러나 이런 아이디어는 굉장히 비현실적이다.
우리가 주목할 것은 변동성이 큰(Volatile) 구체적인 요소이다. 즉, 우리가 개발중이어서 자주 변경되는 모듈들이다.

안정된 추상화

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.