Architecture & Clean Architecture - part.1

짱쫑·2022년 2월 22일
1

  • 모든 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지 가치를 제공하는데, 행위와 구조가 바로 그것이다. 소프트웨어 개발자는 두 가지 가치를 모두 반드시 높게 유지해야하는 책임을 가진다. 그러나 개발자는 한 가지 가치에만 집중하고 나머지 가치는 배제하곤 하는데 아쉬운 점은 대체로 개발자가 둘 중 덜 중요한 가치에 집중하여 결국 소프트웨어 시스템이 쓸모없게 되어버린다는 사실이다.

  • 소프트웨어의 첫 번째 가치는 바로 행위이다. 프로그래머를 고용하는 이유는 이해관계자를 위해 기계가 수익을 창출하거나 비용을 절약하도록 만들기 위해서이다. 이를 위해 프로그래머는 이해관계자가 기능 명세서나 요구사항 문서를 구체화할 수 있도록 돕는다. 그리고 이해관계자의 기계가 이러한 요구사항을 만족하도록 코드를 작성한다. 기계가 이러한 요구사항을 위반하면, 프로그래머는 디버거를 열고 문제를 해결한다. 많은 프로그래머가 이러한 활동이 자신이 해야 할 일의 전부라고 생각하지만 이들은 요구사항을 기계에 구현하고 버그를 수정하는 일이 자신의 직업이라고 믿는다. 슬픈 일이지만 틀렸다.

  • 소프트웨어는 부드러움을 지니도록 만들어졌다. 소프트웨어를 만든 이유는 기계의 행위를 쉽게 변경할 수 있도록 하기 위함이다. 소프트웨어가 가진 본연의 목적을 추구하려면 소프트웨어는 반드시 부드러워야 한다. 다시 말해 변경하기 쉬워야 한다. 이해관계자가 기능에 대한 생각을 바꾸면, 이러한 변경사항을 간단하고 쉽게 적용할 수 있어야 한다. 이러한 변경사항을 적용하는 데 드는 어려움은 변경되는 범위에 비례해야 하며 변경사항의 형태와는 관련이 없어야 한다. 소프트웨어 개발의 비용 증가로 결정짓는 주된 요인은 바로 이 변경사항의 범위와 형태의 차이에 있다. 바로 이 때문에 개발 비용은 요청된 변경사항의 크기에 비례한다. 또한 개발 첫 해가 다음 해보다 비용이 덜 들고, 다음에는 그 다음 해보다 비용이 적게 드는 이유도 이 때문이다.

  • 이해관계자는 범위가 비슷한 일련의 변경사항을 제시할 뿐이지만, 개발자 입장에서는 복잡도가 지속적으로 증가하는 퍼즐판 위에서 이해관계자가 계속해서 퍼즐 조각을 맞추라는 지시를 하는 것처럼 느껴진다. 새로운 요청사항이 발생할 때마다 바로 이전의 변경사항을 적용하는 것보다 조금 더 힘들어지는데, 시스템의 형태와 요구사항의 형태가 아다리가 맞지 않기 때문이다. 문제는 당연히 시스템의 아키텍처다. 아키텍처가 특정 형태를 다른 형태보다 선호하면 할수록, 새로운 기능이 이 구조에 맞추는 것이 더 힘들어진다. 따라서 아키텍처는 형태에 독립적이어야 하고, 그럴수록 더 실용적이다.

::더 높은 가치

  • 기능인가 아키텍처인가? 둘 중 어느 것의 가치가 더 높은가? 소프트웨어 시스템이 동작하도록 만드는 것이 더 중요한가? 아니면 소프트웨어 시스템을 더 쉽게 변경할 수 있도록 하는 것이 더 중요한가? 라고 업무 관리자에게 묻는다면 대다수가 소프트웨어 시스템이 동작하는 것이 더 중요하다고 대답할 것이다. 이어서 개발자에게 묻는다면 업무 관리자의 의견에 대체로 동조하는 태도를 취하게 된다. 하지만 이는 잘못된 태도이다. 완벽하게 동작하지만 수정이 아예 불가능한 프로그램이 있다면 이 프로그램은 요구사항이 변경될 때 동작하지 않게되고, 결국 프로그램이 돌아가도록 만들 수 없게 된다. 따라서 이런 프로그램은 거의 쓸모가 없다. 동작은 하지 않지만 변경이 쉬운 프로그램이 있다면 프로그램이 돌아가도록 만들 수 있고, 변경사항이 발생하더라도 여전히 동작하도록 유지보수 할 수 있다. 따라서 이런 프로그램은 앞으로도 계속 유용한 채로 남을 것이다.

  • 소프트웨어 아키텍처의 목표는 필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 데 있다. 개발자의 입장에서 이런 현상은 지독한 절망감을 안겨줄텐데 모두가 열심히 일하고 있기 때문이다. 개발자가 초인적인 노력을 기울이고, 잔업을 하고, 헌심한에도 불구하고 더 이상 진척이 없는 상황에 처하게 된다. 결국 개발자의 노력은 기능 개발보다는 엉망이 된 상황에 대처하는 데 소모되기 시작한다. 심지어 사소한 기능을 추가하는 일도 그저 엉망이 된 코드를 이곳에서 저곳으로, 다시 다음 곳으로 이동하는 반복 작업으로 변질 될 가능성이 높다. 개발자들이 쏟은 노력의 가치가 결국 보잘것 없게 되어버린다.

  • 좋은 소프트웨어는 유연한 소프트웨어다. 그리고 유연한 소프트웨어란 고객의 요구사항에 따라, 쉽게 그 기능을 변경하거나 추가할 수 있는 제품을 말한다. 따라서 고객의 요구사항이 변화함에 따라, 개발자가 제품의 기능을 쉽게 변화시킬 수 있는 구조를 설계하도록 돕는 아키텍처가 좋은 아키텍처라는 말이된다.

::SRP

  • 모든 모듈이 단 하나만의 일만 해야한다는 원칙으로 받아들이기 쉽지만 그렇지 않다. 정확히는 하나의 모듈은 오직 하나의 엑터에 대해서만 책임져야 한다라는 원칙이다.
  • 예를 들어, 서버는 고객의 주문을 받고 음식을 서비스하고, 캐셔는 계산을 하며, 주방 담당자들은 요리를 담당한다. 이것이 각자의 책임이다. 서버가 주문을 받고 요리를 하고 주방 담당자가 계산과 요리를 하다면 어떻게 될까? 책임 소재가 불분명해지고 작은 오류에도 시스템 전체가 움직이게 된다. 이는 프로그래밍에도 해당된다. 만약에 하나의 클래스에 모든 동작을 집어넣다 보면 시스템은 점차 비대해지고 한 부분이 고장나면 전체가 무너지게 된다. 특히, 하나의 클래스 내에서 메서드들이 서로 거미줄처럼 연결되어 있다면 더욱 끔찍한 상황이 발생한다.

  • worker 클래스가 많은 기능을 담당함으로써 사실상 매장 전체의 책임을 맡고 있다봐도 무방하다. this.food에 문제가 발생한다고 가정하면, this.food와 관련된 메서드는 cook()과 serve()이다. 만일 어디를 고쳐야할 지 모른다면 2개의 메서드를 모두 뜯어봐야한다. 주문, 조리, 매장관리, 고객관리 등 각 클래스는 자신의 역할에 관한 것에만 책임을 져야한다. 깔끔한 설계를 위해서 클래스는 어떠한 경우에도 단일 책임을 져야 한다. 하지만 막상 객체 설계를 시작하면 클래스의 책임 범위를 어디까지 두어야 하는지 고민되는 경우가 상당히 많다. 가령, 주문 관리 담당자와 고객 관리 담당자의 책임 범위가 고객이란 접점에서 겹치는 것과 같은 경우이다. 책임 범위가 애매하면 기능도 모호해질 수밖에 없다. 책임 범위를 설정할 때는, 실패 상황을 가정해보는 방법이 유용하다. 어떤 메서드에 문제가 생겼을 때 큰 피해를 입는 속성들을 생각해보면 도움이 된다.

::OCP

  • 객체를 다룸에 있어서 객체의 확장은 개방적으로, 객체의 수정은 폐쇄적으로 대하는 원칙이다. 쉽게말해 기능이 변하거나 확장은 가능하지만, 해당 기능의 코드는 수정하면 안된다는 뜻이다.

  • 에어비앤비, 힐튼 호텔 모두 숙박업에 속한다. 하지만 이들의 사업방식은 극과 극이다. 에어비앤비는 자체적인 숙박시설을 갖추지 않고, 개인 소유의 주거공간을 아웃소싱한다. 반면, 힐튼 호텔은 토지를 확보하고, 관광 및 숙박시설 허가를 받아서 숙박시설을 건립한다.

  • 사업의 성장 속도를 비교해보면, 에어비앤비 쪽이 압도적으로 확장에 유리하다는 사실을 알 수 있다. 에어비앤비는 땅을 구입해서 대규모 시공 및 운영권을 확보할 필요가 없다. 그들은 주거 공간을 가지고 있는 개인과 계약서만을도 사업을 확장할 수 있다. 이 때문에 에어비앤비는 엄청난 속도로 전통적인 숙박업의 성장 속도를 따라잡았다.

  • 왼쪽의 힐튼부터 살펴보면 고객 서비스부터 건물 인테리어 및 토지 활용 방안까지 모두 Hilton 클래스의 책임 범위에 있기 때문에 추가되는 기능이 증가할 때마다 메서드가 늘어나고 있다. 위의 방식을 사용하면, 기능이 추가될 때마다 Hilton 클래스가 무거워진다. 만일 Hilton 클래스를 상속받는 클래스가 있다고 한다면 해당 클래스는 시작부터 무거운 짐을 떠안아야 할 것이다.

  • 반면 오른쪽 에어비앤비는 힐튼에 비해 간단하다. contract() 메서드의 인자값에 Host 클래스만 계속 추가하기 때문이다. 해당 코드에서 방의 구조 변경과 같은 구체적인 사항은 Host 클래스가 자체적으로 수행한다. Host 클래스는 권한과 자유 그리고 책임을 가지고 있으며 상황은 유동적이다. 자체 service() 내용은 Host가 알아서 관리하면 되기 때문이다.

  • 그 결과 airbnb 클래스는 Host 클래스를 추가하는 것만으로도 확장이 가능해진다. airbnb 클래스는 Host 클래스를 받기만 할 뿐 자신이 뭔가 기능을 구현하지는 않는다. 인자로 받은 하위 클래스의 기능을 오직 이용만 한다. airbnb 클래스는 똑같은 명령(service())을 지시할 뿐, 하위 Host 클래스는 각자 다른 기능을 구현한다.

::LSP

  • 부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다. 자식 객체는 부모 객체의 특성을 가지며 이를 토대로 확장할 수 있다. 하지만 이 과정에서 무리하거나 객체의 의의와 어긋나는 확장으로 인해 잘못된 방향으로 상속되는 경우가 생긴다. 리스코프치환 원칙은 올바른 상속을 위해 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙이다.

  • Rectangle 클래스는 _width, _height라는 속성 값을 가지고 있으며 다음과 같은 기능을 한다.
  1. 너비 값 반환, 높이 값 반환 (get width(), get heigth())
  2. 너비 값 입력, 높이 값 입력 (set width(), set heigth())
  3. 넓이 값 계산 및 반환 (get area())

여기서 리스코프 원칙은 복제 라는 사실을 기억해야 한다.

  • Square 클래스는 Rectangle 클래스의 모든 속성과 기능을 물려받았다. 하지만 Square 클래스는 이름 그대로 정사각형의 넓이 값을 반환해야 하는 특수성을 지닌다.

::ISP

문제를 해결하는 방법은 여러가지가 있다. 그 중,
1. 인터페이스를 상속하는 인터페이스를 통한 해결
2. 여러 개의 인터페이스를 상속하여 해결

  • 인터페이스 분리 원칙이란 객체는 자신이 호출하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.
  • 구현할 객체에게 무의미한 메서드의 구현을 방지하기 위해 반드시 필요한 메서드만 상속/구현 하도록 권고한다. 만약 상속할 객체의 규모가 너무 크다면, 해당 객체의 메서드를 작은 인터페이스로 나누는 것이 좋다. 규모가 너무 큰 객체를 상속했을 때 발생하는 문제와 이를 인터페이스로 분리하여 해결하는 방법을 도식한 것이다.
  • 왼쪽과 오른쪽 객체가 가운데 객체를 각각 상속할 경우, 왼쪽 객체는 필요한 메서드가 모두 구현되기 때문에 아무런 문제가 없지만 오른쪽 객체의 경우, 1번메서드를 제외한 나머지 메서드가 필요없다. 하지만 이를 상속했기 때문에 좋든 싫든 해당 메서드를 가지고 있거나, 최악의 경우 필요없는 메서드를 구현해야 한다.
  • 상속 대상인 객체의 메서드를 각 동작별로 구분해 인터페이스를 만들었다. 각 객체가 필요한 인터페이스만을 상속하여 구현하면 되므로 각자가 필요한 메서드만을 가지게 된다. 이것이 인터페이스 분리 원칙이 지향하는 바이다.

::DIP

  • 객체는 저수준 모듈보다 고수준 모듈에 의존해야 한다

고/저수준 모듈의 정의는 다음과 같다.

고수준 : 인터페이스와 같은 객체의 형태나 추상적 개념

저수준 : 구현된 객체

  • 정의를 의존성 역전 원칙에 적용시켜보면, 객체는 객체보다 인터페이스에 의존해야 한다 라고 이해할 수 있다. 즉, 객체의 상속은 가급적 인터페이스를 통해 이루어져야 한다는 것이다.

❗️ 위 DIP의 예시가 맞지 않는 부분이 있는 것 같아서 추후에 수정하겠다. 좀 더 괜찮은 예시를 찾아보거나 내가 직접 코드로 만들어봐야겠다.

profile
不怕慢, 只怕站

0개의 댓글