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 |
댓글