02. 벽돌부터 시작하기 : 프로그램 패러다임
객체지향 프로그래밍
- 아키텍트 관점에서 Object-Oriented란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
03. 설계 원칙
클래스는 단순히 함수와 데이터를 결합한 집합을 가리킨다. 소프트웨어 시스템은 모두 이러한 집합을 포함하여, 이러한 집합이 클래스라고 불릴 수도 있고 아닐 수도 있다. SOLID 원칙은 이러한 집합에 적용된다.
SRP : 단일 책임 원칙 (Single Responsibility Principle)
소프트웨어 시스템이 가질 수 있는 최적의 구조는 시스템을 만드는 조직의 사회적 구조에 커다란 영향을 받는다. 따라서 각 소프트웨어 모듈은 변경의 이유가 하나, 단 하나여야만 한다.
- 하나의 모듈(소스파일)은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
OCP : 개발-폐쇄 원칙 (Open-Closed Principle)
기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경 할 수 있다.
- 소프트웨어 개체(artifact) 는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
- 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안된다.
- 아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다. 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
LSP : 리스코프 치환 원칙 (Liskov Substitution Principle)
상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야 한다.
- S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2 가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2 의 자리에 o1 을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.
ISP : 인터페이스 분리 원칙 (Interface Segregation Principle)
소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.
- 불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실이다.
DIP : 의존성 역전 원칙 (Dependency Inversion Priciple)
고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안 된다. 대신 세부사항이 정책에 의존해야 한다.
- 의존성 역전 원칙에서 말하는 ‘유연성이 극대화된 시스템’이란 소스 코드 의존성이 추상 (abstract) 에 의존하며 구체 (concretion) 에는 의존하지 않는 시스템이다.
- 제어흐름은 소스코드 의존성과는 정반대 방향으로 곡선을 가로지른다는 점에 주목하자. 다시 말해 소스코드 의존성은 제어흐름과는 반대 방향으로 역전된다.
04. 컴포넌트 원칙
컴포넌트
- 컴포넌트는 배포단위다. 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위다. 자바의 경우 jar 파일이 컴포넌트다. 루비에서는 gem 파일이다. 닷넷에서는 dll이다. 컴파일형 언어에서 컴포넌트는 바이너리 파일의 결합체다. 인터프리터형 언어의 경우는 소스 파일의 결합체다. 모든 언어에서 컴포넌트는 배포할 수 있는 단위 입자다.
컴포넌트 응집도
- REP : 재사용 / 릴리스 등가원칙 (Reuse / Release Equivalence Principle)
- 재사용 단위는 릴리스 단위와 같다.
- 소프트웨어 설계와 아키텍처 관점에서 보면 단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 함을 뜻한다.
- 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스할 수 있어야 한다. 하나의 컴포넌트로 묶인 클래스와 모듈은 버전 번호가 같아야 하며, 동일한 릴리스로 추적 관리되고, 동일한 릴리스 문서에 포함되어야 한다는 사실은 컴포넌트 제작자 입장이나 사용자 입장에서도 이치에 맞는 얘기다.
- CCP : 공통 폐쇄 원칙 (Common Closure Principle)
- 동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라.
- SRP 를 컴포넌트 관점에서 다시 쓴 것.
- OCP 의 폐쇄와 그 뜻이 같다.
- CRP : 공통 재사용 원칙 (Common Reuse Principle)
- 컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 말라.
- 어떤 클래스를 한데 묶어도 되는지보다는, 어떤 클래스를 한데 묶어서는 안 되는지에 대해서 훨씬 더 많은 것을 이야기 한다. CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말한다.
- ISP 는 사용하지 않은 메서드가 있는 클래스에 의존하지 말라고 조언한다. CRP 는 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라고 조언한다. ⇒ 필요하지 않은 것에 의존하지 말라.
컴포넌트 결합
- ADP : 의존성 비순환 원칙
- 컴포넌트 의존성 그래프에 순환(cycle)이 있어서는 안된다.
- 순환 컴포넌트
- 컴포넌트 분리가 힘들다.
- 단위 테스트를 하고 릴리스를 하는 일도 굉장히 힘들고 어려워진다.
- 어떤 순서로 빌드해야 올바를지 파악하기가 상당히 힘들어진다.
- 순환 끊기
- 의존성 역전 원칙을 적용한다.
- 하향식 설계
- 컴포넌트 구조는 하양식으로 설계될 수 없다. 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 오히려 시스템이 성장하고 변경될 때 함께 진화한다.
- 컴포넌트와 같이 큰 단위로 분해된 구조는 고수준의 기능적인 구조로 다시 분해할 수 있다고 기대하기 때문이다.
- 아직 아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도한다면 상당히 큰 실패를 맛볼 수 있고, 재사용 가능한 요소도 알지 못하며, 컴포넌트를 생성할 때 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장하며 또 진화해야 한다.
-
- SDP : 안정된 의존성 원칙
- 안정성의 방향으로 (더 안정된 쪽에) 의존하라.
- 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 절대로 안된다. 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 번경이 어려워진다.
- SAP : 안정된 추상화 원칙
- 컴포넌트는 안정된 정도만큼만 추상화 되어야 한다.
- 안정된 컴포넌트는 추상화 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다고 말한다. 다른 한편으로는 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다고 말하는데, 컴포넌트가 불안정하므로 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문이다. 따라서 안정적인 ㄴ컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 한다. 안정된 컴포넌트가 확장이 가능해지면 유연성을 얻게 되고 아키텍처를 과도하게 제약하지 않게 된다.
- SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마찬가지가 된다.
05. 아키텍처
아키텍처란?
- 소프트웨어 아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태다. 그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴퍼넌트를 배치하는 방법, 컴포넌트가 서로 의사소통하는 방식에 따라 정해진다. 그리고 그 형태는 아키텍처 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수되도록 만들어진다.
- 이러한 일을 용이하게 만들기 위해서는 가능한 한 많은 선택지를, 가능한 한 오래 남겨두는 전략을 따라야 한다.
독립성
- 선택사항 열어두기
- 소프트웨어를 부드럽게 유지하는 방법은 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어 두는 것이다. 그렇다면 열어 둬야 할 선택사항이란 무엇일까? 그것은 바로 중요치 않은 세부사항 이다.
- 계층 결합 분리 (수평분할)
- UI 계층
- 업무로직 계층
- 데이터 베이스 계층
- 유스케이스 결합 분리 (수직분할)
- 주문하기 유스케이스, 주문삭제 유스케이스 등..
- 중복
- 계층을 수평으로 분리하는 경우, 특정 데이터베이스 레코드의 데이터 구조가 특정 화면의 데이터 구조와 상당히 비슷하다는 점을 발견 할 수도 있다. 이 때 데이터베이스 레코드와 동일한 현태의 뷰 모델을 만들어서 각 항목을 복사하는게 아니라, 데이터베이스 레코드를 있는 그대로 UI까지 전달하고 싶다는 유혹을 받을 수도 있다. 조심하라. 이러한 중복은 거의 확실히 우발적이다.
- 결합 분리 모드
- 소스 수준 분리 모드 : 소스 코드 모듈 사이의 의존성을 제어 할 수 있다. 이를 통해 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들수 있다.
- 배포 수준 분리 모드 : jar, DLL, 공유 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어할 수 있다.
- 서비스 수준 분리 모드 : 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있다. 이를 통해 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독집적이게 된다. (서비스 또는 마이크로 서비스)
- 프로젝트 초기단계는 어떤 모드가 최선인지 알기 어렵다는게 답이다. 사실 프로젝트가 성숙해 갈수록 최적인 모드가 달라질 수 있다.
- (현시점에 가장 인기 있어 보이는) 한 가지 해결책은 단순히 서비스 수준에서의 분리를 기본 정책으로 삼는 것이다. 이 방식은 비용이 많이 들고, 결합이 큰 단위(coarse-grained) 에서 분리된다는 문제가 있다. 마이크로서비스가 아무리 작다(micro) 하더라도 충분히 작은 단위(fine-grained)에서 분리될 가능성은 거의 없다.
- 서비스 수준의 결합 분리가 지닌 또 다른 문제점은 개발 시간 측면뿐 아니라 시스템 자원 측면에서도 비용이 많이 든다는 사실이다. 필요치도 않은 서비스 경계를 처리하는 데 드는 작업은 노력, 메모리, 계산량 측면에서 모두 낭비다.
- 이처럼 컴포넌트가 서비스화 될 가능성이 있다면 나는 컴포넌트 결합을 분리하되 서비스가 되기 직전에 멈추는 방식을 선호한다. 그러고는 컴포넌트들을 가능한 한 오랫동안 동일한 주소 공간에 남겨둔다. 이를 통해 서비스에 대한 선택권을 열어 둘 수 있다.
- 좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야 한다. 또한 좋은 아키텍처라면 나중에 상황이 바뀌었을 때 이 진행 방향을 거꾸로 돌려 원래 형 태인 모노리틱 그조로 되돌릴 수도 있어야 한다.
경계: 선 긋기
- 소프트웨어 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할 해야 한다.
정책과 수준
- 소프트웨어 시스템이란 정책을 기술한 것이다. 실제로 컴퓨터 프로그램의 핵심부는 이게 전부다.
- 소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다. 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 한다. 서로 다른 이유로, 혹은 다른 시점에 변경되는 정책은 다른 수준에 위치하며, 반드시 다른 컴포넌트로 분리해야 한다.
- 수준 (Level)은 엄밀하게 정의하자면 ‘입력과 출력까지의 거리’ 다.
업무 규칙
- 엔티티
- 컴퓨터 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화 한다. 엔터티 객체는 핵심 업무 데이터를 직접 포함하거나 핵심 업무 데이터에 매우 쉽게 접근할 수 있다. 엔티티의 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구성된다.
- 이러한 종류의 클래스를 생성할 때, 업무에서 핵심적인 개념을 구현하는 소프트웨어는 한데 모으고, 구축중이 자동화 시스템의 나머지 모든 고려사항과 분리시킨다. 이 클래스는 업무의 대표자로서 독립적으로 존재한다. 이 클래스는 데이터베이사, 사용자 인터페이스, 서드파티 프레임워크에 대한 고려사항들로 인해 오염되어서는 절대 안된다. 이 클래스는 어떠 시스템에서도 업무를 수행할 수 있으며, 시스템의 표현 형식이나 데이터 저장 방식, 그리고 해당 시스템에서 컴퓨터가 배치되는 방식과도 무관하다. 엔티티는 순전히 업무에 대한 것이며, 이외의 것은 없다.
- 요청 및 응답 모델
- 엔티티 객체를 가리키는 참조를 요청 및 응답 데이터 구조에 포함하려는 유혹을 받을 수도 있다. 엔티티와 요청/응답 모델은 상당히 많은 데이터를 공유하므로 이러한 방식이 적합해 보일 수도 있다. 하지만 이 유혹을 떨쳐내라! 이들 두 객체의 목적은 완전히 다르다. 시간이 지나면 두 객체는 완전히 서로 다른 이유로 변경될 것이고, 따라서 두 객체를 어떤 식으로든 함께 묶는 행위는 공통 폐쇄 원칙과 단일 책임 원칙을 위배하게 된다.
소리치는 아키텍처
상위수준의 디렉터리 구조, 최상위 패키지에 담긴 소스 파일을 볼때, 이 아키텍처는 “헬스 케어 시스템이야” 또는 “재고 관리 시스템이야”라고 소리치는가? 아니면 “레일스야“, ”스프링/하이버네이트야“, 아니면 ”ASP야“라고 소리치는가?
목적
- 좋은 아키텍처는 유스케이스를 그 중심에 둔다.
- 프레임워크, 데이터베이스, 웹 서버, 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만든다. 프레임워크는 열어둬야 할 선택사항이다.
클린 아키텍처
- 의존성 규칙
- 내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못한다. 특히 내부의 원에 속한 코드는 외부의 원에 선언된 어떤 것에 대해서도 그 이름을 언급해서는 절대 안된다. 여기에는 함수, 클래스, 변수, 그리고 소프트웨어 엔티티로 명명되는 모든 것이 포함된다.
- 같은 이유로, 외부의 원에 선언된 데이터 형식도 내부의 원에서 절대로 사용해서는 안 된다. 특히 그 데이터 형식이 외부의 원에 있는 프레임워크가 생성한 것이라면 더더욱 사용해서는 안 된다. 우리는 외부 원에 위치한 어떤 것도 내부의 원에 영향을 주지 않기를 바란다.
‘크고 작은 모든’ 서비스들
횡단 관심사 (cross-cutting concern)가 지닌 문제다. 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하게 마련이다.
- 객체가 구출하다.
- SOLID 설계 원칙을 잘 들여다 보면, 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함을 알 수 있다.
- 컴포넌트 기반 서비스
- “서비스에도 이렇게 할 수 있을까?” ⇒ 그 대답은 물로 “예” 다. SOLID 원칙대로 설계할 수 있으며 컴포넌트 구조를 갖출 수 도 있다.
- 횡단 관심사
- 아키텍처 경계가 서비스 사이에 있지 않다. 오히려 서비스를 관통하며, 서비스를 컴포넌트 단위로 분할 한다.
06. 세부사항
데이터베이스는 세부사항이다.
웹은 세부사항이다.
프레임워크는 세부사항이다.
빠져있는 장
- 계층기반 패키지 (수평 계층화)
- 처음 시작하기에는 계층형 아키텍쳐가 적합하다.
- 문제는 소프트웨어가 커지고 복잡해지기 시작하면, 머지 않아 큰 그룻 세개만으로는 모든 코드를 담기엔 부족하다는 사실을 깨닫고, 더 잘게 모듈화해야 할지를 고민하게 될 것이다.
- 기능기반 패키지 (수직 계층화)
- 코드의 상위수준 구조가 업무 도메인에 대해 무언가를 알려주게 된다. 드디어 우리는 이 코드 베이스가 웹, 서비스, 리포지터리가 아니라 주문과 관련한 무언가를 한다는 걸 볼 수 있다.
- 수평 계층화, 수직 계층화 모두 차선책이다.
- 포트와 어댑터
- 컴포넌트 기반 패키지
- 계층형 아키텍처의 목적은 기능이 같은 코드끼리 서로 분리하는 것이다. 웹 관련 코드는 업무 로직으로부터 분리하고, 업무 로직은 다시 데이터 접근으로부터 분리한다. Controller 가 Service 인터페이스에 의존하려면 Service 인터페이스는 반드시 public 으로 선언되어야 하는데, 두 인터페이스는 서로 다른 패키지에 속하기 때문이다. 마찬가지로 Repository인터페이스도 public 이어야만 repository 패키지 외부에 있는 service impl 클래스에서 접근할 수 있다.
- 엄격한 계층형 아키텍처에서는 의존성 화살표는 항상 아래를 향해야 하며, 각 계층은 반드시 아래 계층에만 의존해야 한다. 그런데 여기에는 큰 문제가 있다. Controller 에서 Repository 에 우회 접근
- 컴포넌트 기반 패키지를 도입해야 하는 이유는 바로 이 때문이다. 이 접근법은 지금까지 우리가 본 모든 것들을 혼합한 것으로, 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는데 주안점을 둔다. 이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다. 포트와 어댑터에서 웹을 그저 또 다른 전달 메커니즘으로 취급하는 걱과 마찬가지로, 컴포넌트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다.
- 구현 세부사항엔 항상 문제가 있다.
- 자바와 같은 언어에서 public 접근 지시자를 지나칠 정도로 방만하게 사용하는 모습을 자주 본다. 개발자인 우리는 public 키워드를 아무런 고민없이 마치 본능적으로 사용하는 거처럼 보인다.
- 조직화 vs. 캡슐화
- 자바의 접근 지시자가 완변ㄱ하지는 않지만, 그렇다고 무시하면 사소 고생하는 길이다. 자바에서 접근 지시자를 적절하게 사용하면, 타입을 패키지로 배치하는 방식에 따라서 각 타입에 접근할 수 있는 정도가 실제로 크게 달라질 수 있다.
- 분명하게 해 두고 싶은 점은 여기에서 설명한 내용은 모노리틱 애플리케이션에 대한 것으로, 모든 코드가 단 하나의 소스 코드 트리에 존재하는 경우다.
- 빠져 있는 조언
- 최적의 설계를 꾀했더라도, 구현 전략에 얽힌 복잡함을 고려하지 않으면 설계가 순식간에 망가질 수도 있다는 사실을 강조하는 데 그 목적이 있다. 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타암에 어떤 결한 분리 모드를 적용할지를 고민하라. 가능하다면 선택사항을 열어두되, 실용주의적으로 행하라. 그리고 팀의 규모, 기술 수준, 해결책의 복잡성을 일정과 예산이라는 제약과 동시에 고려하라. 또한 선택된 아키텍처 스타일을 강제하는 데 컴파일러의 도움을 받을 수 있을지를 고민하며, 데이터 모델과 같은 다른 영역에 결합되지 않도록 주의하라. 구현 세부사항에는 항상 문제가 있는 법이다.
'프로그램' 카테고리의 다른 글
Clean Code (0) | 2024.02.20 |
---|