본문 바로가기
CS

Plug-in Architecture(pf4j을 곁들인 예시)

by 열정적인 이찬형 2025. 3. 6.

Plug-in Architecture

일상생활에서 사용하고 싶은 전자제품에 대해서 플러그를 꽂고 사용한 뒤 플러그를 빼는 활동을 자연스럽게 행동합니다.

위 행동을 개발하는 용어로 살펴본다면 아래와 같이 볼 수 있습니다.

일반적인 우리의 행동 개발자로 보는 관점
일상생활을 하고 있는 자신 RunTime 중인 서버
하고 싶은 활동 인터페이스
전자 제품 인터페이스가 구현된 객체

 

Plug-in Architecture는 위 개념에 따라 코어 시스템을 중심으로 미리 정의한 인터페이스으로 구현된 Plug-in을 필요에 따라 붙여서 사용하는 아키텍처입니다.

 

코어 시스템에 대한 최소한의 기능을 구현하고 사용에 따라 변화할 수 있는 부분은 Plug-in으로 RunTime의 대처할 수 있어서 확장성이 용이한 방식입니다.

 

 

용어 설명
코어 시스템 - 시스템을 실행의 필요한 최소한의 기능
인터페이스 - 시스템의 부가적인 기능에 대한 명세를 정의한 내용
플러그인 - 정의된 인터페이스를 기반으로 정의한 독자적인 확장자
- 코어 시스템의 확장을 하는 역할로 다른 플러그인과 연관
- 관계를 가진 것이 아닌 단독으로 실행이 가능해야 한다.


구현

 

[구현하게 될 서버 구성도]

 

Java 환경에서 Plug-in Architecture을 도와주는 오픈 소스 pf4j을 이용해서 구현합니다.

 

GitHub - pf4j/pf4j: Plugin Framework for Java (PF4J)

Plugin Framework for Java (PF4J). Contribute to pf4j/pf4j development by creating an account on GitHub.

github.com

 

 

예제 코드는 아래 링크에서 자세히 확인하실 수 있습니다.

 

Spring-Boot-Template/pf4j at main · Tussle0410/Spring-Boot-Template

Contribute to Tussle0410/Spring-Boot-Template development by creating an account on GitHub.

github.com

 

[dependencies]

implementation group: 'org.pf4j', name: 'pf4j', version: '3.10.0'
annotationProcessor(group: 'org.pf4j', name: 'pf4j', version: "3.10.0")

 

[package]

Package
pf4j
  ├── pf4j-api  //부가 기능 인터페이스
  ├── pf4j-app  //플로그인을 사용하는 app
  └── pfj4-plugin //부가 기능을 정의하는 플로그인들
      ├── pf4j-plugin-01
      ├── pf4j-plugin-02
      └── pf4j-plugin-03

 

[pf4j-api]

코어 시스템에 들어갈 부가 기능을 정의하는 인터페이스가 정의됩니다.

import org.pf4j.ExtensionPoint;
public interface Operator extends ExtensionPoint {
    String getOperatorName();
    int plus(int a, int b);
    int minus(int a, int b);
    int multiply(int a, int b);
    int divide(int a, int b);
    int remainder(int a, int b);
}

 

method description
String getOperatorName() 연산을 진행하는 계산기 이름 얻는 함수
int plus(int a, int b) 덧셈 연산
int minus(int a, int b) 뺄셈 연산
int multiply(int a, int b) 곱셈 연산
int divide(int a, int b) 나눗셈 연산
int remainder(int a, int b) 나머지 연산

 

[pf4j-plugin]

plugin description
pf4j-plugin-01(Basic Calculator) 기본 계산기로 정상적인 연산을 작업하는 계산기 입니다.
pf4j-plugin-02(Broken Calculator) 망가진 계산기로 덧셈은 뺄셈을 하는 것처럼 다른 연산을 진행하는 계산기
pf4j-plugin-03(Custom Calculator) 커스텀 계산기로 연산을 진행할 때 추가적인 계산이 들어가는 계산기

 

 

     

    ⚠️  코드를 작성하기 이전에 각 plugin들은 미리 정의된 Operator 인터페이스를 가져온다.

           1. pf4j-api를 build하여 .jar 파일로 만듭니다.

           2. 각 plugin의 libs 폴더를 만든 뒤 해당 pf4j-api.jar파일을 저장합니다.

           3. pf4j-api.jar 파일을 읽어서 인터페이스를 사용할 수 있도록 한다.

 

implementation files('libs/pf4j-api-0.0.1.jar')

pf4j-plugin-01(Basic Calculator)

[BasicCalculatorPlugin.java]

import lombok.extern.slf4j.Slf4j;
import org.pf4j.Plugin;
@Slf4j
public class BasicCalculatorPlugin extends Plugin {
    @Override
    public void start() {
        log.info("기본 계산기 플러그인이 시작되었습니다.");
    }
    @Override
    public void stop() {
        log.info("기본 계산기 플러그인이 중지되었습니다.");
    }
}

 

[OperationExtension.java]

import org.pf4j.Extension;
@Extension
public class OperationExtension implements Operator {
  @Override
  public String getOperatorName() {
    return "BasicCalculator";
  }
  @Override
  public int plus(int a, int b) {
    return a + b;
  }
  @Override
  public int minus(int a, int b) {
    return a - b;
  }
  @Override
  public int multiply(int a, int b) {
    return a * b;
  }
  @Override
  public int divide(int a, int b) {
    return a / b;
  }
  @Override
  public int remainder(int a, int b) {
    return a % b;
  }
}

 

[gradle.properties]

version=0.0.1
pluginId=basic-calculator
pluginClass=com.chan.pf4j.BasicCalculatorPlugin
pluginProvider=Chan
pluginDependencies=

 

pf4j-plugin-02(Broken Calculator)

[BrokenCalculatorPlugin.java]

import lombok.extern.slf4j.Slf4j;
import org.pf4j.Plugin;
@Slf4j
public class BrokenCalculatorPlugin extends Plugin {
    @Override
    public void start() {
      log.info("망가진 계산기 플러그인이 시작되었습니다.");
    }
    @Override
    public void stop() {
        log.info("망가진 계산기 플러그인이 중지되었습니다.");
    }
}

 

[OperationExtension.java]

import org.pf4j.Extension;
@import org.pf4j.Extension;
@Extension
public class OperationExtension implements Operator{
  @Override
  public String getOperatorName() {
    return "BrokenCalculator";
  }
  @Override
  public int plus(int a, int b) {
    return a - b;
  }
  @Override
  public int minus(int a, int b) {
    return a + b;
  }
  @Override
  public int multiply(int a, int b) {
    return a / b;
  }
  @Override
  public int divide(int a, int b) {
    return a * b;
  }
  @Override
  public int remainder(int a, int b) {
    return  a * b * 2;
  }
}

 

[gradle.properties]

pluginVersion=0.0.1
pluginId=broken-calculator
pluginClass=com.chan.pf4j.CustomCalculatorPlugin
pluginProvider=Chan
pluginDependencies=

 

pf4j-plugin-03(Custom Calculator)

[CustomCalculatorPlugin.java]

import lombok.extern.slf4j.Slf4j;
import org.pf4j.Plugin;
@Slf4j
public class CustomCalculatorPlugin extends Plugin {
    @Override
    public void start() {
      log.info("커스텀 계산기 플러그인이 시작되었습니다.");
    }
    @Override
    public void stop() {
        log.info("커스텀 계산기 플러그인이 중지되었습니다.");
    }
}

 

[OperationExtension.java]

import org.pf4j.Extension;
@Extension
public class OperationExtension implements Operator{
  @Override
  public String getOperatorName() {
    return "CustomCalculator";
  }
  @Override
  public int plus(int a, int b) {
    return a + b + 1;
  }
  @Override
  public int minus(int a, int b) {
    return a - b + 1;
  }
  @Override
  public int multiply(int a, int b) {
    return a * b + 1;
  }
  @Override
  public int divide(int a, int b) {
    return a / b + 1;
  }
  @Override
  public int remainder(int a, int b) {
    return  (a % b) + 1;
  }
}

 

[gradle.properties]

pluginVersion=0.0.1
pluginId=custom-calculator
pluginClass=com.chan.pf4j.CustomCalculatorPlugin
pluginProvider=Chan
pluginDependencies=

 

pf4j-app(Core System)

 

   

   ⚠️  코드를 작성하기 이전에 정의한 plugin, interface들을 등록해줍니다.

           1. libs 폴더를 만든 뒤 해당 pf4j-api.jar파일을 저장합니다.

           2. pf4j-plugin-01pf4j-plugin-02, pf4j-plugin-03를 build하여 .jar 파일로 만듭니다.

           3. plugins 폴더를 만든 뒤 plugin.jar 파일들을 저장합니다.

 

[PluginManagerConfig.java]

import java.io.File;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.JarPluginManager;
import org.pf4j.PluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class PluginManagerConfig {
  @Bean
  public PluginManager pluginManager() {
    // plugin .jar file folder path
    String pathString = "path/plugins";
    Path plugins = new File(pathString).toPath();
    return new JarPluginManager(plugins);
  }
}

 

[PluginManagerConfig.java]

import com.chan.pf4j.Operator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/plugins")
@Slf4j
public class PluginController {
  private final PluginManager pluginManager;
  @GetMapping("/load")
  public void loadPlugins() {
    pluginManager.loadPlugins();
  }
  @GetMapping("/unload")
  public void unloadPlugins() {
    pluginManager.unloadPlugins();
  }
  @GetMapping("/start")
  public void startPlugins() {
    pluginManager.startPlugins();
  }
  @GetMapping("/start/{pluginId}")
  public void startPluginByPluginId(@PathVariable String pluginId) {
    pluginManager.startPlugin(pluginId);
  }
  @GetMapping("/stop")
  public void stopPlugins() {
    pluginManager.stopPlugins();
  }
  @GetMapping("/stop/{pluginId}")
  public void stopPluginByPluginId(@PathVariable String pluginId) {
    pluginManager.stopPlugin(pluginId);
  }
  @GetMapping("/operator")
  public void operator(@RequestParam int a, @RequestParam int b) {
    List<Operator> extensions = pluginManager.getExtensions(Operator.class);
    printOperatorLog(a, b, extensions);
  }
  @GetMapping("/operator/{pluginId}")
  public void operatorByPluginId(@RequestParam int a, @RequestParam int b, @PathVariable String pluginId) {
    List<Operator> extensions = pluginManager.getExtensions(Operator.class, pluginId);
    printOperatorLog(a, b, extensions);
  }
  private void printOperatorLog(int a, int b, List<Operator> extensions) {
    for (Operator operator : extensions) {
      log.info("==================================================================");
      log.info("[------------------------ {}------------------------] ", operator.getOperatorName());
      log.info("a = {}, b = {}", a, b);
      log.info("plus >>> {}", operator.plus(a, b));
      log.info("minus >>> {}", operator.minus(a, b));
      log.info("multiply >>> {}", operator.multiply(a, b));
      log.info("divide >>> {}", operator.divide(a, b));
      log.info("remainder >>> {}", operator.remainder(a, b));
      log.info("==================================================================");
    }
  }
}

테스트

 

/plugins/load :등록된 Plugin들을 불러온다.

 

/plugins/unload : 불러온 Plugin들을 취소한다.

 

/plugins/start : 현재 불러온 Plugin들을 실행한다.

 

/plugins/start/{pluginId} : 특정 Plugin만 실행한다.

= /plugins/start/basic-calculator

 

/plugin/stop : 현재 실행 중인 Plugin을 중지한다.

 

/plugins/stop/{pluginId} : 현재 실행 중인 특정 plugin을 중지한다

= /plugins/stop/basic-calculator

 

/plugins/operator : 실행 중인 Plugin들의 계산기 연산을 진행한다.

/plugins/operator?a=4&b=2

 

/plugins/operator/{pluginId} : 실행 중인 특정 Plugin의 계산기 연산을 진행한다.

/plugins/operator/broken-calculator?a=6&b=3

'CS' 카테고리의 다른 글

OpenStack KeyStone  (0) 2025.02.24
DB connection Pool  (0) 2023.04.10
Blocking, Non-blocking & Synchronous, Asynchronous  (0) 2023.04.03
HTTP & HTTPS  (0) 2023.03.30
대칭키(비밀키) & 비대칭키(공개키)  (0) 2023.03.23

댓글