티스토리 뷰

나의 공부방

[개발서적] 클린 아키텍처 5부 아키텍처 - 내용 정리 및 요약

망나니개발자 2022. 10. 27. 11:00
반응형

이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다.

 

 

 

 

 

15장. 아키텍처란?


[ 서론 ]

  • 소프트웨어 시스템의 아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태임
    • 그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴포넌트를 배치하는 방법, 컴포넌트의 의사소통하는 방식에 따라 정해짐
    • 그 형태는 아키텍처 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수되도록 만들어짐
  • “이러한 일을 용이하게 만들기 위해서는 가능한 많은 선택지를, 가능한 오래 남겨두는 전략을 따라야한다.”
  • 소프트웨어 아키텍처의 목표는 시스템을 제대로 동작하도록 만드는것도 중요하지만 시스템의 생명주기를 지원하는 것
  • 이를 통해 시스템의 수명과 관련된 비용은 최소화하고, 프로그래머의 생산성은 최대화하는 것

 

 

[ 선택사항 열어두기 ]

  • 모든 소프트웨어 시스템은 정책(policy)과 세부사항(detail)으로 분해할 수 있음
    • 정책은 모든 업무 규칙과 절차를 구체화하는데, 시스템의 진정한 가치가 살아 있는 곳임
    • 세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요하지만, 정책이 갖는 행위에는 절대 영향을 미치지 않음
    • 세부사항에는 입출력 장치, 디비, 웹시스템, 서버, 프레임워크, 통신 프로토콜 등이 있음
  • 아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 세부사항은 정책에 무관하게 만드는 시스템을 구축하는 것
  • 소프트웨어를 부드럽게 유지하는 방법은 세부사항에 몰두하지 않고, 선택사항을 가능한 많이 오랫동안 열어두는 것
  • 좋은 아키텍트는 결정되지 않은 사항의 수를 최대화한다.”

현재 회사에서는 테이블을 추가하려면 DBA에게 요청해야 한다. 작년에 개발을 할 때 "테이블이 나오지 않아서 개발을 못하고 있어요"라는 비슷한 얘기를 했던 적이 있는데.... 살짝 부끄럽고 제대로 개발을 못하고 있다는 생각이 든다. 그래도 다행히 최근에 있었던 비슷한 상황에서는 세부 사항인 테이블 설계와 영속성 부분을 가장 마지막으로 미룰 수 있었다. 

 

 

[ 선택사항 열어두기 ]

  • 좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리함
  • 이를 통해 정책은 세부사항에 관한 어떠한 지식도 갖지 못하며, 어떤 경우에도 세부 사항에 의존하지 않게 됨
  • 좋은 아키텍트는 세부 사항에 대한 결정을 가능한 오래 미룰 수 있는 방향으로 정책을 설계함

 

 

 

16장. 독립성


[ 서론 ]

  • 좋은 아키텍처는 다음을 지원해야 함
    • 시스템의 유스케이스
    • 시스템의 운영
    • 시스템의 개발
    • 시스템의 배포

 

 

[ 유스케이스 ]

  • 시스템의 아키텍처는 시스템의 의도인 유스케이스를 지원해야 한다는 뜻
  • 아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에서도 유스케이스가 최우선임
  • 좋은 아키텍처는 행위를 명확히 하고, 외부로 드러내며, 이를 통해 시스템의 의도를 아키텍처 수준에서 알아보게 만들 수 있음
  • 이는 시스템의 최상위 수준에서 알아볼 수 있으므로, 개발자가 일일이 찾아 헤매지 않아도 됨

최근에 내가 선호하는 패키지 구조는 헥사고날 아키텍처 기반이다. 유스케이스는 도메인 패키지의 포트에 위치시키고 있는데, 책을 읽으면서 생각해보니.... 유스케이스를 찾기 위해 들어가야 하는 뎁스가 많아서 최상위 수준에서 알아볼 수 있는 구조가 아닌 것 같다. 연말에 한번 더 직관적인 패키지구조에 대해서 고민을 해봐야겠다.

 

 

[ 운영 ]

  • 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며, 덜 피상적인 역할을 맡음
  • 만약 시스템이 초당 10만명의 고객을 처리해야 한다는 요구가 있다면, 이를 허용할 수 있는 형태로 아키텍처를 구조화해야 함
  • 시스템의 처리 요소를 일련의 작은 서비스들로 배열하는 등의 결정은 열어두어야 하는 선택사항 중 하나임

 

 

[ 개발 ]

  • 아키텍처는 개발환경을 지원하는데 핵심적인 역할을 수행하는데, 콘웨이의 법칙이 작용하는 지점이 바로 여기임
  • 콘웨이의 법칙: “시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.”
  • 많은 팀으로 구성되는 조직에서는 각 팀이 독립적으로 행동하기 편한 아키텍처를 확보하여 개발동안 서로를 방해하지 않아야 함
  • 이러한 아키텍처를 만드려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 함

책에서는 계속해서 아키텍처와 콘웨이의 법칙을 연결시키는데, 솔직히 제대로 와닿지 못했다. 어림 짐작으로는 아키텍처가 좋지 않으면 각 팀은 계속해서 서로를 방해하게 되고(의사소통이 좋지 않음), 이는 개발생산성을 덜어뜨린다는 얘기가 아닌가 싶다. 즉, 아키텍처가 좋아야 의사소통이 좋아지고, 개발이 유리해진다는 얘기가 아닌가 싶은데.... 정확히는 모르겠다.

 

 

[ 배포 ]

  • 아키텍처는 배포 용이성을 결정하는데 중요한 역할을 하는데, 이때 목표는 “즉각적인 배포"임
  • 좋은 아키텍처는 수십 개의 작은 설정 스크립트나 속성 파일을 하나씩 수정하는 방식을 사용하지 않음
  • 좋은 아키텍처는 꼭 필요한 디렉토리나 파일을 수작업으로 생성하게 내버려두지 않음
  • 대신 시스템이 빌드된 후 즉각 배포할 수 있도록 지원하며, 이를 위해 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 함

 

 

[ 중복 ]

  • 소프트웨어 중복은 일반적으로 나쁜 것이며, 코드가 진짜로 중복되었다면 전문가로서의 명예를 걸고 중복을 제거해야 함
  • 하지만 중복에도 여러 종류가 있음
    • 진짜 중복: 한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야 함
    • 가짜 중복: 중복으로 보이는 두 코드가 각자의 경로로, 즉 서로 다른 속도와 이유로 변경됨
  • 자동반사적으로 중복을 제거해버리는 잘못을 저지르는 유혹을 떨쳐내고, 중복이 진짜 중복인지 확인해야 함

 

 

[ 결합 분리(다시) ]

  • 소스 수준 분리 모드
    • 소스 코드 모듈 사이의 의존성을 제어할 수 있음
    • 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있음
    • 모든 컴포넌트가 같은 주소 공간에서 실행되고, 함수 호출로 통신함
    • 컴퓨터 메모리에는 하나의 실행 파일 만이 로드되며, 흔히 모노리틱 구조라고 부름
  • 배포 수준 분리 모드
    • jar, DLL, 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어할 수 있음
    • 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드 및 재배포 하지 않도록 만들 수 있음
    • 분리된 컴포넌트는 독립적으로 배포할 수 있는 단위로 분할되어 있음
  • 서비스 수준 분리 모드
    • 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷으로만 통신하도록 만들 수 있음
    • 모든 실행가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적임
    • 마이크로서비스에 해당함

프로젝트 초기부터 서비스로 분리하면 매우 관리 비용이 커진다. 회사에 무턱대고 별도의 서비스로 나눠진 컴포넌트가 있는데, 덕분에(?) 항상 새로운 환경을 세팅하게 되면 작업 비용이 하나 더 늘어난다. 이러한 방식은 지양하는 것이 좋음을 몸소 배울 수 있었다.

그래서 저자는 "컴포넌트 결합을 분리하되 서비스가 되기 직전에 멈추는 방식을 선호한다. 가능한 한 오랫동안 동일한 주소 공간에 남겨둔다. 이를 통해 서비스에 대한 선택권을 열어둘 수 있다."고 나와있다. 상상컨데 패키지를 우선 별도의 컴포넌트처럼 나눠야할 것 같고, 세부 구현에 대한 부분은 인터페이스로 나누되 호출 방식을 함수 호출로 하는 것 같다.

 

 

 

17장. 경계: 선 긋기


[ 서론 ]

  • 경계(boundary)는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편의 요소를 알지 못하도록 막음
  • 선 중 일부는 프로젝트 초기에, 심지어 코드가 작성되기도 전에 그어지며, 어떤 선은 매우 나중에 그어짐
  • 초기에 그어지는 선들은 가능한 오랫동안 결정을 연기시켜, 이들 결정이 핵심적인 업무 로직을 오염시키지 못하게 하는 목적임
  • 아키텍트의 목표는 인적 자원을 최소화하는 것인데, 인적 자원의 효율을 떨어뜨리는 요인은 결합임
  • 특히 너무 일찍 내려진 결정에 따른 결합인데, 여기서 이른 결정이란 바로 업무 요구사항(유스케이스)와 아무런 관련이 없는 결정임

책에서는 "프레임워크, 데이터베이스, 웹 서버, 유틸리티 라이브러리, 의존성 주입 등과 같은 것들이 너무 일찍 내려진 결정이라고 적혀있다. 물론 우리나라에서 이렇게 결정을 미루기는 하기 쉽지 않지만, 개인적으로 결정 권한이 있다면 최대한 미뤄보도록 하자. 큰 규모가 불가능하다면 데이터베이스, 캐시서버 부터 하나의 테이블 혹은 컬럼 조차도 좋다.

 

 

[ 플러그인 아키텍처 ]

  • 소프트웨어 개발 기술의 역사는 플러그인을 손쉽게 생성하여, 확장 가능하며 유지보수가 쉬운 아키텍처를 확립하는 방법의 이야기임
  • 핵심적인 업무 규칙은 선택적이거나 수 많은 형태로 구현될 수 있는 나머지 컴포넌트로부터 분리되어 있고, 독립적임
    • 위의 설계에서 UI와 DB는 플러그인 형태로 고려되었기에 어떠한 기술이라도 가능함
    • GUI는 업무 규칙과 다른 시점에 다른 속도로 변경되므로, 둘 사이에는 반드시 경계가 필요함(다음 챕터의 내용임)
  • 물론 기술 교체는 사소한 일이 아니지만 플러그인 구조를 가정한 채 시작함으로써, 최소한 이를 현실성 있도록 만들 수 있음

스터디에서 "세부 기술의 변경은 흔치 않은데 인터페이스로 구현을 분리해야 할까?"는 얘기가 나왔다. 사용중인 세부기술이 바뀌는건 흔치 않다고 누군가는 얘기할 수 있지만 나는 2년 동안 데이터베이스가 2번이나 바뀌었다. 세부 구현이 바뀌는게 아니여도 환경에 따라 구현이 달라지는 경우에 매우 유용한데, 개인적인 경험으로는 이러한 경우가 꽤나 많았다.

 

 

[ 결 ]

  • 소프트웨어 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야 함
  • 일부는 핵심 업무 규칙에 해당하며 나머지는 플러그인으로, 핵심 업무와는 직접적인 관련이 없지만 필수 기능을 포함함
  • 분할한 다음 컴포넌트 사이의 화살표가 특정 핵심 업무를 향하도록 이들 컴포넌트의 소스를 배치함
  • 의존성 화살표는 저수준 세부사항에서 고수준의 추상화를 향하도록 배치됨(의존성 역전 원칙과 안정된 추상화 원칙의 응용)

 

 

 

19장. 정책과 수준


[ 서론 ]

  • 소프트웨어 시스템이란 정책을 기술한 것이며, 하나의 정책은 이 정책을 서술하는 여러 개의 조그만 정책들로 쪼갤 수 있음
  • 소프트웨어 아키텍처를 개발하는 기술에는 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함됨
    • 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 함
    • 서로 다른 이유 혹은 시점에 변경되는 정책은 다른 수준에 위치하며, 반드시 다른 컴포넌트로 분리해야 함
  • 좋은 아키텍처는 컴포넌트 의존성의 방향이 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 설계되어야 함

 

 

 

[ 수준(Level) ]

  • 수준(Level)을 엄밀하게 정의하자면 “입력과 출력까지의 거리”임
    • 시스템의 입력과 출력 모두로부터 멀리 위치할수록 정책의 수준은 높아짐(고수준)
    • 고수준 정책은 저수준 정책에 비해 덜 빈번하게 변경되고, 보다 중요한 이유로 변경되는 경향이 있음
    • 입력과 출력을 다루는 정책이라면 시스템에서 최하위 수준에 위치함(저수준)
    • 저수준 정책은 더 빈번하게 변경되며, 보다 긴급성을 요하며, 덜 중요한 이유로 변경되는 경향이 있음
  • 단일 책임 원칙(SRP)과 공통 폐쇄 원칙(CCP)에 따르면 동일한 이유로 동일한 시점에 변경되는 정책은 함께 묶임
  • 모든 소스 코드 의존성이 고수준 정책을 향한다면(저수준 컴포넌트가 고수준 컴포넌트에 플러그인) 변경의 영향도를 줄일 수 있음
  • 시스템의 최저 수준에서 중요하지 않지만 긴급한 변경이 발생하더라도, 보다 높은 위치의 중요한 수준에 미치는 영향이 거의 없게 됨

여기저기서 고수준, 저수준을 얘기하는데, 개인적으로는 "그렇다면 무엇이 고수준인가?"를 판단하기 위한 명쾌한 기준이 없어서 이는 어려운 결정이라고 생각했었다. 그런데 책에서 명확하게 "입력과 출력까지의 거리"라고 정의를 해줘서, 이후에 무엇이 플러그인되어야 하는지를 결정하는데 큰 도움이 될 것 같다. 물론 그럼에도 어려운 케이스가 많겠지만, 개인적으로 가장 인상깊은 내용 중 하나였다.

 

 

 

 

20장. 업무 규칙(Business Logic)


[ 서론 ]

  • 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차이며, 컴퓨터상으로 구현했는지와 상관없음
  • 이렇게 규칙을 자동화하는 시스템이 없더라도, 사업 자체에 핵심적이며 존재하는 업무 규칙을 핵심 업무 규칙이라고 부름
  • 핵심 업무 규칙은 보통 데이터를 요구하는데 이를 핵심 업무 데이터라고 부름
  • 핵심 업무 규칙과 핵심 데이터는 본질적으로 결합되어 있으므로 객체로 만들기 좋은 후보가 되는데, 이를 엔티티라고 함
  • 예를 들어 대출에 N%의 이자를 부과한다는 사실은 은행이 돈을 버는 업무 규칙이며, 대출 잔액이나 이자율 등은 업무 데이터임

여기서의 엔티티는 JPA의 엔티티와 개념적으로 다르다. JPA의 엔티티는 디비와 매핑되는 영속성 엔티티이지만 책에서 말하는 엔티티는 핵심 업무 규칙을 갖는 도메인 엔티티이다. 물론 JPA 엔티티 업무 규칙을 가져 도메인 엔티티 역할까지 가질 수도 있지만 기본적으로 다름을 이해하고 있어야 한다. 엔티티라고 하면 의사소통에 헷갈리는 경우가 있어서, 나는 영속성 엔티티와 도메인 엔티티라는 워딩을 선호한다.

 

 

[ 엔티티 ]

  • 엔티티는 컴퓨터 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 핵심 업무 규칙을 구체화함
  • 엔티티 객체는 핵심 업무 데이터를 직접 포함하거나, 핵심 업무 데이터에 매우 쉽게 접근할 수 있음
  • 엔티티의 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구현됨
  • ex) 대출을 의미하는 Loan 엔티티는 3가지 핵심 업무 데이터를 포함하며, 3가지 핵심 업무 규칙을 인터페이스로 제공함

 

 

  • 엔티티 클래스는 비즈니스의 대표자로서 독립적으로 존재하는데, 순전히 업무에 대한 것이며 이외의 것은 없음
  • 이 클래스는 DB, UI, 프레임워크에 대한 고려사항들로 인해 오염되어서는 안됨
  • 어떠한 시스템에서도 업무를 수행할 수 있으며, 시스템의 표현 형식이나 데이터 저장 방식, 컴퓨터가 배치되는 방식과도 무관함
  • 엔티티의 유일한 요구 조건은 핵심 업무 데이터와 규칙을 하나로 묶어서 별도의 소프트웨어 모듈로 만들어야 하는 것

의미있는 비즈니스 로직을 갖는 객체를 풍부만 도메인 객체, 반대로 데이터와 Getter와 Setter 만을 갖는 객체를 빈약한 도메인 객체라고 한다. 빈약한 도메인 모델에 익숙한 사람들이 많을테지만, 객체라는 것은 메세지를 가져야 한다. Getter와 Setter는 메세지가 아닌 자바 빈 스펙에 가깝다. 나도 빈약한 도메인 객체에 익숙했다가 풍부한 도메인 객체로 넘어가고 있는데, 직접 사용해보니 외부 의존성이 없어 테스트가 편리해지고, 중복이 제거되며, 코드를 찾기 쉬워지는 등의 수 많은 장점이 있다.

 

 

[ 유스케이스 ]

  • 모든 업무 규칙이 엔티티처럼 순수한 것은 아니고, 자동화된 시스템으로 동작을 정의하고 제약하는 업무 규칙도 존재함
  • 이렇듯 자동화된 시스템이 사용되는 방법을 설명하는 것이 바로 유스케이스임
  • 유스케이스는 사용자가 제공해야 하는 입력, 사용자에게 보여줄 출력, 그리고 해당 출력을 생성하기 위한 처리 단계를 기술함
  • 엔티티 내의 핵심 업무 규칙과는 반대로, 유스케이스는 애플리케이션에 특화된 업무 규칙을 설명함

 

 

  • 유스케이스는 엔티티 내부의 핵심 규칙을 언제 그리고 어떻게 호출할 지를 명시하는 규칙을 담음
  • 또한 입/출력 데이터를 형식 없이 명시한다는 점을 제외하고 UI를 기술하지 않음
  • 유스케이스는 객체로써, 애플리케이션에 특화된 규칙을 설명하며, 이를 통해 사용자와 엔티티 사이의 상호작용을 규정함
  • 유스케이스는 애플리케이션에 특화된 업무 규칙을 구현하는 하나 이상의 함수를 제공함
  • 또한 입력 데이터, 출력 데이터, 상호작용하는 엔티티에 대한 참조 데이터 등의 데이터 요소를 포함한다.
  • 엔티티는 자신을 제어하는 유스케이스에 대해 아무것도 알지 못하며, 이는 의존성 역전 원칙을 준수하는 의존성 방향임
    • 고수준인 엔티티는 저수준인 유스케이스에 대해 알지 못하지만 저수준인 유스케이스는 고수준인 엔티티를 알고 있음
    • 유스케이스는 단일 애플리케이션에 특화되어 있고, 따라서 시스템의 입/출력에 보다 가깝게 위치하기 때문에 저수준임
    • 엔티티는 다양한 애플리케이션에서 사용될 수 있도록 일반화한 것이므로, 입/출력에서 더 멀리 떨어져 있으므로 고수준임
    • 유스케이스는 엔티티에 의존하는 반면 엔티티는 유스케이스에 의존하지 않음

이와 관련해서는 실제 코드로 살펴보는 것이 좋다. 링크를 따라 가면 퀴즈를 발송하는 유즈케이스 클래스가 있고, 그 안에서 도메인 객체인 Member나 UnsentQuizzes 등이 호출되는 것을 볼 수 있다. 이렇게 빈 파라미터의 입력과 int 타입의 반환, 그리고 그 안에서 도메인 객체들이 상호작용하는 시스템의 작동을 기술한 것이 바로 유스케이스이다. 무책임하게 UserService와 같이 거대한 서비스 객체를 만드는 것 보다는 의미있게 유스케이스 단위로 클래스를 나누는 것이 테스트하기도 편리하고 가독성도 좋아지고, 코드 찾기도 쉬워지는 등의 장점이 있다. UserService만 봐서는 어떠한 비즈니스 로직을 처리하는지 알 수 없다.

 

 

[ 요청 및 응답 모델 ]

  • 유스케이스는 입력 데이터로부터 출력 데이터를 생성하지만, 이 데이터가 사용자나 다른 컴포넌트와 주고 받는 방식은 몰라야 함
  • 유스케이스 클래스의 코드가 HTML이나 SQL에 대해 알아서는 안되며, 웹 뿐만 아니라 그 어떤 UI에도 종속되는게 없어야 함
  • 요청 및 응답 모델이 독립적이지 않다면, 그 모델에 의존하는 유스케이스도 결국 해당 모델이 수반하는 의존성에 간접적으로 결합됨
  • 엔티티와 요청/응답 모델은 상당히 많이 겹치므로 둘을 통합하려는 유혹을 받을 수 있지만, 두 객체의 목적은 완전히 다름
  • 시간이 지나면 두 객체는 완전히 다른 이유로 변경될 것이고, 두 객체를 함께 묶는 행위는 공통 폐쇄 원칙과 단일 책임 원칙에 위배됨

단순 CRUD 위주의 유스케이스라면 입력과 출력 그리고 도메인 및 영속성 모델까지 모두 동일한 데이터를 사용할 수 있다. 이상적인으로는 모두 클래스를 분리하는게 좋겠지만... 개인적으로는 시간에 쫓기는 현실에서 당장은 분리하지 않아도 된다는 생각이다. 대신 분리가 필요해지는 시점이 왔을 때, 분기 처리 등을 하는 것이 아니라 반드시 분리를 해야한다. 만약 본인이 그 시점에도 분리하지 않는 경향이 있다면 처음부터 분리하는 것이 나을 수 있다. 합쳐진 것을 분리하는 것은 귀찮으므로 처음부터 분리해두는 것이다. 나는 만약 이렇게 지름길을 사용했다면, 왜 지름길을 사용했고(의사 결정의 이유) 언젠가 분리가 필요할 수 있음(분리를 하라는 인식 각인)을 주석으로 달아둔다.

 

 

 

 

21장. 소리치는 아키텍처


[ 아키텍처의 테마 ]

  • 소프트웨어 애플리케이션의 아키텍처는 유스케이스에 대해 소리쳐야 함
  • 아키텍처는 프레임워크에 대한 것이 아니며, 프레임워크는 사용하는 도구일 뿐임
  • 아키텍처를 프레임워크 중심으로 만들어버리면, 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없음

 

 

[ 아키텍처의 목적 ]

  • 좋은 아키텍처는 유스케이스가 중심이므로, 프레임워크나 도구 등에 전혀 구애받지 않고 유스케이스를 지원하는 구조를 가짐
  • 좋은 아키텍처는 프레임워크, 데이터베이스, 웹 서버, 여타 개발 환경 문제나 도구에 대한 결정을 미룰 수 있음
  • 좋은 아키텍처는 프로젝트의 훨씬 후반까지 결정을 하지 않아도 되도록 도와줄 뿐만 아니라 결정을 쉽게 번복할 수 있음
  • 좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합은 분리시킴

 

 

[ 하지만 웹은? ]

  • 웹은 전달 메커니즘(입출력 장치)이며, 아키텍처에서도 그와 같이 다뤄야 함
  • 애플리케이션이 웹으로 전달된다는 사실은 세부 사항이며, 시스템 구조를 지배해서는 절대 안됨
  • 실제 애플리케이션을 웹으로 전달할지 여부는 미루어야 할 결정사항 중 하나임
  • 시스템 아키텍처는 시스템이 어떻게 전달될지에 대해 가능하다면 아무것도 몰라야 함

 

 

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

  • 프레임워크는 매우 강력하고, 상당히 유용할 수 있음으며, 누군가는 “프레임워크가 모든 것을 하게 하자”라는 태도를 취함
  • 하지만 이는 우리가 취하고 싶은 태도가 아님
  • 우리는 어떻게 하면 아키텍처를 유스케이스에 중점을 둔 채 그대로 보존할 수 있을지를 생각해야 함

 

 

[ 테스트하기 쉬운 아키텍처 ]

  • 아키텍처가 유스케이스를 최우선으로 한다면, 프레임워크 없이도 필요한 유스케이스 전부를 단위 테스트 할 수 있어야 함
  • 테스트를 돌리는데 반드시 웹서버나 데이터베이스가 필요한 상황이어서는 안됨
  • 엔티티는 반드시 POJO여야 하며, 프레임워크나 데이터베이스 및 다른 것들에 의존해서는 안되고 유스케이스가 엔티티를 조작해야 함
  • 최종적으로 프레임워크로 인한 어려움을 겪지 않고도 반드시 이 모두를 있는 그대로 테스트할 수 있어야 함

 

 

[ 결론 ]

  • 아키텍처는 시스템을 이야기해야 하며, 시스템에 적용된 프레임워크에 대해 얘기해서는 안됨
  • 헬스 케어 시스템을 구축중이라면, 새로온 프로그래머가 소스 저장소를 본 첫 인상은 “오, 헬스케어 시스템이군”이여야 함
  • 새로 합류한 프로그래머는 시스템이 어떻게 전달될 지 알지 못한 상태에서도 시스템의 모든 유스케이스를 이해할 수 있어야 함

 

 

 

 

22장. 클린 아키텍처


[ 아키텍처의 테마 ]

  • 지난 수 십년간 시스템 아키텍처와 관련된 여러 아이디어가 나옴
    • Hexagonal Architecture(육각형 아키텍처 혹은 포트와 어댑터)
    • DCI(Data, Context, Interaction)
    • BCE(Boundary-Control-Entity)
  • 세부적인 면에서는 차이가 있지만, 목표는 모두 계층 분리를 통한 관심사의 분리이며 다음과 같은 특징을 지님
    • 프레임워크 독립성: 프레임워크을 도구로 사용하며, 프레임워크가 지닌 제약사항으로 시스템을 강제하지 않음
    • 테스트 용이성: 업무 규칙은 UI, DB, 웹서버 및 다른 외부 요소 없이도 테스트할 수 있음
    • UI 독립성: 시스템의 나머지를 변경하지 않고도 UI를 쉽게 변경할 수 있음
    • 데이터베이스 독립성: 오라클이나 MS-SQL 서버 등을 다른 DB로 교체할 수 있고, 업무 규칙은 DB에 결합되지 않음
    • 외부 에이전시에 대한 독립성: 실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못함

 

 

한가지 흥미로운 점은 모두 "계층 분리"를 기준으로 한다는 것이다. 분리의 기준은 계층이 아닐 수도 있다. "만들면서 배우는 클린 아키텍처"의 저자는 최근에 Spring I/O 발표에서 "컴포넌트 기반의 분리"로 발표를 하기도 했다.

 

 

 

[ 의존성 규칙 ]

  • 각각의 동심원은 서로 다른 영역이며, 안으로 들어갈수록 고수준의 소프트웨어가 됨
  • 이러한 아키텍처가 동작하도록 하는 가장 중요한 것은 의존성 규칙임
  • 바깥은 메커니즘 안은 정책인데, “소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다." 임
  • 우리는 외부 원에 위치한 어떤 것도 내부의 원에 영향을 주지 않기를 바람
  • 내부의 원에 속한 요소는 외부의 원에 속한 어떤 것(함수, 클래스, 변수 등의 모든 소프트웨어 엔티티)도 알지 못함

 

엔티티(Entity)

  • 엔티티는 전사적인 핵심 업무 규칙을 캡슐화하며 메소드를 갖는 객체 혹은 데이터와 함수의 집합일 수도 있음
  • 다양한 애플리케이션에서 재사용만 가능하다면 형태는 중요하지 않음
  • 외부의 무언가가 변경되더라도 엔티티가 변경될 가능성은 지극히 낮음
  • 운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티에는 절대로 영향을 주면 안됨
  • ex) 엔티티는 핵심 비즈니스 로직을 다루므로, UI 단의 페이징 처리 등이 필요해도 변경이 일어나서는 안됨

 

 

유스케이스(UseCase)

  • 유스케이스는 애플리케이션에 특화된 업무 규칙을 포함하며, 시스템의 모든 유스케이스를 캡슐화하고 구현함
  • 유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정함
  • 또한 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끔
  • 유스케이스의 변경이 엔티티에 영향을 주면 안되며, 외부 요소의 변경이 이 계층에 영향을 주는 것도 안됨
  • 하지만 운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받고, 유스케이스 세부사항이 변하면 일부 코드는 영향을 받음

 

 

인터페이스 어댑터(Interface Adapter)

  • 프레젠터나 컨트롤러 등과 같은 어댑터들로 구성되며, 컨트롤러에서 유스케이스로 전달된 요청은 다시 컨트롤러로 되돌아 감
  • 어댑터는 유스케이스와 엔티티에 맞는 데이터에서 DB나 웹 등과 같은 외부 요소에 맞는 데이터로 변환함
  • 또한 데이터를 외부 서비스에 맞는 형식에서 유스케이스나 엔티티에서 사용되는 내부적인 형식으로 변환하는 또 다른 어댑터가 필요함
  • 어댑터에서 객체를 변환하는 이유는 어댑터가 특정 기술에 종속되며 때문임
  • 만약 유스케이스 계층에서 변환을 한다면 유스케이스가 세부 기술에 의존하게 됨

외부 서비스에 맞는 형식에서 유스케이스나 엔티티에 맞는 내부적인 형식으로 사용되는 케이스가 무엇이 있을까 생각해보니 Long 타입으로 받는 값을 유스케이스 내부에서는 일급 개체로 사용하는 등의 케이스가 있지 않을까 싶다. 또한 어댑터에서 변환해야 하는 이유가 상당히 인상깊었다. 어디에서 변환을 해야 적합할까 고민을 했던 적이 있는데, 어댑터에서 변환을 하는 것이 적합하다는 확신이 들었다.

 

 

프레임워크와 드라이버(Framework and Driver)

  • 가장 바깥쪽 계층은 데이터베이스나 웹 프레임워크 같은 것들로 구성됨
  • 이 계층에서는 안쪽 원과 통신하기 위한 접합 코드 외에는 특별히 더 작성할 게 없음
  • 모든 세부사항이 위치하는 곳으로, 웹과 데이터베이스도 세부사항이므로 이를 외부에 위치시켜 피해를 최소화함

 

 

원은 4개여야만 하나?

  • 위의 그림의 원들은 개념을 설명하기 위한 예시일 뿐이며, 더 많은 원이 필요할 수 있음
  • 하지만 어떤 경우에도 소스코드 의존성은 항상 안쪽을 향하며, 안쪽으로 이동할수록 추상화와 정책의 수준이 높아짐
  • 가장 바깥쪽 원은 저수준의 세부사항이며, 안쪽으로 이동할수록 점점 더 추상화되고 더 높은 수준의 정책들을 캡슐화함
  • 따라서 가장 안쪽 원은 가장 범용적이며, 높은 수준을 가짐

 

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

  • 경계를 가로지르는 데이터는 흔히 간단한 데이터 구조로 이루어지며 구조체나 DTO 등 원하는 대로 고를 수 있음
  • 중요한 점은 격리되어 있는 간단한 데이터 구조가 경계를 가로질러 전달된다는 사실임
  • 데이터 구조가 어떤 의존성을 가져 의존성 규칙을 위배되게 하는 일은 원치 않음
  • 따라서 경계를 가로 질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태여야 함

 

 

 

[ 전형적인 시나리오 ]

 

 

  • 아래의 다이어그램은 웹 기반 자바 시스템의 전형적인 시나리오임
    • 웹 서버는 입력 데이터를 모아 좌상단의 Contoller로 전달함
    • Controller는 데이터를 POJO로 묶은 후, InputBoundary 인터페이스를 통해 UseCaseInteractor로 전달함
    • UseCaseInteractor는 이 데이터를 해석해서 Entities가 어떻게 춤출지 제어하는데 사용함
    • 또한 UseCaseInteractor는 DataAccessInterface를 사용해 Entities가 사용할 데이터를 불러와 메모리로 로드함
    • Entities가 완성되면 UseCaseInteractor는 Entities로부터 데이터를 모아서 POJO인 OutputData를 구성함
    • 그리고 나서 OutputData는 OutputBoundary 인터페이스를 통해 프레젠터로 전달됨
    • Presenter가 맡은 역할은 OutputData를 ViewModel과 같이 화면에 출력할 수 있는 형태로 재구성하는 일임
    • ViewModel 또한 평범한 자바 객체로, 주로 문자열과 플래그로 구성되며 View에서는 이 데이터를 화면에 출력함
    • Presenter는 ViewModel을 로드할 때 OutputData가 갖는 Date 객체를 View에 적절한 형태의 문자열로 변환함
  • 의존성의 방향이 중요한데, 모든 의존성은 경계선을 안쪽으로 가로지르며 의존성 규칙을 준수함

위의 다이어그램은 웹 기반 자바 시스템의 전형적인 시나리오라고 책에 나와있다. 그런데 위의 "인터페이스 어댑터"에도 나와있듯 컨트롤러로 들어온 요청은 유스케이스에서 다시 컨트롤러로 흐름이 넘어오는데... 어떻게 OutputBoundary 인터페이스가 존재하는지, 그리고 어떻게 컨트롤러로 흐름이 넘어가는게 아니라 Presenter로 넘어가는지 이해가 가지 않았다... 만약 UseCaseInteractor가 OutputBoundary를 호출해 ViewModel을 그대로 Controller로 반환했다면 Interactor의 반환 타입이 ViewModel로 선언될 것이므로, 의존성 방향이 뒤집혀 이러한 구조는 아닐 텐데, 코드로 직접 쳐봤지만 그림대로 구현이 되지 않는다. 그래도 큰 그림에서 어떻게 구현을 하면 되는지는 머리에 그림이 그려졌으므로.... 우선 넘어가도록 하자.

 

 

 

[ 결론 ]

  • 위의 간단한 규칙들을 준수하는 일은 어렵지 않으며, 향후에 겪을 수 많은 고통거리를 덜어줌
  • 소프트웨어를 계층으로 분리하고 의존성 규칙을 준수한다면 본질적으로 테스트하기 쉬운 시스템을 만들게 될 것
  • DB나 UI 같은 외부 요소가 구식이 되더라도, 이를 쉽게 교체할 수 있음

 

 

 

23장. 프레젠터와 험블 객체


[ 험블 객체 패턴 ]

  • 험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 쉬운 행위를 단위 테스트 작성자가 쉽게 분리하는 방법으로 고안됨
  • 험블 객체 패턴은 행위들을 두 개의 모듈 또는 클래스로 나누는데, 이 중 하나가 험블임
    • 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮김
    • 나머지 모듈에는 험블 객체에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮김
  • 예를 들어 GUI의 각 요소가 필요한 위치에 표시되었는지 검사하는 것은 어렵지만, 수행하는 행위의 대다수는 테스트가 쉬움
  • 따라서 험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여, 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있음

결국 코드에도 테스트가 쉬운 코드와 테스트하기 어려운 코드가 있다는 것이다. 가능하면 최대한 테스트 가능한 부분을 남겨두고, 테스트 불가능한 부분은 독립적으로 떼어 두는 것이 좋다.

 

 

 

[ 프레젠터와 뷰 ]

    • 험블 객체이며, 테스트하기 어려움
    • 이 객체에 포함된 코드는 가능한 간단하게 유지함
  • 프레젠터
    • 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것
    • 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만듬
  • 뷰는 ViewModel의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 없으므로 보잘것 없음(humble)
    • 예를 들어 애플리케이션에서 어떤 필드에 날짜를 표시하고자 한다면 프레젠터에 Date 객체를 전달함
    • 그러면 프레젠터는 해당 데이터를 적절한 포맷의 문자열로 만들고, 이를 ViewModel에 담음
    • 그러면 뷰는 ViewModel에서 데이터를 찾음

 

 

[ 테스트와 아키텍처 ]

  • 테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 오랫동안 알려짐
  • 험블 객체 패턴이 좋은 예인데, 테스트하기 쉬운 부분과 어려운 부분으로 분리하면 아키텍처가 정의되기 때문임
  • 프레젠터와 뷰는 이러한 경계 중 하나이며, 이 밖에도 수 많은 경계가 존재함

 

[ 결론 ]

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

 

 

 

24장. 부분적 경계


[ 서론 ]

  • 아키텍처 경계를 완벽하게 만드는 데는 비용이 많이 들며, 유지하는데도 엄청난 노력이 필요함
    • 쌍방향의 다형적 Boundary 인터페이스
    • Input/Output 데이터구조
    • 두 영역을 독립적으로 컴파일하고 배포할 수 있는 컴포넌트로 격리하는데 필요한 모든 의존성 관리
  • 애자일 커뮤니티의 많은 사람들은 YAGNI(You are not going to need it) 원칙을 위배하는 선행적인 설계를 탐탁치 않아함
  • “하지만 어쩌면 필요할지도”라는 생각이 들 수 있는데, 그렇다면 부분적 경계(partial boundary)를 구현해볼 수 있음

 

 

[ 결론 ]

  • 아키텍처 경계를 부분적으로 구현하는 간단한 3가지 방법을 살펴보았는데, 방법은 많으며 3가지는 순전히 예시임
    • 마지막 단계를 건너뛰기: 독립적으로 컴파일 및 배포 가능한 컴포넌트로 만들고, 단일 컴포넌트에 그대로 모아둠
    • 일차원 경계: 양방향 Boundary 인터페이스가 아닌 한방향만 경계를 인터페이스로 격리함
    • 파사드: Facade 클래스에 모든 서비스 클래스를 메소드 형태로 정의하고, 호출이 발생하면 해당 서비스 클래스로 전달함
  • 각 접근법은 완벽한 형태의 경계를 담기 위한 공간으로써 적절하게 사용할 수 있는 상황이 서로 다름
  • 또한 각 접근법은 해당 경계가 실제로 구체화되지 않으면 떨어질 수 있음
  • 아키텍처 경계가 언제, 어디에 존재해야 할 지 그리고 그 경계를 완벽하게 혹은 부분적으로 구현할지를 결정해야 함

클린 아키텍처를 구현해봤던 사람이라면 비용이 적지 않음을 느낄 수 있을 것이다. 책에서도 이를 알고 있고, 다양한 지름길을 소개해주고 있다. 완벽하게 클린 아키텍처를 구현하는 것은 오버엔지니어링이 될 수 있으므로, 적절히 트레이드 오프를 하도록 하자.

 

 

 

25장. 계층과 경계


[ 서론 ]

  • 시스템이 3가지 컴포넌트(UI, 업무 규칙, DB)로만 구성된다고 생각하기 쉬움
  • 몇몇 단순한 시스템에서는 이 정도로 충분하지만, 대다수의 시스템에서 컴포넌트는 이보다 많음

 

 

[ 결론 ]

  • 아키텍처 경계는 어디에나 존재하며, 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 함
    • 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 함
    • 동시에 이러한 경계가 무시되었다면 나중에 다시 추가하는 비용이 크다는 사실도 알아야 함
  • 매우 똑똑한 일부 사람들이 수년 동안 말해왔듯이, 추상화가 필요하리라고 미리 예측해서는 안됨(YAGNI)
    • YAGNI(You Aren’t Going to Need It)에는 지혜가 담겨 있는데, 오버 엔지니어링이 언더 엔지니어링보다 나쁠때가 훨씬 많음
    • 반대로 어떤 아키텍처 경계도 존재하지 않는 상황에서 경계가 정말로 필요하다는 사실을 발견할 수 있음
    • 하지만 그때서야 경계를 추가하려면 비용이 많이 들고 큰 위험을 감수해야 함
  • 그러므로 소프트웨어 아키텍트는 미래를 내다보고 현명하게 추측해야 함
    • 비용을 산정하고, 어디에 경계를 둘지, 그리고 완벽하게 구현할 경계와 부분적으로 구현할 경계, 무시할 경계를 결정해야 함
    • 이는 일회성 결정이 아니며, 프로젝트 초반에는 구현할 경계와 무시할 경계인지를 쉽게 결정할 수 없으므로 지켜봐야 함
    • 그리고 시스템이 발전함에 따라 주의를 기울여야 함
    • 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 함
    • 첫 조짐이 보이는 시점에 해당 경계를 구현하는 비용과 무시할 때 감수할 비용을 가늠해보고, 결정 사항을 자주 검토해야 함
    • 목표는 경계의 구현 비용이 무시해서 생기는 비용보다 적어지는 그 변곡점에서 경계를 구현하는 것
    • 목표를 달성하려면 빈틈없이 지켜봐야 한다.

책에서도 명쾌하게 얘기해주고 앞에서도 얘기했지만, 은탄환(Silver Bullet)은 없다. 결국 모든 것에는 투자 대비 효용(ROI, Return On Investment)을 계산해야 한다. 클린 아키텍처도 변경에 매우 유연하지만 추상화하여 이를 전부 구현하려면 ROI가 매우 떨어진다. 그러므로 트레이드 오프가 필요한데, 책에서는 추상화의 조짐이 보이는 시점에 비용을 예상해보라고 한다. 추상화 도입으로 비용이 더 커질지를 가늠해보는 것을 기준으로 삼도록 하자. 나도 곧바로 구현체를 만들기도 하고 추상화를 도입하기도 하는데, 예를 들어서 잠깐 쓰고 버릴 코드는 굳이 인터페이스를 두고, 구현체를 나누지 않는다.

 

 

 

26장. 메인 컴포넌트(Main)


[ 서론 ]

  • 모든 시스템에는 최소한 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성하고 조정하며 관리함
  • 이 컴포넌트를 메인 컴포넌트(Main)이라고 부름

 

 

[ 궁극적인 세부사항 ]

  • 메인 컴포넌트는 궁극적인 세부사항으로, 가장 낮은 수준의 정책임
  • 메인은 시스템의 초기 지입점으로, 운영체제를 제외하면 어떤 것도 메인에 의존하지 않음
  • 메인은 모든 팩토리와 전략 및 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘김
  • 의존성 주입 프레임워크를 이용해 의존성을 주입하는 일은 바로 이 메인 컴포넌트에서 이뤄져야 함
  • 메인에 일단 의존성이 주입되고 나면, 메인은 의존성 주입 프레임워크 없이도 의존성을 분배할 수 있어야 함
  • 메인을 지저분한 컴포넌트 중에서도 가장 지저분한 컴포넌트라고 생각하면 됨(클린 아키텍처 가장 바깥의 지저분한 저수준 모듈)
  • 메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘김

 

 

[ 결론 ]

  • 메인은 초기 조건과 설정을 구성하고, 외부 자원을 모두 수집한 후 제어권을 애플리케이션의 고수준 정책으로 넘기는 플러그인임
  • 메인은 플러그인이므로 메인 컴포넌트를 애플리케이션의 설정 별로 하나씩 둬서 둘 이상의 메인 컴포넌트를 만들 수도 있음
  • 메인을 플러그인 컴포넌트로 여기고 아키텍처 경계 바깥에 위치한다고 보면, 설정 관련 문제를 훨씬 쉽게 해결할 수 있음

 

 

 

27장. "크고 작은 모든" 서비스들


[ 서론 ]

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

 

 

[ 서비스 아키텍처? ]

  • 먼저 서비스를 사용한다는 것은 본질적으로 아키텍처에 해당하지 않음
  • 시스템 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 정책으로부터 분리하는 경계에 의해 정의됨
  • 단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 아키텍처 관점에서 꼭 중요하다고 볼 수 없음
  • 아키텍처적으로 중요한 서비스도 있지만, 중요하지 않은 서비스도 존재하는데, 이 장에서 우리가 관심을 갖는 서비스는 전자임

 

 

[ 서비스의 이점? ]

결합 분리의 오류

  • 시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점 중 하나는 서비스 사이의 결합이 확실히 분리된다는 점임
  • 어쨌든 각 서비스는 다른 프로세스와 프로세서에서 실행되므로, 서비스는 개별 변수 수준에서는 각각 결합이 분리됨
  • 하지만 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재하며, 서로 공유하는 데이터에 의해 강력하게 결합됨

서비스로 시스템을 분리해도 네트워크를 통해 결합이 존재한다는 것은 나에게 또 다른 인사이트를 준 엄청난 부분이였다. 개인적으로 서비스를 분리하면서 생기는 서비스 팀들 간의 배포 디펜던시는 골칫거리 중 하나이다. 이는 당연한 것인줄 알았는데, 이것은 네트워크 상의 결합 때문임을 이 책을 통해 깨달을 수 있었다.

 

 

 

개발 및 배포 독립성의 오류

  • 서비스를 사용함에 따라 예측되는 또 다른 이점은 전담팀이 서비스를 소유 및 운영한다는 점임
  • 그래서 데브옵스 전략의 일환으로 전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있음
    • 이러한 개발 및 배포 독립성은 확장 가능한(scalable) 것으로 간주됨
    • 대규모 엔터프라이즈 시스템을 독립적으로 개발 및 배포 가능한 서비스들을 이용하여 만들 수 있다고 믿음
    • 시스템의 개발, 유지보수, 운영 또는 비슷한 수의 독립적인 팀 단위로 분할할 수 있다고 여김
  • 이러한 믿음도 일리가 있지만, 극히 일부일 뿐임
    • 첫째로, 모노리틱이나 컴포넌트 기반으로도 대규모 엔터프라이즈 시스템을 구축할 수 있다는 사실은 역사적으로 증명되어 옴
    • 따라서 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아님
    • 둘째로 “결합 분리 오류”에 따르면 서비스라고 해서 항상 독립적으로 개발, 배포 및 운영가능하지 않음
    • 데이터나 행위에서 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야 함

 

 

[ 야옹이 문제 ]

  • 택시 통합 시스템은 해당 도시에서 운영되는 많은 택시 업체를 알고 있고, 고객은 승차 요청을 할 수 있음
  • 고객은 승차 시간, 비용, 고급 택시 여부, 운전사 경력 등 다양한 기준에 따라 택시를 선택할 수 있다고 가정함
  • 확장 가능한 시스템을 위해 수 많은 작은 마이크로 서비스를 기반으로 구축하기로 결정했음
  • 개발팀을 소규모 팀으로 세분화했고, 각 팀이 규모에 맞게 적당한 수의 서비스를 개발 및 유지보수, 운영하는 책임을 지도록 했음

 

 

  • 위의 그림은 가상의 아키텍트가 서비스를 배치하여 구현한 모습임
    • TaxiUI 서비스는 고객을 담당하며, 고객은 모바일 기기로 택시를 호출함
    • TaxiFinder 서비스는 여러 TaxiSupplier의 현황을 검토하여 사용자에게 적합한 택시 후보들을 선별함
    • TaxiFinder 서비스는 해당 사용자에게 할당된 단기 데이터 레코드에 후보 택시들의 정보를 저장함
    • TaxiSelector 서비스는 사용자가 지정한 비용, 시간, 고급 여부 등의 조건을 기초로 후보 택시 중에서 적합한 택시를 선택함
    • TaxiSelector 서비스가 해당 택시를 TaxiDispatcher 서비스로 전달하면, TaxiDispatcher 서비스는 배차 지시를 함
  • 어느날, 마케팅 부서의 마케터들은 도시에 야옹이를 배달하는 서비스를 제공하겠다는 계획을 발표함
    • 사용자는 자신의 집이나 사무실로 야옹이를 배달해달라고 주문할 수 있음
    • 회사는 도시 전역에 야옹이를 태울 다수의 승차 지점을 설정해야 할 것임
    • 야옹이 배달 주문이 오면, 근처의 택시가 선택되고, 승하 지점 중 한 곳에서 야옹이를 태운 후, 올바른 주소로 야옹이를 배달함
    • 택시 업체 한 곳이 이 프로그램에 참여하기로 했고, 다른 업체도 함여할 것이고, 참여를 거부하는 업체도 있을 것임
    • 어떤 운전자는 고양이 알러지가 있을 수 있으므로 해당 운전자는 이 서비스에서 제외되어야 함
    • 또한 배차를 신청한 고객이 알러지가 있다고 밝혀진 경우라면 지난 3일 사이에 야옹이를 배달했던 차량은 배차되지 않아야 함
  • 서비스 다이어그램을 살펴보면 이 기능을 구현하려면 이들 서비스 전부를 변경해야 함
    • 의심의 여지 없이 야옹이 배달 기능을 추가하려면 개발과 배포 전략을 모두 신중하게 조정해야 함
    • 다시 말해 이 서비스들은 모두 결합되어 있어서 독립적으로 개발하고, 배포하거나 유지될 수 없음
  • 이게 바로 횡단 관심사가 지닌 문제임
    • 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하기 마련임
    • 위의 서비스 다이어그램과 같은 종류의 기능적 분해는 새로운 기능이 기능적 행위를 횡단하는 상황에 매우 취약함

사실 대부분의 회사라면 의심의 여지 없이 모든 서비스가 투입되어 배포를 할 것이다. 이는 기능적으로 서비스가 분해되었기 때문인데, 새로운 고양이 만을 위한 서비스를 띄운다고 해도 비용이 엄청나다. 이것에 많이 공감이 되었다.

 

 

 

[ 객체가 구출하다 ]

 

 

  • 컴포넌트 기반 아키텍처에서는 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함
  • 클래스들은 처음 그림의 서비스들과 거의 일치하지만 경계와 의존성 규칙이 준수함을 주목해야 함
  • 기존의 서비스 로직 중 대다수가 이 객체 모델의 기반 클래스들 내부로 녹아들었음
  • 하지만 배차에 특화된 로직 부분은 Rides 컴포넌트로, 야옹이에 대한 신규 기능은 Kittens 컴포넌트로 추출됨
  • 이 두 컴포넌트는 기존 컴포넌트들에 있는 추상 기반 클래스를 템플릿 메소드나 전략 패턴 등으로 오버라이드 함
  • 두 개의 신규 컴포넌트인 Rides와 Kittens가 의존성 규칙을 준수함에 주목해야 함
  • 또한 이 기능을 구현하는 클래스들은 UI의 제어 하에 팩토리(Factories)가 생성한다는 점도 주목해야 함
  • 이 전략을 따르더라도 야옹이 기능을 구현하려면 TaxiUI는 어쩔수 없이 변경해야 하지만 그 외의 것들은 변경할 필요가 없음
  • 대신 야옹이 기능을 구현한 새로운 jar을 시스템에 추가하고, 런타임에 동적으로 로드하면 됨
  • 따라서 야옹이 기능은 결합이 분리되며, 독립적으로 개발하여 배포할 수 있음

 

 

[ 컴포넌트 기반 서비스 ]

  • 서비스는 반드시 소규모 단일체(monolith)여야 할 이유는 없고, SOLID 원칙대로 설계할 수 있으며 컴포넌트 구조를 갖출 수도 있음
  • 이를 통해 서비스 내의 기존 컴포넌트들을 변경하지 않고도 새로운 컴포넌트를 추가할 수 있음
  • 자바의 경우 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각하면 됨
  • 새로운 기능 추가 혹은 기능 확장은 새로운 jar로 만드는데, 이때 기존 jar에 정의된 추상 클래스들을 확장해서 만들어짐
  • 그러면 새로운 기능 배포는 서비 재배포가 아니라, 단순히 새로운 jar 파일을 추가하는 문제가 됨(개방 폐쇄 원칙을 준수)

 

 

[ 횡단 관심사 ]

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

 

[ 결론 ]

  • 서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소가 아님
  • 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의됨
  • 시스템의 구성 요소가 통신하고 실행되는 물리적인 메커니즘에 의해 아키텍처가 정의되는 것은 아님

"시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의됨" 굉장한 인사이트를 주는 문구인 것 같다. 네트워크가 아닌 시스템 내부의 의존성을 기준으로 아키텍처를 고려하도록 하자!

 

 

 

 

28장. 테스트 경계


[ 서론 ]

  • 테스트는 시스템의 일부이며, 시스템의 나머지 요소가 아키텍처에 관여하는 것과 동등하게 아키텍처에도 관여함
  • 어떤 면에서는 정말 평범하게 관여하고, 또 다른 면에서는 상당히 독특하게 관여함

 

[ 시스템 컴포넌트인 테스트 ]

  • 아키텍처 관점에서 테스트는 세부적이며 구체적인 것으로, 태생적으로 의존성 규칙을 따름(의존성이 항상 테스트 대상을 향함)
  • 시스템 내부의 어떤 것도 테스트에는 의존하지 않으며, 테스트는 아키텍처에서 가장 바깥 원으로 생각할 수 있음
  • 테스트는 독립적으로 배포 가능하며, 가장 고립되어 있고, 시스템 운영에 꼭 필요치는 않고, 어떤 사용자도 테스트에 의존하지 않음
  • 테스트의 역할은 운영이 아니라 개발을 지원하는데 있지만, 그렇다고 해서 테스트가 시스템 컴포넌트가 아니라는 뜻은 아님

 

 

[ 테스트를 고려한 설계 ]

  • 테스트가 시스템의 설계와 잘 통합되지 않으면 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워짐
  • 시스템에 강하게 결합된 테스트라면 시스템이 변경될 때 함게 변경되어야만 함
    • 시스템 컴포넌트에서 생긴 아주 사소한 변경도, 이와 결합된 수 많은 테스트를 망가뜨릴 수 있음
    • 시스템의 공통 컴포넌트가 변경되면 수백, 수천 개의 테스트가 망가지며, 이는 "깨지기 쉬운 테스트 문제"로 알려져있음
    • 깨지기 쉬운 테스트는 시스템을 뻣뻣하게 만드는 부작용을 낳을 때가 많음
    • 시스템에 가한 간단한 변경이 대량의 테스트 실패로 이어진다면, 개발자는 변경을 하지 않을 것임
  • 이를 해결하려면 테스트를 고려해서 설계해야 하며, 소프트웨어 설계의 첫 번째 규칙은 언제나 변동성이 있는 것에 의존하지 않는 것
  • GUI는 변동성이 크므로 시스템과 테스트를 설계할 때 GUI를 사용하지 않고 업무 규칙을 테스트할 수 있게 해야 함

누구나 "깨지기 쉬운 테스트 문제"를 많이 겪었을 것고, 이를 위해 테스트 역시 많은 공부를 필요로 한다. 테스트가 깨지는 이유를 생각해보니 테스트 더블(가짜 객체)을 이용해 스텁(반환값 지정 후 호출)하는 코드 때문인 것 같다. 추상화를 도입하여 의존하는 대상만 바뀌어도 테스트가 깨져버린다. 그렇다면 가장 중요한 "유스케이스에 대해 입력과 출력만 신경쓰는 블랙박스 기반의 테스트를 작성한다면 문제가 해결되지 않을까!?" 하는 생각이 들었다. 도메인 별로 AutoConfig를 두고, 필요한 부분만 임포트해서 통합 테스트 설정을 만들고 TDD로 개발해나가면 될 것 같은 생각이다. 이론적으로는 매우 훌륭한 것 같은데, 실전은 또 다를 수 있으니 업무에서 한번 시도해봐야겠다. 

 

 

[ 테스팅 API ]

구조적 결합

  • 모든 클래스에 테스트 클래스가 각각 존재하고, 또 모든 메소드에 테스트 메소드 집합이 존재하는 테스트 스위트는 강하게 결합됨
  • 클래스나 메소드 중 하나라도 변경되면 딸린 다수의 테스트가 변경되어야 함
  • 결과적으로 테스트는 깨지기 쉬워지고, 이로 인해 상용 코드를 뻣뻣하게 만듬
  • 구조적 결합이 강하면 필수적인 진화 과정을 방해할 뿐만 아니라, 상용 코드의 범용성과 유연성이 충분히 좋아지지 못함

개인적으로 모든 테스트에서 메소드의 호출 행위를 검증하는 테스트가 정말 깨지기 쉬웠던 것 같다. 개인적으로는 반환값이 void인 경우 혹은 케이스에 따라서 다른 컴포넌트 호출 유/무가 갈리는 경우에만 verify를 사용하는 것 같다.

 

 

[ 결론 ]

  • 테스트는 시스템 외부에 있지 않고 시스템의 일부이므로, 테스트를 통한 안정성과 회귀의 이점을 얻으려면 잘 설계 되어야 함
  • 테스트를 시스템의 일부로 설계하지 않으면, 테스트는 깨지기 쉽고 유지보수가 어려워지는 경향이 있음
  • 이러한 테스트는 유지보수하기 너무 힘들기 때문에 방바닥의 휴지처럼 버려지는 최후를 맡게 됨

 

 

 

29장. 클린 임베디드 아키텍처


[ 서론 ]

  • “소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안으로부터 파괴될 수 있다.”
  • 임베디드 엔지니어가 아닌 엔지니어들도 또한 펌웨어를 작성할 수 있음
  • 코드에 SQL을 심어두거나, 개발하는 코드 전반에 플랫폼 의존성을 퍼뜨려 놓는다면 본질적으로 펌웨어를 작성하는 셈임
  • 안드로이드 앱 개발자 역시 업무 로직을 안드로이드 API로부터 분리하지 않는다면 펌웨어를 작성하는 셈임

 

 

[ 앱-티튜드(App-titude) 테스트 ]

  • 켄트백은 소프트웨어를 구축하는 세 가지 활동을 다음과 같이 기술했고, 설명은 글쓴이가 덧붙인 해설임
    • “먼저 동작하게 만들어라”: 소프트웨어가 동작하지 않는다면 사업은 망한다.
    • “그리고 올바르게 만들어라”: 코드를 리팩토링해서 당신을 포함한 나머지 사람들이 이해할 수 있게 만들고, 요구가 변경되거나 요구를 더 잘 이해하게 되었을 때 코드를 개선할 수 있게 만들어라.
    • “그리고 빠르게 만들어라”: 코드를 리팩토링해서 “요구되는” 성능을 만족시켜라
  • 프레드 브룩스는 맨먼스 미슨에서 “버리기 위한 계획을 세우라”고 제안함
  • 켄트와 프레드는 사실상 똑같은 충고를 하고 있음. "동작하는 것을 배워라. 그리고 나서 더 나은 해결책을 만들어라."
  • 대다수의 코드들이 올바르게 작성해서 유효 수명을 길게 늘리는데는 거의 관심 없이, 그저 동작하도록 만들어짐
  • 하지만 프로그래밍에는 단순히 앱이 동작하도록 만드는 것보다 중요한 것이 훨씬 많음

회사에서 업무를 하다 보면 특정 라이브러리에 대한 의존성을 갖게 되는 경우가 있다. 문제는 이것을 바로 비즈니스 로직에 결합시켜버리면, 해당 라이브러리와 강하게 결합되어 버린다는 것이다. 해당 라이브러리의 변경에 매우 취약해진다. 그러므로 특정 라이브러리를 바로 사용하는 것이 아니라, 분리해서 사용하는 것이 좋다. 이는 안드로이드, 운영체제, 데이터베이스, 캐시 등 모든 부분에 포함되는 얘기이다.

 

 

 

 

위의 내용은 마틴 파울러의 클린 아키텍처 책을 읽고 정리한 내용입니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다! 혹시 추가적인 의견 있으면 편하게 댓글 남겨주세요ㅎㅎ

 

 

 

관련 포스팅

  1. 클린 아키텍처 1부 소개 - 내용 정리 및 요약
  2. 클린 아키텍처 2부 벽돌부터 시작하기: 프로그래밍 패러다임 - 내용 정리 및 요약
  3. 클린 아키텍처 3부 설계 원칙 - 내용 정리 및 요약
  4. 클린 아키텍처 4부 컴포넌트 원칙 - 내용 정리 및 요약
  5. 클린 아키텍처 5부 아키텍처 - 내용 정리 및 요약
  6. 클린 아키텍처 6부 세부사항 - 내용 정리 및 요약

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함