Crate singleton_macro

Crate singleton_macro 

Source
Expand description

§Singleton Macro

Spring Framework-inspired dependency injection and singleton pattern macros for Rust backend services.

Copyright (c) 2025 Janghoon Park ceo@dataengine.co.kr

Licensed under either of:

at your option.

§개요

백엔드 서비스를 위한 컴파일 타임 의존성 주입 및 싱글톤 패턴 구현 매크로 크레이트입니다.

이 크레이트는 Spring Framework의 DI 컨테이너와 유사한 방식으로 Rust 서비스의 의존성을 관리하며, 메모리 효율성과 타입 안전성을 보장합니다.

§주요 기능

  • 자동 싱글톤 관리: OnceCellArc를 사용한 thread-safe 싱글톤 구현
  • 컴파일 타임 의존성 주입: Arc<T> 타입 필드 자동 감지 및 주입
  • 전역 레지스트리: inventory 크레이트를 통한 자동 서비스 등록
  • Zero-cost abstraction: 런타임 오버헤드 최소화
  • Static context 호환: 컴파일 타임에 안전한 등록 시스템

§제공 매크로

§#[service]

비즈니스 로직을 담당하는 서비스 컴포넌트를 정의합니다.

§사용법
use std::sync::Arc;

#[service]
struct UserService {
    user_repo: Arc<UserRepository>,  // 자동 주입
    email_service: Arc<EmailService>, // 자동 주입
    config: Config,                   // Default::default() 사용
}

// 커스텀 이름 지정 (자동으로 _service suffix 추가됨)
#[service(name = "auth")]  // "auth_service"로 등록됨
struct AuthenticationService {
    // ...
}
§생성되는 메서드
// 싱글톤 인스턴스 가져오기
let service = UserService::instance();

// Service trait 자동 구현
assert_eq!(service.name(), "userservice_service");

§#[repository]

데이터 액세스 계층을 담당하는 리포지토리 컴포넌트를 정의합니다. MongoDB 컬렉션과의 통합 및 Redis 캐싱을 지원합니다.

§사용법
#[repository]
struct UserRepository {
    db: Arc<Database>,        // 자동 주입 (필드명이 db/database인 경우)
    redis: Arc<RedisClient>,  // 자동 주입 (필드명이 redis/cache인 경우)
}

// 커스텀 설정 (자동으로 _repository suffix 추가됨)
#[repository(name = "user", collection = "users")]  // "user_repository"로 등록됨
struct UserRepo {
    db: Arc<Database>,
}
§생성되는 메서드
let repo = UserRepository::instance();

// MongoDB 컬렉션 접근 (db 필드가 있을 때)
let collection = repo.collection::<User>();

// 컬렉션 이름 가져오기
let name = repo.collection_name(); // "users"

// Redis 캐싱 메서드 (redis/cache 필드가 있을 때)
let key = repo.cache_key("user123"); // "user_repository:user123"
repo.invalidate_cache("user123").await?;
repo.invalidate_collection_cache(Some("active")).await?;
repo.invalidate_pattern_cache("user_repository:*").await?;

§의존성 주입 규칙

필드 타입과 이름에 따라 자동으로 의존성이 주입됩니다:

필드 타입필드 이름주입 방식
Arc<T>모든 이름ServiceLocator::get::<T>()
모든 타입db, databaseServiceLocator::get::<Database>()
모든 타입redis, cacheServiceLocator::get::<RedisClient>()
기타기타Default::default()

§레지스트리 시스템

§자동 등록

  • 서비스: {name}_service 형태로 자동 등록
  • 리포지토리: {name}_repository 형태로 자동 등록
  • Static 호환: 컴파일 타임에 안전한 함수 포인터 사용

§등록 구조체

pub struct ServiceRegistration {
    pub name: &'static str,
    pub constructor: fn() -> Box<dyn Any + Send + Sync>,
}

pub struct RepositoryRegistration {
    pub name: &'static str,
    pub constructor: fn() -> Box<dyn Any + Send + Sync>,
}

§동작 원리

  1. 컴파일 타임: 매크로가 구조체를 분석하고 필요한 코드 생성
  2. 프로그램 시작: inventory가 모든 서비스/리포지토리 수집
  3. 첫 사용: instance() 호출 시 의존성 주입 및 인스턴스 생성
  4. 재사용: 캐시된 싱글톤 인스턴스 반환

§필수 의존성

이 매크로를 사용하려면 프로젝트에 다음 크레이트가 필요합니다:

  • once_cell: 싱글톤 저장
  • inventory: 전역 레지스트리
  • async-trait: Repository/Service trait 구현
  • mongodb (repository 사용 시)
  • redis (캐싱 사용 시)

§예제: 완전한 서비스 구성

// 1. Repository 정의
#[repository(collection = "users")]
struct UserRepository {
    db: Arc<Database>,
    redis: Arc<RedisClient>,
}

impl UserRepository {
    async fn find_by_id(&self, id: &str) -> Option<User> {
        // 캐시 확인
        let cache_key = self.cache_key(id);
        if let Ok(Some(cached)) = self.redis.get(&cache_key).await {
            return Some(cached);
        }

        // DB 조회
        let user = self.collection()
            .find_one(doc! { "_id": id }, None)
            .await
            .ok()
            .flatten();

        // 캐시 저장
        if let Some(ref user) = user {
            self.redis.set_with_expiry(&cache_key, user, 600).await;
        }

        user
    }
}

// 2. Service 정의
#[service]
struct UserService {
    repo: Arc<UserRepository>,
}

impl UserService {
    async fn get_user(&self, id: &str) -> Result<User> {
        self.repo.find_by_id(id).await
            .ok_or_else(|| Error::NotFound)
    }
}

// 3. 사용
#[actix_web::main]
async fn main() {
    // ServiceLocator 초기화 (Database, RedisClient 등록)

    // 서비스 사용
    let user_service = UserService::instance();
    let user = user_service.get_user("123").await?;
}

§주의사항

  • 순환 의존성 방지: A가 B를 의존하고 B가 A를 의존하면 런타임 패닉 발생
  • ServiceLocator 초기화: Database, RedisClient 등 핵심 서비스는 미리 등록 필요
  • Thread-safety: 모든 의존성은 Arc로 감싸져 있어야 함
  • Static context: 모든 등록은 컴파일 타임에 결정됨

§디버깅

의존성 주입 과정은 콘솔에 로그가 출력됩니다:

ServiceLocator::get called for type: UserRepository
  - Creating new instance for UserRepository
  - Found matching repository: user_repository

Attribute Macros§

repository
데이터 액세스 리포지토리를 위한 싱글톤 매크로
service
비즈니스 로직 서비스를 위한 싱글톤 매크로