ServiceLoader는 포함된 모듈이 module-info.java 설명자에서 provides ... with 지시문을 선언하지 않을 때 프로바이더를 찾지 못합니다. **Java Platform Module System (JPMS)**는 기본적으로 강력한 캡슐화를 시행하여 ServiceLoader(자바 기본 모듈에 있음)가 비공식으로 수출되거나 열려 있지 않은 패키지의 클래스를 접근하는 것을 방지합니다. provides 지시문은 특정 프로바이더 클래스를 인스턴스화하기 위해 ServiceLoader에 권한 있는 반사 접근을 부여하는 계약적 선언으로 작용하며, 패키지가 모든 모듈에 수출되지 않고도 일반적 패키지 접근성 제약을 우회할 수 있게 합니다.
맥락: 레거시 기업 CRM 시스템이 Java 8에서 Java 17로 마이그레이션 되고 있었습니다. 목표는 단일 아키텍처를 crm-core, crm-email, 및 crm-api와 같은 별도의 도메인으로 모듈화하는 것이었습니다. crm-email 모듈은 crm-api 에 정의된 NotificationService 인터페이스의 구현체를 포함하고 있었습니다.
마이그레이션 후, 애플리케이션은 부트스트랩 중에 ServiceConfigurationError 를 발생시켰습니다. 이는 EmailNotificationService 클래스가 공개되어 있고 JAR 파일이 모듈 경로에 존재함에도 불구하고 발생했습니다. 스택 트레이스는 서비스 유형에 대한 프로바이더를 찾지 못했음을 나타내었고, 이로 인해 알림 서브시스템이 초기화에 실패했습니다.
문제: 개발 팀은 구현 클래스의 공용 가시성이 충분하다고 가정하였습니다. 이는 공용 클래스가 전역적으로 가시했던 클래스 경로 시대의 가정을 반영하고 있습니다. 그러나 JPMS는 ServiceLoader가 다른 모듈의 비공식 패키지에 있는 클래스를 접근하는 것을 방지합니다. crm-email 모듈은 com.crm.email.internal 패키지를 수출하지 않았습니다. 특히 module-info.java는 provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService 선언이 없었습니다. 결과적으로 ServiceLoader는 프로바이더를 찾거나 인스턴스화할 수 없었으며, 모듈 시스템은 구현을 캡슐화된 내부 세부 사항으로 처리했습니다.
고려된 솔루션:
패키지 수출: crm-email 모듈 설명자에 exports com.crm.email.internal;을 추가. 이 접근법은 내부 구현 세부 정보를 모든 다른 모듈에 노출시키기 때문에 기각되었습니다. 이는 캡슐화를 위반하고 모듈 시스템이 방지하도록 설계된 긴밀한 결합을 초래했습니다.
패키지 열기: opens com.crm.email.internal; 또는 특정 opens com.crm.email.internal to java.base; 사용. 이는 반사 접근을 허용하지만 지나치게 관대하고 의미론적으로 부정확하다고 판단되었습니다. 이는 기술적으로 제공 메커니즘을 통해 서비스를 제공하는 것이 아니라 패키지가 일반적으로 깊은 반사를 받을 수 있음을 신호합니다.
provides ... with 지시문 사용: module-info.java에 provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; 선언을 추가. 이는 관례적인 JPMS 솔루션입니다. 서비스 관계를 명시적으로 선언하고 ServiceLoader에 필요한 접근 권한을 부여하여 클래스를 인스턴스화할 수 있게 하며, 엄격한 캡슐화를 유지합니다.
선택된 솔루션: 팀은 세 번째 옵션을 선택했습니다. 이 접근법은 구현 코드 자체에는 변경이 필요하지 않았습니다. 패키지의 내부 가시성을 보존하고 모듈 메타데이터에서 서비스 의존성을 명시적으로 만들었습니다.
결과: 애플리케이션은 런타임에서 성공적으로 EmailNotificationService를 로드했습니다. 모듈 경계는 그대로 유지되어 다른 모듈이 내부 구현 클래스를 직접 인스턴스화하거나 의존하는 것을 방지했습니다. ServiceLoader는 선언된 계약을 통해 서비스를 올바르게 발견하고 공급할 수 있었습니다.
왜 ServiceLoader는 프로바이더 클래스가 public 기본 생성자를 가져야 하며, 이 제약이 위반될 경우 어떤 특정 예외가 발생하는가?
ServiceLoader는 Class.getConstructor().newInstance()를 사용하여 프로바이더 클래스를 반사적으로 인스턴스화합니다. 이는 공용 기본 생성자를 엄격히 요구합니다. 이 생성자가 없거나 public이 아닌 경우 ServiceLoader는 ServiceConfigurationError를 던집니다. 이 오류는 일반적으로 반복자 순회 중 NoSuchMethodException 또는 IllegalAccessException으로 등록됩니다. 후보자들은 이 생성자가 다른 생성자가 정의될 경우 반드시 명시적으로 제공되어야 한다는 점을 종종 간과합니다. 또한 인스턴스화가 초기 ServiceLoader.load() 호출이 아닌 Iterator.next()가 호출될 때 발생한다는 점을 놓칩니다.
서비스 인터페이스가 명명된 모듈에 존재하고 프로바이더 클래스는 비명명된 모듈에 있을 때 ServiceLoader 메커니즘은 어떻게 처리하는가?
서비스 인터페이스가 명명된 모듈에 있지만 구현이 비명명된 모듈(클래스 경로)에 있을 때, ServiceLoader는 여전히 프로바이더를 찾을 수 있습니다. 비명명된 모듈은 모든 명명된 모듈을 암묵적으로 읽고, 모든 명명된 모듈은 암묵적으로 비명명된 모듈을 읽습니다. 하지만 프로바이더 클래스는 여전히 public이며 공용 기본 생성자를 가져야 합니다. 강력한 캡슐화가 이 시나리오를 완전히 방지한다는 오해가 있습니다. 실제로 비명명된 모듈은 호환성 계층으로 작용합니다. 비명명된 모듈의 프로바이더는 명명된 모듈에서 명시적으로 비명명된 모듈을 읽지 않는 경우 접근할 수 없습니다. 이는 후보자들이 자주 고려하지 않는 방향성 접근성 제약을 만듭니다.
ServiceLoader.loadInstalled() 메서드와 ServiceLoader.load() 메서드는 클래스 로더 위임 및 프로바이더 가시성과 관련하여 어떻게 구별되는가?
ServiceLoader.loadInstalled()는 시스템 클래스 로더(현대 JVM 버전에서 플랫폼 클래스 로더)를 사용하여 프로바이더를 검색합니다. 이는 발견을 설치된 확장 디렉토리나 플랫폼 모듈로 제한합니다. 이는 명시적으로 애플리케이션 모듈 경로나 클래스 경로의 프로바이더를 무시합니다. 대조적으로, ServiceLoader.load()는 일반적으로 스레드 컨텍스트 클래스 로더 또는 지정된 클래스 로더를 사용합니다. 이를 통해 애플리케이션 수준의 프로바이더를 발견할 수 있도록 합니다. 후보자들은 종종 이러한 메서드를 혼동하여 애플리케이션 프로바이더가 발견되지 않는 조용한 실패를 초래합니다. 이는 loadInstalled()가 잘못 사용되어 표준 로드 방법처럼 작동할 것이라고 기대되지만, 더 넓은 가시성을 기대하는 경우 발생합니다.