아키텍처
- 5.1 아키텍처의 목적
- 5.2 개발, 배포, 운영, 유지보수
- 5.3 선택사항을 열어 두기
- 5.4 독립성과 결합 분리
- 5.5 결합 분리 모드
- 5.6 경계란 무엇인가
- 5.7 경계선을 언제 그을까
- 5.8 플러그인 아키텍처
- 5.9 경계의 물리적 형태
- 5.10 정책과 수준
- 5.11 엔티티와 유스케이스
- 5.12 소리치는 아키텍처
- 5.13 클린 아키텍처 동심원
- 5.14 의존성 규칙
- 5.15 험블 객체 패턴
- 5.16 부분적 경계
- 5.17 계층과 경계
- 5.18 메인 컴포넌트
아키텍처는 프레임워크에 대한 것이 아니야. 시스템의 생명주기를 지원하는 것이고, 궁극적 목표는 시스템의 수명 동안 드는 총비용을 최소화하는 거지.
소프트웨어 아키텍트는 프로그래머야. 코딩을 그만두고 다이어그램만 그리는 사람이 아니라, 최고의 프로그래머이면서 동시에 팀을 올바른 설계 방향으로 이끄는 사람이지. 아키텍처란 시스템의 형태를 결정하는 거야 -- 컴포넌트 분할 방법, 배치 방법, 소통 방식.
개발 관점에서 -- 5명짜리 소규모 팀이라면 모놀리식으로도 잘 굴러가지만, 7명짜리 팀 5개로 구성된 조직이라면 시스템을 잘 정의된 컴포넌트로 분리하지 않으면 개발이 진행되지 않아. 배포 관점에서 -- 배포 비용이 높으면 시스템의 유용성이 떨어져. 마이크로서비스를 선택하면 개발은 쉬워질 수 있지만 수십 개의 서비스를 연결하고 시작 순서를 맞추는 배포는 악몽이 될 수 있지. 운영 관점에서 -- 아키텍처의 영향은 상대적으로 덜 극적이야. 하드웨어를 더 투입해서 해결할 수 있는 부분이 많으니까. 하지만 좋은 아키텍처는 시스템의 운영 요구사항을 명확히 드러내. 유지보수가 가장 비용이 많이 드는 부분이야. 특히 "탐색(spelunking)" -- 어디를 고쳐야 하는지, 고치면 어떤 부작용이 생기는지를 파악하는 데 엄청난 시간이 들거든. 신중하게 만든 아키텍처는 이 탐색 비용을 크게 줄여줘.
소프트웨어를 소프트하게 유지하는 방법은 가능한 한 많은 선택사항을 가능한 한 오래 열어 두는 거야. 소프트웨어 시스템은 **정책(policy)**과 **세부사항(detail)**으로 분해할 수 있어. 정책은 시스템의 진짜 가치가 있는 업무 규칙이고, 세부사항은 데이터베이스, 웹 서버, REST, 프레임워크 같은 것이지. 아키텍트의 목표는 정책을 핵심 요소로 인식하면서, 세부사항은 정책에 무관하게 만드는 거야. 이렇게 하면 세부사항에 대한 결정을 최대한 미룰 수 있고, 미룰수록 더 많은 정보를 가지고 올바른 결정을 내릴 수 있어.
독립성 이야기도 중요해. 시스템의 아키텍처는 시스템의 **의도(intent)**를 드러내야 해. 쇼핑 카트 애플리케이션의 아키텍처를 보면 "아, 이건 쇼핑 카트 시스템이구나"라고 알 수 있어야 하지. 콘웨이의 법칙 -- 시스템의 구조는 그 시스템을 만드는 조직의 커뮤니케이션 구조를 따른다는 거야. 여러 팀이 개발하는 시스템이라면 각 팀이 독립적으로 작업할 수 있도록 컴포넌트를 분리해야 해.
계층 결합 분리 -- UI, 업무 규칙, 데이터베이스를 서로 다른 계층으로 분리해서 각 계층이 독립적으로 변경될 수 있게 하자. 유스케이스 결합 분리 -- "주문 추가"와 "주문 삭제" 유스케이스를 수직으로 쪼개서 각각이 시스템의 얇은 수직 조각이 되게 해. 이렇게 하면 새 유스케이스를 추가할 때 기존 유스케이스에 영향을 주지 않지. 분리를 어떤 수준에서 할 것인가에는 세 가지 결합 분리 모드가 있어 -- 소스 수준(모놀리식), 배포 수준(jar/DLL), 서비스 수준(네트워크 패킷). 좋은 아키텍처는 모놀리식으로 시작해서 필요에 따라 서비스 수준까지 성장할 수 있게 열어 둬.
**경계(boundary)**란 소프트웨어 요소를 서로 분리하고, 한쪽이 다른 쪽을 알지 못하게 막는 선이야. 저자가 두 가지 슬픈 이야기를 들려줘 -- 한 회사가 SOA를 너무 일찍 도입해서 쓸데없는 서비스들만 가득한 채 망했고, 다른 회사는 서버에 아키텍처가 너무 깊이 결합되어 바꿀 수 없었지. 교훈은 같아 -- 너무 일찍, 너무 구체적인 결정을 하면 나중에 큰 대가를 치러. 저자가 만든 FitNesse 프로젝트에서는 데이터베이스 결정을 미뤘고, 업무 규칙과 데이터 저장 사이에 경계선을 긋고 일단 플랫 파일로 시작했어. 결과적으로 데이터베이스는 필요하지 않았지. 경계선은 업무 규칙과 관련 없는 결정 사이에 그어야 해. 데이터베이스, GUI, 프레임워크 -- 전부 업무 규칙과 관련 없어.
이 경계선들을 모아보면 하나의 패턴이 보여 -- 플러그인 아키텍처. 핵심 업무 규칙이 중심에 있고, UI, 데이터베이스, 프레임워크 등이 플러그인처럼 꽂혀. 데이터베이스를 바꾸고 싶으면 플러그인만 교체하면 돼. 이건 SRP와 DIP가 만들어내는 구조야.
경계의 물리적 형태도 다양해. 가장 단순한 건 물리적 경계가 없는 단일체(monolith) -- 함수와 데이터가 모두 하나의 주소 공간에서 실행돼. 단일체에서도 경계를 잘 지키는 게 중요해. 규율이 없으면 경계가 순식간에 무너져서 진흙 덩어리가 되거든. 배포형 컴포넌트(DLL, jar)는 배포 시점에 독립적으로 교체할 수 있어. 로컬 프로세스는 같은 프로세서에서 실행되지만 서로 다른 주소 공간에 있고, 소켓이나 메시지 큐로 통신하지. 서비스는 가장 강한 물리적 경계야 -- 네트워크를 통해 통신하고, 함수 호출에 비하면 수십만 배 느릴 수 있어.
소프트웨어 시스템은 결국 정책을 기술한 것이야. **수준(level)**이란 입력과 출력으로부터의 거리인데, 입출력에 가까운 정책이 저수준이고 멀수록 고수준이야. 의존성 방향은 소스 코드 수준에서 반드시 저수준에서 고수준으로 향해야 해. 이렇게 해야 가장 가치 있고 잘 변하지 않는 고수준 정책을 저수준 세부사항의 변경으로부터 보호할 수 있어.
**엔티티(Entity)**는 핵심 업무 규칙과 핵심 업무 데이터를 하나로 묶은 객체야. 컴퓨터가 없어도 존재하는 규칙이지 -- 은행 대출의 이자 계산 같은 것. 엔티티는 데이터베이스, UI, 프레임워크와 아무 관련이 없어. **유스케이스(Use Case)**는 자동화된 시스템이 사용되는 방법을 기술하는 업무 규칙이야. 엔티티의 핵심 업무 규칙을 언제, 어떻게 호출하는지를 명시하지. 유스케이스는 엔티티에 의존하지만, 엔티티는 유스케이스를 몰라. 유스케이스는 UI를 기술하지 않고, 엔티티 객체를 요청이나 응답의 데이터 구조로 사용하면 안 돼.
최상위 디렉터리 구조를 봤을 때 "이건 Rails 앱이구나"가 아니라 "이건 헬스케어 시스템이구나"가 보여야 해. 소리치는 아키텍처 -- 프레임워크가 아닌 유스케이스가 소리쳐야 하지. 프레임워크는 열어 두어야 할 선택사항이고, 아키텍처가 유스케이스 중심이면 프레임워크 없이도 유스케이스를 단위 테스트할 수 있어.
이 책의 하이라이트 -- 클린 아키텍처 동심원. 핵심은 딱 하나, 의존성 규칙(Dependency Rule): 소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 해. 네 개의 동심원은 안쪽부터 엔티티, 유스케이스, 인터페이스 어댑터, 프레임워크와 드라이버야. 인터페이스 어댑터 계층은 유스케이스/엔티티에게 편리한 형식과 외부 에이전시에게 편리한 형식 사이의 데이터 변환을 담당하지 -- 컨트롤러, 프레젠터, 게이트웨이가 여기 해당해. 프레임워크와 드라이버는 가장 바깥쪽 원이고, 글루 코드 외에는 많은 코드를 작성하지 않아. 원은 네 개여야만 하는 건 아니지만, 의존성 규칙은 항상 적용돼.
제어 흐름은 컨트롤러에서 유스케이스로, 유스케이스에서 프레젠터로 흐르는데, 소스 코드 의존성을 안쪽만 향하게 하려면 DIP를 써. 유스케이스 계층에 인터페이스를 두고 바깥쪽 계층이 구현하게 하면, 소스 코드 의존성은 안쪽을 향하면서도 제어 흐름은 바깥으로 나갈 수 있어. 경계를 횡단하는 데이터는 단순한 데이터 구조여야 해 -- 엔티티나 DB Row를 그대로 전달하면 안 돼.
아키텍처 경계 근처에서 자주 쓰이는 험블 객체 패턴(Humble Object Pattern) -- 테스트하기 어려운 행위와 쉬운 행위를 분리하는 패턴이야. **뷰(View)**가 험블 객체고, **프레젠터(Presenter)**가 테스트하기 쉬운 객체야. 프레젠터가 데이터를 뷰 모델로 포맷팅하고, 뷰는 이미 완료된 데이터를 화면에 옮기기만 해. 데이터베이스 게이트웨이, 서비스 리스너에서도 같은 패턴이 나타나지.
완전한 아키텍처 경계를 만드는 건 비용이 많이 들어서 **부분적 경계(partial boundary)**라는 선택지도 있어. 모든 작업을 하되 마지막 컴포넌트 분리만 안 하는 방법, 전략(Strategy) 패턴을 쓰는 일차원 경계, 퍼사드(Facade) 패턴으로 가장 단순하게 경계를 정의하는 방법이 있지. 중요한 건 경계가 어디에 있는지를 인지하고, 완전하게 구현할지 부분적으로 할지를 의식적으로 결정하는 거야.
계층과 경계 -- 간단한 텍스트 어드벤처 게임조차 아키텍처 관점에서 보면 경계가 여러 군데 있어. 다국어 지원, 네트워크 멀티플레이어를 추가하면 흐름이 여러 갈래로 나뉘지. 경계를 너무 일찍 구현하면 비용이 크고, 너무 늦게 구현하면 비용이 더 커. 감시하고 있다가 적절한 시점에 경계를 구현하는 것이 아키텍트의 역할이야.
마지막으로 메인(Main) 컴포넌트 -- 모든 시스템에서 가장 저수준의 정책인 최초 진입점이야. 메인의 역할은 가장 지저분한 컴포넌트가 되는 것이지. 모든 팩토리와 전략을 생성하고, 의존성을 주입하고, 고수준 시스템에 제어를 넘겨. 메인을 시스템의 플러그인이라고 생각해 -- 개발 환경, 테스트 환경, 운영 환경별로 다른 플러그인을 꽂으면 돼.
정리
5장 읽고 기억할 거 세 가지:
- 의존성 규칙이 클린 아키텍처의 전부야. 소스 코드 의존성은 반드시 안쪽으로, 고수준 정책을 향해야 해. 엔티티가 중심이고, 유스케이스가 그 다음, UI/DB/프레임워크는 바깥쪽 플러그인이야.
- 정책과 세부사항을 분리하고, 세부사항에 대한 결정을 최대한 미뤄. 데이터베이스, 웹 서버, 프레임워크는 전부 세부사항이야. 경계선을 긋고 업무 규칙이 이것들을 모르게 만들면, 결정을 미룰수록 더 좋은 결정을 내릴 수 있어.
- 경계는 비용이야. 완전한 경계는 비싸고, 부분적 경계도 선택지야. 중요한 건 경계의 존재를 인지하고, 프로젝트가 진화하면서 적절한 시점에 적절한 수준으로 구현하는 거지.