Java

[Java 9] 자바 플랫폼 모듈 시스템 (JPMS)

빠쿤 2024. 5. 16. 21:44

자바 9 - 자바 플랫폼 모듈 시스템 (JPMS) (vs 빌드툴 Module)

  • 자바 9에서는 모듈 시스템이 등장
    • 모듈 (Module) : 여러 코드가 모인 독립적인 구성요소
    • 비슷한 역할을 하는 코드끼리 모아 유지보수성 높일 수 있음 / 역할 간 의존성 제어 = 안전성 증대
  • 자바 9 이전에도 Maven, Gradle과 같은 빌드툴을 활용해 모듈 시스템이 구성되어 있긴 했음
    • 한 프로젝트 내에 여러 모듈을 만들어 의존성 관리 (멀티모듈)

Gradle 빌드툴을 통한 멀티모듈 설정

  • JPMS vs 빌드툴 Module?
    • JPMS는 빌드툴 없이 모듈 구성이 가능 
    • 이를 통해 JDK에서 필요한 일부 코드(모듈)만 다운로드 가능 (성능,용량 측면 유지보수성 증대, Modular run time image)
      • JPMS와 빌드툴 Module 함께 사용 가능
    • JPMS는 특정 패키지 기준으로 특정 모듈에 open 가능 / private member에 대한 reflection open 여부 설정 가능

 

 

JPMS - 모듈 생성 및 특정 패키지 기준 open

  • src/main/java 폴더에 module-info.java 를 추가하면 해당 모듈은 JPMS로 간주 (named module)
    • module-info.java가 없는 모듈은 unnamed module

 

  • 특정 패키지에 대한 의존성을 열어주기 위해서는 module-info.java에 exports {패키지 경로}를 명시
  • 특정 모듈에만 의존성을 열어주고 싶다면 to {모듈명} 명시
module com.domain {
    exports open.domain;
    exports open.domain to com.api; // api 모듈에만 의존성 open
}

 

  • 모듈에 대한 의존성을 추가하기 위해서는 module-info.java에 requires {static | transitive} 모듈명 명시
    • static : 컴파일 타임 의존성만 추가 (리플렉션 등의 런타임 의존성 X, 오직 코드에 작성된 의존성)
    • transitive : 추이적 의존성 (B->C 의존성 추가 / A->B 의존성 추가 시 A->C 의존성도 전이됨)
    • X : 컴파일 + 런타임 의존성 추가
module com.admin {
    requires com.domain;
}

 

 

 

 

JPMS private member reflection open 여부 설정

  • module-info.java에 open 키워드로 Deep Reflection 여부 설정 가능
    • Deep Reflection : private 멤버에 대한 리플렉션
    • Shallow Reflection : public 멤버에 대한 리플렉션 (private X)
// 모듈 전체에 대해 Deep Reflection open
open module com.domain {
    exports open.domain;
}

// open.domain 패키지에 대한 Deep Reflection open
module com.domain {
    opens open.domain;
}

// open.domain 패키지에 대해 com.api 모듈만 Deep Reflection open
module com.domain {
    opens open.domain to com.api;
}

 

 

  • exports 없이 open만 할 경우 직접적인 참조는 불가능하지만 Class.forName(com.domain.Person) 활용해 클래스를 가져와 Deep Reflection 가능
  • exports만 할 경우 Shallow Reflection은 가능

 

 

 

JPMS - 서비스

  • 코드의 변경 없이 기능을 갈아끼울 수 있는 매커니즘 (개념적으로 DI와 동일)
  • Service Provider가 Service Consumer에게 의존성(구현체)을 주입하는 방식
  • Service Loader에서 여러 구현체 중 주입할 구현체를 선택
// domain 모듈

// 인터페이스
public interface StringRepository {
    void save(String newStr);
}

// 구현체 1
public class MemoryStringRepository implements StringRepository {

    private final List<String> strings = new ArrayList<>();

    @Override
    public void save(String newStr) {
        strings.add(newStr);
        System.out.println("문자열 메모리 저장");
    }
}

// 구현체 2
public class DatabaseStringRepository implements StringRepository {

    @Override
    public void save(String newStr) {
        System.out.println("DB 메모리 저장");
    }
}

// domain 모듈 module-info.java
open module com.domain {
    exports open.domain;
    exports org.domain.service;
    
    // 서비스 프로바이더에 구현체 등록
    provides org.domain.service.StringRepository with
            org.domain.service.MemoryStringRepository,
            org.domain.service.DatabaseStringRepository;
}

 

 

// api 모듈
module com.api {
    requires com.domain;
    uses org.domain.service.StringRepository; // StringRepository 사용
}

public class StringRepositoryLoader {

    // 외부 config 파일 등을 import해 외부에서 변경 가능
    public static final String DEFAULT = "org.domain.service.DatabaseStringRepository";
    
    public static StringRepository getDefaultRepository() {
        return getRepository(DEFAULT);
    }
    
    private static StringRepository getRepository(String name) {
        // ServiceLoader에서 해당 Interface에 해당하는 구현체 찾음
        for (StringRepository repository : ServiceLoader.load(StringRepository.class)) {
            if (repository.getClass().getName().equals(name)) {
                return repository;
            }
        }
        throw new IllegalArgumentException("Repository Not Found!");
    }
}


public class StringSaveConsumer {

    // Service Consumer는 외부에서 주입받아 그대로 사용
    private final StringRepository stringRepository = StringRepositoryLoader.getDefaultRepository();

    public void consume() {
        stringRepository.save("test");
    }
}

 

 


Reference

인프런 - 자바 9부터 자바 21까지