테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

진화적 아키텍처

요구가 바뀌어도 망가지지 않게, 지켜야 할 특성을 자동으로 검사하는 적합성 함수와 점진적 변경으로 아키텍처를 키우는 법이다.

목차 (6)
  1. 진화적 아키텍처와 적합성 함수
  2. 적합성 함수의 예: 의존 규칙, 순환 검사, 지연 예산
  3. 점진적 변경: 배포 파이프라인과 기능 토글
  4. 스트랭글러 피그: 옛 시스템을 조금씩 감싸며 바꾸기
  5. 확장–축소: 데이터베이스를 멈추지 않고 바꾸기
  6. 결합 관리와 콘웨이 법칙

진화적 아키텍처와 적합성 함수

예전에는 아키텍처를 "나중에 바꾸기 어려운 것"이라 보고 처음에 오래 고민해 정했다. 하지만 업무, 기술, 규모는 몇 해 안에 반드시 바뀐다. 포드·파슨스·쿠아(2판에 사달라지 합류)의 Building Evolutionary Architectures는 반대로 묻는다. 바꾸기 어려운 것을 처음부터 맞히려 하지 말고, 바꿀 수 있게 만들면 어떨까. 이 책이 말하는 진화적 아키텍처는 여러 차원에 걸친 점진적 변경을, 정해 둔 방향을 잃지 않게 이끌며 받아들이는 아키텍처다.

방향을 잃지 않게 하는 장치가 적합성 함수(fitness function)다. 진화 계산에서 해가 목표에 얼마나 가까운지 매기는 함수를 빌린 이름으로, 여기서는 "이 아키텍처 특성이 지금도 지켜지는가"를 객관적으로 판정하는 검사를 말한다. 아키텍처 특성 노트에서 특성을 잴 수 있게 바꿨다면, 그 측정을 자동으로 계속 돌리는 것이 적합성 함수다. 소트웍스 테크놀로지 레이더도 Architectural fitness function을 기법으로 올렸다.

적합성 함수의 두 축. 원자적이고 촉발형인 예는 계층 의존 방향 규칙과 순환 의존 검사, 전체적이고 촉발형인 예는 보안 필터를 켠 채 부하 테스트로 지연을 재는 것, 원자적이고 지속형인 예는 운영 중 응답 시간 감시, 전체적이고 지속형인 예는 장애를 주입하며 가용성을 확인하는 카오스 실험이다원자적한 특성, 한 부분을 본다전체적여러 특성이 함께 맞는지 본다촉발형빌드·배포 때 돈다계층 의존 방향 규칙패키지 순환 의존 없음보안 필터를 켠 채로부하 테스트 p99 ≤ 200ms지속형운영 중 늘 돈다운영 p99 응답 시간 감시오류율 경보장애 주입 중에도가용성 목표를 지키나축의 이름은 Building Evolutionary Architectures의 분류를 따른다
적합성 함수를 나누는 두 축. 한 칸에 몰지 않고 고르게 두면 빌드 때 잡을 것은 빌드 때, 운영에서만 드러나는 것은 운영에서 잡는다.
  • 원자적(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);
배포 파이프라인의 적합성 함수. 커밋 단계의 정적 분석, 빌드 단계의 단위 테스트와 커버리지, 아키텍처 검사 단계의 계층 규칙과 순환 의존 검사, 성능 검사 단계의 지연 예산이 변경마다 돌고, 운영 단계에서는 SLO 감시와 카오스 실험이 늘 돈다. 아키텍처 규칙을 어기면 그 자리에서 멈춘다촉발형: 변경마다 돈다지속형커밋정적 분석복잡도 한도빌드·테스트단위 테스트커버리지 하한아키텍처 검사계층 규칙순환 의존 없음성능 검사p99 ≤ 200ms(지연 예산)운영SLO 감시카오스 실험규칙을 어기면 여기서 멈춘다숫자는 예다. 기준은 아키텍처 특성의 목표 값에서 온다
파이프라인에 놓인 적합성 함수들. 사람이 리뷰 때 기억해야 했던 규칙이 자동 검사로 바뀌고, 어긴 변경은 배포 전에 멈춘다.

운영 특성도 파이프라인에 넣을 수 있다. 성능 검사 단계에서 부하 테스트를 돌리고 "p99 응답 200ms 이하" 같은 지연 예산을 넘으면 실패시키는 식이다. 숫자는 아키텍처 특성 노트에서 정한 목표 값에서 온다. 이런 검사의 예는 소트웍스의 Fitness function-driven development가 특성별로 모아 두었다. 코드 품질, 복원력, 관측 가능성, 성능, 법규 준수, 보안, 운영 가능성 같은 항목마다 파이프라인 관문을 둔다.

주의할 점은 적합성 함수가 많다고 좋은 것이 아니라는 것이다. 실패해도 아무도 고치지 않는 검사, 매번 흔들리는 검사는 신호를 잡음으로 바꾼다. 특성 순위에서 위에 있는 것부터, 어기면 실제로 손해가 나는 것만 골라 자동화한다.

점진적 변경: 배포 파이프라인과 기능 토글

진화의 두 번째 조건은 점진적 변경이다. 작은 변경을 자주, 안전하게 운영까지 보낼 수 있어야 아키텍처도 조금씩 바꿀 수 있다. 그 통로가 배포 파이프라인이다. 변경 하나가 빌드, 테스트, 적합성 함수를 차례로 통과하며 운영에 갈 수 있는지 판정받는다. 한 번에 크게 바꾸면 무엇이 문제였는지 찾기 어렵고 되돌리기도 어렵다.

작게 자주 배포하려면 덜 만든 기능도 배포해야 한다. 이때 쓰는 것이 기능 토글(feature toggle, feature flag)이다. 새 코드를 꺼진 채 배포하고, 설정만 바꿔 직원에게 먼저, 다음에 사용자 일부에게 켠다. 이렇게 하면 배포(코드를 운영에 올림)와 출시(사용자에게 기능을 보임)가 갈라진다. 피트 호지슨이 마틴 파울러 사이트에 쓴 Feature Toggles (aka Feature Flags)가 이 주제의 표준 문서다.

기능 토글. 새 결제 화면 코드는 꺼진 채 배포되고, 토글 설정이 직원과 사용자 5%에게만 새 화면을, 나머지에게 옛 화면을 보인다. 토글은 출시 토글, 실험 토글, 운영 토글, 권한 토글의 네 갈래가 있고 수명이 다르다배포새 코드 포함기본값: 꺼짐출시 (설정만 바꾼다)토글: new-checkout직원: 켬 · 사용자: 5%새 결제 화면직원 + 사용자 5%옛 결제 화면나머지 95%출시 토글미완성 기능 숨기기며칠~몇 주실험 토글A/B 테스트실험 기간 동안운영 토글부하 때 기능 끄기짧게, 일부는 오래권한 토글유료·베타 사용자만몇 년도 간다
기능 토글. 위는 배포와 출시를 가르는 흐름, 아래는 호지슨이 나눈 토글의 네 갈래와 대략의 수명이다.

토글은 공짜가 아니다. 토글 하나마다 코드 경로가 둘이 되어 테스트할 조합이 늘고, 다 쓴 출시 토글을 지우지 않으면 아무도 의미를 모르는 분기가 쌓인다. 그래서 출시 토글은 만들 때 지울 날짜나 담당자를 함께 정하고, 토글 판단은 코드 곳곳이 아니라 한곳에서 하게 한다.

스트랭글러 피그: 옛 시스템을 조금씩 감싸며 바꾸기

오래된 시스템을 새로 만들 때 가장 위험한 방법은 한 번에 갈아엎는 것이다. 새 시스템이 옛 시스템의 모든 기능을 따라잡을 때까지 몇 해 동안 아무 가치도 내지 못하고, 그 사이 옛 시스템도 계속 바뀐다. 마틴 파울러는 호주 열대 우림의 교살자 무화과나무에서 이름을 따 다른 길을 제안했다(StranglerFigApplication). 이 무화과는 다른 나무 위에서 싹터 뿌리를 내리며 숙주를 감싸고, 결국 숙주가 사라진 자리에 혼자 선다.

스트랭글러 피그 이전 단계 그림. 라우터를 옛 시스템 앞에 세운 뒤 상품 조회, 주문, 계정을 하나씩 새 서비스로 옮기며 라우팅을 바꾸고, 옛 시스템으로 가는 요청이 없어지면 옛 시스템을 끈다사용자라우터(파사드)옛 시스템 (모놀리스)옛 시스템 꺼짐새 서비스상품 조회/products상품 조회 (옮김, 안 쓰임)주문/orders주문 (옮김, 안 쓰임)계정/account계정 (옮김, 안 쓰임)상품 조회 서비스/products주문 서비스/orders계정 서비스/account
1 / 5

옛 시스템 앞에 라우터(파사드)를 세운다. 아직은 모든 요청을 그대로 옛 시스템으로 넘긴다. 사용자는 아무것도 느끼지 못한다.

스트랭글러 피그 이전. 새 시스템이 옛 시스템을 감싸며 기능 단위로 자라고, 옛 시스템은 할 일이 없어진 다음에 걷어 낸다.

핵심은 옛 시스템 앞에 요청을 가를 수 있는 지점(라우터, API 게이트웨이, 파사드)을 먼저 세우는 것이다. 그러면 기능 하나를 옮길 때마다 라우팅만 바꾸면 되고, 문제가 생기면 라우팅을 되돌린다. 무엇부터 옮길지는 가치와 위험으로 고른다. 의존이 적어 떼기 쉬운 가장자리 기능으로 이전 방법을 익히고, 다음에 자주 바뀌어서 옮기면 이득이 큰 기능을 옮긴다. 데이터를 언제 어떻게 함께 옮길지는 분해와 분산 데이터 노트에서 다룬다.

확장–축소: 데이터베이스를 멈추지 않고 바꾸기

코드는 토글로 나눠 배포할 수 있지만 데이터베이스 스키마는 하나다. 열 이름 하나를 바꾸는 단순한 변경도, 옛 코드와 새 코드가 동시에 도는 배포 도중에는 둘 중 하나를 깨뜨린다. 그래서 바꾸기 전과 후가 함께 살 수 있는 중간 단계를 끼워 넣는다. 다닐로 사토가 파울러 사이트에 정리한 ParallelChange(병행 변경)이고, 단계 이름을 따 확장–축소(expand–contract)라고도 부른다.

확장–축소 데이터베이스 이전. 확장 단계에서 phone_number 열을 새로 만들고 앱이 두 열에 모두 쓰며 옛 행을 백필한다. 이전 단계에서 읽기를 새 열로 옮기고 소비자를 하나씩 이전한다. 축소 단계에서 옛 phone 열을 지운다. 각 단계는 따로 배포되고 앞 단계로 되돌릴 수 있다① 확장users 테이블idphonephone_number (새)두 열에 모두 쓴다읽기는 옛 열, 백필 실행② 이전users 테이블idphonephone_number읽기를 새 열로 옮긴다소비자를 하나씩 이전③ 축소users 테이블idphone (삭제)phone_number새 열만 쓴다옛 열을 지운다시간 →단계마다 따로 배포하고, 어느 단계에서든 앞 단계로 되돌릴 수 있다
열 이름 바꾸기를 세 번의 배포로 나눈 예. 어느 순간에도 옛 코드와 새 코드가 모두 동작하므로 서비스를 멈추지 않고, 문제가 생기면 바로 앞 단계로 돌아간다.
  1. 확장: 새 구조(새 열)를 옛 구조 옆에 더한다. 앱은 두 곳에 모두 쓰고, 기존 행은 백필 작업으로 새 열에 채운다.
  2. 이전: 읽기를 새 구조로 옮긴다. 이 테이블을 읽는 다른 서비스나 보고서가 있다면 하나씩 옮긴다.
  3. 축소: 아무도 옛 구조를 읽지 않는 것을 확인한 뒤 쓰기를 멈추고 옛 열을 지운다.

같은 생각은 API에도 그대로 쓴다. 새 필드를 먼저 더하고, 소비자가 옮겨 가면 옛 필드를 지운다. 단계가 늘어 귀찮아 보이지만, 단계마다 배포가 작아지고 되돌릴 수 있어 큰 스키마 변경을 한밤중 점검 시간에 몰아서 할 필요가 없어진다.

결합 관리와 콘웨이 법칙

진화를 가장 크게 막는 것은 부적절한 결합이다. 여러 서비스가 함께 쓰는 데이터베이스, 모두가 기대는 공용 라이브러리, 동기 호출로 줄줄이 엮인 서비스는 한 곳을 바꿀 때 다른 곳을 함께 바꾸게 만든다. 그래서 진화적 아키텍처는 결합을 아키텍처 퀀텀 안에 가두고, 퀀텀 사이에는 계약과 비동기 메시지 같은 약한 결합만 두려 한다(모듈성과 결합 노트). 이 결합 규칙을 앞의 적합성 함수로 지키면, 시간이 지나도 구조가 조용히 엉키지 않는다.

결합에는 조직도 영향을 준다. 멜빈 콘웨이는 1968년 글 How Do Committees Invent?에서, 시스템을 설계하는 조직은 자기 조직의 의사소통 구조를 닮은 설계를 만들게 된다고 주장했다. 이것이 콘웨이 법칙이다. 팀 사이에 대화가 적은 곳에 시스템의 경계가 생기고, 한 팀 안에서는 경계가 흐려진다.

콘웨이 법칙. 화면 팀, 서버 팀, DB 팀으로 나뉜 조직은 화면, 서버, DB 계층으로 된 시스템을 만든다. 주문 팀, 결제 팀, 배송 팀으로 나뉜 조직은 팀마다 자기 데이터를 가진 서비스를 만들고, 플랫폼 팀이 공통 도구를 댄다기술별 팀화면 팀서버 팀DB 팀닮는다팀 경계를 따라 계층이 생긴다업무 흐름별 팀주문 팀결제 팀배송 팀닮는다팀마다 서비스 하나씩 생긴다화면 계층 (모든 기능)서버 계층 (모든 기능)DB 계층 (모든 기능)기능 하나 = 세 팀의 조율주문DB결제DB배송DB플랫폼 팀: 배포·관측 도구를 댄다기능 하나 = 한 팀 안에서
같은 쇼핑몰도 팀을 어떻게 나누느냐에 따라 다른 구조가 된다. 원하는 구조가 있다면 팀부터 그 모양으로 짜는 것이 역 콘웨이 전략이다.

파울러의 ConwaysLaw는 이를 거스르지 말고 이용하라고 말한다. 서비스 경계를 업무 흐름으로 긋고 싶다면 팀도 업무 흐름별로 짜는 것, 이른바 역 콘웨이 전략(inverse Conway maneuver)이다. 매슈 스켈턴과 마누엘 파이스의 팀 토폴로지는 이 생각을 네 가지 팀 유형으로 정리했다(Team Topologies 핵심 개념). 업무 흐름 하나를 처음부터 끝까지 맡는 흐름 정렬 팀이 중심이 되고, 플랫폼 팀은 배포와 관측 같은 공통 기반을 서비스처럼 제공하며, 지원 팀은 다른 팀이 새 능력을 익히도록 돕고, 복잡한 하위 시스템 팀은 깊은 전문 지식이 필요한 부분을 맡는다. 팀 사이 상호작용도 협업, 서비스로 제공, 촉진의 세 방식으로 좁혀 둔다.

결국 아키텍처를 진화시킨다는 것은 코드만의 일이 아니다. 지켜야 할 특성을 적합성 함수로 고정하고, 작은 변경을 자주 보낼 통로를 만들고, 결합을 퀀텀 안에 가두고, 팀 구조를 원하는 경계에 맞추는 일이 함께 가야 한다.