[backstage] 플러그인 개발 후기

히니·2026년 5월 9일

workLogs

목록 보기
9/9

왜 Backstage는 모든 것을 플러그인으로 쪼개놓았을까?

Backstage를 처음 접하고 yarn new 명령어로 백엔드 플러그인을 생성해 보면, 보편적인 라이브러리 구성이나 폴더 구조가 생각보다 복잡하게 나뉘어 있다는 느낌을 받게 됩니다. 단순히 기능을 만드는 것을 넘어 "왜 굳이 플러그인이라는 개념을 도입했을까?" 혹은 "왜 백엔드 앱과 플러그인 앱을 엄격하게 분리했을까?"라는 근본적인 의문이 생기기 마련입니다.

이 설계의 배경에는 Spotify가 겪었던 대규모 조직의 개발 효율성 고민이 녹아 있습니다. 수천 명의 개발자가 사용하는 포털을 단 한 팀이 전담해서 만드는 것은 현실적으로 불가능합니다. 그래서 도입된 것이 바로 '플러그인 중심 아키텍처'입니다.

이 구조의 가장 큰 목적은 코드 소유권의 분산에 있습니다. 인프라 팀, 보안 팀, 데이터 팀 등 각 전문 부서가 자신들이 필요한 도구를 독립적인 플러그인 형태로 직접 개발하고 관리하게 함으로써, 중앙 팀의 병목 현상을 없애고 각 팀에 자율성을 부여한 것입니다.

또한, 이는 시스템의 안정성과도 직결됩니다. 특정 팀에서 개발한 플러그인에 이슈가 발생하더라도 서비스 전체가 마비되는 최악의 상황을 방지합니다. 프론트엔드에서는 에러 바운더리를 통해 해당 컴포넌트만 에러를 표시하고, 백엔드 역시 독립된 라우트로 동작하기 때문에 문제가 발생한 특정 기능만 멈출 뿐 나머지 카탈로그나 템플릿 기능은 정상적으로 유지됩니다.

결국 Backstage가 앱(App)과 플러그인(Plugin)을 철저히 나눈 이유는 명확합니다. 앱은 인증이나 데이터베이스 연결 같은 공통 기반 시설을 제공하는 '표준 껍데기' 역할을 하고, 플러그인은 그 위에서 돌아가는 '독립적인 비즈니스 로직'이 됩니다. 이러한 구조 덕분에 개발자는 공통 설정에 신경 쓰지 않고 오직 기능 개발에만 집중할 수 있으며, 기업은 필요한 플러그인만 선택적으로 조합해 우리 회사만의 맞춤형 포털을 완성할 수 있게 되는 것입니다.

코드(백앤드 플러그인) 살펴 보기

plugin.ts 뜯어보기

plugin.ts는 Backstage 백엔드 플러그인의 진입점(Entry Point)입니다. 단순히 코드가 시작되는 지점을 넘어, 플러그인이 동작하기 위해 필요한 부품들을 정의하고 조립하는 Composition Root(조립 레이어) 역할을 수행합니다.

  • 이 파일은 비즈니스 로직을 직접 수행하지 않습니다. 대신 "우리 플러그인은 로거가 필요하고, 설정값이 필요해"라고 선언한 뒤, 준비된 부품들을 router, service, repository에 공급하여 하나의 기계처럼 맞물려 돌아가게 만듭니다.

  • 계층 구조상의 위치: Router, Service, Repository 어느 계층에도 속하지 않는 최상위 레이어입니다. 모든 계층의 인스턴스를 생성하고 연결하는 'DI 컨테이너'로서 존재합니다.

선언만 했는데 어떻게 주입(DI)이 될까?

가장 궁금해하신 "단순히 deps에 선언만 했는데 어떻게 인스턴스가 자동으로 들어오는가?"에 대한 답은 Backstage 프레임워크의 의존성 주입(Dependency Injection) 엔진에 있습니다.

시스템은 init 함수를 실행하기 전, deps에 적힌 서비스들의 인스턴스를 미리 준비합니다.

준비된 인스턴스들을 init({ logger, config, ... }) 처럼 함수의 인자로 쏙 넣어줍니다.

개발자는 인스턴스를 직접 생성(new Logger())할 필요 없이, 이미 완성된 객체를 받아서 쓰기만 하면 됩니다.

결론: 맞습니다. 선언이 곧 요청입니다. Backstage 프레임워크가 일종의 '중매인' 역할을 하여, 필요한 서비스와 플러그인을 런타임에 자동으로 연결해 주는 구조입니다.

export const exampleBackendPlugin = createBackendPlugin({
  pluginId: 'example',
  register(env) {
    env.registerInit({
      // 1. [선언] "이 서비스 토큰들에 해당하는 인스턴스를 가져다줘!" (주문서)
      deps: {
        logger: coreServices.logger,
        config: coreServices.rootConfig,
        httpRouter: coreServices.httpRouter,
        // 필요하다면 더 추가 가능 (예: database: coreServices.database)
      },

      // 2. [수령 및 조립] 시스템이 위에서 요청한 인스턴스들을 init 함수의 인자로 배달함
      async init({ logger, config, httpRouter }) {
        // 여기서부터는 배달된 진짜 객체(logger, config 등)를 사용해서 
        // 로직을 조립하기만 하면 됩니다.
        
        httpRouter.use(
          await createRouter({
            logger,
            config,
          }),
        );
      },
    });
  },
});

Router.ts 뜯어보기

  • http의 요청을 받아 주입받은 서비스를 통해 로직을 처리하고 응답을 돌려주는 역할을 한다. 보통 Router - Service - Repository 계층을 이야기한다.
import { Router } from 'express';
import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';

// 1. 필요한 부품들을 객체(Options) 형태로 정의합니다.
export interface RouterOptions {
  logger: LoggerService;
  httpAuth: HttpAuthService;
}

export async function createRouter(
  options: RouterOptions,
): Promise<express.Router> {
  const { logger, httpAuth } = options;
  const router = Router();

  // 2. 실제 API 핸들러 작성
  router.get('/hello', async (req, res) => {
    // [보안 체크] 말씀하신 allow 옵션을 사용하는 핵심 지점입니다!
    const credentials = await httpAuth.credentials(req, {
      allow: ['user', 'service'], // 사람과 서비스 모두 허용
    });

    logger.info(`Request received from: ${credentials.principal.userEntityRef}`);

    res.json({ status: 'ok', message: 'Hello from Backstage!' });
  });

  return router;
}
profile
안녕하세요

0개의 댓글