[230831] java spi(service provider interface)

Table of Contents

SPI(Service Provider Interface)는 어플리케이션을 보다 확장성있게 만들기 위해 도입됨. 핵심제품을 수정하지 않고, 제품의 특정 부분을 향상시킬 수 있다. 우리가 해야할 일은 특정 규칙을 따르는 서비스의 새로운 구현을 만들고 이를 어플리케이션에 연결하는 것이다. SPI를 이용하여 어플리케이션은 새로운 구현을 로드할 수 있다.

1 Disclaimer

아래 글은 개인 블로그이므로 친절하지 않음. 정확한 내용은 맨 아래 레퍼런스 참조.

2 용어

2.1 Service Provider Interface

서비스가 정의하는 인터페이스 또는 추상클래스 집합. 어플리케이션에서 사용할 수 있는 클래스와 메서드를 말함.

2.2 Service Provider

Provider라고 부르는 서비스 구현체. resource 디렉터리에 META-INF/services 에다가 provider configuration file을 위치시켜야 식별된다. 어플리케이션의 클래스패스에서 사용할 수 있어야 한다.

2.3 ServiceLoader

서비스 구현체를 lazy하게 검색 및 로딩하는데 사용되는 기본 클래스. ServiceLoader는 이미 로드된 서비스의 캐시를 유지한다. 서비스로더를 호출할 때마다 캐시에 있는 요소를 먼저 나열한 다음 나머지 provider를 검색하고 인스턴스화한다.

2.4 How does ServiceLoader work?

SPI 클래스는 classpath 내에 정의된 다양한 Provider를 자동으로 로드하므로 검색 메커니즘으로 설명해야한다.

ServiceLoader는 검색을 제공하는 몇 가지 방법을 제공하며 이를 수행하는데 사용되는 기본도구이다.

  • iterate() : available providers 를 lazy하게 로드하고 인스턴스화 하는 iterator를 리턴한다. #+BEGINRC Iterator<ServiceInterface> providers = loader.iterator(); while (providers.hasNext()) { ServiceProvider provider = providers.next(); } #+ENDSRC
  • stream() : iterator 대신 stream을 리턴하는 것이다. #+BEGINSRC Stream<ServiceInterface> providers = ServiceLoader.load(ServiceInterface.class) .stream() .map(Provider::get) #+END
  • reload() : 로더의 provider 캐시를 지우고 provider를 다시 로드한다. 이 방법은 실행 중인 JVM에 새로운 서비스 공급자가 설치되는 상황에서 사용한다.

ServiceLOader가 이러한 Provider를 식별하고 로드할 수 있도록 등록하는 프로세스가 있다. META-INF/services 폴더에 configuration file을 추가해야 한다.

service provider interface의 fully qualified class name 이 파일명이고 1개 이상의 fully qualified class name을 가진다. 한줄에 하나씩이다.

예를들어, InterfaceName 이라는 Service Provider가 있다. ServiceProviderImplementation 을 공급자로 등록하고 싶다면, package.name.IterfaceName 이라는 파일명과, package.name.ServiceProviderImplementation 한 줄이 있어야 한다.

classpath 에 같은 이름을 가진 많은 configuration 파일이 있음을 알 수 있다. 이런 이유로, ServiceLoader는 ClassLoader.getResources() 메서드를 사용하여 각 공급자를 식별하기 위해 모든 Configuration의 Enumeration을 가져온다.

3 Driver 서비스 탐색하기

기본적으로 Java는 다양한 서비스 공급자가 포함되어 있다. 그 중 하나는 데이터베이스 드라이버를 로드하는데 사용하는 드라이버다.

드라이버에 대해 더 자세히 살펴보고 애플리케이션에 데이터베이스 드라이버가 어떻게 로드되는지 이해해보자.

PostgreSQL JAR 파일을 보면, java.sql.Driver 라는 파일이 포함된 META-INF/services 라는 폴더를 찾을 수 있다. 이 컨피그파일은 Driver Interface에 대해 PostgreSQL이 제공하는 구현 클래스의 이름을 보유하고 있을 것이다. (org.postgresql.Driver)

Mysql Driver 도 마찬가지다. META-INF/services 에 있는 java.sql.Driver 라는 이름의 파일에는 드라이버 인터페이스의 MySQL 구현인 com.mysql.cj.jdbc.Driver 가 포함되어 있다.

두 드라이버가 클래스 경로에 로드되면 ServiceLoader는 각 파일에서 구현 클래스의 이름을 읽은 다음 클래스 이름과 함께 Class.forName() 을 호출한 다음 newInstance()를 호출하여 구현 클래스의 인스턴스를 만든다.

이제 두 가지 구현(mysql, postgresql)이 로드되었으므로 데이터베이스에 대한 연결은 어떻게 작동할까?

java.sql 패키지에 있는 DriverManager 클래스의 getConnection() 메서드에서 다른 드라이버를 사용할 수 있을 때 데이터베이스에 대한 연결이 어떻게 설정되는지 확인할 수 있다.

코드를 보자.

for (DriverInfo aDriver : registeredDrivers) {
  if (isDriverAllowed(aDriver.driver, callerCL)) {
    try {
      println("trying " + aDriver.driver.getClass().getName());
      Connection con = aDriver.driver.connect(url, info);
      if (con != null) {
	// Success
	println("getConnection returning " + aDriver.driver.getClass().getName());
	return (con);
      }
    } catch (SQLException ex) {
      if (reason == null) {
	reason = ex;
      }
    }
  } else {
    println("skipping: " + aDriver.getClass().getName())
  }
}

이처럼 알고리즘은 registeredDrivers를 통과하고, 데이터베이스 URL을 사용하여 데이터베이스에 연결을 시도한다. 연결이 설정되면 Connection 객체가 반환되고, 그렇지 않으면 모든 드라이버가 처리될 때 까지 다른 드라이버가 시도된다.

4 Implementing a Custom Service Provider

고객이 요청할 때 도서관에 책이 있는지 여부를 확인하기 위한 애플리케이션이 필요한 사서가 있다고 하자. LibraryService라는 클래스와 Library 라는 서비스 공급자 인터페이스를 정의하여 이를 수행할 수 있다.

LibraryService는 싱글톤 LibraryService 객체를 제공한다. 이 객체는 Library provider 로부터 book 을 검색한다.

library service client(라이브 서비스 사용자, 우리의 경우 현재 만들려는 앱)는이 서비스의 인스턴스를 가져오고 서비스는 서비스 공급자를 검색, 인스턴스화 및 사용한다.

애플리케이션 개발자는 우선 모든 라이브러리에서 사용할 수 있는 표준 도서 목록을 사용할 수 있다. 컴퓨터 과학 서적을 다루는 다른 사용자는 해당 라이브러리에 대해 다른 서적 목록을 요구할 수 있다. (다른 library provider)

이 경우 사용자가 핵심 기능을 수정하지 않고 기존 응용 프로그램에 원하는 책이 있는 새 라이브러리를 추가할 수 있다면 더 좋을 것이다.

  • library-service-provider : Service Profider Interface인 Library를 가짐. 공급자를 로드하기 위한 서비스 클래스를 가짐.
  • classic-library : 개발자가 선택한 고전 서적 라이브러리 Provider
  • computer-science-library : 사용자가 필요로 하는 컴퓨터과학서적 라이브러리 Provider
  • library-client : 모든 것을 모아 작업예제를 만드는 프로그램

4.1 library-service-provider module

서적 객체.

public class Book {
  String name;
  String author;
  String description;
}

서비스를 위한 Service provider interface

package org.library.spi;

public interface Library {
  String getCategory();
  Book getBook(String name);
}

마지막으로, 우리는 LibraryService 클래스를 만든다. 클라이언트가 도서관의 책을 검색하기 위해 사용한다.

public class LibraryService {
  private static LibraryService libraryService;
  private final ServiceLoader<Library> loader;

  public static synchronized LibraryService getInstance() {
    if (libraryService == null) {
      libraryService = new LibraryService();
    }
    return libraryService;
  }

  private LibraryService() {
    loader = ServiceLoader.load(Library.class);
  }

  public Optional<Book> getBook(String name) {
    Book book = null;
    Iterator<Library> libraries = loader.iterator();
    while (book == null && libraries.hasNext()) {
      Library library = libraries.next());
      book = library.getBook(name);
    }
    return Optional.ofNullable(book);
  }

  public Optional<Book> getBook(String name, String category) {
    return loader.stream()
      .map(ServiceLoader.Provider::get)
      .filter(library -> library.getCategory().equals(category))
      .map(library -> library.getBook(name))
      .filter(Objects::nonNull)
      .findFirst();
  }
}

클라이언트는 getInstance() 메서드를 사용하여 필요한 책을 검색하기 위한 싱글톤 LibraryService 객체를 가져온다.

생성자 LibraryService는 정적 팩터리 메서드 load()를 호출하여 라이브러리 구현을 검색할 수 있는 ServiceLoader의 인스턴스를 가져온다.

getBook(String name)은 iterate()을 이용해서 사용 가능한 모든 라이브러리 구현을 순회하여 찾아낸다.

getBook(String name, String category) : 특정 카테고리에서 책을 찾는다. 여기선 stream() 으로 공급자(Provider)를 로드한 다음 getBook() 메서드를 호출하여 책을 찾는 방식을 이용한다.

5 classic-library 모듈

이 하위모듈은 service provider에 의존성을 가진다. (구현체니까)

<dependency>
  <groupId>org.library</groupId>
  <artifactId>library-service-provider</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

이제 Library SPI 를 구현하자.

public class ClassicLibrary implements Library {
  public static final String CLASSIC_LIBRARY = "CLASSICS";
  private final Map<String, Book> books;

  public ClassicLibrary () {
    books = new TreeBook<>();

    Book nineteenEightyFour = new Book("Nineteen Eighty-Four",
	"George Orwell", "Description");
    Book theLordOfTheRings = new Book("The Lord of the Rings",
	"J. R. R. Tolkien", "Description");

    books.put("Nineteen Eighty-Four", nineteenEightyFour);
    books.put("The Lord of the Rings", theLordOfTheRings);
  }
  @Override
  public String getCategory() {
    return CLASSICS_LIBRARY;
  }

  @Override
  public Book getBook(String name) {
    return books.get(name);
  }
}

이 구현체는 2개의 책을 검색할 수 있다.

이제 리소스 디렉터리에 org.library.spi.Library 라는 파일을 META-INF/services 에 위치시켜야 한다. 파일내용은 ServiceLoader가 인스턴스화 할 떄 사용할 구현체 fully qualified class name 이 써진다. 여기서는 org.library.ClassicsLibrary 가 된다.

5.1 The computer-science-library Module

package org.library;
import java.util.Map;
import java.util.TreeMap;

import org.library.spi.Book;
import org.library.spi.Library;

public class ComputerScienceLibrary implements Library {

    public static final String COMPUTER_SCIENCE_LIBRARY = "COMPUTER_SCIENCE";

    private final Map<String, Book> books;

    public ComputerScienceLibrary() {
	books = new TreeMap<>();

	Book cleanCode = new Book("Clean Code", "Robert C. Martin",
		"Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees");
	Book pragmaticProgrammer = new Book("The Pragmatic Programmer", "Hunt Andrew, Thomas David",
		"This book is filled with both technical and professional practical advices for developers in order become better developers.");

	books.put(cleanCode.getName(), cleanCode);
	books.put(pragmaticProgrammer.getName(), pragmaticProgrammer);
    }

    @Override
    public String getCategory() {
	return COMPUTER_SCIENCE_LIBRARY;
    }

    @Override
    public Book getBook(String name) {
	return books.get(name);
    }
}

resources/META-INF/services/org.library.spi.Library 안에 org.library.ComputerScienceLibrary 가 있다.

5.2 The library-client Module

이 모듈은 이제 사용자이므로, Library에서 책을 찾기위해 LibarayService를 사용할 것이다. 처음은 데모용으로 classic library만 쓰고 이후에 computer science library를 의존성에 넣자. 그러면 ServiceLoader가 알아서 검색해서 인스턴스화 할 것이다.

library-client pom.xml 에 드디어 classic-library 모듈이 들어간다.

<dependency>
  <groupId>org.library</groupId>
  <artifactId>classics-library</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
  <groupId>org.library</groupId>
  <artifactId>library-service-provider</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

이제 책을 검색하자

public class LibraryClient {
  public static void main(String[] args) {
    LibraryService libraryService = LibraryService.getInstance();
    requestBook("Clean Code", libraryService);
    requestBook("The Lord of the Rings", libraryService);
    requestBook("The Lord of the Rings", "COMPUTER_SCIENCE", libraryService);
  }

  private static void requestBook(
      String bookName,
      String category,
      LibraryService library) {
    library.getBook(bookName, category)
      .ifPresentOrElse(
	book -> System.out.println("The book '" + bookName +
	  "' was found in  " + category + ", here are the details:" + book),
	() -> System.out.println("The library " + category + " doesn't have the book '"
	  + bookName + "' that you need."));
  }
}
--- output ---
The library doesn't have the book 'Clean Code' that you need.
The book 'The Lord of the Rings' was found, here are the details:Book{name='The Lord of the Rings',...}
The library COMPUTER_SCIENCE doesn't have the book 'The Lord of the Rings' that you need.

Clean Code는computer science library 에 있을 것이다.

자 그럼 이것을 추가하자

<dependency>
  <groupId>org.library</groupId>
  <artifactId>computer-science-library</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

다시 시도하면 Clean Code가 검색될 것이다.

The book 'Clean Code'was found, here are the details:Book{name='Clean Code...}
The book 'The Lord of the Rings' was found, here are the details: Book{name='The Lord of ...}
The library COMPUTER_SCIENCE doesn't have the book 'The Lord of the Rings' that you need.

이렇게 provider를 플러그인만 하면 되었다.

6 reference

Date: 2023-08-31 Thu 00:00

Author: Younghwan Nam

Created: 2024-01-04 Thu 09:13

Emacs 27.2 (Org mode 9.4.4)

Validate