진화적 아키텍처
요구가 바뀌어도 망가지지 않게, 지켜야 할 특성을 자동으로 검사하는 적합성 함수와 점진적 변경으로 아키텍처를 키우는 법이다.
목차 (6)
진화적 아키텍처와 적합성 함수
예전에는 아키텍처를 "나중에 바꾸기 어려운 것"이라 보고 처음에 오래 고민해 정했다. 하지만 업무, 기술, 규모는 몇 해 안에 반드시 바뀐다. 포드·파슨스·쿠아(2판에 사달라지 합류)의 Building Evolutionary Architectures는 반대로 묻는다. 바꾸기 어려운 것을 처음부터 맞히려 하지 말고, 바꿀 수 있게 만들면 어떨까. 이 책이 말하는 진화적 아키텍처는 여러 차원에 걸친 점진적 변경을, 정해 둔 방향을 잃지 않게 이끌며 받아들이는 아키텍처다.
방향을 잃지 않게 하는 장치가 적합성 함수(fitness function)다. 진화 계산에서 해가 목표에 얼마나 가까운지 매기는 함수를 빌린 이름으로, 여기서는 "이 아키텍처 특성이 지금도 지켜지는가"를 객관적으로 판정하는 검사를 말한다. 아키텍처 특성 노트에서 특성을 잴 수 있게 바꿨다면, 그 측정을 자동으로 계속 돌리는 것이 적합성 함수다. 소트웍스 테크놀로지 레이더도 Architectural fitness function을 기법으로 올렸다.
- 원자적(atomic): 한 가지 특성을 한 맥락에서 본다. 예: 패키지 사이에 순환 의존이 없다.
- 전체적(holistic): 여러 특성이 함께 맞는지 본다. 보안 검사를 켜면 성능이 떨어지듯 특성끼리는 부딪히므로, 따로 통과해도 함께는 실패할 수 있다.
- 촉발형(triggered): 코드 변경, 배포 같은 사건이 있을 때 돈다. 대부분의 테스트가 여기 든다.
- 지속형(continuous): 운영 중 늘 돈다. 응답 시간 감시, 일부러 장애를 넣어 보는 카오스 실험이 예다.
적합성 함수의 예: 의존 규칙, 순환 검사, 지연 예산
구조 특성은 코드로 검사할 수 있다. 자바에서는 ArchUnit이 아키텍처 규칙을 단위 테스트처럼 쓰게 해 준다. 아래는 사용자 가이드의 계층 규칙과 순환 검사를 이 노트의 패키지 이름으로 옮긴 것이다. 규칙을 어기는 의존이 생기면 테스트가 실패하고 파이프라인이 멈춘다.
// 계층 규칙: controller → service → persistence 방향으로만 부른다
ArchRule layers = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
// 순환 검사: com.shop 아래 최상위 패키지끼리 고리를 만들지 않는다
ArchRule noCycles = slices().matching("com.shop.(*)..").should().beFreeOfCycles();
layers.check(importedClasses);
noCycles.check(importedClasses);운영 특성도 파이프라인에 넣을 수 있다. 성능 검사 단계에서 부하 테스트를 돌리고 "p99 응답 200ms 이하" 같은 지연 예산을 넘으면 실패시키는 식이다. 숫자는 아키텍처 특성 노트에서 정한 목표 값에서 온다. 이런 검사의 예는 소트웍스의 Fitness function-driven development가 특성별로 모아 두었다. 코드 품질, 복원력, 관측 가능성, 성능, 법규 준수, 보안, 운영 가능성 같은 항목마다 파이프라인 관문을 둔다.
주의할 점은 적합성 함수가 많다고 좋은 것이 아니라는 것이다. 실패해도 아무도 고치지 않는 검사, 매번 흔들리는 검사는 신호를 잡음으로 바꾼다. 특성 순위에서 위에 있는 것부터, 어기면 실제로 손해가 나는 것만 골라 자동화한다.
점진적 변경: 배포 파이프라인과 기능 토글
진화의 두 번째 조건은 점진적 변경이다. 작은 변경을 자주, 안전하게 운영까지 보낼 수 있어야 아키텍처도 조금씩 바꿀 수 있다. 그 통로가 배포 파이프라인이다. 변경 하나가 빌드, 테스트, 적합성 함수를 차례로 통과하며 운영에 갈 수 있는지 판정받는다. 한 번에 크게 바꾸면 무엇이 문제였는지 찾기 어렵고 되돌리기도 어렵다.
작게 자주 배포하려면 덜 만든 기능도 배포해야 한다. 이때 쓰는 것이 기능 토글(feature toggle, feature flag)이다. 새 코드를 꺼진 채 배포하고, 설정만 바꿔 직원에게 먼저, 다음에 사용자 일부에게 켠다. 이렇게 하면 배포(코드를 운영에 올림)와 출시(사용자에게 기능을 보임)가 갈라진다. 피트 호지슨이 마틴 파울러 사이트에 쓴 Feature Toggles (aka Feature Flags)가 이 주제의 표준 문서다.
토글은 공짜가 아니다. 토글 하나마다 코드 경로가 둘이 되어 테스트할 조합이 늘고, 다 쓴 출시 토글을 지우지 않으면 아무도 의미를 모르는 분기가 쌓인다. 그래서 출시 토글은 만들 때 지울 날짜나 담당자를 함께 정하고, 토글 판단은 코드 곳곳이 아니라 한곳에서 하게 한다.
스트랭글러 피그: 옛 시스템을 조금씩 감싸며 바꾸기
오래된 시스템을 새로 만들 때 가장 위험한 방법은 한 번에 갈아엎는 것이다. 새 시스템이 옛 시스템의 모든 기능을 따라잡을 때까지 몇 해 동안 아무 가치도 내지 못하고, 그 사이 옛 시스템도 계속 바뀐다. 마틴 파울러는 호주 열대 우림의 교살자 무화과나무에서 이름을 따 다른 길을 제안했다(StranglerFigApplication). 이 무화과는 다른 나무 위에서 싹터 뿌리를 내리며 숙주를 감싸고, 결국 숙주가 사라진 자리에 혼자 선다.
옛 시스템 앞에 라우터(파사드)를 세운다. 아직은 모든 요청을 그대로 옛 시스템으로 넘긴다. 사용자는 아무것도 느끼지 못한다.
핵심은 옛 시스템 앞에 요청을 가를 수 있는 지점(라우터, API 게이트웨이, 파사드)을 먼저 세우는 것이다. 그러면 기능 하나를 옮길 때마다 라우팅만 바꾸면 되고, 문제가 생기면 라우팅을 되돌린다. 무엇부터 옮길지는 가치와 위험으로 고른다. 의존이 적어 떼기 쉬운 가장자리 기능으로 이전 방법을 익히고, 다음에 자주 바뀌어서 옮기면 이득이 큰 기능을 옮긴다. 데이터를 언제 어떻게 함께 옮길지는 분해와 분산 데이터 노트에서 다룬다.
확장–축소: 데이터베이스를 멈추지 않고 바꾸기
코드는 토글로 나눠 배포할 수 있지만 데이터베이스 스키마는 하나다. 열 이름 하나를 바꾸는 단순한 변경도, 옛 코드와 새 코드가 동시에 도는 배포 도중에는 둘 중 하나를 깨뜨린다. 그래서 바꾸기 전과 후가 함께 살 수 있는 중간 단계를 끼워 넣는다. 다닐로 사토가 파울러 사이트에 정리한 ParallelChange(병행 변경)이고, 단계 이름을 따 확장–축소(expand–contract)라고도 부른다.
- 확장: 새 구조(새 열)를 옛 구조 옆에 더한다. 앱은 두 곳에 모두 쓰고, 기존 행은 백필 작업으로 새 열에 채운다.
- 이전: 읽기를 새 구조로 옮긴다. 이 테이블을 읽는 다른 서비스나 보고서가 있다면 하나씩 옮긴다.
- 축소: 아무도 옛 구조를 읽지 않는 것을 확인한 뒤 쓰기를 멈추고 옛 열을 지운다.
같은 생각은 API에도 그대로 쓴다. 새 필드를 먼저 더하고, 소비자가 옮겨 가면 옛 필드를 지운다. 단계가 늘어 귀찮아 보이지만, 단계마다 배포가 작아지고 되돌릴 수 있어 큰 스키마 변경을 한밤중 점검 시간에 몰아서 할 필요가 없어진다.
결합 관리와 콘웨이 법칙
진화를 가장 크게 막는 것은 부적절한 결합이다. 여러 서비스가 함께 쓰는 데이터베이스, 모두가 기대는 공용 라이브러리, 동기 호출로 줄줄이 엮인 서비스는 한 곳을 바꿀 때 다른 곳을 함께 바꾸게 만든다. 그래서 진화적 아키텍처는 결합을 아키텍처 퀀텀 안에 가두고, 퀀텀 사이에는 계약과 비동기 메시지 같은 약한 결합만 두려 한다(모듈성과 결합 노트). 이 결합 규칙을 앞의 적합성 함수로 지키면, 시간이 지나도 구조가 조용히 엉키지 않는다.
결합에는 조직도 영향을 준다. 멜빈 콘웨이는 1968년 글 How Do Committees Invent?에서, 시스템을 설계하는 조직은 자기 조직의 의사소통 구조를 닮은 설계를 만들게 된다고 주장했다. 이것이 콘웨이 법칙이다. 팀 사이에 대화가 적은 곳에 시스템의 경계가 생기고, 한 팀 안에서는 경계가 흐려진다.
파울러의 ConwaysLaw는 이를 거스르지 말고 이용하라고 말한다. 서비스 경계를 업무 흐름으로 긋고 싶다면 팀도 업무 흐름별로 짜는 것, 이른바 역 콘웨이 전략(inverse Conway maneuver)이다. 매슈 스켈턴과 마누엘 파이스의 팀 토폴로지는 이 생각을 네 가지 팀 유형으로 정리했다(Team Topologies 핵심 개념). 업무 흐름 하나를 처음부터 끝까지 맡는 흐름 정렬 팀이 중심이 되고, 플랫폼 팀은 배포와 관측 같은 공통 기반을 서비스처럼 제공하며, 지원 팀은 다른 팀이 새 능력을 익히도록 돕고, 복잡한 하위 시스템 팀은 깊은 전문 지식이 필요한 부분을 맡는다. 팀 사이 상호작용도 협업, 서비스로 제공, 촉진의 세 방식으로 좁혀 둔다.
결국 아키텍처를 진화시킨다는 것은 코드만의 일이 아니다. 지켜야 할 특성을 적합성 함수로 고정하고, 작은 변경을 자주 보낼 통로를 만들고, 결합을 퀀텀 안에 가두고, 팀 구조를 원하는 경계에 맞추는 일이 함께 가야 한다.
- Fitness function-driven development — Thoughtworks
- Architectural fitness function — Thoughtworks Technology Radar
- ArchUnit User Guide — ArchUnit
- DeploymentPipeline — Martin Fowler
- Feature Toggles (aka Feature Flags) — Pete Hodgson
- StranglerFigApplication — Martin Fowler
- ParallelChange — Danilo Sato
- How Do Committees Invent? — Melvin Conway
- ConwaysLaw — Martin Fowler
- Key concepts — Team Topologies
- Building Evolutionary Architectures — Ford·Parsons·Kua·Sadalage (책 소개)