Java 9에서 도입된 모듈 시스템(JPMS, Java Platform Module System)은 자바 개발에 혁명적인 변화를 가져왔습니다.
기존의 클래스패스(classpath) 기반 시스템의 한계를 극복하고, 더 안전하고 효율적인 애플리케이션 개발을 가능하게 했습니다.
이 글에서는 Java 모듈 시스템의 핵심 개념부터 실무 적용까지 상세히 알아보겠습니다.
Java 모듈 시스템이란 무엇인가
Java 모듈 시스템은 Java 9에서 도입된 새로운 구조화 메커니즘입니다.
모듈은 관련된 패키지들의 집합으로, 명시적으로 다른 모듈에 대한 의존성을 선언하고 외부에 공개할 API를 정의합니다.
// module-info.java 파일 예시
module com.example.userservice {
requires java.base;
requires java.sql;
requires spring.core;
exports com.example.userservice.api;
exports com.example.userservice.model;
}
이러한 모듈 시스템은 기존 JAR Hell 문제를 해결하고, 애플리케이션의 런타임 이미지 크기를 최적화하며, 보안성을 향상시킵니다.
모듈 시스템 도입 이전의 문제점들
Java 8 이전의 클래스패스 기반 시스템은 여러 한계점을 가지고 있었습니다.
JAR Hell 문제가 가장 대표적인 문제였습니다.
동일한 클래스가 여러 JAR 파일에 존재할 때 어떤 것이 로드될지 예측하기 어려웠습니다.
캡슐화 부족 또한 심각한 문제였습니다.
public 클래스는 애플리케이션 전체에서 접근 가능했고, 내부 API와 외부 API의 구분이 모호했습니다.
런타임 의존성 검증 부재로 인해 클래스패스에 필요한 JAR이 없어도 컴파일은 성공하지만 런타임에서 ClassNotFoundException이 발생하는 경우가 빈번했습니다.
모듈 시스템의 핵심 구성 요소
module-info.java 파일
모든 모듈의 핵심은 module-info.java
파일입니다.
이 파일은 모듈의 루트 디렉토리에 위치하며, 모듈의 메타데이터를 정의합니다.
// 기본적인 module-info.java 구조
module com.example.bookstore {
// 의존하는 모듈 선언
requires java.base; // 기본적으로 암시적 의존
requires java.logging;
requires transitive java.sql;
// 외부에 노출할 패키지 선언
exports com.example.bookstore.api;
exports com.example.bookstore.model to com.example.bookstore.web;
// 서비스 제공
provides com.example.bookstore.spi.PaymentProcessor
with com.example.bookstore.impl.CreditCardProcessor;
// 서비스 사용
uses com.example.bookstore.spi.NotificationService;
}
requires 지시문
requires
지시문은 현재 모듈이 다른 모듈에 의존함을 선언합니다.
module com.example.orderservice {
requires java.base; // 명시적 의존성
requires transitive java.logging; // 전이적 의존성
requires static java.compiler; // 컴파일 타임 전용 의존성
requires com.example.userservice; // 다른 모듈 의존성
}
transitive 키워드는 특히 중요합니다.
현재 모듈을 사용하는 다른 모듈도 자동으로 해당 의존성에 접근할 수 있게 합니다.
exports 지시문
exports
지시문은 모듈 외부에서 접근 가능한 패키지를 정의합니다.
module com.example.paymentservice {
// 모든 모듈에 공개
exports com.example.paymentservice.api;
// 특정 모듈에만 공개 (qualified export)
exports com.example.paymentservice.internal
to com.example.orderservice,
com.example.billingservice;
}
이를 통해 강력한 캡슐화를 구현할 수 있습니다.
exports 되지 않은 패키지는 모듈 외부에서 절대 접근할 수 없습니다.
실무에서의 모듈 시스템 적용 예제
마이크로서비스 아키텍처에서의 모듈 활용
다음은 전자상거래 시스템에서 모듈 시스템을 활용한 예제입니다.
// 사용자 서비스 모듈
// modules/user-service/module-info.java
module ecommerce.user.service {
requires java.base;
requires java.sql;
requires spring.context;
requires spring.data.jpa;
exports ecommerce.user.api;
exports ecommerce.user.model;
// 내부 구현은 노출하지 않음
// ecommerce.user.repository 패키지는 exports 되지 않음
}
// 주문 서비스 모듈
// modules/order-service/module-info.java
module ecommerce.order.service {
requires java.base;
requires transitive ecommerce.user.service; // User 모델 전이적 노출
requires ecommerce.product.service;
requires spring.context;
exports ecommerce.order.api;
exports ecommerce.order.model;
uses ecommerce.payment.spi.PaymentProcessor;
}
서비스 프로바이더 인터페이스(SPI) 활용
모듈 시스템의 강력한 기능 중 하나는 서비스 로더 패턴의 개선입니다.
// 결제 처리 서비스 인터페이스 모듈
// modules/payment-spi/module-info.java
module ecommerce.payment.spi {
exports ecommerce.payment.spi;
}
// PaymentProcessor 인터페이스
package ecommerce.payment.spi;
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
boolean supports(PaymentType type);
}
// 신용카드 결제 구현 모듈
// modules/payment-creditcard/module-info.java
module ecommerce.payment.creditcard {
requires ecommerce.payment.spi;
provides ecommerce.payment.spi.PaymentProcessor
with ecommerce.payment.creditcard.CreditCardProcessor;
}
모듈 시스템 마이그레이션 전략
기존 프로젝트의 점진적 모듈화
기존 Java 8 프로젝트를 모듈 시스템으로 마이그레이션할 때는 점진적 접근법이 중요합니다.
1단계: 자동 모듈(Automatic Module) 활용
# 기존 JAR를 모듈 패스에 배치
java --module-path libs --add-modules ALL-SYSTEM -cp app.jar com.example.Main
2단계: 명시적 모듈 생성
// 첫 번째 모듈 생성
module com.example.core {
requires java.base;
requires java.logging;
// 기존 코드와의 호환성을 위해 모든 패키지 노출
exports com.example.core.model;
exports com.example.core.service;
exports com.example.core.util;
}
3단계: 점진적 캡슐화
module com.example.core {
requires java.base;
requires java.logging;
// API 패키지만 노출
exports com.example.core.api;
exports com.example.core.model;
// 내부 패키지는 더 이상 노출하지 않음
// exports com.example.core.internal; // 제거됨
}
성능 최적화와 JLink 활용
모듈 시스템의 가장 큰 장점 중 하나는 커스텀 런타임 이미지 생성입니다.
JLink를 통한 최소화된 런타임 생성
# 필요한 모듈만 포함한 커스텀 JRE 생성
jlink --module-path $JAVA_HOME/jmods:mods \
--add-modules com.example.app \
--output custom-runtime \
--compress=2 \
--strip-debug
이를 통해 수백 MB의 JRE를 수십 MB로 줄일 수 있습니다.
Docker와의 조합
# 멀티 스테이지 빌드를 활용한 최적화
FROM openjdk:17-jdk-slim as builder
WORKDIR /app
COPY . .
# 애플리케이션 빌드 및 커스텀 런타임 생성
RUN javac -d mods --module-path libs $(find . -name "*.java")
RUN jlink --module-path $JAVA_HOME/jmods:mods \
--add-modules com.example.app \
--output custom-jre \
--compress=2 --strip-debug
FROM debian:bullseye-slim
COPY --from=builder /app/custom-jre /opt/jre
COPY --from=builder /app/mods /app/mods
CMD ["/opt/jre/bin/java", "-m", "com.example.app"]
모듈 시스템 실무 적용 시 주의사항
순환 의존성 방지
모듈 시스템은 순환 의존성을 컴파일 타임에 방지합니다.
// 잘못된 예: 순환 의존성
module com.example.moduleA {
requires com.example.moduleB; // B에 의존
}
module com.example.moduleB {
requires com.example.moduleA; // A에 의존 - 컴파일 에러!
}
이를 해결하기 위해서는 공통 모듈 추출 또는 의존성 방향 재설계가 필요합니다.
리플렉션과 모듈 시스템
리플렉션 사용 시 opens 지시문을 활용해야 합니다.
module com.example.app {
requires java.base;
requires spring.core;
// Spring이 리플렉션으로 접근할 수 있도록 허용
opens com.example.app.entity to spring.core;
opens com.example.app.config to spring.context;
}
트러블슈팅과 디버깅
모듈 관련 일반적인 오류들
1. 모듈 찾을 수 없음 오류
Error: Module com.example.app not found
해결방법:
java --module-path mods --list-modules # 사용 가능한 모듈 확인
2. 패키지 접근 불가 오류
package com.example.internal is not visible
해결방법: module-info.java에서 적절한 exports 추가
3. Split Package 문제
동일한 패키지가 여러 모듈에 존재하는 경우:
// 해결방법: 패키지 재구성
module com.example.moduleA {
exports com.example.moduleA.service; // 고유한 패키지명 사용
}
모듈 시스템의 미래와 발전 방향
Java 모듈 시스템은 지속적으로 발전하고 있습니다.
Java 14 이후의 개선사항들:
- 패키징 도구(jpackage)의 정식 도입
- 모듈 시스템과 기본 클래스 로더의 성능 개선
- 네이티브 이미지 생성 도구와의 더 나은 통합
클라우드 네이티브 환경에서의 활용:
모듈 시스템은 마이크로서비스, 서버리스, 컨테이너 환경에서 특히 유용합니다.
작은 런타임 이미지와 빠른 시작 시간은 클라우드 비용 절감과 직결됩니다.
실전 프로젝트 적용 체크리스트
모듈 시스템을 실무에 도입할 때 확인해야 할 체크리스트입니다:
설계 단계:
- 모듈 간 의존성 그래프 작성
- 순환 의존성 제거 계획 수립
- API와 구현의 명확한 분리
구현 단계:
- module-info.java 파일 작성
- exports/requires 지시문 최소화
- 서비스 프로바이더 인터페이스 활용 검토
테스트 단계:
- 모듈 경계에서의 접근 제어 검증
- 리플렉션 사용 부분 opens 처리
- 커스텀 런타임 이미지 생성 테스트
배포 단계:
- JLink를 통한 최적화된 배포 이미지 생성
- Docker 이미지 크기 최적화
- 성능 벤치마크 수행
마무리
Java 모듈 시스템은 단순한 새로운 기능이 아닙니다.
자바 애플리케이션의 구조화, 보안성, 성능을 근본적으로 개선하는 패러다임 변화입니다.
기존의 클래스패스 시스템의 한계를 극복하고, 현대적인 애플리케이션 개발 요구사항에 부합하는 강력한 도구입니다.
처음에는 복잡해 보일 수 있지만, 점진적인 도입과 꾸준한 학습을 통해 모듈 시스템의 이점을 충분히 활용할 수 있습니다.
특히 마이크로서비스 아키텍처나 클라우드 네이티브 환경에서 개발하는 경우, 모듈 시스템의 도입은 선택이 아닌 필수가 되어가고 있습니다.
앞으로의 자바 개발에서는 모듈 시스템에 대한 깊은 이해가 경쟁력의 핵심 요소가 될 것입니다.
지금부터라도 모듈 시스템을 학습하고 실무에 적용해보시기 바랍니다.
'자바(Java) 실무와 이론' 카테고리의 다른 글
Java로 메모리 캐시 직접 구현해보기: 성능 최적화를 위한 실무 가이드 (0) | 2025.05.28 |
---|---|
Java 패턴 매칭 기능 완벽 가이드: 모던 자바 개발자를 위한 실무 활용법 (0) | 2025.05.28 |
Java의 GC 튜닝 실전 사례: Throughput vs Latency 중심 (0) | 2025.05.23 |
Java로 Kafka Producer/Consumer 구성하기: 실무 활용 완벽 가이드 (0) | 2025.05.23 |
Java와 Kotlin 비교 – Spring 개발자 관점에서 (0) | 2025.05.23 |