Java 모듈

발행: (2026년 2월 9일 오후 07:28 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

표지 이미지

소개

Java 모듈은 Java 애플리케이션이 구조화되고, 빌드되며, 유지 관리되는 방식을 크게 바꾸는 변화를 의미합니다. Java 9에서 Java Platform Module System(JPMS)의 일부로 도입된 모듈은 대규모 코드베이스에서 확장성, 유지 보수성 및 보안과 관련된 오랜 문제들을 해결합니다. 개발자, 테스터, 자동화 애호가에게 Java 모듈을 숙달하는 것은 관리와 진화가 쉬운 견고하고 모듈화된 애플리케이션을 만드는 데 필수적입니다.

이 블로그 포스트에서는 Java 모듈이 무엇인지, 왜 중요한지, 그리고 프로젝트에서 효과적으로 활용하는 방법을 살펴봅니다.

왜 모듈이 Java에서 중요한가

모듈이 도입되기 전, Java 애플리케이션은 패키지와 클래스패스에 크게 의존했습니다. 패키지는 관련 클래스를 그룹화했지만, 클래스패스는 애플리케이션의 서로 다른 부분—또는 서로 다른 애플리케이션 간—에 대한 엄격한 캡슐화나 명확한 경계를 제공하지 못했습니다.

모듈 이전 Java 환경의 주요 문제점

  • 클래스패스 지옥 – 대규모 프로젝트에서 의존성을 수동으로 관리하면 충돌, 버전 문제, 혹은 내부 API를 실수로 사용하는 상황이 자주 발생했습니다.
  • 강력한 캡슐화 부족 – 패키지는 엄격한 경계를 강제하지 않으며, public 클래스는 클래스패스 어디에서든 접근할 수 있습니다.
  • 유지보수 및 확장성 어려움 – 거대한 단일 JAR 파일은 관리가 번거로워 업데이트, 리팩터링, 배포가 복잡해집니다.
  • 런타임 성능 및 보안 문제 – 클래스패스에 있는 모든 클래스를 미리 로드하면 리소스가 낭비되거나 내부 API가 의도치 않게 노출될 수 있습니다.

모듈은 이러한 문제들을 해결하기 위해 도입되었습니다. 모듈을 사용하면 패키지보다 높은 수준에서 명확한 의존성 및 접근 규칙을 정의할 수 있어 코드 모듈성, 보안, 유지보수성이 향상됩니다.

Java에서 모듈이란 무엇인가?

Java 모듈은 다른 모듈에 대한 의존성을 명시하고 외부에 내보내는 패키지를 선언하는 자체 포함 코드 단위입니다. 이러한 선언은 모듈 내부에 있는 module-info.java 라는 특수 파일에 작성됩니다.

Java 모듈의 주요 특성

  • 강력한 캡슐화 – 모듈은 다른 모듈에 대한 패키지 가시성을 직접 제어합니다.
  • 명시적 의존성 – 모듈은 기능 수행에 필요한 다른 모듈을 선언합니다.
  • 보안 향상 – 내부 API를 외부 접근으로부터 숨길 수 있습니다.
  • 성능 향상 – JVM은 명시적 의존성을 기반으로 모듈 로딩을 최적화할 수 있습니다.

Java 모듈의 구조

모든 Java 모듈에는 module-info.java 라는 기술자 파일이 포함됩니다. 이 파일은 모듈 계층 구조의 루트에 위치하며 모듈 이름, 의존성 및 노출 내용을 정의합니다.

예시: module-info.java

module com.example.myapp {
    requires java.sql;
    requires com.example.utils;
    exports com.example.myapp.api;
}
  • module com.example.myappcom.example.myapp이라는 이름의 모듈을 정의합니다.
  • requires java.sqljava.sql 모듈에 대한 의존성을 선언합니다.
  • requires com.example.utils – 다른 사용자 정의 모듈에 의존합니다.
  • exports com.example.myapp.apicom.example.myapp.api 패키지를 다른 모듈이 접근할 수 있도록 공개합니다.

Source:

모듈 사용: 단계별 가이드

1. 모듈 만들기

모듈 이름을 반영하는 프로젝트 디렉터리 구조를 생성합니다:

myapp/
 └─ src/
     └─ com.example.myapp/
         ├─ module-info.java
         └─ com/
             └─ example/
                 └─ myapp/
                     └─ Main.java

module-info.java 은 모듈 메타데이터를 정의하고, Java 코드는 패키지 디렉터리 안에 위치합니다.

2. 모듈 디스크립터 작성

module-info.java 에서 의존성과 내보내기를 정의합니다:

module com.example.myapp {
    requires java.base;  // 암묵적으로 필요하지만 명시적으로 선언할 수 있습니다
    exports com.example.myapp.api;
}

3. 모듈 컴파일

--module-path 옵션을 사용해 javac 컴파일러로 모듈을 컴파일합니다:

javac -d out --module-source-path src $(find src -name "*.java")

이 명령은 src 디렉터리 아래에서 찾은 모든 모듈을 컴파일하고, 결과물을 out 디렉터리에 저장합니다.

4. 모듈형 애플리케이션 실행

모듈과 메인 클래스를 지정하여 모듈형 애플리케이션을 실행합니다:

java --module-path out -m com.example.myapp/com.example.myapp.Main

Source:

모듈의 실용적인 예시

두 개의 모듈, com.example.utilscom.example.myapp을 가진 간단한 프로젝트를 살펴보겠습니다.

모듈: com.example.utils

module-info.java

module com.example.utils {
    exports com.example.utils.text;
}

TextUtils.java

package com.example.utils.text;

public class TextUtils {
    public static String toUpperCase(String input) {
        return input.toUpperCase();
    }
}

모듈: com.example.myapp

module-info.java

module com.example.myapp {
    requires com.example.utils;
    exports com.example.myapp.api;
}

Main.java

package com.example.myapp.api;

import com.example.utils.text.TextUtils;

public class Main {
    public static void main(String[] args) {
        System.out.println(TextUtils.toUpperCase("hello, modules!"));
    }
}

앞서 보여준 명령어들로 애플리케이션을 실행하면 다음과 같은 결과가 출력됩니다:

HELLO, MODULES!

추가 예시

import com.example.utils.text.TextUtils;

public class Main {
    public static void main(String[] args) {
        String result = TextUtils.toUpperCase("hello modules");
        System.out.println(result);
    }
}

모듈 의존성 예시

com.example.myappcom.example.utils에 의존하지만 자체 API 패키지만 노출합니다.

Java 모듈 사용의 장점

향상된 유지보수성

모듈은 기본적으로 강력한 캡슐화를 적용하여 시간이 지나도 코드를 더 쉽게 유지보수할 수 있게 합니다. 내부 구현 세부 사항이 숨겨져 있어 모듈 내부의 변경이 다른 모듈에 영향을 주는 경우가 거의 없습니다.

향상된 보안

모듈은 명시적으로 내보낸 경우를 제외하고 내부 패키지를 노출하지 않으므로, 민감한 내부 API가 접근 불가능하게 유지되어 공격 표면을 줄여줍니다.

의존성 명확성

module‑info.java는 각 모듈이 필요로 하는 의존성을 명확히 선언하여 빌드 및 배포 과정을 단순화합니다.

대규모 프로젝트 간소화

모듈은 거대한 단일 JAR을 더 작고 재사용 가능한 구성 요소로 분할합니다. 팀은 서로 다른 모듈을 독립적으로 작업할 수 있어 협업이 향상됩니다.

일반적인 문제와 모범 사례

분할 패키지 처리

분할 패키지는 두 모듈이 동일한 패키지 이름을 내보낼 때 발생합니다. 이는 모듈 시스템에서 허용되지 않으며 런타임 오류를 초래합니다.

모범 사례: 각 패키지가 정확히 하나의 모듈에만 속하도록 코드를 재구성하여 분할 패키지를 방지합니다.

레거시 프로젝트 마이그레이션

레거시 Java 프로젝트는 종종 모듈 디스크립터가 없는 단일 코드베이스를 가지고 있습니다. 마이그레이션에는 신중한 계획이 필요합니다:

  • 높은 수준에서 모듈화를 시작합니다.
  • 전환 중에 --patch-module 옵션을 사용합니다.
  • 핵심 모듈에 점진적으로 module-info.java 파일을 추가합니다.

모듈 명명 규칙

모듈 이름은 역도메인 명명 규칙을 따라야 합니다(예: com.company.project). 이렇게 하면 충돌을 방지하고 고유성을 보장할 수 있습니다.

모듈과 테스트

모듈형 애플리케이션을 테스트하려면 테스트 코드가 테스트 대상 모듈에 접근할 수 있도록 해야 합니다.

테스트 접근 방식

모듈에 opens 지시자 사용 – JUnit과 같은 리플렉션 기반 테스트 프레임워크가 비내보내기 패키지에 접근할 수 있게 합니다.

module com.example.myapp {
    exports com.example.myapp.api;
    opens com.example.myapp.internal to org.junit.platform.commons;
}

테스트 모듈 생성 – 테스트를 별도의 모듈에 조직하고, 테스트 대상 모듈에 requires 선언을 추가할 수 있습니다.

결론

Java 모듈은 애플리케이션과 라이브러리를 조직하는 현대적이고 확장 가능한 접근 방식을 제공합니다. 모듈은 의존성 관리, 캡슐화, 보안과 관련된 오랜 문제들을 해결합니다. 의존성을 명시적으로 선언하고 패키지 내보내기를 제어함으로써, 모듈은 코드 가독성, 유지 보수성 및 런타임 성능을 향상시킵니다.

개발자와 테스터에게 모듈 시스템을 수용하는 것은 신뢰할 수 있고 모듈화된 Java 애플리케이션을 구축하기 위한 구조화된 기반을 제공합니다. 올바른 모듈 디스크립터 파일을 작성하고 JVM의 모듈 요구 사항을 이해함으로써 프로젝트를 미래에도 견고하게 유지하고 관리하기 쉬워집니다.

모듈을 도입하면서 명확한 의존성에 집중하고, 분할 패키지를 피하며, 모듈 시스템을 활용해 내부 API를 보호하십시오. 이렇게 하면 코드베이스가 더 깔끔해지고 협업이 원활해지며 애플리케이션의 견고성이 향상됩니다.

Back to Blog

관련 글

더 보기 »

모의 인터뷰-2

인터뷰 질문 1. 자기소개를 해주세요? 2. 왜 Mechanical Engineering에서 IT로 전향했나요? 3. Mechanical Engineering이 첫 번째 선택이었다면, 왜 ...를 선택했나요?