소프트웨어 아키텍처(Software Architecture)는 시스템을 구성하는 주요 요소를 어떻게 나누고, 서로 어떤 관계를 가지도록 설계할 것인지 결정하는 상위 수준의 설계다.
클래스 하나의 구조를 결정하는 것보다 범위가 크다.
예를 들어 게임 서버를 만든다고 하면 단순히 Player 클래스나 Packet 클래스를 어떻게 작성할지를 결정하는 것이 아니라,
Client -> Network -> Session -> Game Logic -> Database -> Cache
각 영역을 어떻게 분리할지, 서로 어떤 방식으로 통신할지, 데이터와 책임을 어디에 둘지를 결정하는 것이 아키텍처에 해당한다.
아키텍처의 목적은 특정 구조를 사용하는 것 자체가 아니다.
시스템이 커지고 변경되더라도 복잡성을 관리할 수 있는 구조를 만드는 것이 핵심이다.
아키텍처와 설계의 차이
아키텍처와 디자인 패턴, 클래스 설계는 서로 연결되어 있지만 범위가 다르다.
대략적인 관계를 보면 다음과 같다.
Software Architecture -> 시스템 구성 / 모듈 분리 / 데이터 흐름 / 통신 방식 / 배포 구조 -> Software Design -> 클래스 / 인터페이스 / 객체 관계 -> Design Pattern -> Factory / Strategy / Observer / Adapter
예를 들어 서버를 Layered Architecture로 구성한다고 하자.
Presentation -> Application -> Domain -> Infrastructure
이것이 아키텍처 수준의 결정이다.
그리고 Domain 내부에서 Strategy 패턴을 사용하거나 Factory를 사용하는 것은 세부 설계에 가깝다.
좋은 아키텍처란 무엇인가
좋은 아키텍처를 판단할 때 클래스 수나 파일 수를 기준으로 보면 안 된다.
핵심은 변경의 영향 범위를 관리할 수 있는가다.
예를 들어 DB를 MySQL에서 PostgreSQL로 교체한다고 하자.
아키텍처가 잘 분리되어 있다면
Game Logic -> Repository Interface -> MySQL Repository
에서
Game Logic -> Repository Interface -> PostgreSQL Repository
로 바꾸는 정도로 끝낼 수 있다.
반대로 게임 로직 곳곳에서 SQL을 직접 실행하고 있다면 DB를 바꾸는 순간 많은 코드가 영향을 받는다.
따라서 아키텍처에서 중요한 것은 특정 기술보다 의존성의 방향과 변경의 경계다.
관심사의 분리
아키텍처에서 가장 기본이 되는 개념 중 하나다.
하나의 시스템 안에서도 서로 다른 관심사를 분리한다.
예를 들어 게임 서버라면
Network / Session / Authentication / Game Logic / Database / Cache / Logging / Monitoring
같은 영역이 존재할 수 있다.
이를 하나의 거대한 클래스나 모듈에 넣으면 처음에는 개발하기 편해 보인다.
하지만 시간이 지나면 문제가 생긴다.
GameServer -> Network / Authentication / Game Logic / Database / Cache / Logging / ...
한 부분을 수정할 때 다른 부분까지 영향을 받을 가능성이 커진다.
관심사를 분리하면
Network / Authentication / Game / Persistence / Infrastructure
처럼 각 영역의 책임을 구분할 수 있다.
이것이 아키텍처의 출발점이다.
계층형 아키텍처
가장 익숙한 형태 중 하나다.
대표적으로 다음과 같이 구성한다.
Presentation -> Application -> Domain -> Infrastructure
각 계층은 자신이 담당하는 역할을 가진다.
Presentation
외부 요청을 받아들이는 영역이다.
웹이라면 Controller나 API Endpoint가 될 수 있고, 게임 서버라면 Packet Handler가 될 수도 있다.
HTTP Request / Packet / RPC -> Presentation
등을 받아 내부 로직으로 전달한다.
Application
하나의 작업 흐름을 조정한다.
예를 들어
회원 가입 -> 입력 검증 -> 사용자 생성 -> 저장 -> 이벤트 발생
같은 Use Case를 처리할 수 있다.
Domain
서비스가 해결하려는 핵심 비즈니스 규칙을 담당한다.
게임이라면
Damage Calculation / Character Rule / Item Rule / Combat Rule
등이 들어갈 수 있다.
Infrastructure
DB, 파일, 네트워크 외부 서비스처럼 구체적인 기술 구현을 담당한다.
MySQL / Redis / File System / External API / Message Queue
등이 여기에 들어갈 수 있다.
의존성의 방향
계층을 나누는 것보다 더 중요한 것이 의존성이다.
잘못된 구조에서는 Domain이 직접 DB 구현에 의존할 수 있다.
Domain -> MySQL
그러면 핵심 로직이 특정 DB 기술에 묶인다.
반대로 인터페이스를 사용하면
Domain -> IRepository <- MySQLRepository
구조를 만들 수 있다.
Domain은 MySQL을 알 필요가 없다.
실제 구현은 Infrastructure에 존재하고 Domain은 필요한 기능의 추상적인 계약만 알고 있다.
이 구조가 의존성 역전(Dependency Inversion)의 핵심적인 형태다.
Clean Architecture
Clean Architecture는 로버트 C. 마틴이 제안한 아키텍처 개념이다.
핵심 아이디어는 비즈니스 규칙을 외부 기술로부터 분리하는 것이다.
대표적인 구조는 다음과 같이 표현할 수 있다.
Frameworks & Drivers -> Interface Adapters -> Application Business Rules -> Enterprise Business Rules
중심으로 갈수록 시스템의 핵심 규칙이 위치한다.
외부의 DB, UI, 프레임워크가 핵심 비즈니스 로직에 직접 침투하지 않도록 의존성 방향을 관리한다.
예를 들어
Game Logic -> IPlayerRepository <- MySQLPlayerRepository
처럼 구성하면 게임 로직이 DB 구현에 종속되지 않는다.
Clean Architecture는 결국 의존성의 방향을 제어하는 아키텍처로 이해하면 된다.
Hexagonal Architecture
Hexagonal Architecture는 Ports and Adapters Architecture라고도 한다.
핵심 로직을 중심에 두고 외부 세계와의 연결을 Port와 Adapter로 분리한다.
HTTP -> Adapter -> Domain <- Adapter <- Game Client
DB -> Adapter -> Domain
Domain은 외부 시스템을 직접 알지 않는다.
외부 시스템과 연결하기 위한 인터페이스가 Port이고, 실제 기술을 연결하는 구현이 Adapter다.
예를 들어
IUserRepository
가 Port라면
MySQLUserRepository / RedisUserRepository
가 Adapter가 될 수 있다.
이 구조는 테스트에서도 유용하다.
실제 DB 대신
MockUserRepository
를 넣어 핵심 로직을 테스트할 수 있기 때문이다.
이벤트 기반 아키텍처
Event-Driven Architecture는 시스템의 상태 변화나 사건을 Event로 표현하고 이를 다른 컴포넌트가 처리하는 구조다.
예를 들어
PlayerRegistered -> Send Welcome Mail / Create Achievement / Log / Analytics
여러 시스템이 이를 구독할 수 있다.
직접 호출하는 구조라면
PlayerService -> MailService -> AchievementService -> AnalyticsService -> LogService
처럼 연결될 수 있다.
이벤트 기반 구조에서는
PlayerService -> Event Bus -> Mail / Achievement / Analytics / Log
처럼 분리할 수 있다.
시스템 간 결합도를 낮출 수 있다는 장점이 있지만 이벤트 흐름이 많아지면 디버깅과 데이터 일관성 관리가 어려워진다.
따라서 모든 통신을 이벤트로 바꾸는 것은 좋은 설계가 아니다.
Message Queue 기반 아키텍처
Message Queue는 시스템 간 통신을 메시지 단위로 분리한다.
Producer -> Message Queue -> Consumer
Producer는 Consumer가 즉시 처리할 필요 없이 메시지를 Queue에 넣을 수 있다.
대표적으로
Kafka / RabbitMQ / Amazon SQS
등을 사용할 수 있다.
예를 들어 게임 서버에서 로그나 통계 데이터를 처리한다면
Game Server -> Kafka -> Analytics / Log Processor / Data Pipeline
처럼 구성할 수 있다.
게임 로직과 분석 시스템을 직접 연결하지 않아도 된다는 장점이 있다.
반면 메시지 지연, 중복 처리, 순서 보장, 장애 복구 등의 문제가 추가된다.
Microservices Architecture
Monolithic Architecture에서는 하나의 애플리케이션 안에 여러 기능이 함께 존재한다.
Application -> User / Payment / Game / Inventory / Notification
Microservices Architecture에서는 기능을 여러 서비스로 분리한다.
User Service / Payment Service / Game Service / Inventory Service / Notification Service
각 서비스는 독립적으로 배포하고 운영할 수 있다.
장점은 서비스별 확장과 배포가 가능하다는 것이다.
예를 들어 트래픽이 많은 Payment Service만 별도로 확장할 수 있다.
하지만 대가가 크다.
Microservices -> Network / Service Discovery / Load Balancing / Monitoring / Logging / Distributed Transaction / Deployment / Security
같은 문제가 추가된다.
따라서 서비스가 많다고 좋은 아키텍처가 되는 것은 아니다.
작은 서비스 여러 개를 운영하는 것보다 잘 구성된 Monolith 하나가 훨씬 관리하기 쉬운 경우도 많다.
Monolithic Architecture
Monolith는 하나의 애플리케이션으로 시스템을 구성하는 방식이다.
Application -> User / Game / Payment / Inventory / Notification
단순하다는 것이 가장 큰 장점이다.
하나의 프로세스에서 동작하므로
함수 호출 / 메모리 접근 / DB 접근 / 트랜잭션 / 디버깅 / 배포
등을 상대적으로 단순하게 관리할 수 있다.
문제는 시스템이 커지면서 모듈 간 결합도가 높아지는 경우다.
따라서 Monolith라고 해서 무조건 나쁜 구조는 아니다.
잘 분리된 Modular Monolith는 상당히 강력한 구조가 될 수 있다.
Modular Monolith
Modular Monolith는 배포 단위는 하나지만 내부적으로 모듈을 강하게 분리한 구조다.
Application -> User Module / Game Module / Inventory Module / Payment Module / Notification Module
각 모듈 사이의 접근 규칙을 제한한다.
예를 들어
Game Module -> Payment Module
의 내부 구현을 직접 접근하지 못하도록 만들 수 있다.
필요한 인터페이스만 공개한다.
이 구조는 Microservices로 넘어가기 전에 사용할 수도 있고, 처음부터 Microservices의 복잡성을 가져가지 않고 모듈 간 경계를 명확하게 만들고 싶을 때도 유용하다.
MVC
MVC는 애플리케이션의 책임을 Model, View, Controller로 나누는 구조다.
Controller -> Model -> View
전통적인 웹 애플리케이션이나 GUI 애플리케이션에서 많이 사용되었다.
Model
데이터와 비즈니스 상태를 담당한다.
View
사용자에게 보여지는 화면을 담당한다.
Controller
사용자의 요청을 받아 Model과 View 사이의 흐름을 제어한다.
MVC의 핵심은 세 글자를 외우는 것이 아니라 화면 처리와 데이터 및 로직의 책임을 분리하는 것이다.
MVP와 MVVM
UI 아키텍처에서는 MVC 외에도 여러 변형이 존재한다.
MVP는 Model, View, Presenter로 구성한다.
View -> Presenter -> Model
View와 로직 사이를 Presenter가 연결한다.
MVVM은 Model, View, ViewModel로 구성한다.
View -> ViewModel -> Model
특히 데이터 바인딩을 지원하는 UI 프레임워크에서 자주 사용된다.
WPF의 구조를 이해할 때 MVVM이 대표적인 예다.
이런 패턴들은 결국 UI와 애플리케이션 로직의 결합도를 낮추려는 목적을 공유한다.
게임 아키텍처
게임에서는 일반적인 웹 서비스와 다른 구조도 많이 등장한다.
대표적으로 게임 루프를 중심으로 시스템을 구성할 수 있다.
Game Loop -> Input / Simulation / Physics / AI / Animation / Rendering / Audio
게임 서버에서는
Network -> Session -> Packet Handler -> Game Logic -> Repository / Cache -> Database
같은 구조를 생각할 수 있다.
멀티플레이어 게임에서는 서버가 권위 있는 상태를 관리하는 Server Authoritative 구조도 중요하다.
Client -> Request -> Server -> Validate -> Simulate -> Update State -> Game State
클라이언트가 모든 게임 상태를 결정하게 만들면 치팅이나 동기화 문제를 해결하기 어려워진다.
게임 아키텍처에서는 일반적인 코드 구조뿐만 아니라 실시간성, 동기화, 상태 관리, 네트워크 지연, 서버 권위 같은 요구사항까지 고려해야 한다.
아키텍처에서 중요한 품질 속성
아키텍처는 기능만 고려해서 결정하지 않는다.
비기능 요구사항도 중요하다.
대표적으로 다음과 같은 것들이 있다.
성능 / 확장성 / 가용성 / 안정성 / 보안 / 유지보수성 / 테스트 가능성 / 배포 용이성 / 관측 가능성
예를 들어 초당 수만 개의 요청을 처리해야 하는 시스템과 하루에 수백 개의 요청만 처리하는 시스템은 같은 구조를 사용할 이유가 없다.
또한 실시간 게임 서버와 관리자용 웹 페이지의 요구사항도 다르다.
아키텍처는 결국 시스템의 요구사항에 대한 설계 결과다.
아키텍처와 성능
아키텍처가 복잡해질수록 성능 비용이 발생할 수 있다.
예를 들어
Game Logic -> Service -> Repository -> Network -> Another Service -> Database
같은 호출이 지나치게 많아지면 단순한 함수 호출보다 비용이 커진다.
특히 분산 시스템에서는 네트워크 통신이 들어간다.
Function Call
과
Network Request
는 비용 구조가 완전히 다르다.
그래서 Microservices를 적용할 때 “서비스를 작게 나누면 좋은 것 아닌가?”라고 생각하면 안 된다.
서비스 경계를 나누는 순간 네트워크라는 새로운 경계가 생긴다.
아키텍처는 유지보수성만 높이는 문제가 아니라 성능과 복잡성 사이의 트레이드오프를 결정하는 문제이기도 하다.
아키텍처와 데이터
시스템 아키텍처를 설계할 때 데이터의 흐름도 중요하다.
예를 들어
Client -> API Server -> Cache -> Database
구조라면 Cache와 Database의 역할을 명확하게 구분해야 한다.
Cache가 원본 데이터인지 임시 데이터인지에 따라 장애 대응 방식도 달라진다.
분산 시스템에서는 더 복잡하다.
Service A -> Database A
Service B -> Database B
각 서비스가 데이터를 독립적으로 관리한다면 서비스 간 데이터를 어떻게 동기화할지 결정해야 한다.
이 과정에서
Event / Message Queue / Transaction / Eventual Consistency / Saga
같은 개념이 등장한다.
아키텍처는 결국 코드 구조뿐 아니라 데이터와 상태가 시스템 안에서 어떻게 이동하는가까지 포함한다.
아키텍처 결정은 트레이드오프다
아키텍처에는 모든 상황에서 좋은 구조가 존재하지 않는다.
예를 들어
Monolith -> 단순한 배포와 개발
이 장점이다.
반면
Microservices -> 독립적인 배포와 확장
에 유리하다.
하지만 운영 복잡성은 증가한다.
또한
Sync Communication -> 이해하기 쉬운 흐름 / 즉각적인 응답
은 호출하는 서비스가 즉시 응답해야 한다.
반대로
Async Communication -> 낮은 결합도 / 높은 확장성
은 메시지 처리와 장애 상황을 추가로 관리해야 한다.
따라서 아키텍처를 선택할 때는
현재 요구사항 -> 예상되는 변경 -> 트래픽 -> 팀 규모 -> 운영 환경 -> 장애 허용 수준 -> 개발 비용
을 함께 봐야 한다.
아키텍처를 설계할 때 보는 것
처음부터 특정 아키텍처를 정해놓고 시스템을 끼워 맞추는 것보다 먼저 문제를 보는 편이 낫다.
먼저 시스템의 주요 책임을 나눈다.
무엇을 하는 시스템인가? -> 어떤 기능이 핵심인가? -> 어떤 데이터가 중요한가?
그다음 변경 가능성이 높은 부분을 본다.
DB가 변경될 수 있는가? -> 외부 API가 변경될 수 있는가? -> UI가 변경될 수 있는가? -> 서비스가 분리될 가능성이 있는가?
그리고 시스템 간 경계를 정한다.
어떤 모듈이 어떤 모듈을 호출할 수 있는가? -> 어떤 데이터에 접근할 수 있는가? -> 동기 통신이 필요한가? -> 비동기 처리가 가능한가?
마지막으로 운영을 고려한다.
어떻게 배포하는가? -> 어떻게 장애를 감지하는가? -> 어떻게 로그를 확인하는가? -> 어떻게 확장하는가?
아키텍처는 코드를 작성하기 전에 끝나는 문서가 아니다.
구현하고 운영하면서 계속 검증하고 수정해야 한다.
정리
소프트웨어 아키텍처는 시스템을 어떻게 구성하고 각 요소의 책임과 의존성을 어떻게 관리할 것인지 결정하는 상위 수준의 설계다.
대표적인 구조를 정리하면 다음과 같다.
Layered Architecture -> 계층별 책임 분리
Clean Architecture -> 핵심 비즈니스 로직과 외부 기술 분리
Hexagonal Architecture -> Port와 Adapter를 통한 외부 시스템 분리
Event-Driven Architecture -> 이벤트를 통한 비동기적 협력
Microservices -> 서비스를 독립적인 배포 단위로 분리
Modular Monolith -> 하나의 애플리케이션 안에서 모듈 경계를 강하게 분리
MVC / MVP / MVVM -> UI와 애플리케이션 로직 분리
하지만 아키텍처의 본질은 특정 패턴이나 이름에 있지 않다.
요구사항 -> 책임 분리 -> 경계 설정 -> 의존성 관리 -> 데이터 흐름 설계 -> 통신 방식 결정 -> 운영 방식 결정
이 과정을 통해 시스템의 복잡성을 통제하는 것이 아키텍처다.
좋은 아키텍처는 클래스가 많거나 구조가 거창한 아키텍처가 아니다.
변경이 필요한 곳을 격리하고, 시스템의 핵심 로직을 불필요한 기술과 결합시키지 않으며, 현재 요구사항에 맞는 복잡성만 가지는 구조가 좋은 아키텍처에 가깝다.
아키텍처를 공부할 때도 패턴의 이름부터 외우기보다 “이 구조가 어떤 문제를 해결하기 위해 존재하는가”부터 보는 편이 훨씬 낫다.