Service Provider Framework ? Interface ?

jiho·2021년 5월 20일
0

Java

목록 보기
4/6
post-custom-banner

Effective Java 3판을 "Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라" 학습 중에 Service Provider Framework를 설명하는 부분을 따로 정리하겠습니다.

Service Provider Framework

Service Provider Pattern을 구현한 프레임워크를 Service Provider Framework라 합니다.

짧게 정의하자면, 클라이언트가 필요한 서비스의 구현체를 클라이언트에게 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리시켜줍니다.

대표적인 예시가 JDBC가 됩니다.

하지만 자바에서는 따로 공부해서 구현할 필요없이 Java SE 6 부터 Service Provider Framework를 제공합니다. Service Provider Interface라는 API가 이에 해당합니다.

무엇보다 SPI를 사용하는 이유는 확장성 있는 어플리케이션을 작성하기 위해서입니다.
SPI를 활용해서 어떻게 확장성있는 어플리케이션을 작성할 수 있는지 살펴보겠습니다.

Java SE SPI를 활용한 확장성있는 어플리케이션

확장성있는 어플리케이션은 기존 코드 베이스의 수정없이 우리가 확장할 수 있는 어플리케이션을 말합니다.

즉, 우리는 새로운 플러그인 혹은 모듈을 가지고 특정 기능을 더욱 확장할 수 있습니다. 개발자나 소프트웨어 벤더, 고객들은 Java Archive(JAR) file을 application의 classpath 혹은 어플리케이션의 특정한 확장 디렉토리에 추가함으로써 새로운 기능이나 API를 추가 할 수 있습니다.

기존 어플리케이션에 어떠한 수정없이 서비스 구현체를 제공할 수 있는 확장성있는 서비스들을 가지고 어플리케이션을 어떻게 만드는 방법을 정리해보겠습니다.

여기서 사용할 확장성있는 어플리케이션의 예제는 사용자가 새로운 사전을 추가하거나 스펠링 체크방식을 추가하는 것을 가능하게하는 word processor입니다. 이 예제에서는 word processor가 사전과 스펠링 체크 기능을 다른 개발자나, 심지어 고객이 자신만의 기능을 구현해서 제공함으로써 확장할 수 있습니다.

Terms and definitions

확장성 있는 어플리케이션을 이해하기 위해 중요한 단어와 정의가 있습니다.

  • Service
    - 특정 어플리케이션의 기능과 특징에 접근을 제공하는 interface나 class를 말합니다.
    • Client 입장에서는 기능에 접근하는 Entry Point가 될 수 있습니다.
    • Service는 기능에 대한 인터페이스와 구현체를 반환하는 방식을 정의할 수 있습니다. Word processor예제에서 사전 서비스는 단어의 정의와 사전을 반환하는 방식을 정의할 수 있습니다. 하지만 내부적인 기능들은 정의하지 않습니다. 대신에 그것은 기능을 구현하는 Service Provider에 의존합니다.
  • Service Provider Interface(SPI)
    - 서비스가 정의한 public interface와 abstract class들을 의미합니다. SPI는 우리의 어플리케이션에서 이용할 수 있는 class나 method들을 정의합니다.
  • Service Provider
    - SPI의 구현체. 확장성 있는 서비스들을 가진 어플리케이션은 개발자나 벤더, 고객들이 Service Provider들을 기존 어플리케이션의 수정없이 추가할 수 있습니다. // 그림 추가하기

단어 정의 자체가 이해되지않는다면 예제를 보면서 다시 정리해보겠습니다.

Dictionary Example Requirement

어떻게 워드 프로세서와 편집기에 dicionary service를 설계할지에 대해 생각해봅시다.

한가지 방법은 DictionaryService라는 이름의 클래스로 Service를 정의하고 Dictionary라는 이름의 Service Provider Interface를 정의하는 것입니다.
DictionaryService는 싱글톤 Dictionary Object를 제공합니다. 이 객체는 Dictionary providers로부터 단어의 정의들을 받을 수 있습니다. Dictionary Service 클라이언트(우리의 어플리케이션 코드)는 Service의 인스턴스를 이용해서 Dictionary service provider를 탐색하고 인스턴스화해서 사용합니다.

word processor 개발자는 기초적이고 일반적인 Dictionary 기존 상품과 제공할지라도, 고객들은 특별한 사전(법률적이거나 기술적 용어를 포함하는)을 요구합니다. 고객은 어플리케이션에 새로운 사전을 구매하거나 만들어서 추가할 수 있습니다.

요구사항은 위와 같이 정의할 수 있습니다.

DictionaryServiceDemo Project Structure

DictionaryServiceDemo Sample은 Dictionary service를 구현하는 방법을 보여주고 Dictionary Service provider를 만드는 방법을 보여주며, 서비스를 테스트하기 위한 간단한 Dictionary Service client를 만듭니다.

Oracle Tutorial
DictionaryServiceDemo.zip
를 참조하시길 바랍니다.

샘플 이해하기

  1. Service Provider Interface 정의하기
package dictionary.spi;
public interface Dictionary {
	public String getDefinition(String word);
}

우리 프로젝트에서는 하나의 SPI Dictionary.java 를 정의합니다. 오직 하나의 method를 포함합니다. 코드를 보면서 SPI가 무엇인지 이해가 더 잘되는 부분입니다. 우리가 제공받아서 사용할 객체의 인터페이스를 SPI라고 하다는 것을 알게됩니다.

  1. Service Provider의 구현체를 전달해줄 Service 정의하기
package dictionary;

import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;

public class DictionaryService {

    private static DictionaryService service;
    private ServiceLoader<Dictionary> loader;

    private DictionaryService() {
        loader = ServiceLoader.load(Dictionary.class);
    }

    public static synchronized DictionaryService getInstance() {
        if (service == null) {
            service = new DictionaryService();
        }
        return service;
    }


    public String getDefinition(String word) {
        String definition = null;

        try {
            Iterator<Dictionary> dictionaries = loader.iterator();
            while (definition == null && dictionaries.hasNext()) {
                Dictionary d = dictionaries.next();
                definition = d.getDefinition(word);
            }
        } catch (ServiceConfigurationError serviceError) {
            definition = null;
            serviceError.printStackTrace();

        }
        return definition;
    }
}

DictionaryService는 Singleton 으로 하나의 인스턴스만 프로그램에서 유지됩니다. 그리고 사용자가 Dictionary Service Provider를 사용하는데 있어서 진입점이기도 합니다.

눈여겨봐야할 점은 ServiceLoader라는 Java SE의 클래스입니다.

getDifintion 를 호출할 때 찾고자하는 단어를 찾을 때까지 SPI(Interface Dictionary)를 구현한 이용할 수 있는 Dictionary Provider들을 순회하게됩니다.

dictionary service는 target class를 찾기위해 ServiecLaoder.load method를 사용합니다. default로 load method는 application class path를 탐색하게됩니다.

  1. Service Provider 정의하기

Service Provider를 정의하기위해서는 SPI인 Dictionary 인터페이스를 구현해야합니다. 가장 간단한 Sample인 GenericDictionary.java의 코드를 살펴보겠습니다.

package dictionary;

import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;

public class GeneralDictionary implements Dictionary {

    private SortedMap<String, String> map;
    
    public GeneralDictionary() {
        map = new TreeMap<String, String>();
        map.put(
            "book",
            "a set of written or printed pages, usually bound with " +
                "a protective cover");
        map.put(
            "editor",
            "a person who edits");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }
}

위와 같이 정의해두고 추가로 해줘야할 일은 Service Provider라는 사실을 등록해줘야합니다. 등록하는 방법은 간단합니다.

Service Provider의 JAR file의 META-INF/services directory속에 configuration file을 만들어야합니다. 그리고 설정 파일의 이름은 Service Provider Interface의 fully qullified class name으로 지정해줘야합니다.(아래처럼)

그리고 Provider Configurtion file 은 직접 구현한 Serivce Provider의 fully qualified class name을 한줄에 하나씩 지정해줘야합니다.

in GeneralDictionary

dictionary.GeneralDictionary

in ExtendedDictionary

dictionary.ExtenedDictionary
  1. Service 와 Service Provider를 사용하는 Client 작성
package dictionary;

import dictionary.DictionaryService;

public class DictionaryDemo {

  public static void main(String[] args) {

    DictionaryService dictionary = DictionaryService.getInstance();
    System.out.println(DictionaryDemo.lookup(dictionary, "book"));
    System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
    System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
    System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
  }

  public static String lookup(DictionaryService dictionary, String word) {
    String outputString = word + ": ";
    String definition = dictionary.getDefinition(word);
    if (definition == null) {
      return outputString + "Cannot find definition for this word.";
    } else {
      return outputString + definition;
    }
  }
}

결과적으로 DicitonaryService 클래스의 인터페이스만을 활용하기 때문에 새로운 사전을 추가한다해서 변경될 여지는 없어보입니다.

코드자체가 너무 간단해서 포인트가 될만한 부분만 언급해서 정리했습니다. 전체 코드를 한번 다운받아서 실행해보시면 SPI의 의도와 원리정도는 가볍게 파악하실 수 있을 것 같습니다.

마지막으로 JAVA SE 6부터 지원되는 Service Provider Framework인 java.util.ServiceLoader를 조금더 살펴보겠습니다.

java.util.ServiceLoader

ServiceLoader는 서비스 Provider를 찾고 로드하고 사용하는 것을 도와줍니다.

Service Providers를 탐색하는 장소는 어플리케이션의 classpath 혹은 런타임 환경의 확장 directory입니다.

provider의 API를 사용할 수 있도록 로드합니다.

만약 classpath 나 런타임 확장 디렉토리에 새로운 Provider를 추가한다면, ServiceLaoder는 그것을 동적으로 찾을 수 있습니다.

ServiceLoader 클래스는 final(상속 불가)이며, 이것은 loading algorithm을 override할 수 없다는 것을 의미합니다. 예를들어 다른 위치에서 서비스를 탐색하도록 변경할 수 없다는 것을 의미합니다.

Providers 필요에 따라 인스턴스화 될 수 있습니다. 하나의 service loader는 로드된 provider의 캐시를 유이합니다. 각 loader의 iterator 메소드 호출은 캐시의 모든 요소를 순서대로 반환하게됩니다. 물론 reload method를 사용해서 cache를 clear할 수 있습니다.

ServiceLoader API의 한계

ServiceLoader API는 유용하지만 한계를 가지고 있습니다. 예를 들어, ServiceLoader class로 부터 상속을 하는 것은 불가능합니다. 그래서 우리는 그 동작을 수정할 수는 없습니다.

물론 ServiceLoader class는 새로운 provider를 런타임 도중에 이용할 수 있게 되었을 때 우리 어플리케이션에게 알려줄 수 없다.

public ServiceLoader API는 Java SE 6에 이용할 수 있습니다. 비록 loader service 가 JDK 1.3 초기에 존재했을 지라도, 이 API는 private 였었고 오직 Java runtime code내부에서만 이용할 수 있었다.

요약

Service Provider Framework의 간단한 샘플을 java.util.ServiceLoader를 통해 살펴보았습니다.

Reference

https://docs.oracle.com/javase/tutorial/ext/basics/spi.html#introduction

https://dzone.com/articles/java-service-loader-vs-spring-factories-loader

profile
Scratch, Under the hood, Initial version analysis
post-custom-banner

0개의 댓글