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.mode가 permissive(기본값) 또는 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.MyUserProviderSPI 목록 요약
| SPI 인터페이스 | 필수 여부 | 설명 |
|---|---|---|
QfUserProvider | ✅ 필수 | 현재 사용자 정보 제공 |
QfOrganizationProvider | 조건부 | 조직 모델 사용 시 필수 |
QfRuntimeInitializationHook | 선택 | 초기화 시점 훅 |
QfInitDataContributor | 선택 | 초기 데이터 삽입 |
QfDiagnosticListener | 선택 | 진단 이벤트 수신 |
QfRuntimeConfigProvider | 선택 | 동적 설정 제공 |
QfErrorNotificationHandler | 선택 | 오류 알림 처리 |
다음 단계
- 아키텍처 개요 — Q-Framework 내부 구조
- 어노테이션 레퍼런스 — 전체 어노테이션 목록