- Published on
axum rust backend: Rust로 안정적인 API 서버 만들기
- Authors

- Name
- Indo Yoon
axum은 Tokio 팀이 관리하는 Rust 웹 프레임워크이고, 백엔드 API 서버를 새로 만들 때 가장 먼저 검토할 만한 선택지예요. 라우팅과 미들웨어를 Tower의 서비스 모델로 통일했기 때문에, 프레임워크 전용 문법보다 표준 생태계 지식이 그대로 쓰여요. 이 글은 axum rust backend 구성을 처음 잡는 백엔드 개발자를 위해 라우팅 설계부터 배포와 운영 점검까지 순서대로 정리해요.
한눈에 보기
axum의 핵심은 "핸들러는 그냥 async 함수"라는 점이에요. 라우터에 경로와 메서드를 등록하고, 핸들러의 인자로 필요한 값을 선언하면 프레임워크가 알아서 꺼내 줘요. 이때 인자 타입이 곧 검증 규칙이 돼서, 형식이 잘못된 요청은 핸들러 본문에 들어오기 전에 거절돼요.
미들웨어는 Tower의 Service와 Layer 개념을 그대로 써요. 로깅, 타임아웃, 압축, CORS 같은 기능을 프레임워크 전용 플러그인이 아니라 tower-http 같은 공용 크레이트에서 가져와 조합해요. 그래서 axum rust backend 구조를 이해하려면 Router, Service, Layer 세 단어의 관계를 먼저 잡는 게 좋아요.
서버 실행은 Tokio 런타임 위에서 axum::serve 계열 진입점을 호출하는 형태예요. 종료 신호를 받으면 새 연결을 막고 처리 중인 요청을 마무리하는 graceful shutdown도 같은 자리에서 연결해요. 여기에 tracing으로 요청 로그를 남기고 헬스체크 경로를 따로 두면 운영의 기본 골격이 완성돼요.
의존성은 많지 않아요. 웹 계층은 axum, 비동기 실행은 tokio, 직렬화는 serde와 serde_json, 로깅은 tracing과 tracing-subscriber 정도로 시작해요. 데이터베이스가 필요해지면 sqlx나 sea-orm 같은 크레이트를 추가하는데, 이 결정은 라우팅 구조를 잡은 뒤에 해도 늦지 않아요.
axum과 다른 Rust 웹 프레임워크 비교
| 항목 | axum | actix-web | warp |
|---|---|---|---|
| 기반 구조 | hyper 위에서 Tower 서비스 모델 사용 | 자체 런타임 계층과 액터 계열 설계, Tokio 생태계와 연동 | hyper 위에서 Filter 조합 사용 |
| 라우팅 표현 | Router에 경로와 메서드 라우터를 등록 | 매크로와 속성 기반 라우팅을 폭넓게 지원 | 필터를 and/or로 합성해 경로 구성 |
| 미들웨어 | Tower의 Layer와 Service를 그대로 사용 | 자체 미들웨어 트레이트 사용 | 필터 합성으로 전후 처리 |
| 요청 파싱 | FromRequestParts, FromRequest 추출기 | 추출기 트레이트 | 필터 단계에서 직접 추출 |
| 배울 것 | Tower 개념과 추출기 타입 규칙 | 프레임워크 문법 전반 | 필터 합성 사고방식 |
표에서 볼 수 있듯이 axum의 개성은 "프레임워크가 새로 정의하는 개념이 적다"는 쪽에 있어요. 대신 Tower를 이해해야 미들웨어를 자유롭게 쓸 수 있어요. actix-web은 프레임워크 안에서 해결되는 범위가 넓고, warp은 필터 합성이라는 독특한 표현력을 가져요. 어느 쪽이 더 낫다고 단정하기보다, 팀이 이미 익숙한 개념이 무엇인지로 고르는 편이 안전해요.
단계별로 axum 백엔드 구성하기
먼저 Cargo 프로젝트를 만들고 Cargo.toml에 의존성을 추가해요. tokio는 features = ["full"] 또는 필요한 기능만 골라 켜고, axum은 기본 기능으로 시작해요. JSON 응답이 필요하면 serde의 derive 기능을 함께 켜요.
다음은 라우터 구성이에요. Router::new()에 route로 경로를 등록하고, 각 경로에 get, post 같은 메서드 라우터를 붙여요. 경로 파라미터는 콜론 표기를 쓰고, 핸들러에서는 Path 추출기로 받아요. 이때 파라미터 타입을 정수나 UUID로 선언하면 변환 실패가 자동으로 거절 응답이 돼요.
핸들러는 async fn으로 작성하고 반환 타입은 impl IntoResponse로 두는 형태가 가장 흔해요. 문자열, Json<T>, (StatusCode, Json<T>), Result<T, E> 모두 응답으로 변환돼요. 그래서 성공 경로와 실패 경로를 같은 함수 안에서 타입으로 표현할 수 있어요.
상태 공유는 with_state로 라우터에 상태를 붙이고, 핸들러에서 State<T>로 꺼내는 방식이에요. 상태 타입은 Clone이어야 하고, 커넥션 풀처럼 내부에 공유 자원을 가진 타입은 보통 Arc로 감싸요. 상태를 라우터에 붙이면 하위 라우터 전체가 같은 상태를 참조해요.
마지막으로 실행 단계예요. Tokio 런타임을 켜고 리스너를 만든 뒤 axum::serve에 라우터를 넘겨요. 여기에 with_graceful_shutdown으로 종료 신호를 연결하면, 배포 중 연결이 끊기는 상황을 줄일 수 있어요.
라우팅과 추출기 설계 요령
추출기는 크게 두 갈래예요. 요청 본문을 소비하지 않는 FromRequestParts 계열과, 본문을 소비하는 FromRequest 계열이에요. Path, Query, HeaderMap, State는 앞쪽에 속하고, Json, Form, Bytes, String은 뒤쪽에 속해요. 이 구분을 알아 두면 "왜 이 추출기를 두 번 쓸 수 없지" 같은 문제를 스스로 풀 수 있어요.
핸들러 인자 순서에도 규칙이 있어요. 본문을 소비하는 추출기는 하나만 쓸 수 있고, 보통 마지막에 놓아요. 본문 추출기 뒤에 다른 추출기를 두면 컴파일 오류로 알려 주기 때문에, 런타임에야 발견되는 실수가 줄어요.
경로 설계는 리소스 중심으로 잡는 편이 유지보수에 유리해요. 목록과 단건 조회, 생성, 수정, 삭제를 같은 접두사 아래에 모으고, 검색 조건은 쿼리 파라미터로 분리해요. 버전이 필요한 API라면 접두사에 버전 구간을 두고, 구버전 라우터를 별도 Router로 유지하면 교체 시점을 통제할 수 있어요.
중첩 라우터는 nest로 구성해요. 도메인별로 Router를 나눠 두면 테스트에서 해당 라우터만 띄워 검증할 수 있어요. 이때 상태 타입이 라우터마다 다르면 with_state 적용 지점을 신중히 잡아야 해요.
미들웨어와 Tower 생태계
axum의 미들웨어는 Tower의 Layer예요. Router::layer는 모든 경로에, route_layer는 매칭된 경로에만 적용돼요. 그래서 인증처럼 경로별로 다른 처리는 route_layer에, 로깅이나 타임아웃처럼 전역 처리는 layer에 두는 식으로 나눠요.
여러 레이어를 조합할 때는 tower::ServiceBuilder를 써요. 위에서 아래로 감싸지는 순서를 코드로 명시할 수 있어서, 로그가 먼저인지 타임아웃이 먼저인지 같은 논쟁을 코드로 정리할 수 있어요. 순서를 바꾸면 동작이 달라지므로, 요청이 어떤 순서로 통과하는지 그림을 그려 두는 게 좋아요.
자주 쓰는 레이어는 tower-http에서 가져와요. 요청 추적은 TraceLayer, 압축은 압축 레이어, CORS는 CORS 레이어를 써요. 헤더 추가나 요청 본문 크기 제한도 레이어로 처리할 수 있어서, 핸들러 코드가 운영 관심사로 지저분해지지 않아요.
미들웨어에서 상태가 필요하면 from_fn_with_state 같은 헬퍼를 써요. 토큰 검증 결과를 요청 확장에 넣고 핸들러에서 Extension으로 꺼내는 패턴이 대표적이에요. 확장에 넣는 타입은 Clone이어야 하고, 이름이 겹치지 않도록 뉴타입으로 감싸는 편이 안전해요.
상태 공유와 데이터베이스 연결
상태는 애플리케이션 설정, 커넥션 풀, 캐시 클라이언트처럼 프로세스 수명 동안 유지되는 자원을 담아요. 구조체로 묶어 두면 핸들러 시그니처가 짧아지고 테스트에서 교체하기 쉬워요.
데이터베이스 연결은 보통 커넥션 풀을 상태에 넣고 핸들러에서 꺼내 써요. 트랜잭션이 필요한 핸들러는 풀에서 커넥션을 하나 잡아 트랜잭션을 열고, 성공 시 커밋하고 실패 시 롤백해요. 이 흐름을 서비스 계층으로 분리해 두면 핸들러는 입출력 변환에만 집중할 수 있어요.
블로킹 작업은 조심해야 해요. 파일 입출력이나 CPU가 무거운 연산을 async 핸들러 안에서 그대로 돌리면 런타임 스레드가 막혀요. 이런 작업은 spawn_blocking으로 넘기거나 별도 워커로 분리하는 편이 좋아요.
상태 초기화는 main에서 한 번만 하고 라우터에 붙여요. 테스트에서는 같은 구조체에 테스트용 풀을 넣어 재사용하면, 핸들러 코드를 바꾸지 않고도 통합 테스트를 만들 수 있어요.
에러 처리와 응답 설계
에러는 도메인 에러 타입을 하나 만들고 IntoResponse를 구현하는 방식이 깔끔해요. 핸들러는 Result<T, AppError>를 반환하고, ? 연산자로 내부 에러를 도메인 에러로 변환해요. 그러면 응답 형식이 한 곳에서 결정돼요.
클라이언트에 내려줄 정보와 로그에만 남길 정보를 분리해야 해요. 내부 오류 메시지나 스택 정보를 응답에 그대로 담지 않고, 요청 식별자를 함께 내려 추적에 쓰는 편이 안전해요. 로그에는 원인을 남기고 응답에는 일반화된 메시지를 담는 원칙이에요.
상태 코드 선택도 일관되게 가져가요. 검증 실패, 인증 실패, 권한 부족, 대상 없음, 충돌을 각각 구분해 두면 클라이언트가 분기하기 쉬워요. 응답 본문 스키마를 에러 전용으로 고정해 두면 프런트엔드와의 계약이 흔들리지 않아요.
배포와 운영에서 확인할 점
첫째, 종료 처리예요. 배포 도구가 프로세스에 종료 신호를 보냈을 때 새 연결을 받지 않고 처리 중인 요청을 마무리해야 해요. 로드밸런서의 헬스체크가 실패로 바뀌는 시점과 프로세스 종료 시점 사이에 여유를 두면 급격한 연결 끊김을 줄일 수 있어요.
둘째, 관측 가능성이에요. 요청마다 추적 식별자를 붙이고, 구조화 로그로 남겨요. 지연 시간과 오류율을 볼 수 있어야 병목을 찾을 수 있어요. axum은 Tower 레이어로 이 작업을 넣기 좋아요.
셋째, 자원 한계 설정이에요. 요청 본문 크기 제한, 동시 연결 수, 타임아웃을 명시적으로 정해 두지 않으면 예상치 못한 큰 요청 하나가 프로세스 전체를 흔들 수 있어요. 리버스 프록시 뒤에 둔다면 프록시 쪽 제한과 애플리케이션 쪽 제한을 함께 맞춰야 해요.
넷째, 빌드 산출물과 이미지 관리예요. 릴리스 빌드로 바이너리를 만들고, 실행 이미지는 가능한 한 얇게 유지해요. 환경 설정은 이미지에 굽지 말고 환경 변수나 설정 파일로 주입해요.
다섯째, 마이그레이션과 롤백 계획이에요. 스키마 변경은 애플리케이션 배포와 분리해 단계적으로 적용하고, 이전 버전이 함께 떠 있는 동안에도 동작하도록 호환성을 확인해요.
상황별 선택 가이드
작은 API 서버를 빠르게 만들어야 한다면 axum의 기본 구성만으로 충분해요. 라우터 몇 개와 추출기 몇 개, tracing 레이어 하나면 시작할 수 있어요.
이미 Tower 생태계를 쓰는 서비스가 있다면 axum이 자연스러워요. 미들웨어를 그대로 재사용할 수 있고, 서비스 합성 개념이 팀에 이미 익숙하기 때문이에요.
프레임워크 안에서 해결되는 범위가 넓은 쪽을 선호한다면 actix-web도 여전히 좋은 선택이에요. 다만 미들웨어와 생태계가 다르므로 한 프로젝트 안에서 두 프레임워크를 섞는 것은 권하지 않아요.
gRPC나 다른 프로토콜을 함께 다뤄야 한다면 HTTP 계층과 비즈니스 계층을 분리해 두세요. axum은 HTTP 계층으로 두고, 핵심 로직은 프레임워크에 의존하지 않는 모듈로 빼면 교체 비용이 줄어요.
자주 묻는 질문
axum은 Tokio 없이도 동작하나요?
axum은 Tokio 런타임과 함께 쓰는 것을 전제로 문서가 작성돼 있어요. 비동기 실행기 자체를 다른 것으로 바꾸는 구성은 공식 문서에서 권장하는 기본 경로가 아니에요. 특별한 이유가 없다면 Tokio를 그대로 쓰는 편이 안전해요.
actix-web 대신 axum을 고르는 기준은 무엇인가요?
Tower 레이어를 재사용하고 싶거나, 서비스 합성 개념에 익숙한 팀이라면 axum이 잘 맞아요. 반대로 프레임워크가 제공하는 기능을 폭넓게 쓰고 싶다면 actix-web도 좋은 선택이에요. 두 프레임워크 모두 성숙했으므로 팀의 기존 자산이 결정 기준이 되는 경우가 많아요.
핸들러에서 데이터베이스 커넥션은 어떻게 꺼내나요?
커넥션 풀을 상태 구조체에 넣고 with_state로 라우터에 붙인 뒤, 핸들러에서 State 추출기로 꺼내요. 풀에서 커넥션을 잡아 쿼리를 실행하고, 트랜잭션이 필요하면 그 위에 트랜잭션을 열어요. 풀 생성과 종료는 main에서 관리하는 편이 좋아요.
미들웨어 적용 순서는 어떻게 정해야 하나요?
요청이 들어와서 응답이 나가는 경로를 먼저 그려 보세요. 로깅은 가장 바깥에 두어 전체 시간을 재고, 인증은 라우터 단위로 적용해 불필요한 경로를 제외해요. 순서를 바꾸면 관측 값과 동작이 모두 달라지므로 테스트로 고정해 두는 편이 좋아요.
라우터가 커지면 어떻게 나누는 게 좋나요?
도메인 단위로 Router를 만들고 nest로 합치는 방식이 일반적이에요. 도메인별 라우터는 자체 테스트를 가질 수 있어 변경 범위를 좁힐 수 있어요. 공통 미들웨어는 최상위에서 한 번만 적용해 중복을 피해요.
출처
- axum 공식 문서: https://docs.rs/axum/latest/axum/
- axum GitHub 저장소: https://github.com/tokio-rs/axum
- Tokio 공식 사이트: https://tokio.rs/
- Tower 공식 문서: https://docs.rs/tower/latest/tower/
axum rust backend 운영은 결국 배포 파이프라인과 관측 가능성 싸움으로 이어져요. 관련해서 Rust 서버 배포 체크리스트, Tokio 런타임 운영 팁, Tower 미들웨어 조합 패턴, 컨테이너 이미지 크기 줄이기 글도 함께 읽어 보시면 배포 단계에서 마주치는 문제를 더 빠르게 정리할 수 있어요.