아키텍처

아키텍처라는 단어는 권력과 신비로움을 연상케 한다.
SW 아키텍처는 기술적 성취의 정점에 서 있따.

그렇다면 SW 아키텍처란 무엇인가?

  • SW 아키텍트 또한 프로그래머이다.
  • 다른 프로그래머들의 생산성을 극대화할 수 있는 설계를 하도록 방향을 이끌어 준다.

시스템 아키텍처는 시스템의 동작 여부와는 관련이 없다.
형편없는 아키텍처를 갑춘 시스템도 잘 동작한다. 이러한 시스템들은 배포, 유지보수, 이어지는 개발 단계에서 어려움을 겪는다.

-> 아키텍처가 시스템이 제대로 동작하도록 지원하는 데는 아무런 역할을 하지 않는다는 말은 아니다.
아키텍처는 시스템의 생명주기를 지원한다.
좋은 아키텍처는 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수한다.

아키텍처의 주된 목적은 프로그래머의 생산성을 최대화하는 데 있다.

개발

개발하기 힘든 시스템이라면 수명도 짧고 건강하지 않을 것이다.
사실 팀 단위(5명)가 작다면, 잘 정의된 컴포넌트나 인터페이스가 없더라도 서로 효율적으로 협력하여 개발할 수 있다.
이러한 팀에서는 아키텍처 관련 제약들이 오히려 방해가 된다고 생각할 것이다.

반대로 총 다섯 팀이 시스템을 개발하고 있다면 안정된 인터페이스, 잘 설계된 컴포넌트 단위로 분리하지 않으면 개발이 진척되지 않는다.
다른 요소를 고려하지 않는다면 이 시스템의 아키텍처는 다섯개의 컴포넌트로 발전될 가능성이 높다.

배포

SW 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 한다.
배포 비용이 높을수록 시스템의 유용성은 떨어진다.
따라서 SW 아키텍처는 시스템을 단 한번에 배포할 수 있도록 만드는 데 목표를 두어야 한다.

하지만 개발 초기 단계에서는 배포 전략을 거의 고려하지 않는다.
이로 인해 개발은 쉽지만, 배포하기는 어려운 아키텍처가 만들어진다.

EX
개발 초기 단계에서 "MSA" 를 사용하자고 결정할 수 있다.

  • 컴포넌트 경계가 뚜렷해지고
  • 인터페이스가 대체로 안정화되므로
    시스템을 쉽게 개발할 수 있다고 판단했을지도 모른다.
    하지만 배포할 시기가 되면 너무 많은 마이크로서비스를 발견하게 될지도 모른다.
    서로를 연결하고 설정하고 순서를 결정하는 과정에서 오작동이 발생할 원천이 스며들 수도 있다.

IF) 배포 문제를 초기에 고려했다면 이와는 다른 결정을 내렸을 것이다.

운영

아키텍처가 시스템 운영에 미치는 영향은 "개발, 배포, 유지보수"에 비해 적다.
대다수의 어려움은 더 많은 하드웨어를 투입해서 해결할 수 있다.

  • 아키텍처가 비효율 적이라면 -> 스토리지와 서버를 추가

시스템 아키텍처는 "유스케이스, 기능, 시스템의 필수 행위" 를 일급 엔티티로 격상시키고,
이들 요소가 개발자에게 주요 목표로 인식되도록 해야 한다.

유지보수

유지보수는 모든 측면에서 봤을 때 SW 시스템에서 비용이 가장 많이 든다.
새로운 기능은 끝도없이 생성되고, 그에 따른 결함도 피할 수 없으며, 결함을 수정하기 위한 인력이 소모된다.

유지보수의 가장 큰 비용은 탐사이며 이로인한 위험부담에 있다.
탐사

  • 기존 SW에 새로운 기능을 추가하거나 결함을 수정할 때, SW를 파헤쳐서 어디를 고치는 게 최선인지, 어떤 전략을 쓰는게 최적일지 결정할 때 드는 비용

선택사항 열어두기

SW의 두 종류의 가치 "행위적 가치, 구조적 가치"
SW를 부드럽게 만드는 것은 구조적 가치이다.

SW를 만든 이유는 기계의 행위를 빠르고 쉽게 변경하는 방법이 필요했기 때문이다.
하지만 이러한 유연성은 시스템의 형태, 컴포넌트의 배치 방식, 컴포넌트가 서로 연결되는 방식에 크기 의존한다.
그렇다면 열어둬야하는 선택사항은 무엇일까?
-> 중요치 않은 세부 사항

모든 SW 시스템은 주요한 2가지 구성요소로 분해할 수 있다.

  • 정책
    • 모든 업무 규칙과 업무 절차를 구체화 한다
  • 세부사항
    • 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소
    • 하지만 정책이 가진 행위에는 조금도 영향을 미치지 않는다.
    • 이러한 세부 사항에는 입출력 장치, DB, 웹 시스템, 서버, 프레임워크 등

아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축하는 데 있다.

  • 개발 초기에는 DB 시스템을 선택할 필요가 없다. 고 수준의 정책은 어떤 종류의 DB를 사용하는 지 신경쓰지 않는다. 신중한 아키텍트라면 관계형 DB인지 분산형인지, 계층형인지는 관련이 없도록 아키텍트를 설계해야 한다.

  • 개발 초기에는 웹 서버를 선택할 필요가 없다. 고수준의 정책은 자신이 웹을 통해 전달된다는 사실을 알아서도 안된다. HTML, AJAX, JSF 같은 웹 개발 기술들에 대해 고수준의 정책이 전혀 알지 못하게 만들면, 프로젝트 후반까지는 어떤 종류의 웹 시스템을 사용할지 결정하지 않아도 된다. 심지어는 시스템을 웹을 통해 전송할 것인지조차도 결정할 필요가 없다.

  • 개발 초기에는 REST를 적용할 필요가 없다. 고수준의 정책은 외부 세계로의 인터페이스에 대해 독립적이어야 하기 때문이다. 마이크로서비스 프레임워크 또는 SOA 프레임워크도 적용할 필요가 없다. 다시 한번 말하지만 고수준의 정책은 이러한 것들에 신경 써서는 안 된다.

  • 개발 초기에는 DI 프레임워크를 적용할 필요가 없다. 고수준의 정책은 의존성을 해석하는 방식에 대해 신경 써서는 안 된다.

요점

  • 세부사항에 몰두하지 않은 고수준의 정책을 만들 수 있다면, 이러한 세부사항에 대한 결정을 오랫동안 미루거나 연기
  • 이러한 결정을 늦게 할수록, 더 많은 정보를 얻고 제대로 된 결정을 내릴 수 있다.
    이를 통해 다양한 실험을 시도해볼 수 있는 선택지도 열어 둘 수 있다.
    현재 동작하고 있는 일부 고수준 정책이 있고, 이들 정책이 DB에 독립적이라면 다양한 DB를 후보로 두고 성능을 검토해 볼 수 있다.

이미 다른 누군가가 결정을 내렸다면?
또는 회사에서 특정 프레임워크, 웹 서버, DB에 기여해왔다면?
-> 그렇다 하더라도 최대한 결정하지 않는 것이 좋다.

결론

좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 분리한다.
이를 통해 정책은 세부사항에 관한 어떤 지식도 갖지 못하며, 의존하지 않는다.

OCP

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

개체의 행위는 확장할 수 있어야 하지만, 개체를 변경해서는 안된다.
만약, 요구사항을 살짝 확장하는 데 SW를 엄청 수저앻야 한다면, 그 SW 시스템 설계는 실패다.

SW 설계를 공부한 지 얼마 안되는 사람들은 OCP를 클래스와 모듈을 설계할 때 도움되는 원칙이라고 알고 있다.
-> (나 역시... 마찬가지)
하지만 아키텍처 컴포넌트 수중에서 OCP를 고려하면 훨씬 중요한 의미를 갖는다.

사고 실험

  1. 재무제표를 Web으로 보여주는 시스템이 있다고 해보자.
  2. 표시되는 데이터는 스크롤할 수 있으며, 음수는 빨간색으로 출력한다.
  3. 보고서 형태로 변환해서 흑백 프린터로 출력해 달라고 요청
    • 페이지마다 번호
    • 페이지마다 머리글/꼬리글
    • 표의 각 열에는 레이블
    • 음수는 괄호로 묶음

새로운 코드를 작성해야 하는 것은 맞다.
하지만 SW 아키텍처가 좋다면 변경되는 코드의 양이 최소화될 것이다.

HOW?

  • SRP : 서로 다른 목적으로 변경되는 요소를 적절하게 분리
  • DIP : 요소 사이의 의존성을 체계화

SRP를 적용하면 그림은 아래와 같다.

웹/프린트 의 책임을 분리시킨다.
이처럼 책임을 분리한다면, 두 책임 중 하나에서 변경이 발생하더라도 다른 하나는 변경되지 않도록 소스 코드 의존성을 확실히 조직화해야 한다.
또한, 새로 조직화한 구조에서는 행위가 확장될 때 변경이 발생하지 않음을 보장해야 한다.

-> 이러한 목적을 달성하려면 처리 과정을 클래스 단위로 분할하고, 이들 클래스를 컴포넌트 단위로 구분해야 한다.

열린 화살표는 사용
닫힌 화살표는 구현/상속 이다.

여기서 모든 의존성이 소스 코드 의존성을 나타낸다는 사실이다.
A 클래스 -> B 클래스 라면, A 클래스에서는 B 클래스에서는 호출하지만, B 클래스는 A 클래스를 모른다.

다른 중요한 점은 이중 선은 화살표와 오직 한 방향으로만 교차한다는 사실이다.

A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존( A -> B)해야 한다.
그림에서는 Presenter의 변경으로부터 Controller를 보호하고
View의 변경으로부터 Presenter를 보호한다.
Interactor는 모든 것으로부터 보호되는데, 그 이유는 interactor가 업무 규칙을 포함하기 때문이다.
Interactor = 주요 업무, 나머지 = 주변 업무

BUT, Controller는 Presenter와 View에 비해서는 중요한 업무를 한다.
마찬가지로 Presenter 또한 View 보다는 더 중요한 업무를 한다.
-> 이것이 바로 아키텝처 수준에서 OCP가 동작하는 방식이다.
아키텍트는 HOW, WHY, WHEN 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다.
컴포넌트를 계층구조로 조직화하므로써 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.

방향성 제어

IF) FinancialDataGateway 인터페이스는 FinancialReportGenerator와 FinancialDataMapper 사이에 위치하는데,
이는 의존성을 역전시키기 위해서다
만약, FinancialDataGateway 인터페이스가 없었다면, 의존성은 Interactor 컴포넌트에서 DB 컴포넌트로 바로 향하게 된다.
FinancialReportPresenter 인터페이스와 2개의 View 인터페이스도 같은 목적을 갖는다.

정보 은닉

FinancialReportRequester 인퍼테이스는 방향성 제어와는 다른 목적을 가진다.
이 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위한 목적을 갖는다.
만약 이 인터페이스가 없었다면 Controller는 FinancialEntities에 대해 추이 종속성을 가지게 된다.

추이 종속성
- A 클래스가 B 클래스에 의존하고, B 클래스가 C 클래스에 의존한다면 -> A 클래스는 C 클래스에 의존하게 된다.

추이 종속성을 가지면, SW 엔티티는 '자신이 직접 사용하지 않는 요소에는 절대 의존해서는 안된다.'라는 원칙을 위반한다.
-> 이 원칙은 ISP와 CRP를 설명할 때 다시 설명한다.

다시 말해, Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만,
반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다.
이를 위해 Interactor 내부를 은닉한다.

결론

OCP는 시스템의 아키텍처를 떠받치는 원동력 중 하나다.
OCP의 목표는 시스템을 확장하기 쉬운 동시에, 변경으로 인해 너무 많은 영향을 받지 않도록 함에 있다.
이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어져야 한다.

SW 아키텍처는 코드로부터 시작한다.

구조적 프로그래밍

  • 최초로 적용된 패러다임
  • 무분별한 goto문은 해롭다.
    • if/then/else , do/while/until 로 대체
  • 제어 흐름의 직접적인 전환에 대해 규칙을 부과한다*

객체 지향 프로그래밍

함수 호출 스택 프레임을 Heap으로 옮기면, 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지됨을 발견
이러한 함수가 클래스의 생성자가 되었고, 지역변수는 인스턴스 변수, 중첩함수는 메서드가 되었다.
제어흐름의 간접적인 전환에 대해 규칙을 부과한다

함수형 프로그래밍

최근에 들어 인기있고 사용되는 패러다임
세 패러다임 중 가장 먼저 만들어졌다.
람다 계산법의 기초가 되는 개념 = 불변성 즉, 심볼의 값이 변경되지 않는다는 점이다.
할당문에 대해 규칙을 부과한다

각 패러다임은 프로그래머에게서 권한을 박탈한다.
어느 패러다임도 새로운 권한을 부여하지 않는다.
규칙을 부과한다. 즉, 무엇을 하면 안되는 지를 알려준다.

결론

패러다임의 역사로부터 얻을 수 있는 교훈은 아키텍처와 어떤 관계가 있는가?
-> 모두 관계있음.

아키텍처 경계를 넘나들기 위한 메커니즘으로 다형성을 이용한다.
우리는 FP를 이용하여 데이터의 위치와 접근 방법에 대해 규칙을 부과한다.
우리는 모듈의 기반 알고리즘으로 구조적 프로그래밍을 사용한다.

함수, 컴포넌트 분리, 데이터 관리가 어떻게 서로 연관되는 지 주목하자.

+ Recent posts