Service Provider Interface

 

SPI, 서비스 제공자 인터페이스 개념

 

 

 

Service Provider Interface는 Service Provider Framework의 한 구성요소입니다. 

 

Service Provider Framework는 확장 가능한 Java Application을 만드는 패턴 중 하나입니다.

 

외부에서 클라이언트에게 Jar파일을 받아, 기존 코드의 변경 없이 기능을 확장할 수 있습니다.

 

 

대표적으로 Java EE(Enterprize Edition)의 핵심 기술 중 하나인 JDBC가 이 패턴을 활용하고 있습니다. 

 

Java Database Connection

 

JDBC가 SPI로 구현된 덕분에

 

우리가 Java 코드로 Database와 Connection을 연결할 때에도

 

Oracle, Mysql, Mssql 등의 모든 드라이버 코드를 먼저 구현하는 것이 아니라, 

 

핵심적인 코드만 남겨두고 각각의 데이터베이스 회사에서 만든 JAR파일을 받아 넣어 사용할 수 있는 것입니다. 

 

기존의 Java 코드는 변화하지 않으면서, 클라이언트가 만든 파일을 통해서 기능을 확장시켜 구현할 수 있고, 

 

만약 클라이언트가 Driver를 넣지 않으면 기본적인 Driver 객체를 반환하여 연결할 수 도 있습니다. 

 

즉, 최소한의 조건 (SPI Interface의 구현) 만을 지키면 얼마든지 클라이언트가 새로운 객체를 만들어서 

 

자유롭게 데이터베이스 연결을 다룰 수 있는 것입니다. 

 

 

SPI의 필수요소 세 가지

 

SPI 를 구현하기 위해서는 핵심적으로 세 가지의 요소가 필요합니다.

 

Service, 변하지 않는 기능

 

먼저 기능을 수행하는 변하지 않는 코드가 필요합니다.

 

Service Provider Interface는 기능을 확장하기 위하여 사용하는 것이므로 

 

사용자가 아무런 값을 넣지 않아도 기본적으로 실행되는 기능이 존재해야 합니다. 

 

또, 이 기능은 외부 Resource(JAR파일) 에 의해서 바뀌지 않아야 합니다. 

 

Service Provider Interface (SPI),

확장기능에 필요한 최소한의 조건을 명시하는 인터페이스 

 

 

만약 기능을 확장하고 싶다면, 최소한 SPI로 명시된 인터페이스를 Implement하여 구현체를 만들고 

 

추상메서드를 오버라이드 하여 필수 조건을 충족시켜야 합니다. 

 

인터페이스를 통해서 구현하기 때문에, 인터페이스에 있는 최소한의 조건 ( 추상메서드 구현 ) 만 맞추면 

 

다른 코드의 작동과 상관없이 앞의 "Service( 변치 않는 기능 )" 을 사용할 수 있도록 구현해야 합니다. 

 

Service Provider, SPI의 구현체 

 

클라이언트가 SPI를 구현하여 확장시킨 SPI의 구현체입니다. 사용자가 자유롭게 커스터마이징하여 기존의 기능에서 

 

확장된 또다른 기능을 코드의 변경없이 제공할 수 있는 리소스입니다. 

 

 

 

 

 

SPI Tutorial Oracle Documentation

( 오라클 SPI 튜토리얼 문서 ) 

 

오라클에서 제공하는 사전을 만드는 예제를 통해서 SPI 패턴을 실습 해 봅시다. 

 

 

 

Creating Extensible Applications (The Java™ Tutorials > The Extension Mechanism > Creating and Using Exten

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

위의 링크에 들어가서 DictionaryServiceDemo.zip 파일을 다운로드 받아 열어보면 다음과 같은 구조를 가집니다. 

 

DictionaryDemo, Ant 빌드 툴을 사용하여 모든 사전을 불러온 후, 검색기능을 사용합니다.

 

DictionaryServiceProvider, SPF의 기본 인터페이스와 클래스(Service, SPI)를 구성합니다.

 

ExtendedDictionary, SPI를 Implement하여 구현한 확장 사전입니다.

 

GeneralDictionary, SPI를 Implement하여 구현한 일반 사전입니다.

 

 

DictionarySerivce, Service 로직

 

DictionaryService 의 코드 구조를 보면, 핵심 로직은 단 하나, getDefinition(String word)라는 메서드로, 

 

Dictionary라는 클래스를 가진 ServiceLoader<Dictionary>를 호출하여 

 

Dictionary 클래스들을 돌아가면서 그 안의 getDefinition() 함수를 부르고, 

 

다음 사전이 있다면 같은 작업을 반복하는 것을 확인하실 수 있습니다.  

 

이 DictionaryService는 이 Service Provider Framework의 핵심 로직 중 하나로, 

 

어떤 외부 Dictionary ( 다른 사전, 즉 영어사전, 한문사전 등등.. ) 가 들어오건 간에  변형되지 않는 원본 코드입니다. 

 

public class DictionaryService {

    private static DictionaryServiceExample example;
    private ServiceLoader<Dictionary> loader = ServiceLoader.load(Dictionary.class);

		// 1. 생성자를 private 처리하여 일반 생성하지 못하도록 막는다. 
		// ** static synchronized를 이용하여 Singleton Pattern으로 하나의 객체만을 만든다.
    private DictionaryServiceExample() {}
    public static synchronized DictionaryServiceExample getInstance() {
        if (example == null) {
            example = new DictionaryServiceExample();
        }
        return example;
    }

    // 2. 모든 사전을 경유하며 단어를 찾는 핵심 서비스 로직이다.
    public String getDefinition(String word) {
        String definition = null;

        Dictionary d;
        try {
			// Dictionary 객체들을 경유하며 map에 특정 단어를 찾는다. 
            for(Iterator<Dictionary> dictionaries = this.loader.iterator(); definition == null && dictionaries.hasNext(); definition = d.getDefinition(word)) {
                d = (Dictionary)dictionaries.next();
            }
        } catch (ServiceConfigurationError var5) {
            definition = null;
            var5.printStackTrace();
        }
        return definition;
    }
}

 

 

 

Dictionary, 서비스 제공 인터페이스 (SPI) 

 

 

 

Dictionary는 클라이언트가 새로운 사전 ( 확장된 기능 ) 을 만들기 위해 반드시 구현해야 하는 인터페이스입니다. 

 

추상클래스 (Abstract Class) 로 getDefinition()이라는 메서드가 선언되어 있습니다. 

 

이 Dictionary를 implement 한다면 getDefinition() 메서드를 강제로 오버라이드 해야합니다. 

 

오버라이드 된 메서드를 구현한다면, 위의 "사전 탐색 서비스", 

 

Service 를 이용할 최소한의 조건을 충족한 것입니다. 

 

사용자는 복잡한 원본 코드 ( DictionaryService ) 를 신경 쓸 필요 없이

 

필요한 사전에 대한 인터페이스를 구현하기만 하면  얼마든지 자신이 원하는대로 사전을 추가할 수 있습니다. 

 

public interface Dictionary {
    public String getDefinition(String word);
}

 

 

 

ExtendedDictionary, 사용자가 추가로 구현한 SPI의 구현체 Service Provider 

 

 

 

아래의 코드처럼 확장 사전을 만드려면, Dictionary(SPI)를 구현한 다음 

 

강제 오버라이드 된 getDefinition만 구현해 주면 됩니다. 

 

 

//---------------------------- 확장 사전 ---------------------------------
public class ExtendedDictionary implements Dictionary {

        private SortedMap<String, String> map;

    // Map을 활용하여 단어를 넣는다. 
    public ExtendedDictionaryExample() {
        map = new TreeMap<String, String>();
        map.put(
            "xml",
            "a document standard often used in web services, among other " +
                "things");
        map.put(
            "REST",
            "an architecture style for creating, reading, updating, " +
                "and deleting data that attempts to use the common " +
                "vocabulary of the HTTP protocol; Representational State " +
                "Transfer");
    }

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

 

 

Dynamic ClassLoading, Java 고유의 동적 컴파일기능 

 

 

이제, 이 SPI를 통해서 사용자가 자유롭게 기능을 확장시킬 준비가 거의 끝났습니다. 

 

그런데, 외부에 있는 Jar파일을 이미 컴파일된 자바 언어가 어떤 방식으로 다시 읽어들이는 걸까요? 

 

사실 이 ExtendedDictionary는 지금의 코드와 같은 형식이 아니라, 

 

JAR파일 ( Java 압축파일 ) 로 사용자가 폴더에 넣어주기 때문에 

 

현재 JVM은 실제로 이 클래스의 존재를 모릅니다. 

 

그러나 Java는 어플리케이션이 가동되는 순간에도 모든 상속관계를 점검해서 

 

해당 클래스를 상속받는 클래스들을 다시 컴파일 할 수 있습니다. 

 

이를 Dynamic ClassLoading 기술이라고 하며, Java의 ClassLoader라는 클래스가 제공하는 기능입니다. 

 

이를 통해서 우리는 사용자가 새로 만든 전혀 새로운 클래스라도 다시 컴파일 할 수 있습니다. 

 

다만, 컴파일 해야하는 경로를 명시해 주어야만이 ClassLoader가 이를 찾아갈 수 있습니다. 

 

 

META-INF에 이름을 해당 클래스로 명시한 후, 

 

ServiceLoader에서 현재 로드하고 싶은 SPI (Dictionary) 를 상속한다면 확장 사전 ( SP )가 동적으로 

 

어플리케이션 실행 도중 로드될 수 있는 모든 조건이 갖추어집니다. 

 

 

public class DictionaryServiceExample {

    private static DictionaryServiceExample example;
    
    // ServiceLoader가 Dictionary라는 클래스를 상속받는 클래스를 찾는다. 
   	// .load가 끝나면 사용자가 넣은 새로운 Dictionary (ExtendedDictionary)가 추가된다. 
    
    // ** 이때 내부적으로  META-INF에 있는 파일 이름을 찾아서 해당 경로의 클래스를 로드한다.
    private ServiceLoader<Dictionary> loader = ServiceLoader.load(Dictionary.class);

    private DictionaryServiceExample() {}
    public static synchronized DictionaryServiceExample getInstance() {
        ...// 생략 
    }

    // 2. 모든 사전을 경유하며 단어를 찾는 핵심 서비스 로직이다.
    public String getDefinition(String word) {
        ...// 생략
    }
}

 

 

실제 코드 실행 

 

이제, 이 SPI를 통해서 사용자가 자유롭게 기능을 확장시킬 준비가 거의 끝났습니다. 

 

이제 DictionaryDemo 클래스에서 main 메서드를 통해 실제 사전탐색을 실시해 봅시다. 

 

Jar 파일로 빌드된 ExtendedDictionary의 xml, rest 를 성공적으로 찾아 반환한다면 

 

기존의 코드인 DictionaryService의 변형 없이, 새로운 클래스 코드의 추가 없이, 

 

사용자가 건네준 ExtendedDictionary라는 클래스 구현과 활용에 성공한 것입니다. 

 

public class DictionaryDemo {
    public DictionaryDemo() {
    }

    public void main(String[] args) {
        DictionaryServiceExample dictionary = DictionaryServiceExample.getInstance();
        System.out.println(lookup(dictionary, "xml"));
        System.out.println(lookup(dictionary, "REST"));
    }

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

 

 

결론, SPI는 자바의 핵심 기술인 Dynamic ClassLoading 의 실제적 활용이다. 

 

 

이처럼 Java는 런타임 도중에도 클래스를 동적으로 로딩함으로써 

 

클라이언트가 어플리케이션 운영 도중 삽입한 자바 코드에 대해서도 

 

유연하게 바로 반영할 수 있는 장점을 지니고 있습니다. 

 

SPI는 Java의 이러한 Dynamic ClassLoading을 활용한 기술이며 

 

서비스 규모가 확장되어야 할 때 아주 유용한 설계 패턴 중의 하나입니다. 

 

 

기회가 되신다면 소개해 드린 Oracle Tutorial을 직접 따라하시면서 

 

이러한 자바의 강력한 런타임 동적 클래스로딩 기능을 실습해보시기 바랍니다. 

 

다음 글에서는 이러한 Service Provider Framework가 

 

JDBC에서 어떻게 작동하는지를 작성해보도록 하겠습니다. 

 

 

'플라자' 카테고리의 다른 글

JAVA의 객채지향 - 상속 관계  (0) 2022.06.30
JAVA의 객채지향  (0) 2022.06.12

+ Recent posts