본문 바로가기

PROGRAMMING

[Clean Architecture] 책 읽기 7장 ~ 11장

클린 아키텍처 책을 읽고 혼자 정리하는 글

3부 설계 원칙

좋은 소프트웨어 시스템은 깔끔한 코드로부터 시작한다.

좋은 아키텍처를 정의하는 원칙을 SOLID 라고 하고

이 원칙을 준수해서 작성하는 코드는 좋은 아키텍처를 만들 수 있다.

 

SOLID 원칙은 클래스를 만들고 결합하는 방법에 대해 설명해준다.

(여기서 클래스는 단순히 함수와 데이터를 결합한 집합의 의미)

 

SOLID 원칙의 목적은?

중간 수준의 소프트웨어 구조가 

1. 변경에 유연하고

2. 이해하기 쉽고

3. 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 될 수 있도록

만드는 것이 목적이다.

 

(중간 수준의 소프트웨어는 모듈 수준이라고 이해하면 된다. 코드 레벨의 상위)

 

 

이 책의 설명 순서는 다음과 같다.

SOLID -> 컴포넌트 세계의 SOLID를 대응하는 원칙 -> 고수준 아키텍처 원칙 

 

단일 책임 원칙 SRP : Single Responsibility Principle
  각 소프트웨어 모듈은 변경의 이유가 단 하나여야만 한다.

개방-폐쇄 원칙 OCP : Open-Closed Principle
  시스템의 행위를 변경할 때 기존의 코드를 수정하기보다 반드시 새로운 코드를 추가하는 방식이어야 쉽게 변경할 수 있다.
  소프트웨어는 확장에 열려있고 변경에 닫혀있어야 한다.

리스코프 치환 원칙 LSP Liskov Subtitution Principle
  상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이 구성요소는 반드시 서로 치환 가능해야한다.

인터페이스 분리 원칙 ISP Interface Segregation Principle
  사용하지 않은 것에 의존하지 않아야 한다.

의존성 역전 원칙 DIP : Dependency Inversion Principle
  고수준 정책을 구현하는 코드는 저수준을 구현하는 코드에 절대로 의존해서는 안 된다.
  세부사항이 정책에 의존해야 한다.

 

 

 

이제 이러한 원칙이 아키텍처 관점에서 지닌 의미에 대해 R아보자


7장. SRP : 단일 책임 원칙

단일 책임 원칙은 말 그대로 한 가지 일만 해야 한다는 것이 아니라

 

단일 모듈의 변경 이유가 단 하나뿐이어야 한다.

= 하나의 모듈은 하나의 액터 (사용자 또는 이해관계자) 에 대해서만 책임 져야 한다.

 

모듈이란?

함수와 데이터 구조로 구성된 응집된 집합이다.

여기서의 응집성을 SRP 라고 할 수 있다.

 

-> 단일 액터를 책임지는 응집된 집합

 

 

단일 책임 원칙 SRP를 지키지 않았을 경우 일어나는 일

1. 하나의 클래스에서 여러가지의 메소드가 있을 경우 (결합된 경우) 서로 다르게 의존하는 무언가에 영향을 미칠 수 있다.

예를 들어,

1팀, 2팀 에서 근무시간을 계산하는 알고리즘을 공유할 때, 1팀이 이 알고리즘을 수정하면 2팀에도 영향이 미치는 것이다.

 

=> 결론은 의존하는 코드를 서로 분리해야 한다. 

 

2. merge conflict

동시에 1팀, 2팀이 동일한 코드를 서로 다른 목적으로 변경하는 경우에 충돌이 발생한다.

 

해결책

1. 데이터와 메서드를 분리

2. Facade 패턴

 

https://ko.wikipedia.org/wiki/%ED%8D%BC%EC%82%AC%EB%93%9C_%ED%8C%A8%ED%84%B4

 

퍼사드 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org

 

결론

SRP 는 메서드와 클래스 수준의 원칙이다.

이보다 상위인 컴포넌트 수준에서는 공통 폐쇄 원칙이 되고, 아키텍처 수준에서는 아키텍처 경계를 책임지는 축이 된다.

 

메서드와 클래스 수준에서부터 아키텍처 수준까지 분리를 열심히 해야겠다..!

 

 


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

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

= 행위는 확장할 수 있어야 하지만, 개체를 변경해서는 안 된다.

 

아키텍처를 공부하는 가장 근본적인 이유라고 한다!!

소프트웨어를 확장할 때 많은 수정이 필요하지 않다!

 

보고서를 생성하고 화면에 출력할지 프린터로 출력할지 나타내는 소프트웨어의 클래스 구조이다.

 

1) FinancialDataMapper 는 FinancialDataGateway 를 알지만 반대로는 알지 못한다!

--> 모든 컴포넌트 관계는 단방향으로 이루어지고 이는 의존성을 나타낸다.

 

2) 의존성 역전

 

3) FinancialReportRequester 인터페이스의 목적은 정보 은닉이다.

이 인터페이스는 Interactor 내부에 대해 자세히 알지 못하도록 하기 위해 존재한다.

 

그리고 만약 이 인터페이스가 없다면

FinancialReportController  -->  FinancialReportGenerator  -->  FinancialEntities

 

이런 의존 관계가 형성되어서 FinancialReportController 이 Entity 에 대해 추이 종속성을 가지게 된다.

(A -> B, B -> C  일때 A -> C의 의존관계를 갖는 것)

이는 ISP를 위반하게 된다.

 

A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.
     A      -->      B
(고수준) --> (저수준)

 

View 는 가장 낮은 수준의 개념 중 하나이며 거의 보호받지 못한다.

Presenter는 View 와 Controller 사이의 수준에 위치한다.

 

--> 이렇게 기능을 분리하고 컴포넌트의 계층구조로 조직화하는 것이 OCP가 동작하는 방식이다.

 

결론

OCP의 목표는 확장하기 쉬운 동시에 변경이 시스템에 많은 영향을 끼치지 않도록 하는데 있다.

 

 

 


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

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

 

상속

상속의 경우를 살펴보자 (LSP)

 

 

그리고 PersonalLicense 와 BusinessLicense 두가지 모두 License 의 하위타입이다.

Billing 의 행위가 두 가지 하위타입 중에 어떤 것을 사용하는지 전혀 의존하지 않고 있기 때문에

 ( = License 타입을 두 가지 하위타입으로 치환할 수 있다.)

 

이 설계는 LSP를 준수하고 있다고 본다.

 

LSP 위반 사례

 

그렇다면 LSP를 위반하는 

정사각형 / 직사각형 문제를 살펴보자

 

 

이 예제에서 Rectangle 의 하위타입으로 Square 은 적합하지 않다.

(가로, 세로가 다른 Rectangle, 가로, 세로가 같은 Square)

 

여기서 LSP 위반을 막기 위한 유일한 방법은 Rectangle 이 Square 인지를 검사하는 매커니즘 (if문) 을 User에 추가하는 것이다.

이렇게 되면 User가 타입에 의존하기 때문에 치환이 불가능해진다. 

 

LSP와 아키텍처

LSP는 인터페이스와 구현체에도 적용되는 소프트웨어 설계 원칙이 되었다.

 

아키텍처 관점에서 LSP를 위배한 사례

여러 택시 업체가 동일한 REST 인터페이스를 사용하지 않고

예를들어 한 업체가 destination을 dest로 사용했다면?

 

그 업체를 구분해주기 위해 if 문을 써서 구분하는 코드를 추가적으로 작성해야한다.

-> 별도 매커니즘을 추가해야하는 잘못 설계된 소프트웨어

 


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

 

내가 아는 ISP : 사용하지 않는 인터페이스에 의존해서는 안된다.

ISP와 아키텍처

이 책에서는 ISP를 아키텍처가 아니라 언어와 관련된 문제라고 한다. (이해가 잘..)

 

잠시)

정적 타입 언어 - 자료형을 컴파일 시에 결정하는 것 (맞지 않으면 컴파일 에러) - C, C++, Java, Swift도 정적 타입 언어겠죠

동적 타입 언어 - 자료형을 실행 시에 결정하는 것 - 자스, 루비, 파이썬

 

정적 타입 언어는 import, use, include 와 같은 선언문을 사용하도록 강제하기 때문에

소스 코드 의존성이 발생하고 -> 이로인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.

 

동적 타입 언어가 정적 타입언어보다 유연하고, 결합도가 낮은 시스템을 만들 수 있는 이유가 이것이다.

 

음..

자바는 정적 타입 언어이지만 비-final, 비-private 인스턴스 변수에 대해서는 호출할 정확한 메서드를 런타임에 결정하는 (late-binding) 늦은 바인딩을 수행하기 때문에 결합도가 낮은 시스템을 만들 수 있다고 한다.

-> Swift와 유사하게 동작하는구나 

 

따라서 ISP는 언어 종류에 따라 받는 영향이 다르다.

 

이해가 잘 가지 않는다. ㅇㅅㅇ

 


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

의존성 역전이 뭔가요 ?

- 구체화된 구현체에 의존하지 않고 추상화된 인터페이스에 의존하게 만드는 것

- 제어흐름과 반대 방향으로 역전되기 때문에 의존성 역전이라고 한다.

 

의존성 역전이 왜 필요한가요 ?

- 모듈/컴포넌트 분리해서 유연성 좋은 소프트웨어를 만들기 위해

 

(Java) 하지만 String 같은 모듈을 사용하려면 java.lang.String 을 import 해야해서 의존성이 생기는데요?

(인터페이스가 아니라 String 구현체를 직접 import)

- String 클래스는 변경될 일이 적기 때문에 매우 안정적

- 따라서 이렇게 안정성이 보장된 환경 (운영체제, 플랫폼) 에서는 DIP를 논하지 않는 편

 

그러면 안정된 소프트웨어를 만들기 위해서는 어떻게 해야하나요?

- 추상화된 인터페이스를 만들때 변경할 일이 적게 만든다.

 

구체적으로 어떻게 구현해야할지 알려주세요 !

1. 변동성이 큰 구체화된 클래스를 참조하지 말고 추상화된 인터페이스를 참조한다.

2. 변동성이 큰 구체화된 클래스를 상속(파생) 하지 말고 신중하게 사용한다. 

    왜냐하면 정적 타입 언어에서 상속은 관계에 있어서 강력함과 동시에 변경이 어렵기 때문이다. (의존성)

3. 구체화된 함수를 오버라이드 하지 말자.

    구체화된 함수는 대체로 의존성을 필요로 하는데 오버라이드하면 이러한 의존성을 제거할 수 없다.

    의존성을 상속하게 됨

    차라리 추상 함수로 선언해라.

 

안정된 소프트웨어 아키텍처

변동성이 큰 구체화된 클래스에 의존하는 것을 지양하고 추상화된 인터페이스에 의존하는 것을 선호하는 아키텍처 겠네요~

 

그렇다면 변동성이 큰 구체화된 객체를 생성할 때는 어떡하죠?

- 주의해서 생성해야하는데 의존성을 관리하기 위해서 팩토리 / 추상 팩토리를 사용할 수 있다.

 

팩토리 패턴에 대해 설명해주세요~

- 팩토리 패턴은 상위 클래스가 알 필요없는 구체 클래스를 생성하는 패턴 (어떤 구체 클래스를 생성할지 결정)

- 추상 팩토리 패턴은 팩토리 패턴을 사용해서 조금 더 추상화 한 개념으로만 이해

 

나중에 코드로 자세히 알아보자 (추상 팩토리 패턴 - 위키)

https://ko.wikipedia.org/wiki/%EC%B6%94%EC%83%81_%ED%8C%A9%ED%86%A0%EB%A6%AC_%ED%8C%A8%ED%84%B4

 

DIP 에 위배되는 코드를 전~부 없앨 수는 없겠지만 추상 팩토리 패턴을 사용해서 최소화하는 것이 좋겠네요~

 

 

 

갑자기 Q&A 형식으로 써보고 싶어진 결과

 

 

 

 

'PROGRAMMING' 카테고리의 다른 글

[Clean Architecture] 책 읽기 1장 ~ 6장  (0) 2023.03.27
LG ThinQ 오픈소스 목록  (0) 2021.08.19
API  (0) 2021.08.13
REST API  (0) 2021.03.23
react native 관해서 끄적  (0) 2021.02.20