만쥬의 개발일기
article thumbnail

클린 아키텍쳐

5장

****객체 지향(Objected - Oriented)의 본질은 무엇인가?****

캡슐화?

객체 지향 언어는 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 제공한다.

이를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다.

구분선 바깐에서 데이터는 은닉되고, 일부 함수만이 노출된다.

c언어에서도 데이터 구조와 함수를 헤더파일(노출부)에 선언하고, 구현 파일(은닉부)에서 구현하는 방식으로 캡슐화를 지킬 수 있다.

상속화?

객체 지향 언어는 캡슐화를 강제하지 않지만, 상속은 확실히 제공한다.

C언어에서는 상속을 흉내낼 수 있을 뿐, 상속의 기능을 제공하진 않았따.

다형성?

다형성은 기존에 존재하던 함수 포인터를 응용한 것이다.

다형성의 매력은 , 의존성을 낮추고 재활용성을 높여 하나의 기능을 여러 프로그램이 활용할 때

수정이 필요없이, 플러그인처럼 사용 가능하게 하는 것이다.

의존성 역전

기존의 소스코드 의존성의 경우, 제어흐름과 동일하게 결정된다.

하지만 다형성을 활용한다면, 소스 코드 의존성의 방향을 스스로 제어할 수 있게된다.

인터페이스를 활용하여 의존성을 역전시킨 예시

궁극적으로 만들고자 하는 형태

위 그림을 보면, 비즈니스 룰은 제어흐름에 따라 데이터베이스와 UI를 사용하지만, 의존하지 않는다. 그렇기에 비즈니스 룰은 UI와 데이터베이스와는 독립적으로 배포가 가능하다.

즉 세가지 컴포넌트들은 모두 개별적이고, 독립적으로 배포가 가능하다.

따라서 프로그램은 배포 독립성을 가지게 되고, 각 팀에서 각 모듈을 독립적으로 개발하여 개발 독립성을 얻을 수 있다.

6장

우리가 함수형 언어를 쓰지 않는다면 , race condition , deadlock , concurrenct update에서 자유롭지 못하다.

가변성의 분리

아키텍쳐를 설계할 때, 가변 컴포넌트와 불변 컴포넌트를 분리하는 방식으로 동시성 문제를 해결 가능하다.

상태 변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로, 흔히 트랜잭션 메모리 transactional memory 와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.

트랜젝션 메모리란 ,, ACID를 준수하는 메모리

가능한 한 많은 처리를 불변 컴포넌트로 옮기는 것이 현명한 아키텍트이다.

이벤트 소싱

상태가 아닌 트랜잭션을 저장하는 전략 .

무한한 저장공간과 처리 능력이 필요해 힘들어 보이지만,현대의 소스 코드 버전 관리 시스템등은 이미 사용하는 중인 전략

결론

  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율이다.
  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율이다.
  • 함수형 프로그래밍은 변수 할당에 부과되는 규율이다.

7장~ 11장을 들어가며

SOLID 원칙의 목적

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

7장

SRP : 단일 책임 원칙

모듈이 아닌 함수가 , 반드시 단 하나의 일만 해야 한다는 원칙으로 오해할 수 있지만,

진정한 SRP는 하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다는 것.

여기서 모듈은 단순히 소스 파일로 볼 수 있다.

징후 1: 우발적 중복

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

  • calculatePay(): 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.
  • reportHours(): 인사팀에서 기능을 정의하며, COO 보고를 위해 사용한다.
  • save(): 데이터베이스 관리자 DBA 가 기능을 저의하고, CTO 보고를 위해 사용한다.

예를 들어 calculatePay() 메서드와 reportHours() 메서드가 초과 근무를 제외한 업무 시간을 계산하는 알고리즘을 공유한다고 해보자.

그리고 개발자는 코드 중복을 피하기 위해 이 알고리즘을 regularHours()라는 메서드에 넣었다고 해보자.

이제 CFO 팀에서 초과 근무를 제외한 업무 시간을 계산하는 방식을 약간 수정하기로 결정했다고 하자. 반면 인사를 담당하는 COO 팀에서는 초과 근무를 제외한 업무 시간을 CFO 팀과는 다른 목적으로 사용하기 때문에 이 같은 변경을 원하지 않는다고 해보자.

이때, CFO 팀에서는 COO 팀또한 regularHours를 사용한다는 사실을 몰랐고, 수정하게 된다.

그리고 COO 팀은 이 상황을 모른채 해당 함수를 썼다가 큰 예산 오류를 내버리게 된다.

이와 같은 상황은 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문에 발생한다. SRP는 서로 다른 액터가 의존하는 코드를 서로 분리하라고 말한다.

징후 2: 병합

한 코드를 동시에 수정하게 되면, 병합 문제가 생기게 된다. 현대의 어떤 도구도 병합이 발생하는 모든 경우를 해결할 순 없기에, 항상 위험이 뒤따르게 된다.

해결책

메서드가 없는 데이터 구조를 만들고, 각 클래스가 해당 데이터를 공유하도록 한다.

세 가지 클래스를 인스턴스화하고 추적해야 하는 문제는 퍼사드패턴으로 해결한다.

세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임한다. (인터페이스 제공)

결론

SRP는 상위 수준에서도 활용이 가능하다.

컴포넌트 수준에서는 공통 폐쇄 원칙, 아키텍쳐 수준에서는 아키텍쳐 경계의 생성을 책임지는 변경의 축이 된다.

8장

OCP : 개방 - 폐쇄 원칙

소프트웨어 개체는 확장할 수 있어야 하지만 ,변경 되어서는 안된다.

아키텍쳐 컴포넌트 수준에서 아주 중요한 의미를 가지게 된다.

사고 실험

재무제표를 웹 페이지로 보여주는 시스템이 있다고 해보자. 이해관계자가 동일한 정보를 보고서 형태로 변환해서 흑백 프린터로 출력해 달라고 요청했다고 해보자.

SRP를 적용한 모습

보고서 생성은 두가지 책임으로 나뉘게 된다.

  1. 보고서용 데이터를 계산하는 책임
  2. 웹으로 보여주거나 종이로 프린트하기에 적합한 형태로 표현하는 책임

소스코드 의존성을 조직화하기 위해,

처리 과정을 클래스 단위로 분할하고, 이들 클래스를 아래와 같이 이중선으로 표시한 컴포넌트 단위로 구분해야 한다.

화살표가 열려 있다면 사용 using 관계이며, 닫혀 있다면 구현 implement 관계 또는 상속 inheritance 관계다. 여기서 주목할 점은 다음과 같다.

  1. 모든 의존성이 소스 코드 의존성을 나타낸다.(화살표가 A클래스가 B클래스를 호출한다면 B 클래스는 A클래스를 호출할 수 없다)
  2. 이중선은 화살표와 오직 한 방향으로만 교차한다. (모든 컴포넌트 관계는 단방향으로만 이루어진다는 뜻)

가장 높은 수준의 정책을 포함하는 컴포넌트는 다른 어떠한 컴포넌트에게도 영향을 받지 않는다.

방향성 제어

위 클래스 설계에서 인터페이스들은 의존성을 역전시키기 위해 존재한다.

정보 은닉

추이 종속성이란 ? 클래스 A가 클래스B에 의존하고, 클래스 B가 클래스 C에 의존한다면 클래스 A는 클래스 C에 의존하는 것.

FinancialReportRequester 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재한다.

즉 , Controller가 FinancialEntities에 대해 추이 종속성을 가지지 못하도록 존재.

결론

OCP의 목표는 시스템을 확장하기 쉽도록 하고, 변경으로 인해 시스템의 여러부분이 영향을 받지 않도록 하는 데 있따.

시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로 부터 고수준 컴포넌트를

보호하는 의존성 계층구조로 설계하자.

9장

LSP: 리스코프 치환 원칙

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

License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문에 , LSP 준수.

LSP와 아키텍쳐

잘 정의된 인터페이스와 , 해당 인터페이스의 구현체끼리의 상호 치환 가능성이 곧 LSP.

LSP를 어겼을 때 아키텍쳐에서는 어떤 일이 발생할까?

택시 파견 서비스

시스템이 고객에게 알맞은 기사를 선택하고 , 해당 기사의 URI를 이용해 파견하는 서비스가 존재한다고 가정하자.

purplecab.com/driver/Bob

put 방식의 호출

purplecab.com/driver/Bob
    /pickupAddress/jayanglo
    /pickupTime/153
    /destination/ord

이때, 서로 다른 택시업체들이 destination필드를 다른 방식으로 처리한다면 , (ex dest)

해당 업체들을 모듈 내에서 모두 조건문으로 다르게 처리해주어야 할 것이다.

효율도 떨어지고, 오류도 많이 발생하는 일이 될 것

여기서 택시업체들은, 한 인터페이스의 구현체들이라고 보면 된다.

결론

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

10장

ISP : 인터페이스 분리 원칙

OP2를 변경할 경우, OP1을 사용하는 USER1 또한 재 컴파일 해야한다.

이제는 그럴 필요가 없게 된다.

ISP와 언어

정적 타입 언어는 import, include 등으로 인해 소스코드 의존성이 발생하고 재컴파일과 재배포가 강제되지만, 파이썬 등에서는 런타임에 추론이 발생하기에 유연하고 결합도가 낮다.

isp는 언어와 관련된 문제라고 생각할 수도 있다.

ISP와 아키텍쳐

하지만, 근본적인 동기를 보게 되면 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일이라고 볼 수 있다.

S 시스템 구축에 F라는 프레임워크를 시스템에 도입하기를 원한다. 그리고 F 프레임워크 개발자는 특정한 D 데이터베이스를 사용하도록 만들었다고 가정 해보자. 따라서 S는 F에 의존하며, F는 다시 D에 의존하게 된다.

이때 , F에는 불필요한 기능이 D에 포함되어 있고, 그 기능때문에 변경이 일어나면 F를 재배포하는 상황이 발생할 수도 있다.

11장

DIP : 의존성 역전 원칙

안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고,

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

  • 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조하라. 또한 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리 Abstract Factory 를 사용하도록 강제한다.
  • 변동성이 큰 구체 클래스로부터 파생하지 말라. 정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 상속은 아주 신중하게 사용해야 한다.
  • 구체 함수를 오버라이드 하지 말라. 대체로 구체 함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다. 이러한 의존성을 제거하려면, 차라리 추상 함수를 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.
  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

곡선은 시스템을 두가지 컴포넌트로 분리한다.

하나는 추상 컴포넌트이고, 하나는 구체 컴포넌트이다.

구체 컴포넌트

위 그림의 구체 컴포넌트에는 구체적인 의존성이 하나 있다. 이는 DIP를 위배한다.

하지만, 이러한 클래스를 적은 수의 구체 컴포넌트로 모을 수 있다.

보통은 이러한 컴포넌트를 하나 이상 포함하고 Main 이라고 부른다.

'📖BOOK > 📙Clean Architecture' 카테고리의 다른 글

[클린 아키텍쳐] 25장 ~ 29장  (0) 2023.09.15
profile

만쥬의 개발일기

@KangManJoo

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!