Skip to content

SPI 확장

Q-Framework는 SPI(Service Provider Interface) 패턴으로 확장 포인트를 제공합니다. 프레임워크 내부 코드를 수정하지 않고 인터페이스 구현만으로 동작을 커스터마이징할 수 있습니다.

SPI 설계 원칙

Q-Framework (인터페이스 정의)
        ↑ 의존
애플리케이션 (구현체 제공)
  • 고수준 프레임워크가 저수준 어댑터에 의존하지 않음 (DIP 원칙)
  • 구현체는 런타임에 자동 탐색
  • 하나의 인터페이스에 여러 구현체 등록 가능

QfUserProvider

현재 요청의 사용자 정보를 제공합니다. 반드시 구현해야 합니다.

java
@Component
public class MyUserProvider implements QfUserProvider {

    @QfAllowedDirectAccess(reason = "SPI 구현체 — 사용자 저장소에 직접 접근 필요")
    private final UserRepository userRepository;

    @Override
    public QfUser getCurrentUser(HttpServletRequest request) {
        // Spring Security, JWT, Session 등 어떤 방식도 사용 가능
        String userId = extractUserIdFromRequest(request);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UnauthorizedException());

        return QfUser.builder()
            .id(user.getId())
            .name(user.getName())
            .organizationId(user.getOrganizationId())
            .privileges(roleService.getPrivileges(user.getRoles()))
            .locale(user.getPreferredLocale())
            .build();
    }
}

QfOrganizationProvider

조직 계층 정보를 제공합니다. 조직 모델을 사용하는 경우 구현이 필요합니다.

java
@Component
public class MyOrganizationProvider implements QfOrganizationProvider {

    @QfAllowedDirectAccess(reason = "SPI 구현체 — 조직 저장소에 직접 접근 필요")
    private final OrganizationRepository organizationRepository;

    @Override
    public List<QfOrganization> getOrganizationHierarchy(String rootOrgId) {
        return organizationRepository.findHierarchy(rootOrgId)
            .stream()
            .map(this::toQfOrganization)
            .collect(Collectors.toList());
    }

    @Override
    public String getCurrentUserOrganizationId() {
        return QfContext.currentUser().getOrganizationId();
    }

    private QfOrganization toQfOrganization(OrganizationEntity org) {
        return QfOrganization.builder()
            .id(org.getId())
            .parentId(org.getParentId())
            .name(org.getName())
            .depth(org.getDepth())
            .build();
    }
}

QfRuntimeInitializationHook

런타임 초기화 시점에 추가 작업을 수행합니다.

java
@Component
public class MyInitializationHook implements QfRuntimeInitializationHook {

    @Override
    public void onBeforeInitialize(QfRuntimeContext context) {
        // 초기화 전: 검증, 전처리 등
        log.info("Q-Framework 초기화 시작");
    }

    @Override
    public void onAfterInitialize(QfRuntimeContext context) {
        // 초기화 후: 캐시 프리로딩, 기본 데이터 설정 등
        log.info("등록된 엔티티 수: {}", context.getEntityCount());
        log.info("등록된 Capability 수: {}", context.getCapabilityCount());
    }
}

QfInitDataContributor

애플리케이션 시작 시 초기 데이터를 삽입합니다.

java
@Component
public class AdminRoleContributor implements QfInitDataContributor {

    @Override
    public int getOrder() {
        return 100;  // 실행 순서
    }

    @Override
    public void contribute(QfInitDataContext context) {
        // 이미 존재하면 건너뜀
        if (context.exists("roles", "ROLE_ADMIN")) {
            return;
        }

        // 기본 관리자 역할 생성
        context.insert("roles", Map.of(
            "id", "ROLE_ADMIN",
            "name", "시스템 관리자",
            "privileges", context.getAllPrivileges()
        ));
    }
}

QfDiagnosticListener

진단 이벤트를 수신합니다.

java
@Component
public class MyDiagnosticListener implements QfDiagnosticListener {

    @Override
    public void onEntityRegistered(QfEntityMetadata metadata) {
        log.debug("엔티티 등록: {}.{}", metadata.getClientApp(), metadata.getName());
    }

    @Override
    public void onValidationError(QfValidationErrorEvent event) {
        // 검증 오류 모니터링 시스템에 전송
        monitoring.track("validation_error", Map.of(
            "entity", event.getEntityName(),
            "field", event.getFieldName()
        ));
    }
}

QfRuntimeConfigProvider

런타임 설정을 외부 소스(DB, Config Server 등)에서 동적으로 제공합니다.

java
@Component
public class DatabaseConfigProvider implements QfRuntimeConfigProvider {

    @QfAllowedDirectAccess(reason = "SPI 구현체 — 설정 저장소에 직접 접근 필요")
    private final ConfigRepository configRepository;

    @Override
    public Map<String, Object> getConfig() {
        // DB에서 설정 로딩
        return configRepository.findAll()
            .stream()
            .collect(Collectors.toMap(
                Config::getKey,
                Config::getValue
            ));
    }

    @Override
    public int getOrder() {
        return 200;  // application.yml보다 높은 우선순위
    }
}

SPI 구현체에서의 직접 데이터 접근

Spring Data Repository, EntityManager, JdbcTemplate 등 직접 데이터 접근 타입을 주입하는 SPI 구현체는 해당 필드에 @QfAllowedDirectAccess(reason = "...") 어노테이션을 선언해야 합니다.

qf.persistence.access.modepermissive(기본값) 또는 strict인 경우 이 어노테이션 없이는 애플리케이션이 시작되지 않습니다.


SPI 등록 방법

Spring Boot 환경에서는 @Component만 추가하면 자동으로 등록됩니다.

java
@Component  // 이것만으로 Q-Framework가 자동 탐색
public class MyUserProvider implements QfUserProvider {
    // ...
}

Spring이 아닌 환경에서는 ServiceLoader 방식을 사용합니다:

META-INF/services/net.softminds.qframework.spi.QfUserProvider
→ com.example.myapp.MyUserProvider

SPI 목록 요약

SPI 인터페이스필수 여부설명
QfUserProvider✅ 필수현재 사용자 정보 제공
QfOrganizationProvider조건부조직 모델 사용 시 필수
QfRuntimeInitializationHook선택초기화 시점 훅
QfInitDataContributor선택초기 데이터 삽입
QfDiagnosticListener선택진단 이벤트 수신
QfRuntimeConfigProvider선택동적 설정 제공
QfErrorNotificationHandler선택오류 알림 처리

다음 단계

Released under the Apache 2.0 License.