بنقرة واحدة
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 ويثبّتها لك.
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>
استنادا إلى تصنيف SOC المهني
| 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의 조립 지점에서만 발생한다.