본문 바로가기
WIKI 기술 지식 베이스

백엔드 플러그인 확장 지점

원문 보기 위키 갱신

플러그인은 가벼운 형태의 사용자 지정을 위해 정적 구성을 사용할 수 있지만, 사용자가 플러그인을 확장할 수 있도록 더 강력한 것이 필요한 한계에 빠르게 도달할 수 있어요.

출처: 문서

본문

플러그인은 가벼운 형태의 사용자 지정을 위해 정적 구성을 사용할 수 있지만, 사용자가 플러그인을 확장할 수 있도록 더 강력한 것이 필요한 한계에 빠르게 도달할 수 있어요. 이를 위해 백엔드 시스템은 플러그인이 확장 지점을 제공할 수 있는 메커니즘을 제공하며, 이는 플러그인의 더 깊은 사용자 지정을 노출하는 데 사용할 수 있어요. 확장 지점은 백엔드에 플러그인과 인접해 설치되는 모듈이 사용해요. 모듈은 다음 섹션에서 더 자세히 다뤄요.

확장 지점은 서비스와 상당히 유사한데, 둘 다 참조 객체에 인터페이스를 캡슐화하기 때문이에요. 핵심 차이는 확장 지점은 플러그인 자체가 등록하고 제공하며, 연관된 팩토리가 없다는 점이에요. 주어진 플러그인의 확장 지점은 같은 플러그인을 확장하는 모듈에만 접근 가능해요.

플러그인 확장 지점은 항상 플러그인 노드 라이브러리 패키지(예: @backstage/plugin-catalog-node)에서 내보내야 해요. 이는 모듈이 플러그인에 직접 의존하지 않도록 하고 시간이 지나도 확장 지점을 발전시키기 쉽게 만들기 위해서예요. 원하는 만큼 많은 확장 지점을 내보낼 수 있지만, API 표면의 복잡성을 염두에 두세요. 그러나 적은 수의 메서드를 가진 여러 확장 지점을 내보내는 것이 많은 메서드를 가진 소수의 확장 지점보다 유지 관리하기 쉬운 경향이 있어 더 좋을 때가 많아요.

확장 지점 정의

확장 지점은 @backstage/backend-plugin-api의 createExtensionPoint 메서드로 만들어요. 타입과 ID를 제공해야 해요.

import { createExtensionPoint } from '@backstage/backend-plugin-api';export interface ScaffolderActionsExtensionPoint {  addAction(action: ScaffolderAction): void;}export const scaffolderActionsExtensionPoint =  createExtensionPoint<ScaffolderActionsExtensionPoint>({    id: 'scaffolder.actions',  });

확장 지점 등록

모듈이 확장 지점을 사용할 수 있으려면 플러그인이 그 구현을 등록해야 해요. 이는 플러그인 정의의 register 콜백에서 registerExtensionPoint 메서드를 사용해 수행돼요.

export const scaffolderPlugin = createBackendPlugin(  {    pluginId: 'scaffolder',    register(env) {      const actions = new Map<string, TemplateAction<any>>();      env.registerExtensionPoint(        scaffolderActionsExtensionPoint,        {          addAction(action) {            if (actions.has(action.id)) {              throw new Error(`Scaffolder actions with ID '${action.id}' has already been installed`);            }            actions.set(action.id, action);          },        },      );      env.registerInit({        deps: { ... },        async init({ ... }) {          // Use the registered actions when setting up the scaffolder ...          const installedActions = Array.from(actions.values());        },      });    },  },);

확장 지점 사용자가 addAction을 호출할 때 공유된 actions 구조에 추가하는 클로저를 만들고 있음을 확인하세요. 플러그인을 확장하는 모든 모듈이 플러그인이 초기화되기 전에 완전히 초기화되므로, 플러그인의 init 메서드에서 actions에 접근하는 것은 안전해요. 이는 init 메서드가 호출되는 시점에 모든 작업이 추가되어 접근할 수 있음을 의미해요.

팩토리 기반 확장 지점

어떤 경우에는 플러그인 시작을 완전히 실패시키는 대신 시작 실패를 확장을 제공한 모듈에 귀속시키고 싶을 수 있어요. 이를 위해 직접 구현을 제공하는 대신 구현을 생성하는 팩토리 함수를 등록하는 registerExtensionPoint의 변형을 사용할 수 있어요. 이 팩토리는 시작 실패를 보고하고 모듈에 귀속시킬 수 있는 reportModuleStartupFailure 메서드를 가진 ExtensionPointFactoryContext를 받아요.

다음은 팩토리를 사용해 확장 지점을 등록하는 예시예요:

import {  createBackendPlugin,  ExtensionPointFactoryContext,} from '@backstage/backend-plugin-api';import { assertError, ForwardedError } from '@backstage/errors';import { createProviderConnection, Provider } from './internal';type ProviderEntry = {  provider: Provider;  context: ExtensionPointFactoryContext;};export const examplePlugin = createBackendPlugin({  pluginId: 'example',  register(env) {    const providers: ProviderEntry[] = [];    // Using the variant of registerExtensionPoint that takes an options object.    env.registerExtensionPoint({      extensionPoint: exampleProvidersExtensionPoint,      // The factory function produces a separate instance for each module.      factory: context => ({        addProvider(provider) {          // Store the context together with the provider so we can report failures later          providers.push({ provider, context });        },      }),    });    env.registerInit({      deps: { database: coreServices.database },      async init({ database }) {        for (const { provider, context } of providers) {          const connection = await createProviderConnection(provider, database);          try {            // This connects each provider that was installed by a module            await provider.connect(connection);          } catch (error: unknown) {            // If the connection fails, we can report this as a failure of the module rather than the plugin            assertError(error);            context.reportModuleStartupFailure({              error: new ForwardedError('Failed to connect provider', error),            });          }        }      },    });  },});

모듈 확장 지점

플러그인처럼 모듈도 자체 확장 지점을 제공할 수 있어요. 확장 지점을 등록하고 사용하는 API는 플러그인과 동일해요. 그러나 모듈은 일반적으로 플러그인 모듈 사용자가 복잡한 내부 사용자 지정을 허용하는 데만 확장 지점을 사용해야 해요. 따라서 그 목적으로 별도의 노드 라이브러리를 만드는 대신 모듈 패키지에서 직접 확장 지점을 내보내는 것이 선호돼요. 모듈이 내보낸 확장 지점은 플러그인이 내보낸 확장 지점과 같은 방식으로 사용되며, 자체 별도 모듈을 만들고 상호 작용하려는 확장 지점에 대한 의존성을 선언해요.

확장 지점 설계

확장 지점 인터페이스를 설계하는 것은 신중한 고려가 필요해요. 플러그인이 시간이 지나며 유지 관리해야 할 공용 API 표면이에요. 모듈 설치는 사용자의 의도적인 행동임을 명심하세요. 즉, 확장 지점 인터페이스를 항상 추가 전용으로 설계할 수 있어요. 예를 들어 사용자가 작업을 추가한 모듈을 제거하지 않고도 scaffolderActionsExtensionPoint가 작업 제거를 지원할 필요는 없어요.

사용할 수 있는 또 다른 패턴은 확장 지점을 사용해 일부 기본 동작을 추가하거나 재정의하는 일종의 싱글턴 패턴이에요. 예를 들어 scaffolder가 템플릿 작업의 실행을 사용자 지정하는 방법을 노출하고 싶다고 가정해 보세요. 여러 모듈이 각자 자신의 작업 러너를 추가하도록 허용하는 것은 말이 되지 않으므로, 대신 setter를 사용해 하나의 작업 러너만 설치되도록 하고 다른 모듈이 다른 작업 러너를 설치하려 하면 오류를 던지도록 해요.

interface ScaffolderTaskRunnerExtensionPoint {  setTaskRunner(taskRunner: SchedulerServiceTaskRunner): void;}

이미 사용 중인 확장 지점에 파괴적인 변경을 하고 싶다면 기존 것을 폐기(deprecate)하고 다른 이름으로 새 것을 만드는 것을 권장해요. 완전히 새로운 이름을 사용할 수도 있지만, 기존 것에 버전 번호를 접미사로 붙일 수도 있어요(예: scaffolderActionsV2ExtensionPoint).

더 알아보기 (Learn more)