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

새 백엔드 시스템

원문 보기 위키 갱신

레거시 문서

출처: 문서

본문

레거시 문서

이 섹션은 레거시 플러그인 문서의 일부예요. 백엔드 시스템의 표준 문서는 Backend System 섹션으로 이동했으며, 플러그인과 모듈 구축, 아키텍처, 핵심 서비스에 대한 더 자세하고 최신의 가이드를 포함해요.

상태

새 백엔드 시스템은 릴리스되어 프로덕션 사용 준비가 되었고, 많은 플러그인과 모듈이 이미 마이그레이션되었어요. 모든 플러그인과 배포가 새 시스템으로 마이그레이션할 것을 권장해요.

예시 백엔드 설정을 backend 패키지에서 찾을 수 있어요.

개요

새 Backstage 백엔드 시스템은 백엔드 플러그인 설치를 더 간단하게 만들고 프로젝트를 최신 상태로 유지하는 데 도움이 되도록 구축되었어요. 또한 플러그인과 시스템 자체를 최소한의 중단이나 중단 변경의 원인으로 진화시키기 훨씬 쉬운 기반으로 변경했어요. 원래 RFC에서 그 근거를 더 읽을 수 있어요.

새 시스템의 목표 중 하나는 Backstage 백엔드 설정과 플러그인 설치에 필요한 코드를 줄이는 것이었어요. 새 시스템에서 백엔드를 만들고, 기능을 추가하고, 시작하는 예시는 다음과 같아요.

import { createBackend } from '@backstage/backend-defaults';// Create your backend instanceconst backend = createBackend();// Install all desired featuresbackend.add(import('@backstage/plugin-catalog-backend'));// Start up the backendbackend.start();

이렇게 훨씬 슬림한 백엔드 설정을 달성하는 데 도움이 된 주목할 만한 변화 중 하나는 Backstage 프론트엔드의 것과 매우 유사한 의존성 주입 시스템의 도입이에요.

구성 요소 (Building Blocks)

이 섹션은 이 새 시스템이 구축된 높은 수준의 구성 요소를 소개해요. 이 모든 것은 현재 시스템에도 어떤 식으로든 존재하는 개념이지만, 새 시스템에서 일급 고려 사항으로 끌어올려졌어요.

백엔드

배포 단위로 생각할 수 있는 백엔드 인스턴스 자체예요. 그 자체로는 기능이 없고, 단순히 여러 것을 연결해 주는 역할을 해요.

얼마나 많은 서로 다른 백엔드를 배포할지는 여러분이 결정해요. 모든 기능을 하나에 둘 수도 있고, 여러 개의 더 작은 배포로 나눌 수도 있어요. 모두 규모 확장과 개별 기능 격리 필요에 따라 달라져요.

플러그인

플러그인은 기존 시스템에서와 마찬가지로 실제 기능을 제공해요. 그것들은 서로 완전히 독립적으로 작동해요. 플러그인이 서로 통신하려면 네트워크를 통해 해야 해요. 플러그인 사이에 코드를 통한 직접 통신은 있을 수 없어요. 이런 제약 때문에 각 플러그인은 자체 마이크로서비스로 간주될 수 있어요.

서비스 (Services)

서비스는 플러그인을 더 간단히 구현할 수 있도록 유틸리티를 제공해, 각 플러그인이 모든 것을 처음부터 구현할 필요가 없게 해줘요. 로깅, 데이터베이스 접근, 구성 읽기를 위한 것 같은 많은 내장 서비스가 있지만, 서드파티 서비스를 가져오거나 자체적으로 만들 수도 있어요.

서비스는 개별 백엔드 설치를 위한 커스터마이즈 포인트이기도 해요. 자체 구현으로 서비스를 오버라이드하고, 기존 서비스에 더 작은 커스터마이즈를 할 수도 있어요.

확장 포인트 (Extension Points)

많은 플러그인에는 이를 확장할 수 있는 방법이 있어요. 예를 들어 Catalog의 엔티티 제공자나 Scaffolder의 사용자 지정 액션이에요. 이런 확장 패턴은 이제 확장 포인트(Extension Points)로 인코딩돼요.

확장 포인트는 서비스와 조금 비슷해 보이는데, 서비스를 의존하듯 의존하기 때문이에요. 핵심 차이는 확장 포인트가 각 개별 플러그인이 노출하고 싶은 커스터마이즈에 기반해 플러그인 자체가 등록·제공한다는 점이에요.

확장 포인트는 또한 플러그인 인스턴스 자체와 별도로 내보내지며, 단일 플러그인이 한 번에 여러 개의 서로 다른 확장 포인트를 노출할 수도 있어요. 이렇게 하면 하나의 거대한 API 표면을 다루는 대신 개별 확장 포인트를 시간에 따라 진화시키고 deprecated 처리하기가 더 쉬워져요.

모듈 (Modules)

모듈은 플러그인 확장 포인트를 사용해 플러그인에 새 기능을 추가해요. 예를 들어 개별 Catalog 엔티티 제공자나 하나 이상의 Scaffolder 액션을 추가할 수 있어요. 모듈은 기본적으로 플러그인을 위한 플러그인이에요.

각 모듈은 단일 플러그인만 확장할 수 있으며, 모듈은 그 플러그인과 같은 백엔드 인스턴스에 함께 배포되어야 해요. 그러나 모듈은 등록된 확장 포인트를 통해서만 자신의 플러그인과 통신할 수 있어요.

플러그인과 마찬가지로 모듈도 서비스에 접근할 수 있고 자체 서비스 구현에 의존할 수 있어요. 그러나 확장하는 플러그인과 서비스를 공유할 거예요, 모듈 전용 서비스 구현은 없어요.

플러그인 만들기

플러그인은 createBackendPlugin 함수로 만들어져요. 모든 플러그인은 ID와 register 메서드를 가져야 해요. 플러그인은 선택적이거나 필수일 수 있는 options 객체도 받을 수 있어요. options는 register 메서드의 두 번째 파라미터로 전달되며, options 타입은 추론되어 반환된 플러그인 팩토리 함수로 전달돼요.

import {  configServiceRef,  coreServices,  createBackendPlugin,} from '@backstage/backend-plugin-api';// export type ExamplePluginOptions = { exampleOption: boolean };export const examplePlugin = createBackendPlugin({  // unique id for the plugin  pluginId: 'example',  // It's possible to provide options to the plugin  // register(env, options: ExamplePluginOptions) {  register(env) {    env.registerInit({      deps: {        logger: coreServices.logger,      },      // logger is provided by the backend based on the dependency on loggerServiceRef above.      async init({ logger }) {        logger.info('Hello from example plugin');      },    });  },});

그러면 플러그인은 반환된 플러그인 팩토리 함수를 사용해 백엔드에 설치될 수 있어요.

backend.add(examplePlugin);

플러그인이 options도 받기를 원한다면, register 메서드의 두 번째 파라미터로 options를 받을 거예요.

export const examplePlugin = createBackendPlugin({  pluginId: 'example',  register(env, options?: { silent?: boolean }) {    env.registerInit({      deps: { logger: coreServices.logger },      async init({ logger }) {        if (!options?.silent) {          logger.info('Hello from example plugin');        }      },    });  },});

설치 중에 플러그인에 옵션을 전달하는 것은 이렇게 보여요.

backend.add(examplePlugin({ silent: true }));

모듈 만들기

모듈에 대한 몇 가지 사실

  • 모듈은 플러그인이 등록한 ExtensionPoint를 사용해 플러그인을 추가 기능으로 확장할 수 있어요.
  • 모듈은 하나의 플러그인만 확장할 수 있지만, 그 플러그인이 등록한 여러 ExtensionPoint와 상호작용할 수 있어요.
  • 모듈은 항상 확장하는 플러그인보다 먼저 초기화돼요.

모듈은 대상 플러그인의 라이브러리 패키지(예: @backstage/plugin-catalog-node)가 내보내는 ExtensionPoint에 의존하며, 플러그인 패키지 자체에 직접 의존성을 선언하지 않아요.

catalogProcessingExtensionPoint를 사용해 새 프로세서를 추가하는 모듈을 만드는 예시는 다음과 같아요.

import { createBackendModule } from '@backstage/backend-plugin-api';import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';import { MyCustomProcessor } from './processor';export const exampleCustomProcessorCatalogModule = createBackendModule({  pluginId: 'catalog',  moduleId: 'example-custom-processor',  register(env) {    env.registerInit({      deps: {        catalog: catalogProcessingExtensionPoint,      },      async init({ catalog }) {        catalog.addProcessor(new MyCustomProcessor());      },    });  },});

확장 포인트

모듈은 deps 섹션에 지정함으로써 일반 의존성처럼 확장 포인트에 의존해요.

확장 포인트 정의

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

확장 포인트 등록

확장 포인트는 플러그인이 등록하고 모듈이 확장해요.

백엔드 서비스

기본 백엔드는 구성, 로깅, 데이터베이스 등에 대한 접근을 포함하는 여러 핵심 서비스를 기본 제공해요. 서비스 의존성은 플러그인이나 모듈의 deps 섹션에서 ServiceRef로 선언되며, 구현은 플러그인이나 모듈의 init 메서드로 전달돼요.

서비스 참조

ServiceRef는 나중에 구체적인 서비스 구현을 해결하는 데 사용되는 인터페이스에 대한 이름 있는 참조예요. 개념적으로는 프론트엔드의 ApiRef와 매우 유사해요. 서비스는 이전에 PluginEnvironment에 있던 Config, Logging, Database 같은 공통 유틸리티를 제공하는 것이에요.

시작 시 백엔드는 의존하는 플러그인/모듈에 전달되기 전에 서비스가 초기화되도록 확인할 거예요. ServiceRef는 서비스 팩토리가 서비스를 만드는지 여부를 결정하는 데 사용되는 scope를 포함하며, 서비스 팩토리는 플러그인/모듈별로 새 인스턴스를 만들거나 공유되게 만들 수 있어요. plugin 범위 서비스는 플러그인/모듈마다 한 번 생성되고, root 범위 서비스는 백엔드 인스턴스마다 한 번 생성돼요.

서비스 정의

import {  createServiceFactory,  coreServices,} from '@backstage/backend-plugin-api';import { ExampleImpl } from './ExampleImpl';export interface ExampleApi {  doSomething(): Promise<void>;}export const exampleServiceRef = createServiceRef<ExampleApi>({  id: 'example',  scope: 'plugin', // can be 'root' or 'plugin'  // The defaultFactory is optional to implement but it will be used if no other factory is provided to the backend.  // This is allows for the backend to provide a default implementation of the service without having to wire it beforehand.  defaultFactory: async service =>    createServiceFactory({      service,      deps: {        logger: coreServices.logger,        plugin: coreServices.pluginMetadata,      },      // Logger is available directly in the factory as it's a root scoped service and will be created once per backend instance.      async factory({ logger, plugin }) {        // plugin is available as it's a plugin scoped service and will be created once per plugin.        return async ({ plugin }) => {          // This block will be executed once for every plugin that depends on this service          logger.info('Initializing example service plugin instance');          return new ExampleImpl({ logger, plugin });        };      },    }),});

서비스 오버라이드

이 예시에서는 기본 root logger 서비스 구현을 로그를 GCP로 스트리밍하는 사용자 지정 구현으로 교체해요. rootLoggerServiceRef는 'root' 범위를 가지므로, 이 서비스의 플러그인 전용 인스턴스가 없어요.

import {  createServiceFactory,  rootLoggerServiceRef,  LoggerService,} from '@backstage/backend-plugin-api';// This custom implementation would typically live separately from// the backend setup code, either nearby such as in//   packages/backend/src/services/logger/GoogleCloudLogger.ts// Or you can let it live in its own library package.class GoogleCloudLogger implements LoggerService {  static factory = createServiceFactory({    service: rootLoggerServiceRef,    deps: {},    async factory() {      return new GoogleCloudLogger();    },  });  // custom implementation here ...}// packages/backend/src/index.tsconst backend = createBackend();// supplies additional or replacement services to the backendbackend.add(GoogleCloudLogger.factory);

테스트

백엔드 플러그인과 모듈을 테스트하기 위한 유틸리티는 @backstage/backend-test-utils에서 사용할 수 있어요. startTestBackend는 플러그인을 테스트하기 위해 supertest와 함께 사용할 수 있는 HTTP를 반환해요.

import { startTestBackend } from '@backstage/backend-test-utils';import request from 'supertest';describe('My plugin tests', () => {  it('should return 200', async () => {    const { server } = await startTestBackend({      features: [myPlugin()],    });    const response = await request(server).get('/api/example/hello');    expect(response.status).toBe(200);  });});

패키지 구조

패키지 아키텍처에 대한 자세한 설명은 Backstage Architecture Overview에서 찾을 수 있어요. 이 시스템에서 고려해야 할 가장 중요한 패키지는 backend, plugin-<pluginId>-backend, plugin-<pluginId>-node, plugin-<pluginId>-backend-module-<moduleId>예요.

  • plugin-<pluginId>-backend는 플러그인 자체의 구현을 담아요.
  • plugin-<pluginId>-node는 확장 포인트와 모듈이나 다른 플러그인이 필요로 할 수 있는 다른 유틸리티를 담아요.
  • plugin-<pluginId>-backend-module-<moduleId>는 확장 포인트를 통해 플러그인을 확장하는 모듈을 담아요.
  • backend는 모든 것을 배포 가능한 것으로 연결하는 백엔드 자체예요.

더 알아보기 (Learn more)