Rust에서의 육각형 아키텍처(Hexagonal Architecture), 그리고 이것이 왜 코딩 에이전트(Coding Agents)가 읽기
요약
코딩 에이전트가 복잡한 코드베이스를 효과적으로 이해하고 수정할 수 있도록 돕는 육각형 아키텍처의 중요성을 다룹니다. Rust의 강력한 타입 시스템과 크레이트 구조를 활용하여 의존성 규칙을 강제하고 에이전트 친화적인 환경을 구축하는 방법을 설명합니다.
핵심 포인트
- 육각형 아키텍처는 에이전트에게 명확한 경계와 명세를 제공함
- 핵심 규칙: 모든 의존성은 안쪽(Core)을 향해야 함
- Rust의 크레이트 시스템을 통해 의존성 규칙을 컴파일 단계에서 강제 가능
- 도메인, 애플리케이션, 어댑터, 서버 레이어로 구조화
코딩 에이전트(Coding Agents)는 로컬 편집에는 능숙하지만, 전체 시스템을 머릿속에 담아두는 데는 서툽니다. 함수 하나를 수정하라고 하면 잘 해냅니다. 하지만 데이터베이스 호출이 HTTP 핸들러 곳곳에 흩어져 있는 코드베이스에 "Postgres 지원을 추가해줘"라고 요청하면, 여섯 군데를 수정하며 매번 추측을 시도하다가 조용히 일곱 번째 곳을 망가뜨리기 시작합니다. 문제는 모델이 아닙니다. 문제는 코드베이스가 인간이든 기계든 누구에게도 무엇이 어디에 위치해도 되는지 알려준 적이 없다는 점입니다.
육각형 아키텍처(Hexagonal architecture)는 그것을 알려주는 방법입니다. 이는 오래된 아이디어(Alistair Cockburn이 2005년에 "포트와 어댑터(ports and adapters)"라고 명명함)이지만, 에이전트에게 정확히 필요한 구조임이 드러났습니다. 즉, 좁은 계약(narrow contracts), 제한된 폭발 반경(bounded blast radius), 그리고 코드를 작성하기 전에 읽을 수 있는 명세(spec)를 제공합니다. 이 포스트에서는 육각형 아키텍처가 무엇인지, Rust에서 어떻게 구현되는지, 그리고 사람이 읽기 좋게 유지해주는 동일한 경계가 왜 에이전트에게도 다루기 쉬운(tractable) 환경을 만드는지 살펴봅니다.
컴파일, 테스트 및 실행이 가능한 동반 리포지토리(repo)가 있습니다: llmgraph-ai/hexagonal-rust-template. 아래의 모든 내용은 그곳에 들어 있습니다.
단 하나의 규칙
육각형 아키텍처에는 많은 용어(포트(ports), 어댑터(adapters), 구동 측(driving side), 구동되는 측(driven side))가 있지만, 중요한 규칙은 단 하나뿐입니다:
의존성은 안쪽을 향합니다. 코어(core)는 아무것에도 의존하지 않습니다.
여러분의 비즈니스 로직(business logic)은 중앙에 위치합니다. 비즈니스 로직은 외부 세계로부터 필요한 인터페이스(데이터를 저장할 장소, 이메일을 보낼 방법 등)를 트레이트(traits)로 정의합니다. 비즈니스 로직은 그것을 무엇이 구현하는지 알 필요도 없고 상관하지도 않습니다. 모든 기술적인 요소(웹 프레워크, 데이터베이스 드라이버, JSON)는 가장자리에 위치하며 코어에 의존합니다. 절대로 그 반대는 성립하지 않습니다.
그림으로 그리면 화살표가 모두 중앙을 향하는 일련의 고리 형태가 됩니다:
server -> adapters -> application -> domain
\______________________________^
- domain은 핵심입니다: 비즈니스 타입과 포트 (ports, traits)를 포함합니다.
- application은 유스케이스 (use cases)이며, 해당 포트들을 대상으로 작성됩니다.
- adapters는 가장자리에 위치합니다: HTTP 핸들러, Postgres 클라이언트, 인메모리 페이크 (in-memory fake) 등이 이에 해당합니다.
- server는 컴포지션 루트 (composition root)입니다: 특정 어댑터를 핵심(core)에 연결하는 단 한 곳입니다.
Rust에서는 컴파일러가 이를 강제합니다
대부분의 언어는 의존성 규칙 (dependency rule)을 문서로 명시하고 그것이 지켜지기를 기대할 뿐입니다. 하지만 Rust는 이를 컴파일 에러로 만들 수 있게 해줍니다. 각 레이어를 별도의 크레이트 (crate)로 분리하고, 각 크레이트의 의존성 목록 (dependencies list)이 오직 안쪽(core)만을 가리키도록 설정하십시오. 이제 핵심(core)은 어댑터를 임포트 (import)할 수 없습니다. 어댑터의 크레이트가 핵심의 의존성 그래프 (dependency graph)에 포함되어 있지 않기 때문입니다. 이 규칙은 시간이 흐르며 침식되는 관습이 아니라, 빌드할 때마다 컴파일러가 확인하는 사실이 됩니다.
여기에 핵심(core) 코드가 있습니다. 무엇이 임포트되지 않았는지 주목하십시오: axum, sqlx, serde가 전혀 없습니다. 오직 표준 라이브러리 (standard library)와 두 개의 작은 유틸리티뿐입니다.
// crates/domain/src/lib.rs
pub struct ShortCode(String);
...
핵심(core)은 또한 포트 (ports)를 정의합니다. 즉, 세상으로부터 필요로 하는 인터페이스 (interfaces)를 트레이트 (traits)로 정의하는 것입니다.
// crates/domain/src/ports.rs
// 드라이븐 (driven, outbound) 포트: 링크를 저장할 어딘가. 핵심(core)은 이를 알지 못함
...
애플리케이션 레이어 (application layer)는 드라이븐 포트 (driven port)를 오케스트레이션 (orchestrating)함으로써 드라이빙 포트 (driving port)를 구현합니다. 이 파일은 복잡한 배관 작업 (plumbing) 없이 시스템의 동작만을 보여줍니다:
// crates/application/src/lib.rs
pub struct ShortenerService {
repo: Arc<dyn LinkRepository>, // 구체적인 DB가 아닌 포트에 의존함
...
HTTP 어댑터 (adapter)는 구체적인 서비스가 아닌 Shortener 포트에 의존합니다. 어댑터의 유일한 임무는 HTTP를 포트 호출로 변환하고, 그 결과를 다시 변환하는 것입니다. 여기에는 비즈니스 로직 (business logic)이 없으며, 바로 이 점 때문에 핵심(core)을 건드리지 않고도 이를 버리고 CLI로 교체할 수 있는 것입니다:
// crates/adapters/src/http.rs
async fn create_link(
State(state): State<AppState>,
...
그리고 모든 구체적인 타입 (concrete type)을 알고 있는 단 하나의 파일인 컴포지션 루트 (composition root)입니다:
// crates/server/src/main.rs
let repo: Arc<dyn LinkRepository> = Arc::new(InMemoryLinkRepository::default());
let shortener: Arc<dyn Shortener> = Arc::new(ShortenerService::new(repo));
...
메모리 대신 Postgres를 사용하고 싶나요? LinkRepository를 구현하는 PostgresLinkRepository를 작성하고 첫 번째 줄을 변경하면 됩니다. HTTP 대신 CLI를 사용하고 싶나요? 세 번째 줄을 변경하세요. 상위 계층(upstream)은 아무것도 알아차리지 못합니다. 상위 계층은 애초에 아무것도 알지 못했기 때문입니다.
에이전트가 이 방식에서 번창하는 이유
이제 보상, 즉 이 오래된 패턴을 2026년에 다시 살펴볼 가치가 있는 이유입니다. 육각형 아키텍처 (Hexagonal Architecture)를 인간에게 즐겁게 만드는 모든 속성은 에이전트 (Agent)가 필요로 하는 요소와 직접적으로 매핑됩니다.
작업(Task)이 명확한 계약(Contract)을 가진 단일 파일로 매핑됩니다. "Postgres 어댑터를 추가해줘"라는 요청은 무엇이든 건드릴 수 있는 모호한 요청이 아닙니다. 그것은 다음과 같습니다: "이 새로운 파일 하나에 이 하나의 트레이트 (Trait)를 구현해줘." 에이전트는 충족해야 할 정확한 시그니처 (Signature)를 가지며, 어댑터 크레이트 (Adapter crate)가 애플리케이션의 내부 구현 (Private application internals)에 의존하지 않기 때문에 작업을 수행하는 동안 비즈니스 로직에 물리적으로 접근할 수 없습니다. 작업의 범위는 당신의 프롬프트 (Prompt)가 아니라 아키텍처에 의해 결정됩니다.
포트 (Ports)가 곧 명세 (Spec)입니다. 에이전트는 단 하나의 어댑터도 존재하기 전에, 트레이트 (Trait) 정의만으로 올바른 유스케이스 (Use-case) 테스트를 작성할 수 있습니다. 템플릿에서 유스케이스 테스트는 다섯 줄짜리 가짜 리포지토리 (Fake repository)를 대상으로 실행됩니다:
#[tokio::test]
async fn shorten_then_resolve_roundtrips() {
let service = ShortenerService::new(Arc::new(FakeRepo::default()));
...
데이터베이스도, HTTP 서버도, 피스처 (Fixtures)도 필요 없습니다. 코어 (Core)의 테스트 커버리지를 높여달라는 요청을 받은 에이전트는 포트 (Port)로부터 필요한 모든 것을 얻을 수 있으며, 모킹 (Mocking)해야 할 불필요한 것들은 전혀 없습니다.
폭발 반경(Blast radius)이 구조적으로 제한됩니다. 이것이 가장 중요한 핵심입니다. 잘 계층화된 코드베이스에서 에이전트가 잘못된 방향으로 나아갈 때, 그 실수는 해당 에이전트가 작업하던 계층 내부에 머뭅니다. HTTP 어댑터 (Adapter)에서의 잘못된 결정이 도메인 (Domain)을 오염시킬 수 없는데, 이는 컴파일러 (Compiler)가 도메인이 어댑터를 볼 수 없도록 차단하기 때문입니다. 국소적으로 잘못될 수 있다는 것, 그것이 수정 비용이 저렴한 유일한 종류의 오류입니다. 데이터베이스 호출이 요청 핸들러 (Request handler) 내부에 나타날 수 있는 코드베이스에서는 이러한 바닥(floor)이 없습니다. 혼란스러운 편집이 그대로 전파됩니다.
구조가 컨텍스트 제한 (Context limits)을 극복합니다. 에이전트는 하나의 어댑터를 작업하기 위해 저장소 전체를 로드할 필요가 없습니다. 관련 있는 표면 (Surface)은 해당 에이전트가 구현하는 포트 (Port)와 그 에이전트가 속한 크레이트 (Crate)뿐입니다. 이 아키텍처는 코드베이스를 독립적으로 추론할 수 있을 만큼 작은 단위로 미리 분할(pre-chunks)해 두는데, 이는 애초에 인간에게도 이 구조가 유용했던 것과 같은 이유입니다. 좋은 구조는 좋은 구조입니다. 에이전트는 그 구조를 갖추었을 때의 이점을 더욱 극대화할 뿐입니다.
시작하는 방법
모든 것을 네 개의 크레이트로 다시 작성하는 것부터 시작하지 마세요. 하나의 규칙과 하나의 이음매 (Seam)부터 시작하세요. 가장 고통을 주는 의존성(보통 데이터베이스)을 선택하고, 이를 코어 (Core) 내에서 트레이트 (Trait)로 정의하세요. 구체적인 클라이언트 (Concrete client)를 해당 트레이트를 구현하는 어댑터 뒤로 옮기세요. 그리고 한 곳에서 이를 연결(Wire it up)하세요. 그러면 코어가 비즈니스와 관련 없는 것들을 임포트(Import)하는 것을 멈추게 되어, 즉시 코어가 더 정숙해지는 것을 느낄 수 있을 것입니다.
그 후, 충분한 가치가 있다고 판단되면, 지금까지 수동으로 유지해 온 규칙을 컴파일러가 강제할 수 있도록 링 (Rings)을 크레이트로 분리하세요. Rust는 이러한 강제를 비용 없이 수행하며, 이러한 무료 강제 여부가 현재 당신이 가진 아키텍처와 6개월 전 당신이 가졌던 아키텍처의 차이를 만듭니다.
전체 템플릿은 컴파일, 테스트 및 실행이 가능합니다: github.com/llmgraph-ai/hexagonal-rust-template. 이를 클론(Clone)하고, cargo test를 실행한 뒤, URL 단축기(URL shortener)를 삭제하고 그 형태를 유지해 보세요.
이것이 바로 LLMGraph에서 우리가 중요하게 생각하는 구조화된 백엔드의 모습입니다. 우리는 사람들이 API로 배포할 수 있는 LLM 및 AI 에이전트 (AI-agent) 워크플로우를 구축합니다. 읽기 쉬운 구조 (Legible structure)는 우리 팀과 우리의 도구들이 중요한 부분을 망가뜨리지 않으면서도 빠르게 움직일 수 있게 해주는 핵심 요소입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기