5부, 아키텍처

2021.11.02

15장 아키텍처란?

  • 아키텍처란 중대한 결정과 심도 있는 기술적 기량을 떠올리게 한다.
  • 소프트웨어 아키텍트라면 코드에서 탈피하여 고수준의 문제에 집중해야 한다는 거짓말에 절대로 속아 넘어가서는 안 된다.
  • 아키텍처 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수되도록 만들어진다.
    • 이러한 일을 용이하게 만들기 위해서는 가능한 한 많은 선택지를, 가능한 한 오래 남겨두는 전략을 따라야 한다.
  • 아키텍처의 주된 목적은 생명주기를 지원하는 것.
    • 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수하고, 쉽게 배포하게 해준다.
  • 궁극적인 목표는 시스템의 수명과 관련된 비용은 최소화, 프로그래머의 생산성은 최대화하는 데 있다.

개발

  • 팀 규모와 서비스 규모에 따라 모노리스, 마이크로 아키텍처의 적합성이 판단되어 보인다.
  • 중요한 것은, 아키텍처는 개발팀(들)이 시스템을 쉽게 개발할 수 있도록 만들어야 한다.

배포

  • 소프트웨어 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 한다.
    • 배포 비용이 높을수록 시스템의 유용성은 떨어진다.
    • 소프트웨어 아키텍처는 시스템을 단 한번에 싑게 배포할 수 있도록 만드는 데 그 목표를 두어야 한다.
  • 초기 개발 단계에서는 배포 전략을 거의 고려하지 않기 때문에, 개발하기는 쉬울지 몰라도 배포하기는 어려운 아키텍처가 만들어진다.
  • 아키텍처가 배포 문제를 초기에 고려했다면, 다른 결정을 내릴 것이다.

운영

  • 아키텍처가 시스템 운영에 미치는 영향은 개발, 배포, 유지보수에 미치는 영향보다는 덜 극적이다.
    • 운영에서 대다수의 어려움은 하드웨어를 더 투입해서 해결할 수 있다.
  • 소프트웨어 아키텍처가 비효율적이라면 단순히 스토리지와 서버를 추가하는 것만으로 제대로 동작하도록 만들 수 있을 대가 많다.
  • 시스템을 쉽게 운영하게 해주는 아키텍처가 바람직하지 않다는 말이 아니며 이러한 아키텍처는 바람직합니다. 다만 비용 공식 관점에서 운영보다는 개발, 배포, 유지보수 쪽으로 더 기운다.
  • 좋은 소프트웨어 아키텍처는 시스템을 운영하는 데 필요한 요구도 알려준다.
  • 시스템 아키텍처는 유스케이스, 기능, 시스템의 필수 행위를 일급(first-class) 엔티티로 격상시키고, 이들 요소가 개발자에게 주요 목표로 인식되도록 해야 한다.
    • 시스템을 이해하기 쉬워지며, 개발과 유지보수에 큰 도움이 된다.

유지보수

  • 모든 측면에서 봤을 때 소프트웨어 시스테멩서 비용이 가장 많이 발생한다.
    • 새로운 기능과, 뛰따라 발생하는 결함, 결함을 수정하는 인적 자원 소모 등이 있다.
  • 유지보수의 가장 큰 비용은 탐사(spelunking) 와 이로 인한 위험부담이 있다.
    • 기존 소프트웨어에 새로운 기능을 추가하거나 결함을 수정할 대, 소프트웨어를 파헤쳐서 어디를 고치는 게 최선인지, 그리고 어떤 전략을 쓰는게 최적일지를 경정할 때 드는 비용이다.
    • 이러한 변경사항을 반영할 때 의도치 않은 결함이 발생할 가능성은 항상 존재하며, 이로 인한 위험부담 비용이 추가된다.
  • 주의를 기울여 신중하게 아키텍처를 만들면 이 비용을 크게 줄일 수 있다.
    • 시스템을 컴포넌트로 분리하고, 안정된 인터페이스를 두어 서로 격리한다.

선택사항 열어 두기

  • 소프트웨어는 행위적 가치와 구조적 가치를 지닙니다. 이중에서 두 번째 가치가 더 중요한데, 소프트웨어를 부드럽게(soft) 만드는 것은 바로 구조적 가치이다.
  • 소프트웨어를 만든 이유는 기계의 행위를 빠르고 쉽게 변경하는 방법이 필요하기 때문이다.
    • 이러한 유연성은 시스템의 형태, 컴포넌트의 배치 방식, 컴포넌트가 상호 연결되는 방식에 상당히 크게 의존한다.
  • 소프트웨어를 부드럽게 유지하는 방법은 서택사항을 가능한 많이, 그리고 가능한 오랫동안 열어 두는 것이다.
    • 열어 둬야 할 선택사항은 중요하지 않은 세부사항을 의미한다.
  • 소프트웨어 시스템은 주요한 두 가지 구성요소인 정책세부사항으로 분해할 수 있다.
    • 정책 요소는 모든 업무 규칙과 업무 절차를 구체화한다.
    • 세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소이며, 정책이 가진 행위에는 조금도 영향을 미치지 않습니다. 예시로 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등이 있다.
  • 아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축하는 데 있다. 이를 통해 세부사항을 결정하는 일은 미루거나 연기할 수 있게 된다.
    • 개발 초기에는 데이터베이스 시스템을 선택할 필요가 없다.
    • 개발 초기에는 웹 서버를 선택할 필요가 없다.
      • 시스템을 웹을 통해 전송할 것인지조차도 결정할 필요가 없다.
    • 개발 초기에는 REST를 적용할 필요가 없다.
    • 개발 초기에는 의존성 주입(DI) 프레임워크를 적용할 필요가 없다.
      • 고수준의 정책은 의존성을 해석하는 방식에 대해 신경 써서는 안된다.
  • 세부사항에 몰두하지 않은 채 고수준의 정책을 만들 수 있다면, 이러한 세부사항에 대한 결정을 오랫동안 미루거나 연기할 수 있다.
    • 이러한 결정을 더 오래 참을 수 있다면, 더 많은 정보를 얻을 수 있고, 이를 기초로 제대로 된 결정을 내릴 수 있다.
  • 선택사항을 더 오랫동안 열어 둘 수 있다면 더 많은 실험을 해볼 수 있고 더 많은 것을 시도할 수 있다.
  • 뛰어난 아키텍트라면 이러한 결정이 아직 내려지지 않은 것처럼 행동하며, 여전히 결정을 가능한 오랫동안 연기하거나 변경할 수 있는 형태로 시스템을 만든다.
  • 좋은 아키텍트는 결정되지 않은 사항의 수를 최대화한다.

장치 독립성

  • 코드를 입출력 장치와 직접 결합한 것은 가장 큰 실수 중 하나였다.
    • 이러한 코드는 장치 종속적(device dependent)이다.
  • 1960년대에 후반에 이르어서야 장치 독립성(device independent)을 생각이 되고, 사용되기 시작했다.
  • 이제는 동일한 프로그램을 아무런 변경 없이도진행할 수 있다.
    • 개방 폐쇄 원칙이 이로써 탄생했다.

결론

  • 좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 둘이 결합되지 않도록 엄격하게 분리한다.
  • 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수 있는 방향으로 정책을 설계한다.

16장 독립성

좋은 아키텍처는 아래 사항들을 지원해야 한다.

  • 시스템의 유스케이스
  • 시스템의 운영
  • 시스템의 개발
  • 시스템의 배포

유스케이스

  • 유스케이스의 경우, 시스템의 아키텍처는 시스템의 의도를 지원해야 한다는 뜻이다.
  • 좋은 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은 행위를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것이다.
  • 애플리케이션이 좋은 아키텍처를 갖춘다면, 해당 시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러날 것이다.
    • 이들 요소는 클래스이거나 함수 또는 모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 이름을 가진다.

운영

  • 시스템의 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡는다.
  • 형태를 결정하는 것은 뛰어난 아키텍트라면 열어 두어야 하는 선택사항 중 하나이다.
    • 만약 시스템이 monolith으로 작성되어 모노리틱 구조를 갖는다면, 다중 프로세스, 다중 스레드, 또는 마이크로서비스 형태가 필요해질 때 개선하기가 어렵다.
    • 아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는하지 않는다면, 시간이 지나 운영에 필요한 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 훨씬 쉬워진다.

개발

  • 아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행한다.

    • 콘웨이(Conway)의 법칙이 이 때 작용한다.

      콘웨이 법칙. 시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.

  • 많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 팀들이 서로를 방해하지 않도록 해야한다.

  • 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다.

배포

  • 아키텍처는 배포 용이성을 결정하는 데 중요한 역할을 한다.
    • 목표는 즉각적인 배포(immediate deployment)이다.
  • 좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않는다.
  • 좋은 아키텍처라면 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 한다.
  • 아키텍처를 만들려면 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 한다.
    • 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 한다.

선택사항 열어놓기

  • 좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킨다.
  • 현실에서는 이러한 균형을 잡기가 어렵다.
  • 몇몇 아키텍처원칙은 구현하는 비용이 비교적 비싸지 않으며, 관심사들 사이에서 균형을 잡는데 도움이 된다.
  • 좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 한다.

계층 결합 분리

  • 뛰어난 아키텍트는 유스케이스에서 UI 부분과 업무 규칙 부분을 서로 분리하고자 한다.
  • 업무 규칙은 서로 분리하고, 독립적으로 변경할 수 있도록 만들어야 한다.

유스케이스 결합 분리

  • 각 유스케이스는 UI의 일부, 애플리케이션 특화 업무 규칙의 일부, 애플리케이션 독립적 업무 규칙의 일부, 그리고 데이터베이스 기능의 일부를 사용한다.
    • 즉, 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는 얇은 수직적인 유스케이스로 시스템을 분할할 수 있다.
  • 시스템에서 다른 이류로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 된다.

결합 분리 모드

  • 위의 형태처럼, 분리된 컴포넌트는 반드시 독립된 서비스가 되어야 하고, 일종의 네트워크를 통해 서로 통신해야 한다.
  • 많은 아키텍트가 이러한 컴포넌트를 '서비스' 혹은 '마이크로서비스'라고 하며, 실제로 서비스에 기반한 아키텍처를 흔히들 서비스 지향 아키텍처(service-oriented architecture)라고 부른다.
  • 좋은 아키텍처는 선택권을 열어 둔다는 사실이며, 결합 분리 모드는 이러한 선잭지 중 하나이다.

개발 독립성

  • 컴포넌트가 완전히 분리되면 팀 사이의 간섭이 줄어든다.
  • 기능 팀, 컴포넌트 팀, 계층 팀, 혹은 또 다른 형태의 팀이라도, 계층과 유스케이스의 결합이 분리되는 한 시스템의 아키텍처는 그 팀 구조를 뒷받침해줄 것이다.

배포 독립성

  • 유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 생긴다.
  • 실제로 결합을 제대로 분리했다면 운영 중인 시스템에서도 계층과 유스케이스를 교체할 수 있다.

중복

  • 중복은 진짜 중복과 거짓된 또는 우발적인 중복이 있다.
  • 거짓된 중복은 두 코드 영역이 각자의 경로로 발전한다면, 서로 다른 속도와 이유로 변경된다면 이 코드는 중복이 아니다.

결합 분리 모드(다시)

  • 계층과 유스케이스의 결합을 분리하는 방법을 다양하다.
    • 소스 수준 분리 모드
      • 소스 코드 모듈 사이의 의존성을 제어할 수 있다.
      • 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있다.
    • 배포 수준 분리 모드
      • jar 파일, DLL, 공유 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어할 수 있다.
      • 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지 않도록 만들 수 있다.
    • 서비스 수준 분리 모드
      • 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있다.
      • 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적이게 됩니다.
  • 현재에서 한 가지 해결책은 단순히 서비스 수준에서 에서의 분리를 기본 정책으로 삼는 것이다.
    • 이 방식은 비용이 많이 들고, 결합이 큰 단위에서 분리된다는 문제가 있다.
  • 서비스 수준의 결함 분리의 문제는 개발 시간 측면과 시스템 자원 측면에서도 비용이 많이든다는 사실이다.
  • 좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야한다.
  • 좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호한다.

결론

  • 시스템의 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며, 뛰어난 아키텍트라면 이러한 변경을 예측하여 큰 무리 없이 반영할 수 있도록 만들어야 한다.

17장 경계: 선 긋기

  • 소프트웨어 아키텍처는 선을 긋는 기술이다.
  • 경계(선)는 소프트웨어 요소를 서로 분리하고, 요소가 반대편에 있는 요소를 알지 못하도록 막는다.
  • 좋은 시스템 아키텍처는 웹 서버, 프레임워크, 의존성 주입 등에 대한 결정을 가능한 최후의 순간에 내릴 수 있게 해준다. 결정에 따른 영향도도 없다.
  • 관련이 있는 것과 없는 것 사이에 선을 긋는다.
  • 소프트웨어 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야 한다.
  • 업무 규칙에 해당하는 컴포넌트와 플러그인에 해당하는 컴포넌트로 분할하고, 플러그인 컴포넌트가 핵심 업무를 향하도록 소스를 배치한다.
    • DIP와 안정된 추상화 원칙을 응용한 것이다.
    • 화살표를 저수준 세부사항에서 고수준의 추상화를 향하도록 배치.

18장 경계 해부학

  • 경계는 변경이 전파되는 것을 막는 방화벽을 구축하고 관리하는 수단으로써 존재한다.

두려운 단일체

  • 소스 수준의 분리 모드
  • 정적환경으로 단일 실행파일을 만들더라도, 그 안에 다양한 컴포넌트를 독립적으로 수행할 수 있게 하는 일은 대단히 가치있다.
  • 이때문에 객체지향개발이 중요한 패러다임이 될 수 있었다.
  • 가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출이다.

배포형 컴포넌트

  • DLL, jar 루비 젬, 유닉스 공유라이브러리 등의 케이스.
  • 배포 과정에서만 차이날 뿐, 단일체와 동일.
  • 배포형 컴포넌트의 경계를 가로지르는 통신은 순전히 함수 호출에 지나지 않으므로 매우 값싸다.

로컬 프로세스

  • 로컬프로세스는 주로 명령행이나 그와 유사한 시스템 호출을 통해 생성된다.
  • 로컬 프로세스를 일종의 최상워 컴포넌트라고 생각하자.
  • 컴포넌트 간 의존성을 동적 다형성을 통해 관리하는 저수준 컴포넌트로 구성된다.
  • 로컬 프로세스 경계를 지나는 통신에는 운영체제 호출, 데이터 마샬링 및 언마샬링, 프로세스 간 문맥 교환 등이 있으며 이들은 비싼 작업에 속한니, 통신이 너무 빈번하게 이뤄지지 않도록 제한하자.

서비스

  • 서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느리다.
  • 소요시간이 수십 밀리초에서 수 초까지 걸릴 수 있다.

결론

  • 단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용.
  • 서비스는 로컬 프로세스 또는 소스 코드 컴포넌트로 구성된 단일체 혹은 동적으로 링크된 배포형 컴포넌트의 집합이다.
  • 한 시스템 안에서도 통신이 빈번한 로컬 경계와 지연을 중요하게 고려해야 하는 경계가 혼합되어 있다.

19장 정책과 수준

  • 소프트 웨어 시스템이란 각 입력을 출력으로 변환하는 정책을 상세하게 기술한 설명서이다.
  • 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 한다.
  • 좋은 아키텍처라면 의존성의 방향이 컴포넌트 수준을 기반으로 연결되도록 만들어야 한다.
  • 저수준의 컴포넌트가 고수준 컴포넌트에 의존하도록 설계되어야 한다.

수준

  • 수준(level)을 엄밀하게 정의하자면 입력과 출력까지의 거리다.
    • 시스템의 입력과 출력 모두로부터 멀리 위치할수록 정책의 수준은 높아진다.
  • 소스 코드 의존성은 그 수준에 따라 결합되어야하며, 데이터 흐름을 기준으로 결합되어서는 안 된다.
    • 모든 소스 코드 의존성의 방향을 이와 같이 분리했다면 변경의 영향도를 줄일 수 있다.

결론

  • 정책에 대한 논의는 단일 책임 원칙, 개방 폐쇄 원칙, 공통 폐쇄 원칙, 의존성 역전 원칙, 안정된 추상화 원칙을 모두 포함합니다.

20장 업무 규칙

  • 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차다.
  • 핵심 규칙과 핵심 데이터는 본질적으로 결합되어 있기 때문에 객체로 만들기 좋은 후보가 되고, 여기서 이를 엔티티라고 표현하겠다.
    • 핵심 규칙 : 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙. 규칙을 수행하는 주체가 사람이든 컴퓨터이든 상관 없다.
    • 핵심 데이터 : 핵심 업무 규칙이 요구하는 데이터

엔티티

  • 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화한다.
  • 엔티티는 데이터베이스, 사용자 인터페이스, 서드파티 프레임워크에 대한 고려사항들로 오염되어서는 안 된다.
  • 유일한 요구조건은 핵심 업무 데이터와 핵심 업무 규칙을 하나로 묶어서 별도의 소프트웨어 모듈로 만들어야 한다는 것이다.

유스케이스

  • 모든 업무 규칙이 엔티티처럼 순수한 것은 아니다. 자동화된 시스템이 동작하는 방법을 정의하고 제약함으로써 수익을 얻거나 비용을 줄이는 업무 규칙도 존재한다.
  • 유스케이스는 자동화된 시스템이 사용되는 방법을 설명한다.
    • 사용자가 제공해야 하는 입력, 사용자에게 보여줄 출력, 해당 출력을 생성하기 위한 처리 단계를 기술한다.
  • 엔티티내의 핵심 업무 규칙과는 반대로, 유스케이스는 애플리케이션에 특화된 업무 규칙을 설명한다.
  • 유스케이스는 엔티티 내부의 핵심 업무 규칙을 어떻게, 그리고 언제 호출할지를 명시하는 규칙을 담는다.
  • 유스케이스는 사용자 인터페이스를 기술하지 않는다.
    • 애플리케이션이 웹을 통해 전달되는지, 콘솔 기반인지, 순수한 서비스인지 등.
  • 유스케이스는 고수준이며 엔티티에 의존한다. 엔티티는 저수준이며 유스케이스에 의존하지 않는다.

결론

  • 업무 규칙은 소프트웨어 시스템이 존재하는 이유다.
  • 업무 규칙을 표현하는 코드는 반드시 시스템의 심장부에 위치해야 하며, 덜 중요한 코드는 이 심장부에 플러그인이 되어야 한다.
  • 업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용할 수 있는 코드여야 한다.

21장 소리치는 아키텍처

아키텍처의 테마

  • 소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조.
  • 소프트웨어 애플리케이션의 아키텍처도 애플리케이션의 유스케이스에 대해 소리쳐야 한다.
  • 아키텍처를 프레임워크로부터 제공받아서는 절대 안 된다.
  • 아키텍처는 프레임워크 중심으로 만들어버리면 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없다.

아키텍처의 목적

  • 좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크나 도구, 환경에 전혀 구애 받지 않고 유스케이스를 지원하는 구조를 아무런 문제 없이 기술할 수 있다.
  • 프레임워크는 열어둬야 할 선택 사항이다. 뿐만 아니라 이러한 결정을 쉽게 번복할 수 있도록 한다.
  • 좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합은 분리시킨다.

하지만 웹은?

  • 웹은 시스템 아키텍처에 영향을 주지 않는다.
  • 전달 메커니즘(입출력 장치)이며, 애플리케이션 아키텍처에서도 그와 같이 다뤄야 한다.

프레임워크는 도구일 뿐, 삶의 방식은 아니다.

  • 프레임워크는 매우 강력하고 상당히 유용할 수 있다.
  • 어떻게 하면 아키텍처를 유스케이스에 중점을 둔 채 그대로 보존할 수 있을지를 생각하라.
  • 프레임워크가 아키텍처의 중심을 차지하는 일을 막을 수 있는 전략을 개발하라.

테스트하기 쉬운 아키텍처

  • 필요한 유스케이스 전부에 대해 단위 테스트를 할 수 있어야 한다.
  • 엔티티 객체는 반드시 오래된 방식의 간단한 객체(plain old object)여야 하며, 프레임워크나 데이터베이스, 또는 여타 복잡한 것들에 의존해서는 안된다.
  • 유스케이스 객체가 엔티티 객체를 조작해야 하며, 프레임워크로 인한 어려움을 겪지 않고도 반드시 이 모두를 있는 그대로 테스트할 수 있어야 한다.

결론

  • 아키텍처는 시스템을 이야기해야 하며, 시스템에 적용한 프레임워크에 대해 이야기해서는 안 된다.
  • 새로 합류한 프로그래머는 시스템이 어떻게 전달될지 알지 못한 상태에서도 시스템의 모든 유스케이스를 이해할 수 있어야 한다.

22장 클린 아키텍처

  • 육각형 아키텍처 : Hexagonal Architecture
  • DCI : Data, Context and Interaction
  • BCE : Boundary-Control-Entity
  • 위의 세 아키텍처의 공통된 목표는 관심사의 분리이다.
  • 이들 아키텍처는 시스팀에 다음가 같은 특징을 지니도록 만든다.
    • 프레임워크 독립성 : 아키텍처는 다양한 기능을 제공하는 프레임워크의 존재 여부에 의존하지 않는다. 도구로만 사용.
    • 테스트 용이성 : 업무 규칙은 UI, 데이터베이스, 웹 서버, 외부 요소가 없이도 테스트할 수 있다.
    • UI 독립성 : 시스템의 나머지 부분을 변경하지 않고도 UI를 쉽게 변경할 수 있다.
    • 데이터베이스 독립성 : 업무 규칙은 데이터베이스에 결합되지 않는다.
    • 모든 외부 에이전시에 대한 독립성 : 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못한다.

의존성 규칙

  • 소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.
  • 외부의 원에서 선언된 데이터 형식도 내부의 원에서 절대로 사용해서는 안 된다.
  • 특히, 외부의 원에 있는 프레임워크가 생성한 것이라면 더더욱 사용해서는 안 된다.

엔티티

  • 엔티티는 전사적인 핵심 업무 규칙을 캡슐화한다.
  • 메서드를 가지는 객체이거나 일련의 데이터 구조와 함수의 집합일 수도 있다.
  • 운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안 된다.

유스케이스

  • 유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함한다.
  • 유스케이스는 엔티티로 들오오고 나가는 데이터 흐름을 조정하며, 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다.
  • 이 계층에서 발생한 변경이 엔티티에 영향을 줘서는 안 된다. 또한 데이터베이서, UI, 공통 프레임워크와 같은 외부 요소에서 발생한 변경으로부터 영향을 받아서는 안 된다.
  • 운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받으며, 이 계층의 소프트웨어에도 영향을 줄 것이다.

인터페이스 어댑터

  • 어댑터는 데이터를 유스케이스와 엔티티에게 가장 편리한 형식에서 데이터베이스나 웹 같은 외부 에이전시에게 가장 편리한 형식으로 변환한다.
  • 이 계층에서는 외부 에이전시의 어떠한 코드에 대해서도 알면 안된다.

경계를 횡단하는 데이터는 어떤 모습인가

  • 경계를 가로지르는 데이터는 흔히 간단한 데이터 구조로 이루어져 있다.
  • 기본적인 구조체, DTO, 함수 호출할 때의 간단한 인자, 데이터를 해시맵으로 묶거나, 객체로 구성할 수도 있다.
  • 경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태를 가져야만 한다.

결론

  • 소프트웨어를 계층으로 분리하고 의존성 규칙을 준수한다면 본질적으로 테스트하기 쉬운 시스템을 만들게 될 것이며, 그에 따른 이점을 누릴 수 있다.

23장 프레젠터와 험블 객체

  • 프레젠터는 험블 객체 패턴을 따른 형태로, 아키텍처 경계를 식별하고 보호하는 데 도움이 된다.

험블 객체 패턴

  • 험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었다.
  • 행위들을 두 개의 모듈 또는 클래스로 나누고, 테스트 하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 객체에는 테스트하기 쉬운 행위를 모두 옮긴다.
  • GUI의 경우 단위 테스트가 어려운데, 화면을 보면서 각 요소가 필요한 위치에 적절히 표시되었는지 검사하는 테스트는 작성하기 매우 어렵다. 하지만 수행하는 행위의 대다수는 쉽게 테스트할 수 있다.
  • 험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있다.

프레젠터와 뷰

  • 뷰는 험블 객체이고 테스트하기 어렵다. 뷰는 데이터를 GUI로 이동시키지만, 데이터를 직접 처리하지 않는다.
  • 프레젠터는 테스트하기 쉬운 객체다. 프레젠터는 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것이다.
  • 뷰는 뷰 모델의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 전혀 없다.

테스트와 아키텍처

  • 테스트 용이성은 좋은 아키텍처가 지녀야할 속성으로 오랫동안 알려져 왔다.
  • 험블 객체 패턴이 좋은 예인데, 행위를 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경게가 정의되기 때문이다.

데이터베이스 게이트웨이

  • 유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이가 위치한다.
  • 게이트웨이는 다형적 인터페이스로 어플리케이션이 데이터베이스에 수행하는 CRUD와 관련된 모든 메서드를 포함한다.
  • 유스케이스 계층은 SQL을 알지 못하므로, 게이트웨이 인터페이스를 호출한다.
  • 구현체에서는 이 게이트웨이의 메서드를 통해 필요한 데이터에 접근한다.

데이터 매퍼

  • 객체 관계 매퍼(Object Relational Mapper, ORM) 같은건 사실 조재 하지 않는다. ( 객체는 데이터 구조가 아니기 때문 )
  • 사용자 입장에선 객체는 단순히 오퍼레이션의 집합이다.

서비스 리스너

  • 애플리케이션이 다른 서비스와 반드시 통신해야 한다면, 혹은 서비스를 제공해야 하는 경우에도 험블 객체 패턴을 발견할 수 있다.
  • 서비스 리스너가 서비스 인터페이스로부터 데이터를 수신하고, 데이터를 어플리케이션에서 사용할 수 있게 간단한 데이터 구조로 포맷을 변경한다.
  • 그런 후 이 데이터 구조는 서비스 경계를 가로질러서 내부로 전달된다.

결론

  • 각 아키텍처 경계마다 경계 가까이 숨어 있는 험블 객체 패턴을 발견할 수 있다.
  • 경계를 넘나드는 통신은 거의 모두 간단한 데이터 구조를 수반할 때가 많고, 그 경계는 테스트하기 어려운 무언가와 테스트하기 쉬운 무언가로 분리될 것이다.
  • 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템의 테스트 용이성을 크게 높일 수 있다.

24장 부분적 경계

  • 아키텍처 경게를 완벽하게 만드는 데는 비용이 많이 든다.
  • 쌍방향의 다형적 Boundary 인터페이스, Input, Output을 위한 데이터 구조 작성, 두 영역을 독립적으로 컴파일 배포 할 수 있는 격리성을 갖추는데 모든 의존성 관리도 해야 한다.
  • 이러한 선행적인 설계를 탐탁치 않게 여기는 사람도 있는데, YAGNI(you aren’t going to need it) 원칙을 위배하기 때문이다.
  • 그래도 어쩌며 필요할 것 같다는 생각이 들 땐, 부분적 경계(partial boundary)를 구현해볼 수 있다.

마지막 단계를 건너뛰기

  • 부분적 경계를 생성하는 방법 하나는 독립적으로 컵파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후, 단일 컴포넌트에 그대로 모아만 두는 것이다.
  • 완벽한 경계를 만들 때 만큼의 코드량과 사전 설계가 필요하다.
  • 하지만 다수의 컴포넌트를 관리하는 작업은 하지 않아도 된다. 추적을 위한 버전 번호, 배포 관리 부담도 없다.

일차원 경계

  • 완벽한 형태의 아키텍처 경계는 양방향으로 격리된 상태를 유지해야 한다.
  • 양방향으로 격리된 상태를 유지하려면 초기 설정할 때나 지속적으로 유지할 때도 비용이 많이 든다.
  • 추후 완벽한 형태의 경계로 확장할 수 있는 공간을 확보하고자 할 때 활용할 수 있는 더 간단한 구조를 나타낸다.

퍼사드

  • Facade 클래스에서는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달한다.
  • 클라이언트는 서비스 클래스에 직접 접근할 수 없다.
  • 하지만 Client가 모든 서비스 클래스에 대해 추이 종속성을 가지게 되었다.
  • 정적 언어일 경우, 서비스 클래스 중 하나에서 소스 코드가 변경되면 Client도 무조건 재컴파일해야 할 것이다.

결론

  • 아키텍처 경계를 부분적으로 구현하는 간단한 세가지를 살펴봤다. ( 다른 방법도 많다 )
  • 각 접근법은 각각 비용과 장점을 지니고, 적절하게 사용할 수 있는 상황이 서로 다르다.
  • 아키텍처 경계가 언제, 어디에 존재해야 할지, 그리고 그 경계를 완벽하게 구현할지 아니면 부분적으로 구현할지를 결정하는 일 또한 아키텍트의 역할이다.

25장 계층과 경계

  • 시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성되다고 생각하기 쉽지만, 대다수의 시스템에서 컴포넌트 갯수는 훨씬 많다.

움퍼스 사냥 게임

  • UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용 할 수 있음
  • 게임 규칙이 데이터 저장소에 대해 알지 못함
  • 의존성이 적절할 방향을 가리키게 만들어야 함

클린 아키텍처

  • UI에서 언어가 유일한 변경의 축은 아니다.
  • 변경의 축에 의해 정의되는 아키텍처 경계가 잠재되어 있을 수 있다.
  • 점선 테두리는 API를 정의하는 추상 컴포넌트를 가리키며, 해당 API는 추상 컴포넌트 위나 아래의 컴포넌트가 구현한다.
  • 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하면 아래처럼 다이어그램을 단순화할 수 있다.
  • 단순화된 다이어그램에서의 화살표는 위를 향한다.
  • 왼쪽의 흐름은 사용자와의 통신에 관여하며, 오른쪽의 흐름은 데이터 영속성에 관여한다.
  • 두 흐름은 상단의 GameRules에서 서로 만나며, GameRules는 두 흐름이 모두 거치게 되는 데이터에 대한 최종적인 처리기가 된다.

흐름 횡단하기

  • 시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리될 것이다.

흐름 분리하기

  • 이쯤되면 모든 흐름이 결국에는 상단의 단일 컴포넌트에서 서로 만난다고 생각할 수 있다.
  • 하지만 현실은 훨씬 복잡하다.
  • 저수준의 정책으로 게임내에서 식량 발견, 구덩이에 빠진다거나 하는 것을 고수준의 정책에게 알려주고, 이에 대한 상태를 받아와야 한다.
  • MoveManagement는 사용자 로컬에서 실행되며, PlayerManagement는 서버에서 처리.
  • PlayerManagement에 접속된 모든 MoveManagement 컴포넌트에 마이크로 서비스 API 를 제공
  • MoveManagement와 PlayerManagement 에 완벽한 형태의 아키텍처 경계가 존재

결론

  • 프로그램의 크기와 상관 없이 아키텍처 경계는 어디에나 존재한다.
  • 아키텍처 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 한다.
  • 경계가 무시되었다면 나중에 다시 추가하는 비용이 크다는 사실도 알아야 한다.
  • 오버엔지니어링이 언더엔지니어링보다 나쁠 때가 훨씬 많다.
  • 어렵지만 현명하게 추측해야한다.
    • 비용을 산정하고, 아키텍처 경계를 어디에 둬야할지, 완벽하게 구현할 경계는 무엇이고 부분적으로 구현할 경계는 무엇인지, 무시할 경계는 무엇인지에 대해.
  • 프로젝트 초기에는 이에 대해 파악하기 쉽지않다. 시스템이 발전함에 따라 주의를 기울이고, 경계가 필요할 수도 있는 부분을 주목하고 비용 등을 고려하여서 경계를 구현하자.

26장 메인(Main) 컴포넌트

  • 모든 시스템에는 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성, 조정, 관리한다. 저자는 이를 메인 컴포넌트라고 부른다.

궁극적인 세부사항

  • 메인은 시스템의 초기 진입점이다. 운영체제를 제외하면 어떤 것도 메인에 의존하지 않는다.
  • 메인은 모든 팩토리와 전략, 그리고 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘기는 역할을 맡는다.
  • 메인은 클린 아키텍처에서 가장 바깥 원에 위치하는, 지저분한 저수준 모듈이라는 점이다.
  • 메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘긴다.

결론

  • 메인을 애플리케이션의 플러그인이라고 생각하자.
  • 초기 조건과 설정을 구성, 외부 자원을 모두 수집한 후 제어권을 애플리케이션의 고수준 정책으로 넘기는 플러그인.
  • 메인은 플러그인이므로 설정별로 하나씩 두어 둘 이상의 메인 컴포넌트를 만들 수 있다.

27장 ‘크고 작은 모든’ 서비스들

  • 서비스 지향 아키텍처와 마이크로서비스 아키텍처는 최근에 큰 인기를 끌고 있다.
    • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다.
    • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다.
    • 나중에 보면 알겠지만 일부만 맞는 말이다.

서비스 아키텍처?

  • 서비스를 사용한다는 것이 본질적으로는 아키텍처에 해당하지 않는다.
  • 시스템의 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다.
  • 단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 아키텍처 관점에서 꼭 중요하다고 볼 수는 없다.
  • 기능을 프로세스나 플랫폼에 독립적이 되게끔 서비스들을 생성하면 의존성 규칙 준수 여부와 상관없이 큰 도움이 될 때가 많다. 그러나 서비스 그 자체로는 아키텍처를 정의하지 않는다.
  • 결국 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다.

서비스의 이점?

결합 분리의 오류

  • 시스템을 서비스들로 분리함으로써 얻게 되라리 예상되는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점이다.
  • 서비스는 다른 프로세서에서 실행이 되어, 다른 서비스의 변수에 직접 접근할 수 없다.
  • 하지만 프로세서 내의 또는 네트워크 상의 공유 자원 때문에 서비스들간 결합될 가능성이 존재한다.
    • 예를 들어 서비스 사이를 오가는 데이터 레코드에 새로운 필드를 추가한다면, 이 필드를 사용해 동작하는 모든 서비스는 반드시 변경되어야 한다.
    • 따라서 서비스들은 이 데이터 레코드에 강하게 결합되고, 서비스들 사이는 서로 간접적으로 결합되어 버린다.
  • 인터페이스가 잘 정의되어 있어야 한다는 이점에 대해서라면 이는 명백히 사실이다.

개발 및 배포 독립성의 오류

  • 서비스를 사용함에 따라서 예측되는 또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점이다.
  • 데브옵스 전략의 일환으로 전담팀에서 각 서비스 작성, 유지보수 및 운영하는 책임을 질 수 있다.
  • 이러한 개발 및 배포 독립성은 확장 가능한 것으로 간주된다.
  • 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 역사적으로 증명되어 왔다.
  • 따라서 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.
  • 결합 분리의 오류’에 따르면 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아니다.

야옹이 문제

  • 택시 통합 시스템을 아래와 같은 서비스로 나누어 개발 / 운영하고 있다고 생각하자.
  • 만약 여기에 야옹이를 배달하는 서비스를 제공해야 한다고 했을 때, 서비스들은 모두 결합되어 있어서 독립적으로 개발하고 배포하거나, 유지될 수 없다.
  • 이게 바로 횡단 관심사가 지닌 문제다. 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하게 마련이다.
  • 위와 같은 서비스 다이어그램에서 묘사된 것과 같은 종류의 기능적 분해는 새로운 기능이 기능적 행위를 횡단하는 상황에 매우 취약하다.

객체가 구출하다

  • 위의 야옹이 문제는 컴포넌트 기반 아키텍처에거 해결이 가능하다.
  • 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함을 알 수 있다.
  • 배차에 특화된 로직은 Rides 컴포넌트에, 야옹이에 대한 신규 기능은 Kittens 컴포넌트에 들어갔다.
  • 이 두 컴포넌트는 기존 컴포넌트들에 있는 추상 기반 클래스를 템플릿 메서드나 전략 패턴 등을 이용해서 오버라이드한다.
  • 컴포넌트 팩토리를 통해 생성된 컴포넌트는 의존성 규칙을 준수하다.
  • 이 전략을 따르더라도 TaxiUI는 어쩔 수 없이 변경해야 하지만, 다른 것들은 변경할 필요가 없다.
  • 따라서, 야옹이 기능은 결합이 분리되며 독립적으로 개발하여 배포할 수 있다.

컴포넌트 기반 서비스

  • 서비스가 반드시 소규모 단일체(monolith)여야 할 이유는 없다.
  • 서비스는 SOLID 원칙대로 설계할 수 있으며 컴포넌트 구조를 갖출 수도 있다.

횡단 관심사

  • 아키텍처 경계는 서비스 사이에 있지 않다. 오히려 서비스를 관통하며 서비스를 컴포넌트 단위로 분할한다.
  • 모든 주요 시스템이 직면하는 횡단 관심사를 처리하려면, 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계해야 한다.
  • 이 서비스들은 시스템의 아키텍처 경계를 정의하지 않는다. 아키텍처 경계를 정의하는 것은 서비스 내에 위치한 컴포넌트다.

결론

  • 서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아니다.
  • 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.

28장 테스트 경계

  • 테스트는 시스템의 일부이며, 아키텍처에도 관여한다.

시스템 컴포넌트인 테스트

  • 테스트는 단위테스트, 통합테스트 등등 종류를 가리지 않고 아키텍처적으로 모두 동등하다.
  • 테스트는 태생적으로 의존성 규칙을 따른다.
  • 테스트는 세부적이며 구체적인 것으로, 의존성은 항상 테스트 대상이 되는 코드를 향한다.
  • 테스트는 아키텍처에서 가장 바깥쪽 원으로 생각할 수 있다.
  • 테스트는 독립적으로 배포 가능하고, 테스트 시스템에만 배포하며 상용 시스템에는 배포하지 않는다.

테스트를 고려한 설계

  • 테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.
  • 시스템에 강하게 결합된 테스트라면 시스템이 변경될 때 함께 변경되어야만 한다.
  • 깨지기 쉬운 테스트는 시스템을 뻣뻣하게 만든다는 부작용을 낳을 때가 많다.
  • 테스트도 마찬가지로 변동성이 있는 것에 의존하면 안된다.
  • GUI로 시스템을 조작하는 테스트 스위트는 분명 깨지기 쉽다.
  • 따라서, 시스템과 테스트를 설계할 때, GUI를 사용하지 않고 업무 규칙을 테스트할 수 있게 해야 한다.

테스트 API

  • 위와 같은 설계를 달성하려면 테스트가 모든 업무 규칙을 검증하는 데 사용할 수 있도록 특화된 API를 만들면 된다.
  • 테스트 API는 테스트를 애플리케이션으로부터 분리할 목적으로 사용한다.
  • 단순히 테스트를 UI에서 분리하는 것만이 아닌, 테스트 구조를 애플리케이션 구조로부터 결합을 분리하는 게 목표다.

구조적 결합

  • 구조적 결합은 테스트 결합 중에서 가장 강하며, 가장 은밀하게 퍼져 나가는 유형이다.
  • 상용 클래스에 테스트 클래스가 각각 존재하고, 모든 상용 메서드에 테스트 스위트가 있다고 가정한다면, 클래스나 메서드 중 하나라도 변경되면 딸려 있는 다수의 테스트가 변경되어야 한다.
  • 테스트 API의 역할은 애플리케이션의 구조를 테스트로부터 숨기는 데 있다.
  • 이렇게 만들면 상용 코드를 리팩터링하거나 진화시키더라도 테스트에는 전혀 영향을 주지 않는다.

보안

  • 테스트 API가 지닌 강력한 힘을 운영 시스템에 배포하면 위험에 처할 수 있다.
  • 테스트 API 자체와 위험한 부분의 구현부는 독립적으로 배포할 수 있는 컴포넌트로 분리해야 한다.

결론

  • 테스트는 시스템의 일부이다.
  • 테스트에서 기대하는 안정성과 회귀의 이점을 얻을 수 있으려면 테스트는 잘 설계돼야만 한다.
  • 테스트를 시스템의 일부로 설계하지 않으면 테스트는 깨지기 쉽고 유지보수하기 어려워지는 경향이 있다.