- Published on
axum rust crate: 라우팅과 미들웨어 구조 이해하기
- Authors

- Name
- Indo Yoon
axum rust crate는 tokio 위에서 동작하는 HTTP 라우팅·요청 처리 라이브러리이고, 핵심은 "라우터에 경로를 등록하고 핸들러 인자로 요청 데이터를 타입으로 선언한다"는 방식이에요. 미들웨어를 자체적으로 만들지 않고 tower 생태계의 레이어를 그대로 조합한다는 점이 다른 프레임워크와 갈리는 지점이에요. 그래서 처음에는 라우터와 추출자, 그다음에 상태 공유와 레이어 순서로 익히는 편이 가장 덜 헤매요.
한눈에 보기
axum을 처음 접하면 문서에 등장하는 개념이 많아 보이지만, 실제로 손대는 지점은 몇 가지로 좁혀져요. 경로를 등록하는 Router, 요청을 처리하는 Handler, 요청 데이터를 꺼내는 Extractor, 여러 핸들러가 공유하는 State, 그리고 공통 처리를 담당하는 Layer예요. 이 다섯 가지가 서로 어떻게 맞물리는지만 잡으면 나머지는 문서 예제를 옮겨 오는 수준에서 해결돼요.
특히 기억할 점은 axum이 tower의 Service 추상화를 그대로 받아들인다는 점이에요. 그래서 로깅, 타임아웃, 압축, 요청 추적 같은 기능을 직접 구현하지 않고 이미 공개된 레이어를 붙이는 방식으로 처리해요. 반대로 말하면 tower의 레이어 개념을 모르면 미들웨어 부분에서 막히기 쉬워요.
정리하면 학습 순서는 라우터와 핸들러, 추출자, 상태 공유, 레이어, 에러 처리, 실행과 종료 순서가 자연스러워요. 이 글도 같은 순서로 진행해요.
axum rust crate를 구성하는 요소
| 구성 요소 | 역할 | 처음 만질 때 확인할 점 |
|---|---|---|
| Router | 경로와 핸들러를 등록하고 라우터를 합침 | 경로 충돌 여부, merge와 nest의 차이 |
| Handler | 요청을 받아 응답으로 바꾸는 비동기 함수 | 반환 타입이 응답으로 변환 가능한지 |
| Extractor | 경로·쿼리·본문·헤더를 인자로 받아냄 | 본문을 소비하는 추출자의 위치 |
| State | 핸들러들이 공유하는 값 | 상태 타입을 라우터에 언제 확정하는지 |
| Layer | tower 미들웨어를 라우터나 핸들러에 적용 | 적용 순서와 요청 흐름의 방향 |
| Service | tower의 공통 인터페이스 | 직접 구현보다 기존 레이어 재사용 |
표에서 눈여겨볼 항목은 Extractor와 Layer예요. 추출자는 핸들러 시그니처에 그대로 드러나기 때문에 컴파일 단계에서 상당 부분 검증되지만, 본문을 소비하는 추출자를 앞쪽에 두면 뒤쪽 추출자가 본문을 읽지 못해 컴파일 오류가 나요. 레이어는 코드에 적힌 순서와 실제 요청이 지나가는 순서가 반대라는 점도 자주 헷갈리는 부분이에요.
다른 Rust 웹 프레임워크와의 결
| 항목 | axum | actix-web | warp |
|---|---|---|---|
| 기반 | tokio와 tower 생태계 | 자체 액터 계열 설계 | 필터 조합 |
| 확장 방식 | tower 레이어 재사용 | 자체 미들웨어 체계 | 필터 합성 |
| 학습 부담 | 추출자와 타입 추론 이해가 필요 | 매크로와 액터 개념 이해가 필요 | 필터 타입 오류 메시지가 길어짐 |
| 잘 맞는 경우 | tokio 기반 서비스에 공통 레이어를 얹을 때 | 기존 actix 생태계 자산을 쓸 때 | 함수형 조합 방식을 선호할 때 |
이 표는 각 프로젝트의 공식 문서가 설명하는 설계 방향을 요약한 것이고, 성능 우열을 뜻하지 않아요. 선택 기준은 이미 쓰고 있는 런타임과 미들웨어 자산이 무엇인지에 가까워요. tokio와 tower 기반 도구를 이미 쓰고 있다면 axum 쪽이 이어 붙이기 쉬운 편이에요.
단계별 도입 방법
첫 단계: 프로젝트와 의존성 준비
먼저 cargo로 바이너리 프로젝트를 만들고 axum과 tokio를 의존성에 추가해요. tokio는 매크로와 네트워킹 기능이 필요하므로 어떤 기능 플래그를 켜는지 Cargo.toml에서 직접 확인하는 편이 좋아요. 로깅이나 압축, 정적 파일 제공이 필요하면 tower-http를 함께 추가하고, 필요한 기능만 켜서 빌드 시간을 줄여요.
두 번째 단계: 라우터와 핸들러
Router를 만들고 경로마다 메서드 라우팅을 등록해요. 핸들러는 비동기 함수로 작성하고, 반환 타입은 응답으로 변환 가능한 형태면 충분해요. 라우터가 커지면 기능 단위로 Router를 따로 만들고 merge나 nest로 합치는 방식이 유지보수에 유리해요. nest는 경로 접두사를 붙여 하위 라우터를 얹는 개념이라, 버전이 다른 API를 나눌 때 유용해요.
세 번째 단계: 추출자로 입력 받기
경로 파라미터, 쿼리 문자열, JSON 본문, 헤더는 각각 대응하는 추출자로 받아요. 핸들러 인자에 타입을 적으면 프레임워크가 알아서 역직렬화를 시도하고, 실패하면 거절 응답으로 바뀌어요. 이때 본문을 소비하는 추출자는 반드시 인자 목록의 마지막에 두세요. 본문은 한 번만 읽을 수 있기 때문이에요.
네 번째 단계: 상태 공유
데이터베이스 연결 풀이나 설정 값처럼 여러 핸들러가 함께 쓰는 값은 상태로 등록해요. 상태 타입은 복제 가능한 형태로 만들고, 라우터에 상태를 붙이는 시점에 타입이 확정돼요. 핸들러에서는 상태 추출자로 꺼내 쓰면 되고, 내부에 공유가 필요한 자원이 있으면 그 자원만 별도로 감싸서 관리해요.
다섯 번째 단계: 미들웨어와 공통 처리
공통 처리는 tower의 레이어를 조합해서 붙여요. 요청 추적, 타임아웃, 압축, CORS 같은 기능은 tower-http에 이미 준비된 레이어가 있으니 직접 만들기 전에 문서를 먼저 확인하는 편이 좋아요. 레이어는 라우터 전체에 걸 수도 있고 특정 경로 그룹에만 걸 수도 있어요. 코드에 적은 순서와 요청이 통과하는 순서가 반대라는 점을 항상 염두에 두세요.
여섯 번째 단계: 에러 처리와 응답
핸들러가 실패할 수 있다면 Result를 반환하고, 에러 타입이 응답으로 변환되도록 구현해요. 도메인 에러를 그대로 노출하지 말고 상태 코드와 메시지를 정해서 변환하는 편이 안전해요. 응답 형식을 공통 구조로 통일해 두면 클라이언트 쪽 처리도 단순해져요.
일곱 번째 단계: 서버 실행과 종료
리스너를 열고 라우터를 넘겨 서버를 실행해요. 배포 환경에서는 종료 신호를 받았을 때 진행 중인 요청을 마무리하는 처리를 함께 넣는 것이 좋아요. 컨테이너 환경이라면 이 부분이 없을 때 배포마다 요청이 끊기는 문제가 생겨요.
상황별 선택 기준
작은 API라면 라우터 하나에 핸들러를 모아도 충분해요. 반대로 도메인이 늘어나면 라우터를 기능 단위로 쪼개고 nest로 묶는 쪽이 리뷰하기 쉬워요. 인증이나 요청 추적처럼 모든 경로에 걸리는 정책은 최상위 라우터에 레이어로 붙이고, 특정 그룹에만 필요한 정책은 해당 라우터에만 붙이세요. WebSocket이나 정적 파일 제공, 리버스 프록시가 필요하다면 공식 문서와 예제 저장소에서 해당 기능의 기능 플래그와 사용 조건을 먼저 확인하는 편이 안전해요.
주의할 점
추출자 순서를 잘못 두면 컴파일 오류가 나거나 본문이 비어 있는 상태로 핸들러가 실행돼요. 경로가 겹치는 라우터를 합칠 때는 어느 쪽이 우선하는지 확인하고, 동적 경로와 정적 경로가 충돌하지 않도록 구조를 정리하세요. 레이어는 겉으로 보이는 순서와 실제 흐름이 반대이므로, 요청 추적과 타임아웃을 함께 쓸 때 어느 레이어가 바깥에 있어야 하는지 문서 예제로 확인하는 것이 좋아요. 상태 타입을 중간에 바꾸면 라우터 전체 타입이 흔들리므로 초기에 확정해 두세요. 마지막으로 버전을 올릴 때는 릴리스 노트와 변경 기록을 먼저 읽고, 컴파일 오류를 기준으로 마이그레이션 범위를 가늠하세요.
자주 묻는 질문
axum과 actix-web 중 무엇을 선택해야 하나요?
이미 tokio와 tower 기반 도구를 쓰고 있다면 axum 쪽이 이어 붙이기 쉬워요. 반대로 actix 생태계의 기존 자산이 많다면 그쪽을 유지하는 편이 비용이 적어요. 성능 비교 수치보다는 런타임과 미들웨어 재사용 가능성을 기준으로 판단하세요.
미들웨어는 어떤 순서로 적용되나요?
코드에서 바깥쪽에 선언한 레이어가 요청을 먼저 받고 응답을 나중에 돌려줘요. 그래서 요청 추적은 바깥쪽, 타임아웃은 그 안쪽처럼 역할에 따라 배치를 정해야 해요. 헷갈릴 때는 레이어를 하나씩 추가하면서 로그로 흐름을 확인하는 방법이 확실해요.
상태 공유는 반드시 감싸서 넘겨야 하나요?
상태 타입이 복제 가능하고 핸들러 사이에서 공유해도 안전한 형태라면 그대로 넘겨도 돼요. 내부에 커넥션 풀처럼 공유가 필요한 자원이 있다면 그 자원만 적절히 감싸서 관리하세요. 중요한 것은 라우터를 만들 때 상태 타입을 일관되게 유지하는 일이에요.
버전을 올릴 때 무엇을 확인해야 하나요?
저장소의 변경 기록과 릴리스 노트를 먼저 읽고, 컴파일러가 알려주는 오류를 기준으로 범위를 좁히세요. 미들웨어나 추출자 관련 시그니처가 바뀌는 경우가 많으므로, 라우터 구성과 핸들러 시그니처부터 확인하는 편이 빨라요.
hyper를 직접 다뤄야 하나요?
일반적인 API 서버라면 라우터와 핸들러 수준에서 해결되고, hyper를 직접 다룰 일은 드물어요. 다만 커스텀 서비스나 저수준 제어가 필요할 때는 공식 문서의 서비스 관련 설명을 참고하세요.
출처
- axum 공식 문서: https://docs.rs/axum/latest/axum/
- axum 저장소와 예제: https://github.com/tokio-rs/axum
- tokio 공식 사이트: https://tokio.rs
- tower-http 공식 문서: https://docs.rs/tower-http/latest/tower_http/
배포와 운영 흐름까지 함께 정리하고 싶다면 관련 운영 글도 참고해 보세요. Rust 서버 배포와 운영 체크리스트에서 런타임 설정, 종료 처리, 로그 수집처럼 axum 애플리케이션을 실제로 띄울 때 확인할 항목을 이어서 다뤄요.