클린아키텍쳐 5부 정리 (22장~28장)
22장 클린 아키텍처
수십년간 아키텍처에 관한 다양한 아이디어가 있었음
- 육각형 아키텍처(포트와 어댑터)
- DCI (Data Context and Interaction)
- BCE (Boundary Control Entitiy)
이들의 공통점은 "관심사의 분리"
소프트웨어를 계층으로 분리하면서 관심사의 목표 달성
- 프레임워크 독립성: 프레임워크의 제약사항을 시스템이 강제하도록 하지 않는다.
- 테스트 용이성: 업무 규칙은 UI, 디비, 웹 서버 또는 다른 요소 없이도 테스트 할 수 있다.
- UI 독립성: 시스템의 나머지 부분을 변경하지 않더라도 UI를 쉽게 변경할 수 있어야 한다.
- 데이터베이스 독립성
- 모든 외부 에이전시에 대한 독립성
DI 프레임워크 SwiInject, Needle도 프레임워크인데 그럼 직접 DI wrapper를 만들어서 쓰라는건가..?
Rx, Combine 도 프레임워크인데 stream을 다루는 wrapper를 만들어서 쉽게 교체가능하게 해야하나..?
의존성 규칙
- 안으로 갈수록 고수준 소프트웨어, 정책
의존성 규칙 을 통해 이러한 아키텍처가 동작하도록 한다.
- 내부 원은 외부의 어떤것도 알지 못함
- 이름도 언급하면 안된다.
- 외부에 선언된 데이터 형식도 내부원을 사용하면 안된다.
(특히 프레임워크가 생성한 거라면)
엔티티 : 전사적인 핵심 업무 규칙을 캡슐화
- 메서드를 가진 객체 또는 데이터구조와 함수의 집합
- 재사용가능한 그 어떤 형태
- 엔티티가 변경될 가능성은 지극히 낮다.
유스케이스 : 애플리케이션에 특화된 업무 규칙을 포함
- 유스케이스는 엔티티로 들어오고 나가는 흐름 조정
- 엔티티가 핵심업무 규칙을 사용하여 유스케이스 목적을 달성하도록 함
- 변경시 당연히 안쪽 레이어인 엔티티에 영향을 주면 안됨
- 운영관점에서 변경이 일어나면 유스케이스는 변경될 수 있음
인터페이스 어댑터
어댑터: 데이터를 유스케이스와 엔티티에 맞는 형식으로 변환(from database etc)
GUI의 MVC 아키텍처를 모두 포괄한다.
- presenter
- view
- controller
위 3가지 모두 인터페이스 어댑터 계층에 속한다.
모델(데이터 구조) 는 controller에서 use-case로 전달되고 다시 use-case에서 presenter->view로 전달된다.
프레임워크와 드라이버
- 가장 바깥쪽 계층
- 웹 프레임워크 데이터 베이스와 같은 세부사항
- 안쪽 레이어와 통신하기 위한 접합 코드 이외에는 특별히 작성할 코드가 없음
원은 4개여야 하나?
원의 갯수는 중요하지 않다.
의존성 규칙(항상 의존성은 안쪽을 향해야 한다) 는 변하지 않는다.
경계 횡단하기
제어흐름과 의존성 방향이 반대여야 하는 경우 DIP로 해결한다.
경계를 횡단하는 데이터는 어떤 모습인가
간단한 데이터 구조로 이루어진다.
DTO: Data transfer object 등으로 고를 수 있다.
중요한건 "격리"되어있는 데이터 구조가 경계를 지나야 한다.
엔티티 객체나 데이터베이스 행 전체가 경계를 지나면 의존성이 생겨버린다.
전형적인 시나리오
디비를 사용하는 웹기반 자바 시스템
- view는 viewModel 인터페이스를 통해 의존성 역전
- viewModel은 presenter에서 관리
- 데이터 베이스 또한 엑세스 인터페이스로 의존성 역전
- 엔티티에서 유스케이스(인터렉터: 비즈니스 로직) 뽑아냄
결국 앱에서도 MVVM, Ribs, MVC 는 Controller+presenter+viewModel+view 쪽 아키텍처이고 유즈케이스(인터렉터)가 없다면 MVVM에서는 ViewModel 유스케이스 로직이 들어가고 Ribs는 interactor가 있네 MVC에서는 Controller에 유스케이스 로직이 들어간다.
Data access interface 는 repository pattern으로 이쪽에 api,persistant storage가 위치한다.
유즈케이스를 나누면 테스트하기 편할것 같은데 유스케이스를.. 어떻게 나누지
음악을 듣는다..? 재생을 한다..?
결론
소프트웨어를 계층으로 분리하고 의존성 규칙을 따르면 테스트 하기 쉬운 시스템(=유지 보수하기 쉬운)을 만들 수 있다.
23장 클린 아키텍처
presenter는 험블 객체 패턴을 따른 형태이고
아키텍처의 경계를 식별하고 보호하는데 이점이 있다.
험블 객체 패턴
humble: 보잘것 없는
- 디자인 패턴
- 테스트를 복잡도에 따라 나누는 방법으로 고안됨
- 행위들을 두개의 모듈/클래스로 나누고 그 중 하나가 험블이다.
- 테스트 하기 어려운 것들은 전부 험블 객체로 옮긴다.
- 나머지 모듈에는 테스트하기 쉬운 행위를 옮긴다.
GUI는 화면 요소에 대한 테스트는 어렵지만
GUI에서 수행하는 행위에 대해서는 쉽게 테스트가 가능하다.
이 두 행위를 분리하여 Presenter와 View로 나눈다.
프레젠터와 뷰
View
- 테스트 하기 힘든 화면요소
- 데이터를 GUI로 이동시키지만 처리하진 않음
- 포멧팅 끝난 날짜를 뷰에 전달
Presenter
- 테스트하기 쉬운 객체
- 데어터를 화면에 표시할 수 있는 포멧으로 만드는 것
- Date 날짜를 원하는 포멧(YYYY-MM-dd)으로 변환하여 전달
viewModel이 View와 Presenter의 역할을 둘다 하고 있는것 같은데..?
뷰에 데이터 넣어주고 포멧팅까지 같이 하고 있으니..?
그래서 테스트하기 어려운건가..?
험블하지 않았구만
테스트와 아키텍처
"좋은 아키텍처 == 테스트 하기 쉬운 아키텍처" 의 관점에서 험블객체는
테스트 하기 쉬운 부분, 어려운 부분을 나눠주고 이 경계가
자연스럽게 아키텍처의 경계가 된다.
테스트 하기 힘든 부분을 테스트 쉬운 부분만 뽑아내고 나머지는 그냥 다른 레이어로 뭉탱이 시키면 좋은 아키텍처..? -> 그럼 테스트 하기 어려운 부분은 테스트를 안하는건가?
그냥 역량이 부족해서 테스트하기 어려운거 일수도 있음
데이터베이스와 게이트웨이
유스케이스 인터렉터와 데이터베이스 사이에 있는 데이터베이스 게이트웨이
다형석 인터페이스로 CRUD작업과 관련된 모든 메서드 포함
유스케이스 계층은 SQL없어야함! -> 게이트웨이 인터페이스 호출
인터페이스 구현체는 디비 계층에 위치
인터렉터는 업무규칙(비즈니스 로직)담당이니 험블객체가 아닌 테스트하기 쉬운 객체
왜 테스트 하기 쉽냐? 게이트 웨이는 Stub이나 test-double로 교체할 수 있기 때문
게이트웨이도 테스트하기 쉽지않나..? SQL적고 예상되는 값 오는지 확인하면 테스트일거 같은데
데이터 매퍼
객체는 데이터 구조가 아니다.
사용자는 데이터는 볼 수 없고 public 메서드만 볼 수 있다.
그래서 사용하는 관점에서는 operation의 집합으로 볼 수 있다.
RDB에서 가져온 데이터를 데이터 구조에 맞게 담아주니까 데이터 매퍼라고 부르자
ORM 시스템은 디비 계층이지만 게이터웨이 인터페이스와 실제 디비 사이에서 또다른 험블 객체 경계를 형성한다.
..?
서비스 리스너
다른 서비스와 통신해야 한다면 서비스 경계를 생성하는 험블 객체 패턴을 찾을 수 있나?
외부로 부터 데이터를 수신하는 경우 '서비스 리스너'가 서비스 인터페이스를 통해 데이터를 수신하고 필요한 데이터 구조로 포매팅한다. 그리고 경게를 가로질러 내부로 전달한다.
결론
아키텍처 경계마다 험블 객체 패턴을 발견할 수 있다.
경계를 넘나드는 통신은 테스트하기 어려운것과 쉬운것으로 분리된다.
24장 부분적 경계
아키텍처의 경계를 완벽하게 만드려면..?
- 쌍방향의 다형적 경계 인터페이스
- Input/output을 위한 데이터 구조
- 두 영역을 독립적으로 컴파일/배포할 수 있는 컴포넌트로 격리하는데 필요한 모든 의존성 관리
불가능 하다를 말하고 싶은건가
이런 경계를 만드는데 공수가 크게 들고 YAGNI원칙에 위배하지만 아키텍트라면
"어쩌면 필요할 수도 있찌" 하면서 부분적 경계를 구현해 볼 수 있다.
마지막 단계를 건너뛰기
방법1. 모두 준비한 뒤에 단일 컴포넌트에 그대로 모아만 두는 것 이다.
단점) 시간이 지날수록 컴포넌트 사이 구분 약화되고 서서히 결합되면 다시 분리해야함
일차원 경계
방법2. 전략패턴을 통해서 미래에 필요할 아키텍처 경계를 위한 공간을 둔다
단점) 준비해돠도 근면성실/제대로 훈련 되있지 않으면 비밀 통로가 뚫린다.
퍼사드
방법3. 퍼사드 패턴
단점) 의존성 역전 희생, 비밀 통로 발생 가능성 높음
결론
각각의 접근법은 나름의 장점, 비용을 가짐
각각 접근법은 실제 구체화 되지 않으면 경계로써 가치가 없음
완벽하게 경계를 나눌지, 대강 나눠놓기만 할지 정하는 것도 아키텍트 역량이다.
25장 계층과 경계
시스템이 UI, 비즈니스로직, 데이터베이스 세가지로 나눠진다 생각하면 경기도 오산이다.
움퍼스 사냥 게임
단순한 게임 만들때도 시스템 컴포넌트가 생각보다 많음
클린 아키텍처?
다양한 요구사항을 통해 몰랐던 변경의 축이 생길 수 있고
변경의 축에 의해 정의되는 아키텍처 경계가 잠재되어 있다.
중요한 아키텍처 경계를 모두 발견했다고 하는 사람은 거짓말쟁이
흐름 횡단하기
만약 게임이 멀티플레이라면..?
흐름 분리하기
결국 모든 흐름이 상단의 단일 컴포넌트에서 서로 만난다고 생각 할 수 있는데 경기도 오산이다.
그런데 이전장에서 메인 컴포넌트 하나가 있다하지 않았나..?
결론
"아키텍처 경계는 어디에나 존재할 수 있다"
26장 메인 컴포넌트
하나의 컴포넌트가 나머지 컴포넌트를 생성,조정, 관리한다. -> 메인 컴포넌트
궁극적인 세부사항
- 메인 컴포넌트는 궁극적인 세부사항, 가장 낮은 수준의 정책이다.
- DI는 이 메인 컴포넌트에서 이뤄져야 한다.
- 메인은 의존성 주입 프레임워크를 사용하지 않고도 일반적은 방식으로 의존성을 분배할 수 있어야한다.
- 가장 바깥원에 위치하는 지저분한 저수준 모듈
- 고수준 시스템이 필요로 하는 모든것을 로드한 뒤 제어권을 고수준 시스템에 넘김
결론
메인은 애플리케이션의 플러그인
-> 둘 이상의 메인 컴포넌트를 만들 수 있다.
- 테스트용 메인 플러그인
- 개발용 메인 플러그인
- 국가별, 고객별 메인 플러그인 등등
여튼 메인을 플러그인 컴포넌트로 여겨라
27장 '크고 작은 모든' 서비스들
MSA는 요새 인기가 많다 왜냐하면
- 서비스를 사용하면 결합이 결합이 분리되는 것 처럼 보인다.
- 서비스를 사용하면 개발/배포 독립성을 지원하는 것 처럼 보인다.
결론엔 MSA는 은탄환이 아니다 이런 얘기 할것같은기분
서비스 아키텍처?
서비스를 사용하는게 아키텍처라고 볼 수 있나? ->아니다
아키텍처: 의존성 규칙을 준수하고 고수준 정책을 저수준의 세부사항으로 분리하는 경계에 의해 정의된다.
서비스: 단순히 앱 행위(도메인)을 분리한 거는 그냥 값비싼 함수 호출에 불과하다.
중요한 요소는 경계를 넘나드는 함수 호출 들이다.
그냥 함수들은 아키텍처적으로 중요하지 않다.
서비스도 동일하게 플랫폼 경계를 가로지르는 함수 호출 에 대해서 관심을 가져야 한다.
서비스의 이점?
제목부터 물음표 달았쥬? 킹받았쥬
결합 분리의 오류
어쨋든 서비스는 다른 프로세스/프로세서에서 분리되는데 네트워크 상의 공유 자원 때문에 결합될 가능성은 여전히 존재한다.
데이터 레코드에 새로운 필드를 추가할때 레코드를 그대로 사용하고 있다면
관련된 서비스들을 전부 수정해야 한다.
그럼 서비스 인터페이스를 잘 정의하면 되지않음? -> "이러한 이점은 환상에 불과하다"
개발 및 배포 독립성의 오류
전담팀이 서비스를 소유하고 운영하니까 독립적인 개발/배포 가능한것 가닝?
- 모노리틱이나 컴포넌트 기반으로도 할 수 있음(msa가 유일한 선택지 아님)
- 데이터나 행위에 대해서 결합되 있다면 개발/배포/운영을 조정해야함
야옹이 문제
횡단 관심사가 지닌 문제에 대한 예시
근데 결국 새로운 피처들도 아키텍처 레이어들을 전부 조금씩 수정해야 하는게 필수 일것 같은데 해결할 수 있을까? 오 SOLID중 새로운 피처는 코드 수정이 아닌 코드 추가로 구현 법칙이 적용되는건가
객체가 구출하다
컴포넌트 기반 아키텍처에서는 이 문제를
-> 다형적으로 확장할 수 있는 클래스 집합을 생성해서 신규피처를 처리하도록 함
개방폐쇄원칙 OCP
컴포넌트 기반 서비스
서비스도 컴포넌트 방식(OCP)으로 구성할 수 있다.
횡단 관심사
아키텍처 경계는 서비스 사이에 있지 않다.
서비스들을 관통하기 때문에 서비스를 컴포넌트 단위로 분할해야 한다.
결론
서비스는 유용한 측면도 있지만 그 자체적으로 아키텍처의 중요한 요소는 아니다.
28장 테스트 경계
테스트도 시스템(아키텍처)의 일부이다.
시스템 컴포넌트인 테스트
아키텍처 관점에서는 TDD, BDD, 유닛, 인수, 단위, 통합 테스트 전부 그냥 동일한 테스트로 인식한다.
테스트는 태생적으로 의존성 규칙을 따른다. -> 테스트는 가장 바깥쪽 원이다.
원 내부의 어떤것에도 의존하면 안된다.
테스트가 개어려웠던 이유
테스트는
- 독립적으로 배포 가능하다.
- 컴포넌트 중 가장 고립되어 있다.
테스트를 고려한 설계
테스트도 시스템의 일부라고 생각해야 한다.
시스템에 강하게 결합된 테스트는 "깨지기 쉬운 테스트"이다.
깨지지 않는 테스트를 위해서는 테스트를 고려하여 설계해야 한다.
예) GUI는 상상 바뀌니 테스트는 GUI사용하지 않고 업무 규칙 테스트 할 수 있도록
테스트 API
테스트에 특화된 API 만들면 좋다.
테스트 구조를 앱 구조로부터 결합을 분리하는게 목표이다.
구조적 결합
테스트 결합 중 가장 강한 결합
상용 클래스나 메서드 중 하나라도 변경되면 테스트도 변경되어야 함
-> 테스가 변경되니 상용 클래스를 변경하기 어려워짐
이때 테스트 API를 통해 앱 구조를 테스트로 부터 숨김
-> 앱 구조 리팩토링해도 테스트 는 안바꿔도됨
-> 시간이 지날수록 테스트는 따로 진화할 수 있음(더 추상, 범용적으로)
보안
테스트 API는 운영 시스템에 배포되면 안되서 구현부는 독립적으로 배포될 수 있는 컴포넌트로 분리해야함
결론
테스트도 아키텍처에 포함되니 잘 설계 해야한다.