Service Provider Interface는 Service Provider Framework의 한 구성요소입니다.
Service Provider Framework는 확장 가능한 Java Application을 만드는 패턴 중 하나입니다.
외부에서 클라이언트에게 Jar파일을 받아, 기존 코드의 변경 없이 기능을 확장할 수 있습니다.
대표적으로 Java EE(Enterprize Edition)의 핵심 기술 중 하나인 JDBC가 이 패턴을 활용하고 있습니다.
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 패턴을 실습 해 봅시다.
위의 링크에 들어가서 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 의 실제적 활용이다.
세상에 수많은 OS들이 존재한다. 대표적으로는 윈도우가 먼저 떠오르는 사람이 있을 수 있고 IOS가 떠오르는 사람도 있을 것이다. OS란 녀석들이 하드웨어에 탑재됨으로써 하드웨어는 하나의 역할이 아닌 다양한 역할을 수행할 수 있게 된다. 또한 OS 특성에 따라 여러 가지의 작업을 동시에 처리도 가능하다.
그리고 많은 OS중에서 여기서는 리눅스라는 녀석에 대해서 이야기를 간단하게 해 보자.
리눅스란?
리눅스는 오픈소스 운영 체제로써 누구나 무료로 사용을 할 수 있다는 것이 큰 장점이다. 그리고 타 OS에 비해서 상대적으로 뛰어난 보안과 안정성으로 서버단에서 많이 채택을 하고 있다. 리눅스의 종류도 여러 가지가 있지만 여기서는 구체적으로 언급하지 않고 넘어간다.
그 외에 리눅스의 역사와 성능 등 할 이야기는 많지만.. 더 전문적인 웹사이트를 보는 것이 바람직한 선택일 것이다.
여기서부터는 윈도우10을 기준으로 작성이 되었습니다.
본격적으로 리눅스를 시작해보자! 자신의 컴퓨터가 윈도우 10 이상이며 2004 버전까지 업데이트를 완료했다면 윈도우에서 제공하는 WSL(Windows Subsystem for Linux)를 이용해서 자신의 윈도우에 리눅스를 설치할 수 있다!
자세한 설치방법은 여기서는 생략을 하도록 하였다.
정상적으로 리눅스가 설치가 되었다면 윈도우에서도 리눅스를 편안하게 즐길 수 있다.
우선 리눅스를 처음 접한다면 많이 당황할 수 있다. 이상한 글자들만 눈앞에 보이고 아이콘은 전혀 보이지 않기 때문이다.
평소에 접하던 윈도우 또는 맥 OS 같은 경우는 모든 사용자가 처음 접했을 때 조금이라도 더 직관적으로 접할 수 있도록 GUI 스타일을 채택하였지만 리눅스에서는 CUI 스타일을 고수하기 때문이다. 물론 리눅스에서도 GUI 스타일을 채택을 한 것도 있지만 대부분 서버용으로 사용하는 리눅스는 CUI 스타일을 사용한다.
처음으로는 낯선 곳에 도착을 했으니 둘러보는 것이 우선이라고 생각한다. 지금 보고 있는 화면에서는 마우스를 통해서 이동한다는 개념이 없다. 즉 모든 행동을 직접 입력해야 한다.
밑의 사진처럼 디렉토리를 하나 이동할 때에도 직접 입력을 해야 한다.
처음 접하게 되면 많이 복잡하고 낯설어서 피하게 되지만 하다 보면 나름의 매력?도 존재하는 방식이다.