클린아키텍처 3부: 설계원칙

Jihyun·2021년 12월 5일
0

이 글은 한달 한권 챌린지의 두번째 책, 클린아키텍처를 읽고 정리한 내용이다.

SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명해준다.


7장. SRP: 단일 책임 원칙

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


모듈은 단순히 함수와 데이터 구조로 구성된 응집된 집합이다. '응집된(cohesive)'이라는 단어가 SRP를 암시한다.

변경의 이유는 변경을 요청하는 집단(사용자, 이해관계자)으로 바꿔 말할 수 있고, 이를 액터(Actor)라 칭하겠다.

단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성(cohesion)이다.

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


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

각기 다른 액터를 책임지고 있는 메서드들을 단일 클래스에 배치한다면 액터가 서로 결합되어 버린다.

  1. 몇 개의 메서드가 일부 알고리즘을 공유하기에 중복을 피하기 위해 하나의 메서드로 관리하는 경우,
    하나의 액터에 의해 공유되던 메서드를 수정하는 순간 다른 액터는 원치 않은 변경이 발생한 것이므로 문제가 발생할 수 있다.

  2. 서로 다른 액터가 다른 이유로 동일한 클래스를 수정하려는 경우,
    이들의 변경사항은 서로 충돌(conflict)하고 결과적으로 병합이 발생한다. 어떤 도구도 병합이 발생하는 모든 경우를 해결할 수는 없다. 결국 병합에는 항상 위험이 뒤따르게 된다.

이 문제의 해결책은 다양한데, 모두 메서드를 각기 다른 클래스로 이동 시키는 방식이다.



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

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

소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다.

OCP는 시스템의 아키텍처를 떠받치는 원동력 중 하나다.
OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다.

이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.



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

S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있다고 가정한다.

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

S는 T의 하위 타입(subtype)이다.

객체 지향이 혁명처럼 등장한 초창기에는 LSP는 상속을 사용 하도록 가이드하는 방법 정도로 간주되었다. 하지만 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해 왔다.



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


정적 타입 언어는 사용자가 import, use 또는 include와 같은 타입 선언문을 사용하도록 강제한다. 이처럼 소스 코드에 포함된 'included’ 선언문으로 인해 소스 코드 의존성이 발생하고, 이로 인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.

동적 타입 언어에서는 소스 코드에 이러한 선언문이 존재하지 않는다. 대신 런타임에 추론이 발생한다. 따라서 소스 코드 의존성이 아예 없으며, 재컴파일과 재배포가 필요없다. 동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때보다 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유는 바로 이 때문이다.

이러한 사실로 인해 ISP를 아키텍처가 아니라, 언어와 관련된 문제라고 결론내릴 여지가 있다.


일반적으로, 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일이다. 소스 코드 의존성의 경우 이는 분명한 사실인데, 불필요한 재컴파일과 재배포를 강제하기 때문이다. 더 고수준인 아키텍처 수준에서도 마찬가지 상황이 발생한다.



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


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


추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정해야 한다. 반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하 는 인터페이스는 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.

실제로 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기 위해 애쓴다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가 할 수 있는 방법을 찾기 위해 노력한다. 이는 소프트웨어 설계의 기본이다.


DIP 원칙에서 전달하려는 내용은 다음과 같이 매우 구체적인 코딩 실천 규칙으로 요약 할 수 있다.

규칙1. 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조 하라.

언어가 정적 타입이든 동적 타입이든 관계없이 모두 적용된다. 또한 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리AbstractFactory를 사용하도록 강제한다.

규칙2. 변동성이 큰 구체 클래스로부터 파생하지 말라.

정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 상속은 아주 신중하게 사용해야 한다. 동적 타입 언어라면 문제가 덜 되지만, 의존성을 가진다는 사실에는 변함이 없다. 따라서 신중에 신중을 거듭하는 게 가장 현명한 선택이다.

규칙3. 구체 함수를 오버라이드 하지 말라.

대체로 구체 함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다. 이러한 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도 에 맞게 구현해야 한다.

규칙4. 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

이 규칙은 DIP 원칙을 다른 방식으로 풀어쓴 것이다.


이 규칙들을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 한다. 사실상 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다. 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.

0개의 댓글