상세 컨텐츠

본문 제목

[Architecture] 클린 아키텍처 - 소프트웨어 구조와 설계 원칙 3부 : 설계 원칙 (SOLID)

프로그래밍/Architecture

by jisooo 2024. 1. 14. 17:41

본문

 

 

이번 3부는

소프트웨어 설계원칙에서 흔히 소개되는 SOLID 원칙에 관한 설명이다.

한창 신입 시절 면접을 준비하면서,

각 원칙에 대한 깊은 이해는 간략한 예시들만 보고 넘어가서, 내용을 달달 외운 기억이 있다.

또한 이책에서 다루는 내용처럼 아키텍트에서의 관점까지

각 원칙의 사고를 확장하지 않고,

메서드, 클래스, 모듈 수준에서까지만 공부를 했었다.

그 때 이 책을 봤음 아키텍처에 대한 이해도까지 높일 수 있었을텐데 하는 후회가 든다.

책에서는 각 원칙에 대해 아키텍트 관점에서 상세하게 분석한다.

해당 원칙을 지키지 않게 되면 초래되는 부작용들,

해당 원칙을 지킴으로써 우리 소프트웨어가 유연해지고 안전해지는 장점들을

잘 풀어서 설명해준다.

3부 도입부에서 저자는

좋은 소프트웨어는 깔끔한 코드에서부터 시작되고,

소프트웨어 시스템 설계를 벽돌과 빌딩에 비유한다.

좋은 벽돌을 사용하지 않으면,

빌딩의 아키텍처가 좋고 나쁨은 그리 큰 의미가 없다.

반대로 좋은 벽돌을 사용하더라도,

빌딩의 아키텍처를 엉망으로 만들 수 있다.

그래서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데,

이것이 바로 SOLID 원칙이라고 한다.

SOLID 원칙은

함수와 데이터 구조를 클래스로 배치하는 방법,

그리고 이들 클래스를 서로 결합하는 방법을 설명한다.

클래스라는 단어를 사용한다고 해서

이 원칙이 객체 지향 소프트웨어에만 적용된다는 뜻은 아니다.

위에서 뜻하는 클래스는 단순히 함수와 데이터를 결합한 집합을 의미한다.

SOLID 원칙을 지킴으로써 우리가 중간 수준의 소프트웨어 시스템에 바라는 것은 아래와 같다.

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

* 중간 수준이라는 것은,

프로그래머가 이들 원칙을 모듈 수준에서 적용할 수 있다는 뜻이다.

코드 수준보다는 조금 상위에 위치 하고,

모듈과 컴포넌트 내부에서 사용되는 소프트웨어 구조를 정의하는데 도움을 준다.

그럼 이제 책에서 설명하는

각 원칙에 대해 상세한 설명을 풀어가보자.


단일 책임 원칙 : Single Responsibility Principle

단일 책임 원칙은 말그대로,

각 하나의 모듈은 한가지의 책임만 져야 한다는

이름 그대로의 이해만 하고 넘어가기 쉽다.

하지만 책에서는 좀더 명확한 정리로 해당 원칙을 설명한다.

즉 하나의 모듈은 한 명의 액터(Actor)에 의해서만 변경되어야 한다.

* 모듈의 정의는 소스파일을 의미한다.

하지만 일부 언어와 개발 환경에서는 코드를 소스 파일에 저장하지 않는다.

이 경우 모듈은 단순히 함수와 데이터 구조로 구성된 응집된 집합이다. (클래스)

"응집된"이라는 표현이 SRP를 암시한다.

단일 액터를 책임지는 코드를 함께 묶어주는 힘이

바로 이 응집성이다.

만약 A 클래스의 액터는 Z인데, 액터 X가 시스템의 요구 사항을 변경한다고 해서 A 클래스에서 로직의 변경이 일어났다면,

이는 A 클래스에서 단일 책임 원칙을 위반한 것이 된다.


징후 1: 우발적 중복

위 예시 그림을 보면,

CFO, COO, CTO 3명의 액터가 있다.

CFO는 Employee 클래스의 calculatePay() 메서드를 사용하고,

COO는 reportHours(),

CTO는 save() 메서드를 사용한다.

클래스 Employee는 각 3명의 액터가 사용하는 메서드를 내부에 정의하고 있다.

즉, Employee 클래스는 3명의 액터를 책임지고 있기 때문에,

단일 책임 원칙을 위배한 클래스이다.

이러한 결합으로 인해서,

CFO 팀에서 결정한 조치가, COO나, CTO 팀이 의존하는 무언가에 영향을 줄 수 있다.

문제는 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에,

각 액터에서 변경사항이 발생했을 때,

다른 액터에서 사용하는 로직에도 영향이 전파될 수 있다는 것이다.

위 상황을 아래 예시를 통해 설명한다.

공유된 알고리즘 : regularHours

이 Employee 클래스에서, 새로운 요구사항이 추가된다.

CFO 팀에서 초과 근무를 제외한 업무시간을 계산하는 방식을 약간 수정하기로 한다.

또한 인사를 담당하는 COO 팀에서도 초과 근무를 제외한 업무시간을 CFO팀과는 다른 목적으로 사용한다.

이 상황에서 CFO 팀의 요구사항대로 정규 근무시간을 계산하는 regularHours 메서드를 수정한다고 해보자.

이 regularHours는 각각 calculatePay, reportHours에서 호출하고 있는 메서드이다.

이는 CFO 액터의 요구사항은 충족하지만,

COO 액터 입장에서는 예상치 못한 변경이 일어난 것이다.

따라서 COO에서 호출하는 reportHours 메서드가 예상과 다른 동작을 하게 된다.


징후 2 : 병합

소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생한다.

이들 메서드가 서로 다른 액터를 책임진다면,

병합이 발생할 가능성은 확실히 더 높다.

이러한 징후는,

많은 사람이 서로 다른 목적으로 동일한 소스파일을 변경하는 경우에 해당한다.

이 문제를 벗어나기 위해,

서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

해결책은, 각 액터에게만 필요한 메서드를,

별도의 클래스로 분리하는 것이다.

위 그림과 같이, 각 액터 별로 클래스를 분리하고,

각 클래스에서는 Employee data를 공유하는 것이다.

이는 다만 개발자가 세 가지 클래스를 인스턴스화하고 추적해야 한다는 게 단점이다.

위에 대한 해결책으로 퍼사드 패턴을 적용한 그림이다.

Facade 클래스의 각 메서드에서는 하는일이 별로 없다.

단지 각 액터 별 클래스의 메서드를 호출하는 "연결다리" 역할만 하는 것이다.

이렇게 되면 개발자는 EmployeeFacade 클래스만 인지하여 인스턴스화 하면 되고,

각 액터별 필요한 곳에서 해당 메서드를 호출하면 된다.

또다른 예시로 위와 같은 구조도 제시한다.

일부 개발자들은,

가장 중요한 업무 규칙을 데이터(도메인)와 가깝게 배치하는 것을 선호한다.

이 경우,

Employee 클래스에 가장 중요한 업무 규칙 calculatePay를 정의하고,

나머지 비교적 덜 중요한 reportHours, saveEmployee와 같은 업무규칙은 퍼사드로서 사용한다.

즉 각 메서드에서는 최소한의 로직으로 HourReporter의 메서드와,

EmployeeSaver의 해당 메서드를 연결만 해주는 구조가 될 것이다.


결론

이처럼

단일책임원칙은 메서드와 클래스 수준의 원칙이지만,

이보다 상위의 두 수준에서도 다른 형태로 다시 등장한다.

컴포넌트 수준에서는 "공통 폐쇄 원칙"이 된다.

아키텍처 수준에서는 "아키텍처 경계"의 생성을 책임지는 변경의 축이 된다.


개방-폐쇄 원칙 : Open-closed Principle

소프트웨어 개체는

확장에는 열려있어야 하고,

변경에는 닫혀있어야 한다.

다시 말해 개체의 행위는 확장할 수 있어야 하지만,

이때 개체를 변경해서는 안 된다.

이 원칙에 따르면,

요구사항을 변경 / 확장하는데,

기존에 설계되어있는 소프트웨어를 엄청나게 수정해야 한다면,

이 아키텍트는 엄청난 실패에 맞닥뜨린 것이다.

이러한 OCP는 클래스와 모듈을 설계할 때 도움되는 원칙이라고 생각하기 쉽지만,

아미텍처 컴포넌트 수준에서 OCP를 고려할 때 훨씬 중요한 의미를 가진다.


사고 실험

재무재표를 웹 페이지로 보여주는 시스템이 있다고 가정해보자.

재무재표를 프린터로 출력하는 기능을 새로 추가해야한다는 요구사항이 있을 때,

소프트웨어 아키텍처가 이미 훌륭하다면,

새로 추가되는 요구사항을 적용하는데 변경되는 코드의 양이 가능한 한 최소화 될 것이다.

서로 다른 목적으로 변경되는 요소를 적절하게 분리하고, (단일 책임 원칙)

이들 요소 사이의 의존성을 체계화함으로써 (의존성 역전 원칙)

변경량을 최소화 할 수 있다.

위와 같은 요구사항이 추가될 때,

단일 책임 원칙을 적용하면 데이터 흐름을 아래와 같이 만들 수 있다.

각 클래스 차원에서 분리를 생각하면,

프린트 리포트를 만드는 클래스와 웹 리포트를 만드는 클래스가 분리되었다.

하지만, 좀더 아키텍트 차원에서의 분리로 우리는 사고를 확장해볼 수 있다.

위 그림에서 중요한 영감은,

보고서 생성의 책임이 2가지로 분리되었다는 것이다.

1) 보고서용 데이터를 계산하는 책임

2) 계산된 보고서 데이터를 웹이나 프린트로 표현하기 적합한 형태로 표현하는 책임

이처럼 책임을 분리했다면,

두 책임 중 하나에서 변경이 발생하더라도,

다른 하나는 변경되지 않오록 소스 코드 의존성도 확실히 조직화해야 한다.

또한 새로 조직화한 구조에서는

행위가 확장될 때, 변경이 발생하지 않음을 보장해야 한다.

이러한 목적을 달성하려면,

처리 과정을 클래스 단위로 분할하고,

이들 클래스를 이중선으로 표시한 컴포넌트 단위로 구분해야 한다.

여기서 말하는 컴포넌트가 바로 위해서 말한 "책임"을 기준으로 분리해놓은 것들이다.

View는 최종적으로 유저에게 데이터를 보여주는 책임,

Presenter는 각 데이터를 원하는 출력 포맷에 맞게 변환하여 View에 전달하는 책임,

Controller는 요청을 받아, 정재된 데이터 결과를 Presenter에 전달하는 책임,

Interactor는 주 업무 책임 비즈니스 로직을 담는,

각 보고서 출력 데이터를 받아 뷰에 맞게 정재시키는 책임,

Database는 데이터 저장소에 저장되어있는 재무 데이터를 뽑아서 제공해주는 책임.

이런식으로 각 컴포넌트는 본인들만의 "역할과 책임"이 구분된다.

위 그림에서 각 클래스간 화살표가 그려져있는데,

이 화살표는 의존성을 나타낸다.

모든 의존성은 소스코드 의존성을 나타내고 있다.

A -> B로 화살표가 향한다면,

A 클래스에서 B 클래스를 호출하지만,

B 클래스에서는 A 클래스를 전혀 호출하지 않는다.

또한 각 컴포넌트 안에서 (이중선 안에서)

화살표는 모두 한 방향으로만 교차하고 있다.

모든 컴포넌트 관계는 단방향으로 이루어진다는 뜻이다.

이들 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.


방향성 제어

추가로

컴포넌트간 경계를 넘나들 때,

항상 가운데에 Interface가 껴있어 중재 역할을 한다는 점이다.

여기서 전 2장에서 배웠던 "의존성 역전 법칙"의 화살표 방향을 우리는 다시 한번 확인 할 수 있다.

해당 인터페이스가 없었다면,

각 컴포넌트의 의존성이 바로 향하게 된다.

위 그림에서 예시를 들면

Interactor -> Database,

Controller -> Presenter,

Presenter -> View

와 같이 소스 코드 제어 흐름이 진행된다.

이사이에 인터페이스를 추가함으로써

우리는 의존성의 방향을 소스 코드 제어 흐름과 반대로 역전시킬 수 있다.

더 덧붙여,

이렇게 의존성 역전 법칙을 이용하여

저수준 컴포넌트의 변경으로부터

고수준 컴포넌트를 보호할 수 있는 형태의 의존성을 계층화하여 작성하여야 한다.

모든 컴포넌트 관계는, 단방향으로 이루어진다.

이들 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.

A->B 라면,

A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면,

반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.

위 그림을 예시로보면,

- Presenter로부터 발생한 변경으로부터 Controller를 보호하고자 한다.

- View에서 발생한 변경으로부터 Presenter를 보호하고자한다.

- Interactor는 다른 모든 것에서 발생한 변경으로부터 보호하고자 한다.

(각 컴포넌트가 모두 Interactor 방향으로 화살표가 뻗어있음)

이렇듯, 위 그림에서

Interactor는 Database, Presenter, View에서 발생한 어떠한 변경도

영향을 받지 않는다.

Interactor가 이렇게 가장 OCP 원칙을 잘 지킬수 있는 위치에 있는 이유는

업무 규칙을 포함하고 있기 때문이다.

즉 애플리케이션에서 가장 수준 높은 정책을 포함한다.

나머지 컴포넌트는 나머지 주변적인 문제를 처리할 뿐이다.

Interactor > Controller > Presenter > View

왼쪽으로 갈수록 고수준의 컴포넌트,

오른쪽으로 갈수록 저수준의 컴포넌트라고 생각하면된다.

이렇듯 각 컴포넌트의 수준을 분리함으로써

우리는 컴포넌트 간의 계층관계를 그릴 수 있다.

어떤 컴포넌트가 어떤 컴포넌트 방향으로 의존해야 하는지,

그럼으로써 변경으로부터 해당 컴포넌트를 보호할수 있도록 말ㄹ이다.

이것이 바로 책에서 강조하는 아키텍처 관점에서 OCP가 동작하는 방식이다.

아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고,

분리한 기능을 컴포넌트의 계층구조로 조직화한다.

컴포넌트 계층구조를 이와같이 조직화하면,

저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.


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

리스코프 치환 원칙은 OCP보다는 비교적 이해가 쉽다.

이 원칙은,

클래스 A에 대한 하위 타입 B, C가 있을 때,

B의 자리에 언제든 C로 대체해도,

기존의 B를 사용하던 개체의 행위에 변화가 없어야 한다.

이 원칙에서는 흔히 상속과 인터페이스의 개념이 또 등장한다.

"하위 타입"에 대한 설명이 있으니 특정 클래스를 상속받는 케이스에서 설명할 수 있는 원칙이지만,

이는 상속에 한정되지 않고 인터페이스를 사용할 때도 설명이 가능한 원칙이다.

 

위와 같은 예시 프로그램이 있다고 생각해보자.

Billing 클래스에서는 License의 calcFee() 메서드를 사용한다.

이 License 인터페이스를 구현한 하위 클래스는

Personal License, Business License 두 가지가 있는데,

초기에는 Personal License 클래스로 주입을 하였다가,

나중에 언제든 Business License 클래스로 주입을 바꿀 경우

Billing 클래스에서 정의된 행위에서는 어떠한 변경도 일어나지 않는다.

단지 프레임워크의 기능까지 추가해서 설명해보면,

팩토리에서 어떤 하위 클래스를 선택할지는 동적으로 결정되어

특정 하위 클래스가 Billing 클래스의 멤버로 주입이 될 것이고,

Billing 클래스에서 사용되는 calcFee() 메서드에서는 향후 팩토리에서 하위클래스를 갈아끼운다고 해도

어떠한 변화도 필요하지 않는 것이다.

이것이 가능한 이유는

Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 대해

전혀 의존하지 않기 때문이다.

(여기서도 의존성 역전이 또 등장한다.

Billing <-> *** License 가운데 License라는 인터페이스를 두어 의존 방향을 소스 제어 흐름과 반대로 역전시키고 있다.)

책에서는 위반사례를 상세히 설명했는데,

포스팅에서는 굳이 언급하지 않겠다.

해당 사례를 간단히 정리하면,

LSP를 어김으로써

언제든 공존하거나 갈아끼울 수 있는 구체 하위클래스로 분리하지 않으면

한 클래스에서 여러가지 요구사항을 만족하기 위해

if문이나 갖가지 예외처리들을 추가하여 코드를 오염시키기 마련이다.

이렇게 치환 가능성을 조금이라도 위배하면,

시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있다.

그러므로 LSP는 아키텍처 수준까지 확장할 수 있어야 한다.


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

이 원칙도 이름에서 유추할 수 있듯이,

한 군데에 모여있는 여러 로직을 인터페이스로 분리함으로써,

영향도가 굳이 필요없는 컴포넌트와 분리하여 의존성을 낮추는 원칙이다.

위 그림에서 보면,

다수의 사용자가 OPS 클래스의 오퍼레이션을 사용한다.

그런데 User1은 op1만, User2는 op2만, User3은 op3만 사용한다고 가정하자.

파이썬과 루비와 같은 동적 언어 외에,

자바와 같은 정적 언어를 사용한다고 했을 때,

만약 op1의 로직이 변경되었다고 치자.

그러면 op1, op2, op3이 한군데 모여있음으로 인해서

OPS 클래스의 변경사앙으로 인해 User1, User2, User3 모두를 다시 컴파일하고 새로 배포해야 한다.

User1이야, op1의 로직을 사용하니 그래야 한다 치자.

하지만 op1을 전혀 호출하지도 않는 User2, User3 입장에서 굳이 컴파일과 배포를 다시해야한다니

이게 얼마나 비효율적이고 번거로운 일일까?

이러한 문제는 아래 그림과 같이

오퍼레이션을 인터페이스 단위로 분리하여 해결할 수 있다.

위와 같이 op1이 변경이 필요한 상황에서 위 그림의 구조로 짜여진 프로그램을 수정한다고 하면,

OPS에서 op1의 내용을 바꾸어도,

op1에 의존하고 있지 않은 User2, User3를 굳이 재컴파일 할 필요가 없다.

OPS의 변경된 op1의 소스가 필요한 입장은 User1 뿐이다.

즉 OPS에서 발생한 변경이, User2, User3와 전혀 관계없는 변경이라면,

User2와 User3를 다시 컴파일하고 새로 배포해야하는 번거로운 상황은 다시 일어나지 않는다.


ISP와 언어

위와 같은 부작용은 아까 말했듯이 정적 언어에서 발생한다고 했다.

정적 언어에서 우리는,

외부 소스를 참조하기 위해 include, import, use와 같은 타입 선언문을 사용하도록 강제한다.

이처럼 소스코드에 "포함된" 선언문으로 인해

소스 코드의 의존성이 발생한고,

이로 인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.

반면 파이썬 루비와 같은 동적 타입 언어에서는 소스코드에 이러한 선언문이 없다.

대신 런타임에 추론이 발생한다.

따라서 소스 코드 의존성이 아예 없으며,

결국 재컴파일과 재배포가 필요 없다.

동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때 보다,

유연하고 결합도가 낮은 시스템을 만들수 있는 이유가 바로 이 때문이다.

이렇듯 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은

항상 변경에 대한 부작용이 따른다.

위에서 설명하듯이 소스 코드 의존성의 경우 불필요한 재컴파일과 재배포를 강제하기 때문이다.


의존성 역전 원칙 : Dependency Inversion Principle

의존성 역전은 이전 2부와 위의 여러 원칙을 설명하면서 꾸준히 언급된 원칙이다.

의존성 역전 원칙에서 말하는 "유연성이 극대화된 시스템"이란,

소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.

이말은 use, import, include와 같은 선언문은

오직 인터페이스나 추상 클래스 같은 선언만을 참조해야 하고,

구체 클래스를 참조하면 안된다.

즉 구체적인 대상에는 절대로 의존해서는 안된다.

특히 우리가 의존하지 않도록 피하고자 하는 것은

바로 변동성이 큰 구체적인 요소이다.

왜냐하면, 변동성이 큰 구체요소를 의존한다는 것은,

그만큼 구체요소가 변동되면 의존하는 다른 모듈에까지 영향이 전파될 수 있기 때문이다.

이 "구체적인" 요소들은 우리가 열심히 개발 중이라

자주 변경될 수 밖에 없는 모듈들이다.


안정된 추상화

추상 인터페이스에 변경이 생기면,

이를 구체화한 구현체들도 따라서 수정해야 한다.

반대로 구체적인 구현체에 변경이 생기더라도,

그 구현체가 구현하는 인터페이스는 대다수의 경우 변경될 필요가 없다.

따라서 인터페이스는 구현체보다 변동성이 낮다.

이렇게 뛰어난 소프트웨어 설계자들은,

인터페이스의 변동성을 낮추기 위해 애쓴다.

인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력한다.

즉, 안정된 소프트웨어란

변동성이 큰 구현체에 의존하는 일은 지양하고,

안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.

이는 아래의 구체적인 실천법을 지킴으로써 실현할 수 있다.

  • 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조하라. (느슨한 의존)
  • 변동성이 큰 구체 클래스로부터 파생하지 말라.
    • 이 실천에 따르면, 변동성이 큰 클래스는 상속을 가능하면 사용하지 말라는 것이다.
    • 상속은 관계가 가장 강력한 동시에 뻣뻣해서 변경하기 어렵기때문에 아주 신중하게 사용해야 한다.
  • 구체 함수를 오버라이드 하지 말라.
    • 차라리 인터페이스의 추상 함수로 선언하고, 구현체들에서 각자의 용도에 맞게 구현해라.
  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.


팩토리

자바 등의 객체 지향 언어에서 바람직하지 못한 의존성을 처리할 때

추상 팩토리를 사용하곤 한다.

위 그림의 예시를 보면,

Application은 Service 인터페이스를 통해 ConcreteImpl을 사용하지만,

이를 사용하려면 어떻게든 ConcreteImpl의 인스턴스를 생성해줘야 한다.

코드상으로 ConcreteImpl에 대해 소스 코드 의존성을 만들지 않으면서

이목적을 이루기 위해

Application은 ServiceFactory 인터페이스의 makeSvc 메서드를 호출할 것이다.

결국 ServiceFactoryImpl 구현체가 ConcreteImpl 인스턴스를 생성한 후 Service 타입으로 반환한다.

그림의 빨간색 곡선은 아키텍처 경계를 뜻한다.

이 곡선을 통해 구체 컴포넌트와 추상 컴포넌트를 분리할 수 있다.

소스 코드 의존성은 모두 한방향, 즉 추상적인 쪽으로 향한다.

추상 컴포넌트는 애플리케이션의 모든 고수준 업무규칙을 포함하고,

구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.

제어 흐름은 소스코드 의존성과는 정 반대의 방향으로 흘러가는데,

이렇기 때문에 이 원칙을 "의존성 역전"이라고 부른다.


구체 컴포넌트

위에서 추상 / 구체 컴포넌트에 대한 경계를 설명했는데,

위 그림을 다시보자.

경계선 위는 추상 컴포넌트 (업무규칙을 포함할것이므로),

아래는 구체컴포넌트라고 볼 수 있는데,

아래에서 ServiceFactoryImpl은 ConcreteImpl을 인터페이스없이 구체 컴포넌트를 바로 의존하고 있다.

이는 계속 설명한 DIP를 위배한 일이다.

하지만 현실적으로 우리가 소프트웨어를 개발하면서

모든곳에서 DIP를 적용할 수 없기 때문에,

이렇게 DIP를 위배하는 클래스들은 구체 컴포넌트 내부로 모을 수 있고,

이를 통해 시스템의 나머지 부분과는 분리하여 의존도를 낮출 수 있다.


마무리

다섯가지 좋은 아키텍처를 정의하기 위한 원칙 SOLID를 3부를 통해 살펴보았다.

각 원칙에서는

인터페이스를 통해서 (물론 책에서 인터페이스만을 장치로 설명하진 않았지만, 그만큼 중요한 요소로 등장했다.)

클래스 간, 더 나아가 컴포넌트 간 경계를 설정하여 직접적인 의존도를 낮추고,

한 컴포넌트의 변경에 영향을 받지 않는 독립적인 컴포넌트를 유지할 수 있다.

이렇게 각 원칙은 결국 모두 변경에 유연하고 독립적인 소프트웨어 아키텍처를 작성할 수 있도록 설명한 장이었다.

다음 포스팅에서는 4부 컴포넌트 원칙을 다룰 예정이다.

관련글 더보기

댓글 영역