자바 디자인 패턴 용어 사전
생성 패턴SPI · META-INF/services

ServiceLoader

구현체를 설정 파일로 배선하는 JDK 표준 메커니즘. JDBC 드라이버·SLF4J 바인딩의 기반.

구현체를 설정 파일로 배선하는 JDK 표준 메커니즘(Service Provider Interface).

public interface PaymentGateway { Receipt charge(long amount); }

// META-INF/services/com.example.PaymentGateway 파일 내용
//   com.example.tosspay.TossGateway

ServiceLoader<PaymentGateway> loaded = ServiceLoader.load(PaymentGateway.class);
PaymentGateway gw = loaded.findFirst().orElseThrow();

무엇이 특별한가

  • 애플리케이션 코드에 구현 클래스 이름이 '한 번도' 등장하지 않는다

  • jar 을 클래스패스에 넣고 빼는 것만으로 구현이 바뀐다

  • 진짜 의미의 플러그인 구조

  • 컴파일 의존도 없다. 구현 jar 이 없으면 그냥 비어 있을 뿐 컴파일은 된다

실제로 이 위에서 도는 것들

  • JDBC — DriverManager 가 java.sql.Driver 구현을 찾는다
    • (그래서 Class.forName("com.mysql.cj.jdbc.Driver") 가 필요 없어졌다)
  • SLF4J — 바인딩 jar 을 하나 넣으면 그것이 선택된다
    • 두 개 넣으면 "Class path contains multiple SLF4J bindings" 경고
  • JAXP — DocumentBuilderFactory.newInstance() 의 구현 탐색
  • 문자셋·로케일 — Charset · LocaleServiceProvider 확장

모듈 시스템에서의 선언

// module-info.java — 파일 대신 언어 문법으로 선언한다
module com.example.tosspay {
    provides com.example.PaymentGateway with com.example.tosspay.TossGateway;
}

// 사용하는 쪽
module com.example.app {
    uses com.example.PaymentGateway;
}

한계 — 왜 스프링을 대체하지 못하나

  • 인스턴스를 만들 때 '기본 생성자' 만 쓴다
    • 의존성을 주입할 방법이 없다
  • 선택 규칙이 없다 (클래스패스 순서에 의존)
    • 여러 구현 중 무엇을 쓸지 조건부로 고르기 어렵다
  • 생명주기 관리가 없다 (초기화·소멸 훅 없음)

그래서 '프레임워크 확장점' 에는 맞고, '애플리케이션 빈 배선' 에는 DI 컨테이너를 쓴다

로딩은 지연되고, 실패는 늦게 드러난다

ServiceLoader<Gateway> loader = ServiceLoader.load(Gateway.class);
// 이 시점에는 아무것도 안 만든다

for (Gateway g : loader) { ... }   // 순회할 때 하나씩 인스턴스화된다

구현 클래스가 없거나 생성자가 실패하면 순회 도중에 ServiceConfigurationError 가 난다 — 시작 시점이 아니다

  • 애플리케이션 기동 시 한 번 훑어 검증하는 편이 안전하다
    • 운영 중 첫 요청에서 터지는 것보다 낫다

스프링에서의 대응물

spring.factories / AutoConfiguration.imports

  • 스프링 부트의 자동 설정이 같은 발상으로 동작한다

  • jar 을 넣으면 설정이 따라 들어온다

  • 차이 — ServiceLoader 는 기본 생성자만 쓴다

    • 스프링은 조건(@ConditionalOnClass · @ConditionalOnMissingBean)으로
    • '언제 켤지' 까지 표현할 수 있다

즉 스프링 부트의 '의존성만 추가하면 동작한다' 는 SPI 발상에 조건부 배선을 얹은 결과다

면접 함정

  • "ServiceLoader는 싱글톤을 준다"load()를 다시 부르면 새로 만든다. 반복 호출은 캐싱해야 한다.
  • "인터페이스만 있으면 동작한다" → 구현 쪽에 등록 파일 또는 provides 선언이 있어야 한다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 적용 심화 — 프레임워크 소스로 읽는 패턴