넘치게 채우기

5-1. 아키텍처란? 본문

개발/Clean Architecture

5-1. 아키텍처란?

riveroverflow 2023. 11. 18. 16:40
728x90
반응형

아키텍처(architecture)라는 단어는 중대한 결정과 심도 있는 기술적 기량을 떠올리게 한다.

소프트웨어 아키텍처란 무엇인가?

소프트웨어 아키텍트는 무슨 일을 하며, 언제 그 일을 하는가?

무엇보다도 소프트웨어 아키텍트는 프로그래머이며, 앞으로도 계속 프로그래머로 남는다.

 

소프트웨어 아키텍트는 코드와 동떨어져서는 안된다.

소프트웨어 아키텍트는 최고의 프로그래머이며, 앞으로도 계속 프로그래밍 작업을 맡을 뿐만 아니라 동시에 나머지 팀원들이 생산성을 극대화할 수 있는 설계를 하도록 방향을 이끌어준다.

프로그램 작업을 계속하는 이유는, 발생하는 문제를 경험해보지 않으면 다른 프로그래머를 지원하는 작업을 제대로 수행할 수 없기 때문이다.

 

소프트웨어 시스템의 아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태다.

그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴포넌트를 배치하는 방법, 컴포넌트가 서로 의사소통하는 방식에 따라 정해진다.

그리고 그 형태는 아키텍처 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수되도록 만들어진다.

 

“이러한 일을 용이하게 만들기 위해서는 가능한 한 많은 선택지를, 가능한 한 오래 남겨두는 전략을 따라야 한다.”

 

아키텍처의 주된 목적은 시스템의 생명주기를 지원하는 것이다. 좋은 아키텍처는 시스템을 쉽게 이해하고, 쉽게 유지보수하고, 또 쉽게 배포하게 해준다.

아키텍처의 궁극적인 목표는 시스템의 수명과 관련된 비용은 최소화하고, 프로그래머의 생산성은 최대화하는 데 있다.

 

개발

개발하기 힘든 시스템이라면 수명이 길지도 않고 건강하지도 않을 것이다.

따라서 시스템 아키텍처는 개발팀(들)이 시스템을 쉽게 개발할 수 있도록 뒷받침해야만 한다.

팀 구조가 다르다면 아키텍처 관련 결정에서도 차이가 난다.

 

일례로 팀이 개발자 다섯 명으로 구성될 정도로 작다면, 잘 정의된 컴포넌트나 인터페이스가 없더라도 서로 효율적으로 협력하여 모노리틱(monolithic)시스템을 개발할 수 있다.

사실 이러한 팀이라면 개발 초기에는 아키텍처 관련 제약들이 오히려 방해가 된다고 여길 가능성이 높다.

수많은 시스템에서 좋은 아키텍처가 결여된 이유는 바로 이 때문이다.

다시 말해 이러한 팀은 아키텍처 없이 시작하는데, 팀 규모가 작은 데다가 상위 구조로 인한 장애물이 없기를 바라기 때문이다.

 

다른 한편으로 일곱 명씩 구성된 총 다섯 팀이 시스템을 개발하고 있다면, 시스템을 신뢰할 수 있고 안정된 인터페이스를 갖춘, 잘 설계된 컴포넌트 단위로 분리하지 않으면 개발이 진척되지 않는다.

다른 요소를 고려하지 않는다면 이 시스템의 아키텍처는 다섯 개의 컴포넌트로(즉, 각 팀마다 하나씩) 발전될 가능성이 높다.

이러한 ‘팀별 단위 컴포넌트’아키텍처가 시스템을 배포, 운영, 유지보수하는 데 최적일 가능성은 거의 없다.

그럼에도 여러 팀이 순전히 일정에만 쫓겨서 일한다면, 결국 이 아키텍처로 귀착될 것이다.

 

배포

소프트웨어 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 한다.

배포 비용이 높을수록 시스템의 유용성은 떨어진다.

따라서 소프트웨어 아키텍처는 시스템을 단 한번에 쉽게 배포할 수 있도록 만드는 데 그 목표를 두어야 한다.

 

안타깝지만 초기 개발 단계에는 배포 전략을 거의 고려하지 않는다.

이로 인해 개발하기는 쉬울지 몰라도 배포하기는 상당히 어려운 아키텍처가 된다.

예를 들어 개발 초기 단계에서 개발자가 ‘마이크로서비스 아키텍처(microservice architecture’를 사용하자고 결정할 수도 있다.

이 접근법을 사용하면 컴포넌트 경계가 매우 뚜렷해지고 인터페이스가 대체로 안정화되므로 시스템을 매우 쉽게 개발할 수 있다고 판단했을지도 모른다.

 

하지만 배포 시기가 되면, 위협적으로 늘어난 수많은 마이크로서비스를 발견하게 될지도 모른다.

마이크로서비스들을 서로 연결하기 위해 설정하고 작동 순서를 결정하는 과정에서 오작동이 발생할 원천이 스며들 수도 있기 때문이다.

만약 아키텍트가 배포 문제를 초기에 고려했다면 이와는 다른 결정을 내렸을 것이다. 더 적은 서비스를 사용하고, 서비스 컴포넌트와 프로세스 수준의 컴포넌트를 하이브리드 형태로 융합하며, 좀 더 통합된 도구를 사용하여 상호 연결을 관리했을 것이다.

 

운영

아키텍처가 시스템 운영에 미치는 영향은 개발, 배포, 유지보수에 미치는 영향보다는 덜 극적이다. 운영에서 겪는 대다수의 어려움은 소프트웨어 아키텍처에는 극적인 영향을 주지 않고도 단순히 하드웨어를 더 투입해서 해결할 수 있다.

실제로 우리는 이러한 일이 벌어지는 경우를 계속해서 목격해왔다.

 

소프트웨어 시스템의 아키텍처가 비효율적이라면 단순히 스토리지와 서버를 추가하는 것만으로 제대로 동작하도록 만들 수 있을 때가 많다.

하드웨어는 값싸고 인력은 비싸다는 말이 뜻하는 바는 운영을 방해하는 아키텍처가 개발, 배포, 유지보수를 방해하는 아키텍처보다는 비용이 덜 든다는 것이다.

시스템을 쉽게 운영하게 해주는 아키텍처가 바람직하지 않다는 말이 아니다.

이러한 아키텍처는 바람직하다. 다만 비용 공식 관점에서 운영보다는 개발, 배포, 유지보수쪽으로 더 기운다는 말이다.

그렇더라도 시스템을 운영할 때 아키텍처의 역할이 하나 더 있다.

 

좋은 소프트웨어 아키텍처는 시스템을 운영하는데 필요한 요구도 알려준다.

시스템 아키텍처가 개발자에게 시스템의 운영 방식을 잘 드러내 준다고 할 수 있다.

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

이를 통해 시스템을 이해하기 쉬워지며, 따라서 개발과 유지보수에 큰 도움이 된다.

 

유지보수

유지보수는 모든 측면에서 봤을 때 소프트웨어 시스템에서 비용이 가장 많이 든다.

새로운 기능은 끝도 없이 행진하듯 발생하고, 뒤따라서 발생하는 결함은 피할 수 없으며, 결함을 수정하는 데도 엄청난 인적 자원이 소모된다.

 

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

탐사란 기존 소프트웨어에 새로운 기능을 추가하거나 결함을 수정할 때, 소프트웨어를 파헤쳐서 어디를 고치는 게 최적인지, 그리고 어떤 전략을 쓰는 게 최적일지를 결정할 때 드는 비용이다.

이러한 변경사항을 반영할 때 의도치 않은 결함이 발생할 가능성은 항상 존재하며, 이로 인한 위험부담 비용이 추가된다.

주의를 기울여 신중하게 아키텍처를 만들면 이 비용을 크게 줄일 수 있다.

 

시스템을 컴포넌트로 분리하고, 안정된 인터페이스를 두어 서로 격리한다.

이를 통해 미래에 추가될 기능에 대한 길을 밝혀 둘 수 있을 뿐만 아니라 의도치 않은 장애가 발생할 위험을 크게 줄일 수 있다.

 

선택사항 열어 두기

소프트웨어의 두 종류의 가치, 행위적 가치와 구조적 가치 중, 구조적 가치가 더 중요한데, 소프트웨어를 소프트하게 만들어주는 것은 바로 이 구조적 가치이기 때문이다.

소프트웨어를 만든 이유는 기계의 행위를 빠르고 쉽게 변경하는 방법이 필요했기 때문이다.

하지만 이러한 유연성은 시스템의 형태, 컴포넌트의 매치 방식, 컴포넌트가 상호 연결되는 방식에 상당히 크게 의존한다.

소프트웨어를 부드럽게 유지하는 법은 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어두는 것이다.

 

그렇다면 열어 둬야 할 선택사항이란 무엇일까?

그것은 바로 중요치 않은 세부사항(detail)이다.

모든 소프트웨어 시스템은 주요한 두 가지 구성요소로 분해할 수 있다.

바로 정책(policy)와 세부사항이다.

 

정책 요소는 모든 업무 규칙과 업무 절차를 구체화한다.

정책이란 시스템의 진정한 가치가 살아 있는 곳이다.

세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소지만, 정책이 가진 행위에는 조금도 영향을 미치지 않는다.

이러한 세부사항에는 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등이 있다.

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

 

이를 통해 세부사항을 결정하는 일은 미루거나 연기할 수 있게 된다.

예를 들어보자.

  • 개발 포기에는 데이터베이스 시스템을 선택할 필요가 없다. 고수준의 정책은 어떤 종류의 데이터베이스를 사용하는지 신경 써서는 안 된다.
    정말이지 신중한 아키텍트라면, 고수준의 정책을 데이터베이스가 관계형인지, 분산형인지, 계층형인지, 아니면 평범한 플랫 파일인지와는 관련이 없도록 만들어야 한다.
  • 개발 초기에는 웹 서버를 선택할 칠요가 없다.
    고수준의 정책은 자신이 웹을 통해 전달된다는 사실을 알아서는 안 된다.
    HTML, AJAX, JSF 같은 웹 개발 기술들에 대해 고수준의 정책이 전혀 알지 못하게 만들면, 프로젝트 후분까지는 어떤 종류의 웹 시스템을 사용할지를 결정하지 않아도 된다.
    심지어는 시스템을 웹을 통해 전송할 것인지조차도 결정할 필요가 없다.
  • 개발 초기에는 REST를 적용할 필요가 없다.
    고수준의 정책은 외부 세계로의 인터페이스에 대해 독립적이어야 하기 때문이다.
    마이크로서비스 프레임워크 또는 SOA 프레임워크도 적용할 필요가 없다.
    다시 한번 말하지만, 고수준의 정책은 이러한 것들에 신경 써서는 안 된다.
  • 개발 초기에는 의존성 주입(Dependency Injection)프레임워크를 적용할 필요가 없다.
    고수준의 정책은 의존성을 해석하는 방식에 대해 신경 써서는 안 된다.

요점을 파악했을 것이다.
세부사항에 몰두하지 않은 채 고수준의 정책을 만들 수 있다면, 이러한 세부사항에 대한 결정을 오랫동안 미루거나 연기할 수 있다.
이러한 결정을 더 오래 참을 수 있다면, 더 많은 정보를 얻을 수 있고, 이를 기초로 제대로 된 결정을 내릴 수 있다.

 

또한 이를 통해 다양한 실험을 시도해볼 수 있는 선택지도 열어 둘 수 있다.

현재 동작하고 있는 일부 고수준 정책이 있고, 이들 정책이 데이터베이스에 독립적이라면 다양한 데이터베이스를 후보로 두고 그 적용 가능성과 성능을 검토해볼 수 있다.

웹 시스템, 웹 프레임워크, 심지어 웹 자체에 대해서도 마찬가지다.

선택사항을 더 오랫동안 열어둘 수 있다면, 더 많은 실험을 해볼 수 있고 더 많은 것을 시도할 수도 있다.

그리고 결정을 더 이상 연기할 수 없는 순간이 닥쳤을 때는 이러한 실험과 시도 덕분에 더 많은 정보를 획득한 상태일 것이다.

좋은 아키텍트는 결정되지 않은 사항의 수를 최대화한다.

 

장치 독립성

60년대의 밥 아저씨와 동료들이 저지른 대표적인 실수 중 하나는 코드를 입출력 장치와 직접 결합해버린 일이었다. 프린터로 인쇄할 일이 있다면, 해당 프린터를 제어하는 입출력 명령어를 직접 사용해서 코드를 작성했다.

이러한 코드는 장치 종속적(device dependent)이었다.

 

처음엔 이러한 전략도 효과가 있었다.

카드 판독기에서 카드를 읽어야 할 경우에도 카드 판독기와 직접 상호작용하도록 코드를 작성했다.

천공 카드에 구멍을 뚫어야 할 경우에도 카드 천공기(punch)를 직접 조작하는 코드를 작성했었다.

그러나, 큰 규모의 천공카드 뭉치는 관리하기 어려웠다.

잃어버리거나, 찢어지거나, 구멍이 뚫리고 서로 섞이거나, 떨어뜨릴 수 있었다.

결국 데이터 무결성이 중요한 문제로 대두되었다.

 

해결책은 자기 테이프였다.

천공카드의 데이터를 테이프로 옮겼다.

자기 테이프는 떨어뜨리더라도 레코드가 섞이지 않았다.

단순히 테이프를 전달하는 과정에서 레코드가 우연히 사라지거나, 또는 비어 있는 레코드가 추가되지 않았다.

이처럼 테이프는 훨씬 안전했다.

뿐만 아니라 더 빨라졌고, 백업용 복사본도 쉽게 만들 수 있었다.

 

안타깝게도 그들은 오직 카드 판독기와 카드 천공기를 조작하도록 프로그램을 작성했다.

자기 테이프를 사용하려면 다시 프로그램을 작성해야 했다.

1960년대 후반에 그들은 교훈을 얻어 장치 독립성(device independence)을 생각해냈다.

 

오늘날의 운영체제는 입출력 장치를 소프트웨어 함수로 추상화했고, 해당 함수는 천공카드와 같은 단위 레코드를 처리한다.

프로그램은 운영체제의 서비스를 호출하고, 해당 서비스가 추상화된 단위 레코드장치를 처리한다.

그리고 오퍼레이터가 해당 추상 서비스를 카드 판독기, 자기 테이프, 아니면 또 다른 단위 레코드 장치 중 어디에 연결해야 하는지를 운영체

제에 알려주었다.

 

이제는 동일한 프로그램을 아무런 변경 없이도 카드에서 읽고 쓰거나 테이프에서 읽고 쓸 수 있게 되었다.

개방 폐쇄 원칙이 탄생한 순간이다.

 

광고 우편

저자 로버트 C 마틴은 광고 우편을 인쇄하는 회사에서 일했다.

의뢰인은 고객의 이름과 주소를 포함하는 단위 레코드가 기록된 자기 테이프를 보여주었다.

그러면 회사에서는 개인화된 광고를 멋지게 인쇄하는 프로그램을 작성했다.

아마 다음과 같은 광고 우편을 본 적이 있을 것이다.

 

안녕하세요, 마틴씨!
축하합니다!
위치우드 레인에 거주하는 모든 사람을 제치고 ‘당신’이 선택되었습니다.
이 환상적인 기회는 오직 한 번만 제공되니, 절대 놓치지 마세요.

 

의뢰인은 이름과 주소가 빠져있는 문구, 그리고 추가로 인쇄하려는 기타 항목이 적혀 있는 수많은 편지 양식 두루마리들을 보여주었다.

그러면 자기 테이프에서 이름, 주소, 기타 항목을 추출하여, 해당 항목들을 편지 양식의 정확한 위치에 인쇄하도록 프로그램을 작성했다.

이 편지 양식 두루마리는 무게가 500파운드(약 227kg)나 나갔고, 편지 수천개를 인쇄할 수 있는 분량이었다.

 

처음에는 IBM360과 라인 프린터 하나만을 이용해서 인쇄했는데, 이 방식은 값비싼 기계를 너무 오랜 시간 점유했다.

그래서 그들은 운영체제에게 라인 프린터 대신 라인 프린터 대신 자기 테이프를 사용하도록 지시했다.

IBM360은 10여분만에 자기 테이프를 가득 채워서 쏟아냈고, 그들은 24시간씩 몇주 내에 다섯 개의 오프라인 프린터에 연결시켜서 수십만 장의 광고 우편을 인쇄했다.

 

장치 독립성이 지닌 가치는 굉장했다.

어떤 장치를 사용할지 전혀 모른채, 그리고 고려하지 않고도 프로그램을 작성할 수 있었다.

이러한 프로그램에는 형태가 있었다.

이 형태는 정책을 세부사항으로부터 분리했다.

이 경우 정책은 이름과 주소 레코드에 대한 서식이었다.

세부사항은 장치였다.

어떤 장치를 사용할지에 대한 결정을 연기 시킨 셈이다.

 

물리적 주소 할당

1970년대 초에 저자는 지역 트럭 운전수 조합을 위한 대규모 회계 시스템을 만들고 있었다.

25MB 크기의 디스크 드라이브에 여러 Agent(중개인), Employer(고용주), Member(회원)의 레코드를 저장했다.

레코드마다 크기도 제각각이었다.

 

그래서 그들은 디스크에서 처음 몇 개 실린더를 포맷하여 각 섹터가 단일 Agent레코드와 크기가 똑같도록 만들었다.

바로 다음 실린더 몇 개도 포맷하여 각 섹터가 단일 Employer 레코드와 크기가 일치하도록 만들었다. 마지막 몇 개의 실린더도 포맷해서 단일 Member 레코드와 크기가 같도록 만들었다.

그렇게 소프트웨어가 디스크의 상세 구조를 알도록 만들었고, 여러 Agent, Employer, Member중 특정 레코드를 탐색할 수 있게 색인을 저장했다.

디스크의 또 다른 몇 개의 실린더를 그에 맞게 포맷하여 이러한 색인을 저장했다.

Agent색인은 각 Agent의 레코드의 중개인ID, 실린더 번호, 헤드 번호, 섹터 번호를 포함하는 레코드로 구성되었다.

Employer와 Member도 그런 식으로 했고, 거기다가 Member는 이중 연결리스트의 형태로 하여 이전과 다음의 Member 레코드까지 저장했다.

 

만약 더 많은 헤더, 실린더, 또는 섹터가 크기의 새로운 디스크 드라이브로 업그레이드 해야 한다면?

이전의 디스크에서 과거 데이터를 읽어서 새로운 디스크로 기록하는, 그래서 실린더/헤드/섹터 번호를 변환해야하는 특수한 프로그램을 만들어야한다.

뿐만 아니라, 하드코딩된 작업들을 수정해야 했다.

 

더 노련한 프로그래머가 팀에 합류하고, 창백한 표정을 보였다가, 친절하게 주소 할 체계를 변경하여 상대 주소를 사용하라고 충고해 주었다.

현명했던 그는 각 섹터의 주소는 순차적인 정수를 이용시켰고, 디스크를 이러한 섹터로 구성된 하나의 거대한 선형 배열로 취급할 것을 제안했다.

그래서 간단한 변환 루틴을 만들 수 있었고, 이 루틴은 디스크의 물리적 구조를 알고 있으며, 이를 통해 상대 주소가 필요하면 즉시 실린더/ 헤드/ 섹터 번호로 변환할 수 있었다.

다행히도 그들은 그의 조언을 받아들여, 시스템에서 고수준의 정책이 디스크의 물리적 구조로부터 독립되도록 수정했다. 그 덕분에 우리는 디스크 드라이브 구조에 대한 결정사항을 애플리케이션으로부터 분리할 수 있게 되었다.

 

결론

좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리한다.

이를 통해 정책은 세부사항에 관한 어떠한 지식도 갖지 못하게 되며, 어떤 경우에도 세부사항에 의존하지 않게 된다.

좋은 아키텍트는 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수 있는 방향으로 정책을 설계한다.

728x90
반응형

'개발 > Clean Architecture' 카테고리의 다른 글

5-3: 경계: 선 긋기  (0) 2023.11.22
5-2. 독립성  (0) 2023.11.20
4-3. 컴포넌트 결합  (0) 2023.11.16
4-2. 컴포넌트 응집도  (0) 2023.11.14
4-1. 컴포넌트  (0) 2023.11.13