<< 데이터에 현혹되지 않고, 데이터를 잘 활용할수 있는 14가지 룰 | Home | Twitter는 어떻게 1초에 3,000개의 이미지를 처리하고 있나? >>

설정 기반의 플러그인 시스템(Java)



Java에는 OSGi라는 동적 모듈 플러그인 시스템을 구현할 수 있도록 가능케하는 오픈 소스가 있지만 다루기 복잡한 면이 있어, Guice, Airlift, ServiceLoader를 활용해 간단하면서 요긴한 플러그인 시스템을 만들어 본다. 기본적인 제약사항은 단일 프로세스에, 플러그인 추가나 제거시 프로세스를 재시작해야 하며, 정의된 플러그인만 가능하다는 점을 일러둔다.

필요한 오픈 소스

1. Guice(구글에서 만든 JSR330 (Dependency Injection for Java)의 레퍼런스)
  • Dependency Injection(의존성 주입).

2. Airlift(페이스북에서 만듬)
  • Distribute Service Framework.
  • Distribute Service Framework이지만, 여기서는 Bootstrap(설정 정보와 함께 플러그인 모듈 로딩), Dependency Injection(의존성 주입), Configuration 기능 등을 사용함.

3. Java 표준 API java.util.ServiceLoader
  • 클래스 로더를 플러그인 시스템화할 수 있도록 만들어 줌.
  • JDK 1.6이상에서 제공.

설정 기반의 플러그인 구현

1. 설정 기반의 플러그인 추상 클래스 정의 : PluginModule
public abstract class PluginModule implements ConfigurationAwareModule {
  private ConfigurationFactory configurationFactory;
  private Binder binder;

  @Override
  public synchronized void setConfigurationFactory(ConfigurationFactory
    configurationFactory) {
    this.configurationFactory = checkNotNull(configurationFactory,
      "configurationFactory is null");
  }

  @Override
  public void configure(Binder binder) {
    checkState(this.binder == null, "re-entry not allowed");
    this.binder = checkNotNull(binder, "binder is null");
    try {
      setup(binder);
    } finally {
      this.binder = null;
    }
  }

  // 설정 파일과 Config 클래스 매핑 처리
  protected synchronized  T buildConfigObject(Class configClass, String prefix) {
    configBinder(binder).bindConfig(configClass, prefix != null ?
      Names.named(prefix) : null, prefix);
    try {
      Method method = configurationFactory.getClass().getDeclaredMethod("build",
        Class.class, String.class, ConfigDefaults.class);
      method.setAccessible(true);
      Object invoke = method.invoke(configurationFactory,
        configClass, prefix, ConfigDefaults.noDefaults());
      Field instance = invoke.getClass().getDeclaredField("instance");
      instance.setAccessible(true);
      return (T) instance.get(invoke);
    } catch (NoSuchMethodException | IllegalAccessException
    | InvocationTargetException | NoSuchFieldException e) {
      throw new IllegalStateException("configuration error. ", e);
    }
  }

  // 클래스 바인딩
  protected abstract void setup(Binder binder);

  @NotNull
  public abstract String name();

  public abstract String description();
}

buildConfigObject 함수는 설정 파일을 설정 파일 클래스와 매핑시켜주고, setup 재정의를 통해 실제 구현 클래스들을 바인딩한다.

2. 확장 플러그인 구현 클래스 : MysqlModule
@AutoService(PluginModule.class)
@ConditionalModule(config = "plugin.adapter.mysql", value = "true")
public class MysqlModule extends PluginModule {
  @Override
  protected void setup(Binder binder) {
    JDBCConfig config = buildConfigObject(JDBCConfig.class, "plugin.adapter.mysql");
    binder.bind(JDBCPoolDataSource.class)
   .annotatedWith(Names.named("plugin.adapter.mysql"))
        .toInstance(JDBCPoolDataSource.getOrCreateDataSource(config));
    binder.bind(MessageService.class).to(MessageServiceImpl.class)
   .in(Scopes.SINGLETON);
  }

  @Override
  public String name() {
    return "MySQL Plugin Module";
  }

  @Override
  public String description() {
    return "MySQL Plugi Module";
  }
}

MySQL DB를 사용할 경우 설정(config.properties)에 "plugin.adapter.mysql=true"가 지정되면 MysqlModule 로딩되어 MessageService는 MySQL 커넥션 풀이 셋팅되어 해당 데이터 소스를 활용할 수 있게 된다.

3. 기타
- 데이터 처리 : MessageDao
public abstract class MessageDao implements GetHandle {
  private final Logger log = Logger.get(MessageDao.class);

  public List getChatMessages(Message message) {
    try (Handle handle = getHandle()) {
      return handle
        .createQuery(
        "SELECT mq_samsung_chat_id as id, from_subs_id, to_subs_id, mq_topic_id,
        payload, read_yn, bookmark_yn, read_date, send_date\n"
          + "FROM mq_samsung_chat\n" + "WHERE to_subs_id = :toSubsId LIMIT 5")
        .bind("toSubsId", message.getToSubsId()).map(new MessageMapper()).list();
    } catch (Exception e) {
      log.error(e, "getChatMessages caught exception");
      return null;
    }
  }
}
 

데이터 핸들링 부분은 JDBI를 통해 처리했다.

- 커넥션 풀은 HikariDataSource를 사용했으며, JDBCPoolDataSource 소스를 보면 확인할 수 있다.

- SystemRegistryGenerator
config.properties 파일 자동 생성해 준다.

4. 실제 사용
- Main 클래스
public class Main {
  private static final Logger log = Logger.get(Main.class);

  public static Set getModules() {
    ImmutableSet.Builder builder = ImmutableSet.builder();
    ServiceLoader modules = ServiceLoader.load(PluginModule.class);
    for (Module module : modules) {
      if (!(module instanceof PluginModule)) {
        throw new IllegalStateException(
            format("Module 은 PluginModule 의 하위 클래스여야 함. %s",
            module.getClass().getName()));
      }
      log.info("Module = " + module.getClass().getName());
      PluginModule pluginModule = (PluginModule) module;
      builder.add(pluginModule);
    }
    return builder.build();
  }

  public static void main(String[] args) throws Throwable {
    if (args.length > 0) {
      System.setProperty("config", args[0]);
    }
    Bootstrap app = new Bootstrap(getModules());
    app.requireExplicitBindings(false);
    Injector injector = app.strictConfig().initialize();
    List messages = injector.getInstance(MessageService.class).getChatMessages(
        new Message(null, null, "test", null, null, null, null, null, null));
    log.debug("messages=" + messages.toString());
  }
}

getModules 함수는 플로그인 구현 클래스들을 로딩하고, main에서 실제 간단한 플러그인 시스템을 사용하는 부분이 정의되어 있다.

- 전제 소스 흐름 정의
Guice를 사용한 이유는 DI 패턴을 통해 행태와 의존성을 분리하는데 있다.
위 소스에서 MessageService가 생성자에서 MySQL dataSource를 선택하는 것을 MessageService가 선택하는 것이 아니고 생성자의 인수로 전달된다. bind를 통해 MessageService의 생성자에 @Inject를 붙이면, Guice는 JDBCPoolDataSource 데이터 소스를 찾아달라고 한다. 그래서 mysql 설정이 되어 있다면 MySQLModule에서 bind 된 JDBCPoolDataSource의 인수를 MessageService의 생성자로 전달해 준다.

그리고 MessageService의 인스턴스를 얻어 getChatMessages 함수를 호출해 MySQL의 데이터베이스에서 데이터를 가지고 온다.

- 실행 환경
  • Main class : com.mimul.plugin.module.Main
  • vm option : -Dlog.levels-file=src/main/resources/log.properties
  • arguments : src/main/resources/config.properties

- 실행 결과
Module = com.mimul.plugin.module.MysqlModule

Bootstrap  Loading configuration
Bootstrap  Initializing logging
HikariDataSource  generic-jdbc-query-executor - is starting.
Bootstrap  PROPERTY                                          RUNTIME
Bootstrap  plugin.adapter.mysql.connection.max-idle-timeout  null
Bootstrap  plugin.adapter.mysql.connection.max-life-time     null
Bootstrap  plugin.adapter.mysql.data-source                  null
Bootstrap  plugin.adapter.mysql.driver_class_name            com.mysql.jdbc.Driver
Bootstrap  plugin.adapter.mysql.max_connection               5
Bootstrap  plugin.adapter.mysql.password                     testadmin
Bootstrap  plugin.adapter.mysql.test_query                   select 1
Bootstrap  plugin.adapter.mysql.url    jdbc:mysql://localhost:3306/test
Bootstrap  plugin.adapter.mysql.username                     test

Main  messages=[Message(id=1, fromSubsId=clientId-23090, toSubsId=1...)]

Main 클래스를 실행하면, 위와 같이 MySQL DB의 데이터를 5건 가져온다.

5. 전체 소스
- PluginSystem.
이 플러그인 모듈은 Netty 기반으로 REST API를 구성하는데 활용할 예정이며, 또한, 프로세스를 내리지 않고도 추가, 제거 가능하게 보완할 것입니다. 어느 정도 완료되면 소스를 공개하겠습니다. 그리고 REST API를 결합한 모듈은 저희 Data Visualization 제품인 U2에도 적용되어 있습니다.



Add a comment Send a TrackBack