소프트웨어 아키텍처 핵심 개념 지도: 모듈 경계에서 이벤트 기반 시스템까지
같은 기능을 만드는데 어떤 팀은 이틀에 끝내고 어떤 팀은 두 달을 씁니다. 실력 차이보다는 앞선 결정들이 무엇을 허용하고 무엇을 막아 놓았는지의 차이인 경우가 많습니다. 아키텍처는 화려한 다이어그램이 아니라, 나중에 바꾸기 어려운 결정들의 집합입니다.
이 문서는 소프트웨어 아키텍처의 중요한 개념을 한곳에서 찾기 위한 지도입니다. 특정 프레임워크의 구조가 아니라, 경계를 어디에 긋고 상태를 어디에 두고 실패를 어떻게 다룰지에 대한 개념을 중심에 뒀습니다. 각 항목은 한두 문장으로 범위를 잡고, 자세한 설명이 필요한 주제는 별도의 심화 글로 연결합니다. 처음부터 끝까지 외울 필요는 없습니다. 설계 논의에서 낯선 용어가 나왔을 때 어느 영역에 속하고 무엇과 연결되는지 확인하는 기준점으로 사용하는 편이 좋습니다.
품질 속성과 제약
-> 모듈 경계
-> 도메인 모델
-> 데이터 소유권
-> 통신 방식
-> 분산과 실패 처리
-> 확장과 성능
-> 운영과 관측
-> 진화와 폐기아키텍처란 무엇인가
- 아키텍처: 시스템을 이루는 구조와 그 구조를 만든 결정들입니다. 코드보다 오래 남고, 바꾸는 비용이 가장 큰 층입니다.
- 아키텍처 결정: 되돌리기 어렵고 여러 부분에 영향을 주는 선택입니다. 나머지 대부분의 선택은 설계이지 아키텍처가 아닙니다.
- 결정 기록: 무엇을 왜 그렇게 정했고 어떤 대안을 버렸는지 남긴 문서입니다. 몇 달 뒤 같은 논의를 처음부터 다시 하지 않게 해 주는 가장 값싼 장치입니다.
- 품질 속성: 성능, 가용성, 보안, 변경 용이성처럼 기능이 아닌 요구입니다. 아키텍처는 사실상 이 속성들을 만족시키기 위해 존재합니다.
- 기능 요구와 비기능 요구: 무엇을 하는지와 얼마나 잘 하는지의 구분입니다. 기능은 코드로 추가할 수 있지만 비기능은 대개 구조를 바꿔야 합니다.
- 제약: 예산, 일정, 인력, 규제, 기존 시스템처럼 선택할 수 없는 조건입니다. 제약을 먼저 적어 두면 검토할 대안이 크게 줄어듭니다.
- 이해관계자: 이 시스템의 성패에 이해가 걸린 사람들입니다. 서로 다른 품질 속성을 원하므로 무엇을 우선할지는 기술이 아니라 합의의 문제입니다.
- 아키텍처 뷰: 같은 시스템을 논리, 배포, 프로세스, 데이터 같은 여러 각도로 그린 그림입니다. 하나의 그림에 전부 담으려 하면 아무도 읽지 못합니다.
- 트레이드오프: 어떤 품질을 높이면 다른 품질이 내려갑니다. 트레이드오프를 명시하지 않은 설계안은 대개 모든 것을 다 잘하겠다는 약속입니다.
- 가역성: 틀렸을 때 되돌릴 수 있는지의 성질입니다. 되돌릴 수 있는 결정은 빨리 내리고, 되돌릴 수 없는 결정에만 시간을 씁니다.
- 아키텍처 부채: 지금 구조가 앞으로의 변경에 물리는 세금입니다. 코드 부채와 달리 리팩터링 한 번으로 갚아지지 않습니다.
- 콘웨이의 법칙: 조직의 소통 구조가 만들어 내는 시스템 구조를 닮습니다. 아키텍처를 바꾸려면 팀 경계를 함께 봐야 하는 이유입니다.
- 아키텍트의 일: 그림을 그리는 것이 아니라 제약을 정하고 트레이드오프를 드러내는 일입니다. 결정을 대신하는 것이 아니라 결정이 가능하게 만듭니다.
모듈과 경계
- 모듈: 함께 바뀌는 것들을 묶고 따로 바뀌는 것들을 나눈 단위입니다. 기준은 기술 종류가 아니라 변경 이유입니다.
- 응집도: 한 모듈 안의 요소들이 얼마나 같은 목적을 향하는지입니다. 응집도가 낮은 모듈은 이름을 붙이기부터 어렵습니다.
- 결합도: 모듈이 서로에게 얼마나 의존하는지입니다. 낮출수록 독립적으로 바꿀 수 있지만 완전히 없앨 수는 없고, 어디에 남길지를 고르는 문제입니다.
- 캡슐화: 내부 구현을 감추고 필요한 것만 드러내는 것입니다. 감춘 것이 없다면 모듈이 아니라 파일 묶음입니다.
- 인터페이스: 모듈이 외부에 약속한 사용 방법입니다. 좁을수록 안쪽을 자유롭게 바꿀 수 있습니다.
- 계약: 입력, 출력, 오류, 그리고 지켜지는 조건까지 포함한 약속입니다. 시그니처만으로는 계약이 되지 않습니다.
- 의존성 방향: 누가 누구를 아는지의 화살표입니다. 자주 바뀌는 쪽이 안정된 쪽을 알아야 하고 반대가 되면 변경이 번집니다.
- 의존성 역전: 상위 정책이 하위 세부를 직접 알지 않도록 추상에 의존하게 만드는 기법입니다. 데이터베이스와 외부 API를 갈아 끼울 수 있게 하는 근거입니다.
- 순환 의존: 서로를 참조해 어느 쪽도 혼자 이해하거나 배포할 수 없는 상태입니다. 발견하면 대개 경계가 잘못 그어져 있다는 신호입니다.
- 공개와 내부: 밖에서 쓰라고 만든 것과 안에서만 쓰는 것의 구분입니다. 이 구분이 없으면 모든 것이 사실상 공개 API가 되어 아무것도 못 바꿉니다.
- 패키지 구조: 폴더를 기술 종류로 나눌지 기능으로 나눌지의 선택입니다. 기능으로 나누면 한 변경이 한 폴더 안에서 끝나는 경우가 늘어납니다.
- 경계의 비용: 경계를 그으면 독립성을 얻고 조율 비용을 냅니다. 필요 없는 경계는 이득 없이 비용만 청구합니다.
- 경계 옮기기: 처음 그은 경계는 대개 틀립니다. 나중에 옮길 수 있도록 경계를 명시적으로 두는 것이, 처음부터 맞히려는 시도보다 현실적입니다.
구조 패턴
- 계층형: 표현, 응용, 도메인, 인프라처럼 층으로 나누고 위에서 아래로만 의존하게 합니다. 이해가 쉽지만 층을 관통하는 변경이 잦으면 층마다 손대야 합니다.
- 포트와 어댑터: 도메인을 가운데 두고 외부와 닿는 지점을 어댑터로 감쌉니다. 데이터베이스와 프레임워크를 세부 사항으로 밀어내 테스트가 쉬워집니다.
- 의존성 규칙: 안쪽 원은 바깥쪽을 알지 못한다는 원칙입니다. 여러 이름의 아키텍처가 사실상 이 한 문장을 공유합니다.
- MVC 계열: 화면, 상태, 로직의 역할을 나누는 패턴군입니다. 이름은 비슷해도 어디에 상태를 두느냐가 실제 차이입니다.
- 파이프라인: 입력을 단계별로 변환해 흘려보내는 구조입니다. 각 단계가 독립적이라 조합과 재사용이 쉽습니다.
- 플러그인과 마이크로커널: 최소한의 핵심에 확장 지점을 열어 두는 구조입니다. 확장 지점을 잘못 잡으면 핵심이 계속 열려야 합니다.
- 모놀리식: 하나의 배포 단위로 만든 시스템입니다. 시작이 빠르고 트랜잭션이 단순하지만 커질수록 배포와 소유권이 얽힙니다.
- 모듈러 모놀리스: 하나로 배포하되 안에서 경계를 엄격히 지키는 구조입니다. 마이크로서비스로 갈지 결정하기 전에 대부분의 팀이 먼저 있어야 할 자리입니다.
- 마이크로서비스: 독립 배포와 독립 운영을 얻는 대신 네트워크, 일관성, 관측의 문제를 전부 떠안습니다. 조직이 나뉘어 있지 않으면 이득 없이 비용만 늘어납니다.
- 서비스 경계: 서비스를 나누는 기준은 코드 크기가 아니라 데이터 소유권과 변경 주기입니다. 두 서비스가 같은 테이블을 쓰면 그것은 한 서비스입니다.
- 분산 모놀리스: 배포는 나뉘었는데 서로 없으면 아무것도 못 하는 상태입니다. 두 구조의 단점만 모은 결과이며 가장 흔한 실패입니다.
- 스타일 선택: 패턴은 목적이 아니라 특정 품질 속성을 사기 위한 수단입니다. 무엇을 사려는지 말할 수 없다면 아직 고를 때가 아닙니다.
- 조합: 실제 시스템은 한 가지 스타일로 되어 있지 않습니다. 영역마다 다른 스타일을 쓰는 것이 정상이며, 경계에서 규칙을 분명히 하면 됩니다.
도메인 모델링
- 도메인: 소프트웨어가 다루는 현실의 문제 영역입니다. 기술이 아니라 이 영역의 규칙이 시스템의 본질입니다.
- 유비쿼터스 언어: 개발자와 업무 담당자가 같은 뜻으로 쓰는 용어 집합입니다. 코드와 대화에서 같은 단어를 쓰지 않으면 번역 비용이 계속 발생합니다.
- 엔티티: 시간이 지나도 같은 것으로 식별되는 대상입니다. 속성이 전부 바뀌어도 같은 주문이면 같은 엔티티입니다.
- 값 객체: 식별자 없이 값 자체로 의미가 정해지는 대상입니다. 금액, 기간, 주소처럼 비교와 교체가 자유롭습니다.
- 애그리게이트: 함께 일관성을 지켜야 하는 객체 묶음과 그 대표입니다. 트랜잭션 경계를 정하는 실질적인 도구입니다.
- 불변식: 항상 참이어야 하는 규칙입니다. 이 규칙을 지킬 책임이 있는 자리가 곧 애그리게이트의 경계입니다.
- 리포지토리: 애그리게이트를 저장하고 꺼내는 통로입니다. 도메인 코드가 저장 방식을 모르게 하는 것이 목적입니다.
- 도메인 서비스: 특정 엔티티에 속한다고 보기 어려운 규칙을 담는 자리입니다. 남용하면 도메인이 비고 서비스만 커집니다.
- 빈약한 도메인 모델: 데이터만 있는 객체와 규칙만 있는 서비스로 갈라진 구조입니다. 동작하지만 규칙이 어디 있는지 찾기 어려워집니다.
- 바운디드 컨텍스트: 하나의 모델과 용어가 일관되게 통하는 범위입니다. 같은 단어가 부서마다 다른 뜻이면 경계가 거기입니다.
- 컨텍스트 맵: 컨텍스트들이 서로 어떤 관계로 이어지는지 그린 그림입니다. 기술 연동보다 팀 사이의 힘 관계를 드러냅니다.
- 안티커럽션 레이어: 외부 모델이 우리 모델을 오염시키지 않도록 번역하는 층입니다. 레거시와 붙을 때 가장 자주 쓰입니다.
- 도메인 이벤트: 도메인에서 의미 있게 일어난 사실입니다. 과거형으로 이름 짓고, 다른 컨텍스트가 반응할 지점을 만듭니다.
데이터 아키텍처
- 데이터 소유권: 어떤 데이터를 누가 쓰고 누가 바꾸는지의 규칙입니다. 아키텍처에서 가장 되돌리기 어려운 결정입니다.
- 스키마 설계: 데이터의 모양을 정하는 일이며 이후 모든 질의와 성능의 기반이 됩니다. 애플리케이션보다 오래 살아남는 경우가 많습니다.
- 정규화와 반정규화: 중복을 없애 일관성을 지키는 쪽과 중복을 허용해 읽기를 빠르게 하는 쪽입니다. 어느 쪽도 기본값이 아니고 읽기와 쓰기의 비율이 정합니다.
- 트랜잭션: 여러 변경을 하나로 묶어 전부 되거나 전부 안 되게 만드는 장치입니다. 경계를 어디에 두느냐가 곧 일관성의 범위입니다.
- 격리 수준: 동시에 실행되는 트랜잭션이 서로를 얼마나 볼 수 있는지의 설정입니다. 기본값이 무엇인지 모르고 쓰면 재현이 어려운 버그가 남습니다.
- 잠금: 충돌을 미리 막는 비관적 방식과 충돌이 났을 때 처리하는 낙관적 방식이 있습니다. 충돌 빈도가 낮으면 낙관적 쪽이 대개 낫습니다.
- 인덱스: 읽기를 빠르게 하는 대신 쓰기와 저장 공간을 씁니다. 질의 패턴을 모르고 만든 인덱스는 비용만 남습니다.
- 읽기와 쓰기의 분리: 두 경로는 요구하는 모양이 다릅니다. 쓰기는 정합성을, 읽기는 화면에 맞는 형태를 원합니다.
- CQRS: 읽기 모델과 쓰기 모델을 아예 분리하는 방식입니다. 복잡도가 크게 늘므로 읽기 부하나 모델 차이가 실제로 클 때만 씁니다.
- 이벤트 소싱: 현재 상태 대신 변경 이력을 저장하고 필요할 때 재생합니다. 감사와 시점 복원에 강하지만 스키마 진화와 조회가 어렵습니다.
- 공유 데이터베이스: 여러 서비스가 같은 테이블을 직접 읽고 씁니다. 가장 빠른 연동이자 가장 강한 결합이며 나중에 떼어내기가 거의 불가능합니다.
- 마이그레이션: 스키마를 바꾸면서도 서비스를 멈추지 않는 절차입니다. 추가와 제거를 같은 배포에 넣지 않는 것이 기본 원칙입니다.
- 데이터 보존: 얼마나 오래 무엇을 남길지의 정책입니다. 지우지 않으면 비용, 성능, 그리고 규제 위험이 함께 자랍니다.
통신과 통합
- 동기와 비동기: 응답을 기다리는지 아닌지의 차이입니다. 동기는 이해가 쉽고 장애가 전파되며, 비동기는 견디지만 흐름을 추적하기 어렵습니다.
- 요청과 응답: 호출자가 결과를 기다리는 형태입니다. 상대가 느려지면 내가 느려진다는 사실을 항상 함께 봐야 합니다.
- REST: 자원을 주소로 표현하고 표준 메서드로 다루는 방식입니다. 널리 알려져 있어 통합 비용이 낮은 것이 가장 큰 장점입니다.
- RPC: 원격 호출을 지역 함수 호출처럼 다루는 방식입니다. 빠르고 계약이 분명하지만 원격이라는 사실을 잊게 만드는 위험이 있습니다.
- GraphQL: 클라이언트가 필요한 필드를 지정해 받아 갑니다. 과다·과소 조회를 줄이는 대신 서버 쪽 질의 비용 관리가 새 숙제가 됩니다.
- 메시지 큐: 보낸 쪽과 받는 쪽을 시간적으로 떼어 놓습니다. 순간 부하를 흡수하고 처리 실패를 다시 시도할 수 있게 합니다.
- 발행과 구독: 보내는 쪽이 누가 받는지 모르는 형태입니다. 새 소비자를 추가해도 생산자를 바꾸지 않아도 됩니다.
- 이벤트 스트림: 이벤트를 순서 있는 로그로 보존해 여러 소비자가 각자 진도로 읽습니다. 재처리와 시점 복원이 가능해집니다.
- 스키마 진화: 메시지와 API의 모양이 바뀌어도 옛 소비자가 깨지지 않게 하는 규칙입니다. 필드 추가는 안전하고 삭제와 의미 변경이 위험합니다.
- 계약 테스트: 제공자와 소비자가 서로의 기대를 코드로 고정합니다. 통합 환경 전체를 띄우지 않고도 깨짐을 잡아냅니다.
- 버저닝: 호환되지 않는 변경을 알리고 옛 버전을 유지하는 방법입니다. 언제 없앨지까지 함께 공지해야 실제로 없앨 수 있습니다.
- 게이트웨이: 인증, 라우팅, 속도 제한 같은 공통 관심사를 앞단에 모읍니다. 여기에 업무 로직이 들어가기 시작하면 새 병목이 됩니다.
- 백프레셔: 받는 쪽이 감당할 수 있는 속도를 보내는 쪽에 알리는 장치입니다. 없으면 큐가 무한히 자라다 한꺼번에 무너집니다.
분산 시스템
- 부분 실패: 일부만 죽고 나머지는 사는 상태가 분산의 기본 조건입니다. 전부 살아 있다고 가정한 코드는 언젠가 반드시 깨집니다.
- 네트워크의 거짓 전제: 네트워크는 안정적이지 않고, 지연은 0이 아니며, 대역폭은 무한하지 않습니다. 이 전제를 코드에 심으면 장애가 설계에 박힙니다.
- CAP: 분단이 생겼을 때 일관성과 가용성 중 무엇을 포기할지의 선택입니다. 평상시가 아니라 장애 순간에 무엇을 할지 정하는 문제입니다.
- 일관성 모델: 강한 일관성부터 최종 일관성까지의 스펙트럼입니다. 무엇이 필요한지는 기술이 아니라 업무 규칙이 정합니다.
- 최종 일관성: 시간이 지나면 같아지지만 지금은 다를 수 있는 상태입니다. 화면과 안내 문구가 이 사실을 반영해야 사용자 신뢰가 유지됩니다.
- 멱등성: 같은 요청을 여러 번 처리해도 결과가 한 번과 같은 성질입니다. 재시도가 존재하는 모든 경로에 필요합니다.
- 재시도: 일시적 실패를 넘기는 기본 수단이며, 지수적 대기와 흔들림을 함께 써야 합니다. 그냥 반복하면 장애를 키우는 강화 루프가 됩니다.
- 타임아웃: 언제 포기할지 정하는 값입니다. 설정하지 않으면 무한정 기다리며 스레드와 커넥션을 잠급니다.
- 서킷 브레이커: 실패가 계속되면 호출을 끊어 상대와 자신을 함께 보호합니다. 회복 여부를 확인할 반개방 상태가 있어야 완성됩니다.
- 분산 트랜잭션: 여러 서비스에 걸친 원자성은 비싸고 취약합니다. 대부분의 경우 피하는 것이 정답이며 경계를 다시 보라는 신호입니다.
- 사가: 긴 작업을 여러 지역 트랜잭션으로 쪼개고 실패 시 보상 동작으로 되돌립니다. 보상이 불가능한 단계가 있으면 성립하지 않습니다.
- 아웃박스: 상태 변경과 메시지 발행을 같은 트랜잭션에 담고 나중에 내보냅니다. 저장은 됐는데 알림이 안 나가는 어긋남을 막는 표준 해법입니다.
- 순서와 시계: 서버들의 시계는 어긋나므로 시각으로 순서를 정하면 안 됩니다. 논리적 순서나 단조 증가 식별자를 씁니다.
확장성과 성능
- 수직 확장과 수평 확장: 한 대를 키우는 것과 여러 대로 늘리는 것입니다. 수직이 언제나 더 쉽고, 한계에 닿기 전까지는 대개 더 싸기도 합니다.
- 무상태: 요청 사이에 서버가 기억하는 것이 없는 상태입니다. 무상태여야 아무 대나 요청을 받을 수 있어 수평 확장이 가능해집니다.
- 세션 상태: 사용자별로 유지해야 하는 정보를 어디에 둘지의 문제입니다. 서버 메모리에 두는 순간 그 서버에 묶입니다.
- 캐시 계층: 브라우저, CDN, 애플리케이션, 데이터베이스 각 층에 캐시가 있습니다. 어느 층에서 맞히느냐에 따라 아끼는 비용이 다릅니다.
- 캐시 무효화: 언제 낡은 값을 버릴지 정하는 문제이며 분산에서 가장 어려운 축에 듭니다. 만료 시간과 명시적 무효화를 함께 씁니다.
- 부하 분산: 요청을 여러 인스턴스에 나눠 보냅니다. 분배 방식보다 죽은 인스턴스를 얼마나 빨리 빼느냐가 실제 가용성을 좌우합니다.
- 샤딩: 데이터를 여러 저장소로 쪼개 각자 일부만 담당하게 합니다. 샤드 키를 잘못 고르면 한쪽에 몰려 확장 효과가 사라집니다.
- 읽기 복제: 읽기 부하를 복제본으로 넘깁니다. 복제 지연 때문에 방금 쓴 값을 못 읽는 경우를 화면이 감당해야 합니다.
- 병목: 전체 처리량을 결정하는 가장 좁은 지점입니다. 병목이 아닌 곳을 개선하면 앞에 쌓이는 대기만 늘어납니다.
- 처리량과 지연: 초당 몇 건인지와 한 건이 얼마나 걸리는지는 다른 지표입니다. 처리량을 올리다 지연이 나빠지는 경우가 흔합니다.
- 대기열의 성질: 이용률이 한계에 가까워질수록 대기 시간이 급격히 늘어납니다. 여유를 남기지 않은 최적화는 평균을 개선하고 최악을 악화시킵니다.
- 꼬리 지연: 상위 몇 퍼센트의 느린 요청이 실제 체감을 좌우합니다. 평균 응답 시간만 보면 사용자가 겪는 문제가 보이지 않습니다.
- 용량 계획: 얼마나 견딜 수 있는지 미리 재는 일입니다. 부하 시험 없이 정한 숫자는 추측이며 장애 때 처음 확인됩니다.
신뢰성과 운영
- 가용성: 서비스가 정상 동작하는 시간의 비율입니다. 목표를 한 단계 올릴 때마다 비용은 계단식으로 뜁니다.
- 서비스 수준 목표: 어느 정도면 충분한지 숫자로 합의한 값입니다. 100퍼센트를 목표로 두면 아무 변경도 못 하게 됩니다.
- 오류 예산: 목표와 100퍼센트 사이의 허용 실패량입니다. 남아 있으면 과감히 배포하고 다 썼으면 안정화에 씁니다.
- 관측 가능성: 밖으로 나온 신호만으로 내부 상태를 알아낼 수 있는 정도입니다. 문제가 난 뒤 계측을 추가하면 그 사건은 영원히 설명할 수 없습니다.
- 로그, 메트릭, 트레이스: 무슨 일이 있었는지, 얼마나 되는지, 요청이 어디를 지났는지에 각각 답합니다. 셋 중 하나만으로는 분산 시스템을 진단할 수 없습니다.
- 상관 식별자: 하나의 요청에 붙어 여러 서비스의 기록을 이어 줍니다. 없으면 로그가 아무리 많아도 한 사건으로 모을 수 없습니다.
- 헬스체크: 살아 있는지와 요청을 받을 준비가 됐는지를 구분해서 알립니다. 둘을 하나로 합치면 기동 중인 인스턴스에 트래픽이 들어갑니다.
- 우아한 성능 저하: 일부가 죽어도 핵심 기능은 남기고 부가 기능만 끄는 설계입니다. 전부 아니면 전무보다 거의 항상 낫습니다.
- 격벽: 자원 풀을 나눠 한쪽의 고갈이 전체로 번지지 않게 합니다. 커넥션과 스레드를 기능별로 나누는 것이 대표적입니다.
- 무중단 배포: 새 버전과 옛 버전이 잠시 함께 도는 것을 전제로 합니다. 그래서 스키마와 메시지가 양쪽에서 모두 읽혀야 합니다.
- 롤백: 되돌리는 능력이 배포 속도를 결정합니다. 되돌릴 수 없는 변경은 배포가 아니라 이주로 다뤄야 합니다.
- 백업과 복구 검증: 백업은 복구를 해 봐야 백업입니다. 복구 시간을 재 보지 않은 계획은 계획이 아닙니다.
- 사후 분석: 사람이 아니라 구조를 대상으로 원인을 찾는 절차입니다. 같은 장애가 반복된다면 아직 구조를 못 본 것입니다.
보안 아키텍처
- 신뢰 경계: 신뢰 수준이 달라지는 지점입니다. 이 선을 넘는 모든 데이터는 검증 대상입니다.
- 위협 모델링: 무엇을 지키고 누구로부터 지키는지 먼저 적는 작업입니다. 대상 없이 하는 보안 강화는 비용만 늘립니다.
- 인증과 인가: 누구인지 확인하는 것과 무엇을 할 수 있는지 정하는 것입니다. 두 관심사를 한곳에 섞으면 권한 규칙을 추적할 수 없게 됩니다.
- 최소 권한: 필요한 만큼만 주고 필요한 동안만 유지합니다. 사고가 났을 때 피해 범위를 결정하는 가장 큰 변수입니다.
- 비밀 관리: 자격 증명을 코드와 이미지 밖에 두고 교체 가능하게 만듭니다. 교체 절차가 없는 비밀은 사실상 영구 비밀입니다.
- 전송과 저장 암호화: 오가는 중과 쌓여 있는 상태를 각각 보호합니다. 둘 중 하나만 하는 경우가 흔하고 대개 저장 쪽이 빠집니다.
- 입력 검증: 모든 외부 입력을 신뢰하지 않고 형식과 범위를 확인합니다. 방어는 경계에서 한 번이 아니라 각 층에서 반복돼야 합니다.
- 출력 인코딩: 데이터를 쓰는 맥락에 맞게 이스케이프합니다. 주입 공격 대부분이 이 지점의 누락에서 나옵니다.
- 감사 로그: 누가 언제 무엇을 했는지 남깁니다. 변조가 불가능해야 의미가 있으므로 애플리케이션이 지울 수 없는 곳에 둡니다.
- 다중 테넌시 격리: 여러 고객의 데이터가 한 시스템에 있을 때 서로 보이지 않게 하는 장치입니다. 질의 조건 하나에 의존하는 격리는 언젠가 새어 나갑니다.
- 공급망: 우리가 쓰는 라이브러리와 이미지, 빌드 도구도 공격 표면입니다. 무엇을 쓰고 있는지 목록으로 알고 있어야 대응할 수 있습니다.
- 제로 트러스트: 내부 네트워크라는 이유로 신뢰하지 않고 매 요청을 검증합니다. 경계 방어만으로는 내부 침투 이후를 막지 못하기 때문입니다.
- 규정 준수: 개인정보와 산업별 규제가 설계를 제약합니다. 나중에 붙이면 데이터 보존과 위치 결정을 되돌려야 합니다.
진화와 리팩터링
- 진화적 아키텍처: 처음에 다 맞히는 대신 바꾸기 쉬운 상태를 유지하는 접근입니다. 요구가 변한다는 사실을 설계의 전제로 삼습니다.
- 적합성 함수: 아키텍처 규칙을 자동으로 검사하는 장치입니다. 의존성 방향과 계층 위반을 문서가 아니라 테스트가 지키게 합니다.
- 스트랭글러 무화과: 옛 시스템 앞에 새 경로를 두고 기능을 하나씩 옮겨 마지막에 옛것을 걷어냅니다. 큰 이주를 되돌릴 수 있는 단계로 쪼개는 방법입니다.
- 추상화를 통한 분기: 교체 지점을 인터페이스로 감싸 놓고 구현을 서서히 바꿉니다. 장기 브랜치 없이 큰 변경을 진행할 수 있습니다.
- 확장 후 수축: 새 것을 먼저 추가하고 양쪽을 함께 쓰다 옛 것을 지웁니다. 스키마와 API를 무중단으로 바꾸는 기본 절차입니다.
- 기능 플래그: 배포와 공개를 분리하는 스위치입니다. 되돌리기가 배포 없이 가능해져 결정이 가벼워집니다.
- 레거시: 나쁜 코드가 아니라 바꾸기 두려운 코드입니다. 두려움의 원인은 대개 테스트와 관측의 부재입니다.
- 재작성의 함정: 처음부터 다시 만들면 알려진 문제 대신 알려지지 않은 문제를 얻습니다. 옛 시스템에 쌓인 예외 처리가 요구사항이었다는 사실이 뒤늦게 드러납니다.
- 기술 부채: 지금 빠르기 위해 나중의 속도를 빌리는 것입니다. 의도적으로 지는 부채와 모르고 쌓인 부채는 다르게 다뤄야 합니다.
- 표준화와 자율성: 팀마다 자유롭게 고르는 것과 하나로 맞추는 것 사이의 균형입니다. 운영 부담이 큰 영역일수록 표준화 쪽이 유리합니다.
- 플랫폼 팀: 공통 기반을 제품처럼 제공하는 팀입니다. 통제 기구가 되면 병목이 되고, 선택 가능한 도구로 남으면 채택률이 말해 줍니다.
- 문서화: 결정과 이유는 남기고 세부는 코드에 맡깁니다. 코드로 확인 가능한 것을 문서로 옮겨 적으면 반드시 어긋납니다.
- 폐기: 더 이상 가치를 만들지 않는 구성요소를 걷어내는 일입니다. 만드는 것보다 어렵고 대개 아무도 하지 않습니다.
판단 기준
- 요구에서 출발: 어떤 품질 속성이 왜 필요한지 말할 수 없으면 아직 설계를 시작할 때가 아닙니다. 패턴 이름에서 출발하는 논의는 대개 거기서 끝납니다.
- 가장 위험한 것 먼저: 틀렸을 때 가장 크게 무너지는 가정부터 확인합니다. 만들기 쉬운 순서로 진행하면 위험은 마지막에 남습니다.
- 단순함: 가장 단순한 구조에서 시작해 필요할 때 복잡도를 삽니다. 나중에 복잡하게 만들기는 쉽고 되돌리기는 어렵습니다.
- 미리 만들지 않기: 아직 필요하지 않은 유연성은 비용만 냅니다. 다만 되돌릴 수 없는 결정은 예외로, 여기서는 미리 생각해야 합니다.
- 옵션 유지: 결정을 늦출 수 있는 구조가 곧 가치입니다. 정보가 가장 많은 시점은 언제나 지금이 아니라 나중입니다.
- 스파이크: 답을 모를 때 작은 실험 코드를 만들어 확인합니다. 결과를 얻으면 버리는 것이 전제입니다.
- 부하 시험: 성능은 논쟁이 아니라 측정의 대상입니다. 숫자 없이 정한 구조는 배포 후에 검증됩니다.
- 운영 부담: 만든 뒤 누가 새벽에 일어나는지가 설계의 일부입니다. 구성요소를 하나 늘릴 때마다 감시와 대응이 함께 늘어납니다.
- 팀 역량: 팀이 운영할 수 없는 구조는 좋은 구조가 아닙니다. 기술 선택은 채용과 학습 계획을 포함한 결정입니다.
- 비용: 인프라 비용뿐 아니라 변경 비용과 조율 비용을 함께 봅니다. 대개 사람 시간이 가장 비쌉니다.
- 되돌릴 수 있는가: 이 질문 하나로 논의에 쓸 시간의 양이 결정됩니다. 되돌릴 수 있으면 빨리 정하고 실제로 해 봅니다.
- 리뷰: 설계는 혼자 검증되지 않습니다. 반대 의견이 나올 자리를 만들지 않으면 확신만 커집니다.
- 기록: 결정과 근거를 남기지 않으면 다음 사람은 그것을 우연으로 봅니다. 우연으로 보이는 구조는 함부로 바뀝니다.
개념들은 요구 하나를 반영하는 과정에서 만납니다
주문에 쿠폰을 적용해 달라는 요구 하나에도 이 지도의 거의 모든 영역이 들어옵니다.
- 어느 경계의 일인지 정합니다. 쿠폰이 주문 안의 규칙인지 별도 컨텍스트인지에 따라 이후 결정이 전부 갈립니다.
- 불변식을 찾습니다. 할인 후 금액이 음수가 될 수 없다는 규칙을 누가 지킬지가 애그리게이트 경계를 정합니다.
- 데이터 소유권을 정합니다. 쿠폰 잔여 수량을 주문 쪽에서 직접 수정하면 그 순간 두 영역은 한 덩어리가 됩니다.
- 트랜잭션 범위를 정합니다. 주문 생성과 쿠폰 차감을 한 트랜잭션에 넣을 수 있는지가 서비스를 나눌지 말지를 결정합니다.
- 통신 방식을 고릅니다. 동기 호출이면 쿠폰 서비스가 느려질 때 주문도 느려지고, 비동기면 사용자에게 결과를 언제 보여 줄지 정해야 합니다.
- 실패를 설계합니다. 차감은 됐는데 주문이 실패한 경우를 보상 동작으로 되돌리고, 재시도가 중복 차감을 만들지 않도록 멱등성을 겁니다.
- 부하를 봅니다. 인기 쿠폰 하나에 요청이 몰리면 그 행이 병목이 되므로 잠금 방식과 대기열을 함께 생각합니다.
- 관측을 붙입니다. 상관 식별자로 주문과 쿠폰 기록을 이어 두지 않으면 어긋남이 생겼을 때 원인을 좁힐 수 없습니다.
- 진화를 준비합니다. 쿠폰 정책은 반드시 바뀌므로 규칙을 코드에 흩지 말고 한 자리에 모아 둡니다.
각 개념을 따로 배우는 이유는 요구 하나를 아홉 단계로 나눠 처리하기 위해서가 아닙니다. 무언가 잘못됐을 때 원인이 경계인지, 트랜잭션 범위인지, 통신 방식인지, 실패 처리인지 좁혀 가기 위해서입니다. 코드가 지저분해서라고 적는 순간 고칠 수 있는 것이 하나도 남지 않습니다.
처음 배운다면 이 순서가 좋습니다
- 응집도와 결합도부터 봅니다. 지금 코드에서 함께 바뀌는 것들이 함께 있는지 확인하는 것만으로 많은 것이 보입니다.
- 의존성 방향을 그려 봅니다. 화살표를 그려 순환이 있는지, 안정된 쪽을 향하는지 확인합니다.
- 트랜잭션 경계를 찾습니다. 어디까지가 한꺼번에 성공하거나 실패해야 하는지 적어 봅니다.
- 데이터 소유권을 정리합니다. 어떤 테이블을 누가 쓰고 누가 바꾸는지 표로 만들어 봅니다.
- 실패를 설계에 넣습니다. 각 외부 호출에 타임아웃과 재시도와 멱등성이 있는지 점검합니다.
- 관측을 붙입니다. 요청 하나를 끝까지 따라갈 수 있는지 실제로 해 봅니다.
- 되돌리는 능력을 만듭니다. 기능 플래그와 롤백 경로를 확보하면 나머지 결정이 가벼워집니다.
이 순서가 절대적인 것은 아닙니다. 실제로 맡고 있는 시스템 하나를 붙들고 필요한 개념을 앞뒤로 오가는 편이 훨씬 오래 남습니다.
이 문서는 시리즈의 허브입니다
이후의 심화 글은 각 문서만 읽어도 이해되도록 쓰고 architecture-core-concepts 태그로 연결합니다. 새 글이 발행되면 이 허브의 관련 항목에도 직접 링크를 추가합니다. 번호대로 읽어야 하는 연재물이 아니라, 관심 있는 지점에서 들어와 전체 지도로 돌아올 수 있는 구조입니다.
먼저 확장하기 좋은 주제는 다음과 같습니다.
- 모듈 경계 긋기: 변경 이유로 나누는 법
- 애그리게이트와 트랜잭션 경계: 어디까지 한꺼번에 지킬 것인가
- 공유 데이터베이스를 떼어내는 순서
- 분산 트랜잭션 대신 사가와 아웃박스
- 재시도가 장애를 키울 때: 멱등성, 백오프, 서킷 브레이커
- 무중단 스키마 변경: 확장 후 수축
- 모듈러 모놀리스에서 서비스로 넘어가는 판단 기준
- 아키텍처 결정 기록을 실제로 쓰이게 만들기
기술과 유행하는 패턴은 계속 바뀌지만 물어야 할 질문은 크게 달라지지 않습니다. 무엇이 함께 바뀌는가, 이 데이터는 누구의 것인가, 어디까지 한꺼번에 성공해야 하는가, 일부가 실패하면 무엇이 남는가, 그리고 이 결정을 되돌릴 수 있는가. 이 다섯 질문을 기준으로 두면 처음 보는 구조도 이미 아는 개념 위에 놓고 이해할 수 있습니다.
함께 읽기
- AI 제품 개발 핵심 개념 지도: 프롬프트에서 평가와 운영까지데모는 십 분 만에 나오는데 제품이 되지 않는 일이 반복됩니다. 잘 되던 프롬프트가 입력이 조금 달라지면 무너지고, 고쳤더니 다른 경우가 깨지고, 무엇이 나아졌는지 말할 근거가 없습니다. 모델을 다루는 어려움보다는 정답이 하나가 아닌 시스템을 어떻게 판정하고 운영할지의 문제입니다.
- 애플리케이션 보안 핵심 개념 지도: 위협 모델에서 사고 대응까지보안 사고는 대개 아무도 모르던 정교한 기법이 아니라, 다들 알고 있었지만 이 자리에는 없을 거라고 생각한 실수에서 시작합니다. 검증하지 않은 입력 하나, 권한 확인을 빠뜨린 엔드포인트 하나, 저장소에 그대로 남은 토큰 하나면 충분합니다. 그래서 보안은 마지막에 붙이는 점검이 아니라 설계할 때 함께 정해지는 성질에 가깝…
- 모바일 앱 개발 핵심 개념 지도: 화면 생명주기에서 스토어 배포까지웹을 만들던 방식 그대로 앱을 만들면 대개 두 지점에서 막힙니다. 하나는 화면이 언제 살아 있고 언제 죽는지 앱이 마음대로 정한다는 것이고, 다른 하나는 만든 것을 바로 내보낼 수 없다는 것입니다. 브라우저에서는 새로고침이 해결하던 문제를 앱에서는 상태 복원과 심사 대기가 대신합니다.
- 웹 플랫폼 핵심 개념 지도: URL에서 렌더링과 보안까지웹 개발을 프레임워크 이름으로만 배우면 기술이 바뀔 때마다 지식도 함께 낡아 보입니다. 하지만 브라우저가 주소를 해석하고, 서버와 통신하고, 문서를 파싱하고, 픽셀을 그리는 기본 과정은 훨씬 천천히 바뀝니다. React, Vue, Svelte와 Next.js도 이 과정 위에서 서로 다른 개발 방식을 제공합니다.
- 클라우드와 DevOps 핵심 개념 지도: 인프라에서 배포와 관측성까지클라우드와 DevOps를 제품 목록으로 익히면 비슷한 기능을 가리키는 이름이 계속 늘어납니다. 가상 머신, 컨테이너, 서버리스, CI/CD와 Kubernetes는 같은 층의 대안이 아닙니다. 어떤 것은 실행 환경이고, 어떤 것은 배포 흐름이며, 어떤 것은 원하는 상태를 유지하는 제어 시스템입니다.