원클릭으로
dependency-injection
Rust Trait 기반 의존성 주입 패턴 - DI 원리, Arc<dyn Trait> vs 제네릭, AppState 구성, Axum 핸들러 주입, Mock 테스트
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Rust Trait 기반 의존성 주입 패턴 - DI 원리, Arc<dyn Trait> vs 제네릭, AppState 구성, Axum 핸들러 주입, Mock 테스트
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Spring Security 5.5.x + jjwt 0.10.7 레거시 JWT 인증 - WebSecurityConfigurerAdapter, OncePerRequestFilter, javax.servlet 환경
Spring Boot 3.x + Spring Security 6.x + jjwt 0.12.x 기반 모던 JWT 인증 패턴. SecurityFilterChain Bean, 람다 DSL, jakarta.servlet, Virtual Threads 적용
Unity 6 LTS 2D 모바일 게임용 uGUI 시스템 전문 스킬. Canvas/RectTransform/TextMeshPro, 모바일 UI 패턴(팝업·무한 스크롤·광고·IAP), 성능 최적화, UI Toolkit과의 선택 기준 포함.
아크라시아(akrasia, 자제력 없음) 학술 논쟁의 핵심 구도와 주요 연구자·문헌을 빠르게 파악할 수 있는 도메인 지식 스킬. 도덕윤리교육 전공 대학원생(석/박사)이 학위논문·KCI 투고·세미나 준비 시 고대–현대–한국 학계–도덕심리학 흐름을 한 번에 짚도록 구성. <example>사용자: "아리스토텔레스의 propeteia와 astheneia 구분을 인용하려는데 출처를 알려줘"</example> <example>사용자: "데이비슨이 의지박약을 어떻게 가능하다고 봤는지 핵심 논증을 정리해줘"</example> <example>사용자: "한국 도덕교육 학계에서 아크라시아 다룬 논문 있어?"</example>
아리스토텔레스 『니코마코스 윤리학』에서 akrasia(자제력없음)와 akolasia(무절제)의 5축 차이를 정밀하게 정리한 학위논문 자료 스킬. NE VII.4 1147b20-1148b14, VII.8 1150b29-1151a28, III.10-12 1117b23-1119b18 절별 분해와 표준 학자 해석(Bostock, Broadie-Rowe, Pakaluk, Hursthouse, Charles 등)을 포함. 도덕교육 적용을 위한 두 상태 차이의 함의 및 한국어 번역어 처리 권장안 제공. <example>사용자: "akrates와 akolastos를 prohairesis 측면에서 어떻게 구분해야 하나요?"</example> <example>사용자: "NE VII.4의 ἁπλῶς akrasia가 akolasia와 어떻게 갈라지는지 절별 분해해주세요"</example> <example>사용자: "Hursthouse의 연속체 모델을 도덕교육 적용 절에서 어떻게 활용할 수 있나요?"</example>
한국 위기 대응 자원(자살·자해·정신건강·여성·청소년·노인·다문화) 핫라인과 앱·챗봇 안전 가드 응답 패턴 종합. 꿈 해몽·정신건강 앱 등 자가 진단/감정 콘텐츠 도메인에서 위험 신호 포착 시 안전한 자원 안내 문구를 작성할 때 참조. <example>사용자: "꿈 해몽 앱에 위기 안내 문구를 어떻게 넣을까?"</example> <example>사용자: "한국에서 자살예방 핫라인 번호가 어떻게 바뀌었지?"</example> <example>사용자: "정신건강 챗봇 안전 가드 응답 템플릿을 짜줘"</example>
| name | dependency-injection |
| description | Rust Trait 기반 의존성 주입 패턴 - DI 원리, Arc<dyn Trait> vs 제네릭, AppState 구성, Axum 핸들러 주입, Mock 테스트 |
소스: https://doc.rust-lang.org/book/ch17-02-trait-objects.html | https://doc.rust-lang.org/reference/items/traits.html | https://docs.rs/axum/latest/axum/extract/struct.State.html | https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html 검증일: 2026-06-20
주의: Axum 코드는 0.8.x 기준입니다. async fn in traits는 Rust 1.75+ 기준이며, dyn Trait에서는 여전히 제약이 있습니다.
DI의 본질은 "구현이 아닌 인터페이스에 의존"하는 것이다. Rust에서는 trait이 인터페이스 역할을 한다.
// 1. 인터페이스(trait) 정의
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
async fn create(&self, input: CreateUser) -> Result<User, AppError>;
}
// 2. 구현체 (프로덕션)
pub struct PgUserRepository {
pool: sqlx::PgPool,
}
impl UserRepository for PgUserRepository {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&self.pool)
.await
.map_err(AppError::from)
}
async fn create(&self, input: CreateUser) -> Result<User, AppError> {
sqlx::query_as!(User, "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
input.name, input.email)
.fetch_one(&self.pool)
.await
.map_err(AppError::from)
}
}
핵심: 비즈니스 로직은 UserRepository trait에만 의존하고, PgUserRepository를 직접 참조하지 않는다.
Rust 1.75.0 (2023-12-28 안정화)부터 trait에서 async fn을 직접 사용할 수 있다.
// Rust 1.75+ : #[async_trait] 없이 동작
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
}
async fn in trait은 각 구현체마다 다른 Future 타입을 반환하므로, dyn Trait으로 직접 사용할 수 없다. 이를 해결하는 방법:
// 방법 1: trait-variant 크레이트 (Rust 공식 팀 제공)
// Cargo.toml: trait-variant = "0.1"
#[trait_variant::make(UserRepository: Send)]
pub trait LocalUserRepository {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
}
// 방법 2: 수동 Box<dyn Future> 반환 (dyn 호환 필요 시)
pub trait UserRepository: Send + Sync {
fn find_by_id(&self, id: i64) -> Pin<Box<dyn Future<Output = Result<Option<User>, AppError>> + Send + '_>>;
}
// 방법 3: 제네릭으로 사용하면 dyn 제약 자체가 없음 (권장)
주의:
trait-variant크레이트는 Rust 공식 팀(rust-lang 조직)에서 관리하지만, 아직 1.0 미만 버전입니다. API 변경 가능성이 있습니다.
pub struct AppState {
pub user_repo: Arc<dyn UserRepository>,
pub post_repo: Arc<dyn PostRepository>,
}
| 장점 | 단점 |
|---|---|
| 구조체 정의가 간결 | vtable 간접 호출 비용 (대부분 무시 가능) |
| 런타임에 구현체 교체 가능 | dyn 호환 trait 제약 (object safety) |
| 컴파일 시간 짧음 | async fn 사용 시 추가 처리 필요 |
pub struct AppState<R: UserRepository, P: PostRepository> {
pub user_repo: R,
pub post_repo: P,
}
| 장점 | 단점 |
|---|---|
| 제로 비용 추상화 (인라이닝) | 제네릭 파라미터 전파 (타입 복잡도 증가) |
| async fn in trait 그대로 사용 | 컴파일 시간 증가 (단형성화) |
| dyn 호환 제약 없음 | 구현체를 런타임에 교체 불가 |
의존성 2-3개 이하, 런타임 교체 불필요 → 제네릭
의존성 다수, 플러그인 구조, dyn 필요 → Arc<dyn Trait>
웹 서버 AppState (가장 일반적) → Arc<dyn Trait> 권장
- 핸들러당 vtable 오버헤드는 네트워크 I/O 대비 무시할 수준
- 타입 파라미터 전파 없이 깔끔한 코드 유지
use std::sync::Arc;
use sqlx::PgPool;
use reqwest::Client;
#[derive(Clone)]
pub struct AppConfig {
pub jwt_secret: String,
pub external_api_url: String,
}
#[derive(Clone)]
pub struct AppState {
pub db: PgPool, // sqlx pool은 내부적으로 Arc
pub http_client: Client, // reqwest Client도 내부 Arc
pub config: Arc<AppConfig>,
pub user_repo: Arc<dyn UserRepository>,
pub post_repo: Arc<dyn PostRepository>,
}
impl AppState {
pub fn new(pool: PgPool, config: AppConfig) -> Self {
let config = Arc::new(config);
let http_client = Client::new();
Self {
db: pool.clone(),
http_client: http_client.clone(),
config: config.clone(),
user_repo: Arc::new(PgUserRepository { pool: pool.clone() }),
post_repo: Arc::new(PgPostRepository { pool, http_client, config }),
}
}
}
주의사항:
PgPool과 reqwest::Client는 내부적으로 Arc를 사용하므로 clone()이 저렴하다 (포인터 복사).AppState 자체도 Clone이어야 Axum State로 사용 가능하다.use axum::{Router, routing::get};
#[tokio::main]
async fn main() {
let pool = sqlx::PgPool::connect("postgres://...").await.unwrap();
let config = AppConfig { /* ... */ };
let state = AppState::new(pool, config);
let app = Router::new()
.route("/users/{id}", get(get_user))
.route("/users", post(create_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
주의: axum 0.8부터 경로 파라미터 문법이
:id에서{id}로 변경되었습니다.
use axum::extract::{State, Path};
use axum::Json;
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<Json<User>, AppError> {
let user = state.user_repo
.find_by_id(id)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(user))
}
async fn create_user(
State(state): State<AppState>,
Json(input): Json<CreateUser>,
) -> Result<Json<User>, AppError> {
let user = state.user_repo.create(input).await?;
Ok(Json(user))
}
fn user_routes() -> Router<AppState> {
Router::new()
.route("/", post(create_user))
.route("/{id}", get(get_user))
}
fn post_routes() -> Router<AppState> {
Router::new()
.route("/", get(list_posts).post(create_post))
}
let app = Router::new()
.nest("/users", user_routes())
.nest("/posts", post_routes())
.with_state(state);
#[cfg(test)]
mod tests {
use super::*;
// Mock 구현체
struct MockUserRepository {
users: Vec<User>,
}
impl MockUserRepository {
fn with_users(users: Vec<User>) -> Self {
Self { users }
}
}
impl UserRepository for MockUserRepository {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError> {
Ok(self.users.iter().find(|u| u.id == id).cloned())
}
async fn create(&self, input: CreateUser) -> Result<User, AppError> {
Ok(User {
id: 1,
name: input.name,
email: input.email,
})
}
}
#[tokio::test]
async fn test_find_user() {
let user = User { id: 1, name: "Alice".into(), email: "a@b.com".into() };
let repo = MockUserRepository::with_users(vec![user.clone()]);
let result = repo.find_by_id(1).await.unwrap();
assert_eq!(result, Some(user));
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt; // oneshot 사용
fn test_state() -> AppState {
let user = User { id: 1, name: "Alice".into(), email: "a@b.com".into() };
AppState {
db: /* test pool 또는 미사용 */,
http_client: Client::new(),
config: Arc::new(AppConfig { /* test config */ }),
user_repo: Arc::new(MockUserRepository::with_users(vec![user])),
post_repo: Arc::new(MockPostRepository::default()),
}
}
#[tokio::test]
async fn test_get_user_handler() {
let app = Router::new()
.route("/users/{id}", get(get_user))
.with_state(test_state());
let req = Request::builder()
.uri("/users/1")
.body(Body::empty())
.unwrap();
let response = app.oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
// Cargo.toml
// [dev-dependencies]
// mockall = "0.13"
use mockall::automock;
#[automock]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: i64) -> Result<Option<User>, AppError>;
async fn create(&self, input: CreateUser) -> Result<User, AppError>;
}
#[tokio::test]
async fn test_with_mockall() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.with(eq(1))
.times(1)
.returning(|_| Ok(Some(User { id: 1, name: "Alice".into(), email: "a@b.com".into() })));
let result = mock.find_by_id(1).await.unwrap();
assert!(result.is_some());
}
주의: mockall 0.13+는 async fn in traits를 지원하지만, 복잡한 trait 구조에서 컴파일 에러가 발생할 수 있습니다. 단순 trait에서는 문제 없이 동작합니다.
main.rs → AppState 조립, 라우터 구성
├── config.rs → AppConfig (환경 변수 로드)
├── state.rs → AppState 정의 + new()
├── domain/
│ └── user.rs → User, CreateUser 타입
├── repo/
│ ├── traits.rs → trait UserRepository
│ └── postgres.rs → PgUserRepository (impl)
├── handler/
│ └── user.rs → get_user, create_user (State 추출)
└── error.rs → AppError (IntoResponse 구현)
의존 방향:
handler → trait (repo/traits.rs) ← impl (repo/postgres.rs)
↑
state.rs (Arc<dyn Trait>으로 연결)
핸들러는 구체 구현체를 알지 못하고, trait만 참조한다. 구현체 교체는 state.rs의 조립 지점에서만 발생한다.