Java 9 모듈 시스템: Jigsaw 프로젝트 🧩
안녕, 자바 개발자 친구들! 오늘은 정말 흥미진진한 주제로 찾아왔어. 바로 Java 9에서 도입된 모듈 시스템, 일명 'Jigsaw 프로젝트'에 대해 깊이 파헤쳐볼 거야. 🕵️♂️ 이 주제는 프로그램 개발, 특히 Java 카테고리에서 아주 중요한 부분이지. 자, 이제 우리의 모험을 시작해볼까?
💡 알고 가자! Jigsaw 프로젝트는 Java 플랫폼을 모듈화하고, 대규모 애플리케이션의 개발을 더 쉽게 만들기 위해 시작되었어. 이 프로젝트의 결과물이 바로 Java 9의 모듈 시스템이지.
우리가 이 여정을 떠나기 전에, 잠깐! 혹시 프로그래밍 실력을 향상시키고 싶은데 어떻게 해야 할지 모르겠다고? 그렇다면 재능넷(https://www.jaenung.net)을 한번 방문해봐. 여기서 다양한 프로그래밍 튜터들을 만나볼 수 있어. Java 모듈 시스템에 대해 더 깊이 있게 배우고 싶다면, 재능넷에서 전문가의 도움을 받는 것도 좋은 방법이 될 거야.
1. Jigsaw 프로젝트: 왜 필요했을까? 🤔
자, 이제 본격적으로 Jigsaw 프로젝트에 대해 알아보자. 근데 잠깐, 왜 이런 프로젝트가 필요했을까? 그 이유를 알아보기 위해, 우리는 시간 여행을 떠나볼 거야. 자바의 과거로 고고! 🚀
🕰️ 시간 여행: Java의 과거
- Java 1.0 (1996): 객체지향 프로그래밍의 혁명
- Java 1.1 (1997): 이너 클래스, JavaBeans 도입
- Java 1.2 (1998): Swing, JIT 컴파일러 추가
- Java 1.3 (2000): HotSpot JVM, JNDI 포함
- Java 1.4 (2002): assert, 정규표현식, NIO 도입
- Java 5 (2004): 제네릭스, 열거형, 어노테이션 추가
- Java 6 (2006): 성능 개선, 스크립팅 언어 지원
- Java 7 (2011): try-with-resources, 다이아몬드 연산자
- Java 8 (2014): 람다 표현식, 스트림 API, 새로운 날짜/시간 API
와우! 자바가 정말 긴 여정을 거쳐왔지? 근데 이렇게 발전하는 과정에서 몇 가지 문제점들이 드러나기 시작했어. 어떤 문제들이었을까?
1.1 Java의 성장통 😖
Java가 발전하면서 생긴 주요 문제점들을 살펴볼까?
- JAR 지옥: 의존성 관리가 너무 복잡해졌어. 프로젝트가 커질수록 JAR 파일들이 쌓여가고, 버전 충돌도 자주 발생했지.
- 모놀리식 rt.jar: Java 런타임의 핵심인 rt.jar 파일이 너무 커져버렸어. 이 거대한 파일 때문에 Java SE 플랫폼의 크기가 계속 늘어났지.
- 캡슐화의 한계: 패키지 레벨의 캡슐화만으로는 대규모 애플리케이션의 구조를 효과적으로 관리하기 어려웠어.
- 플랫폼의 경직성: Java SE 플랫폼이 너무 커지다 보니, 다양한 디바이스에 맞춰 최적화하기 어려워졌어.
이런 문제들 때문에 Java 개발자들은 머리를 싸매고 고민했어. "어떻게 하면 이 문제들을 해결할 수 있을까?" 그리고 그 고민의 결과물이 바로 Jigsaw 프로젝트야! 🎉
1.2 Jigsaw 프로젝트의 목표 🎯
Jigsaw 프로젝트는 이런 문제들을 해결하기 위해 다음과 같은 목표를 세웠어:
🚀 Jigsaw의 주요 목표
- Java SE 플랫폼의 모듈화
- 애플리케이션의 확장성과 유지보수성 개선
- 보안성 강화
- 성능 향상
- IoT 디바이스 등 다양한 환경에서의 Java 사용성 개선
이 목표들을 달성하기 위해, Jigsaw 프로젝트는 Java에 모듈 시스템을 도입하기로 했어. 그럼 이제 모듈이 뭔지, 어떻게 작동하는지 자세히 알아볼까?
2. Java 모듈 시스템: 퍼즐 맞추기 🧩
자, 이제 우리는 Java 모듈 시스템이라는 새로운 세계로 들어가볼 거야. 이 세계는 마치 거대한 퍼즐과 같아. 각각의 모듈은 퍼즐 조각이고, 우리는 이 조각들을 맞춰 멋진 애플리케이션이라는 그림을 완성하는 거지. 재미있지 않아? 😄
2.1 모듈이란 무엇인가? 🤔
모듈은 간단히 말해서 관련된 패키지와 리소스의 모음이야. 근데 그냥 모음이 아니라, 아주 특별한 모음이지. 왜 특별하냐고? 모듈은 자신이 어떤 기능을 제공하고, 어떤 다른 모듈에 의존하는지를 명확하게 선언할 수 있거든.
💡 모듈의 특징
- 이름을 가지고 있어
- 코드(패키지와 클래스)를 포함해
- 다른 모듈에 의존할 수 있어
- 자신의 API를 명시적으로 공개할 수 있어
- 리소스와 네이티브 라이브러리를 포함할 수 있어
모듈을 사용하면 우리 애플리케이션의 구조를 더 명확하게 만들 수 있어. 마치 도시를 계획할 때 구역을 나누는 것처럼, 우리 코드를 논리적인 단위로 나눌 수 있지. 이렇게 하면 코드의 구조를 이해하기 쉬워지고, 유지보수도 편해져.
2.2 모듈 선언하기: module-info.java 📝
자, 이제 실제로 모듈을 어떻게 만드는지 알아볼까? 모듈을 선언하려면 module-info.java라는 특별한 파일을 만들어야 해. 이 파일은 모듈의 "신분증" 같은 거야. 모듈의 이름, 의존성, 공개할 패키지 등을 여기에 적어주는 거지.
간단한 예제를 한번 볼까?
module com.myapp.core {
requires java.base; // java.base 모듈에 의존해
requires java.logging; // java.logging 모듈도 필요해
exports com.myapp.core.api; // 이 패키지는 다른 모듈에서 사용할 수 있어
exports com.myapp.core.util to com.myapp.network; // 이 패키지는 com.myapp.network 모듈에만 공개해
uses com.myapp.core.spi.Plugin; // 이 서비스를 사용할 거야
provides com.myapp.core.spi.Plugin with com.myapp.core.internal.PluginImpl; // 이 서비스의 구현체를 제공해
}
우와, 뭔가 복잡해 보이지? 걱정마, 하나씩 차근차근 설명해줄게.
module com.myapp.core
: 이게 우리 모듈의 이름이야. 다른 모듈이 우리 모듈을 참조할 때 이 이름을 사용하게 될 거야.requires
: 이 키워드는 우리 모듈이 다른 모듈에 의존한다는 걸 나타내. 여기서는java.base
와java.logging
모듈이 필요하다고 말하고 있어.exports
: 이건 우리 모듈에서 다른 모듈에 공개할 패키지를 지정해.com.myapp.core.api
패키지는 모든 모듈에 공개되고,com.myapp.core.util
패키지는com.myapp.network
모듈에만 공개돼.uses
: 이 모듈에서 사용할 서비스를 선언해. 여기서는com.myapp.core.spi.Plugin
서비스를 사용한다고 말하고 있어.provides ... with
: 이건 서비스의 구현체를 제공한다는 뜻이야.com.myapp.core.spi.Plugin
서비스의 구현체로com.myapp.core.internal.PluginImpl
을 사용하겠다고 선언하고 있어.
이렇게 module-info.java
파일을 통해 모듈의 모든 것을 선언할 수 있어. 이 파일 하나로 모듈의 의존성, 공개 API, 사용할 서비스 등을 한눈에 파악할 수 있지. 정말 편리하지 않아?
2.3 모듈 간의 관계: 의존성과 캡슐화 🔗
모듈 시스템의 가장 큰 장점 중 하나는 바로 명시적인 의존성 선언이야. 이전에는 클래스패스에 있는 모든 클래스를 사용할 수 있었지만, 이제는 requires
키워드를 통해 필요한 모듈을 명확하게 선언해야 해. 이렇게 하면 의존성 관리가 훨씬 쉬워지고, 실수로 잘못된 클래스를 사용하는 일도 줄일 수 있어.
또 다른 중요한 특징은 강력한 캡슐화야. exports
키워드를 사용해 특정 패키지만 공개할 수 있어. 공개하지 않은 패키지는 모듈 내부에서만 사용할 수 있고, 외부에서는 접근할 수 없어. 이렇게 하면 내부 구현을 더 잘 숨길 수 있고, API의 안정성을 높일 수 있지.
🔒 강력한 캡슐화의 이점
- 내부 구현의 변경이 외부에 영향을 미치지 않아
- API의 안정성과 일관성을 유지하기 쉬워져
- 보안성이 향상돼 (외부에서 접근하면 안 되는 코드를 숨길 수 있으니까)
- 코드의 재사용성이 높아져 (잘 정의된 인터페이스를 통해 모듈을 쉽게 재사용할 수 있으니까)
이런 특징들 덕분에 모듈 시스템을 사용하면 대규모 애플리케이션을 더 쉽게 구조화하고 관리할 수 있어. 마치 레고 블록처럼, 필요한 모듈들을 조립해서 원하는 애플리케이션을 만들 수 있는 거지. 멋지지 않아? 😎
3. 모듈 시스템의 이점: 왜 사용해야 할까? 🚀
자, 이제 모듈이 뭔지, 어떻게 작동하는지 알았으니까 "그래서 뭐?" 라고 생각할 수도 있어. 왜 이런 복잡한 시스템을 사용해야 하는 걸까? 모듈 시스템을 사용하면 어떤 이점이 있는지 자세히 알아보자!
3.1 향상된 의존성 관리 📊
모듈 시스템의 가장 큰 장점 중 하나는 바로 의존성 관리의 개선이야. 이전에는 클래스패스에 있는 모든 클래스를 사용할 수 있었지만, 이제는 필요한 모듈을 명시적으로 선언해야 해. 이게 왜 좋을까?
- 명확성: 어떤 모듈이 어떤 다른 모듈에 의존하는지 한눈에 알 수 있어.
- 안정성: 필요한 의존성이 없으면 컴파일 시점에 오류가 발생해. 런타임 에러를 줄일 수 있지!
- 버전 충돌 감소: 모듈 시스템은 같은 모듈의 서로 다른 버전이 동시에 로드되는 것을 방지해.
예를 들어, 이전에는 이런 식으로 의존성을 관리했어:
// 클래스패스에 있는 모든 JAR 파일을 로드
java -cp lib/*:classes MyApp
하지만 이제는 이렇게 할 수 있어:
// 필요한 모듈만 명시적으로 지정
java --module-path lib --add-modules com.myapp.core,com.myapp.ui MyApp
훨씬 명확하고 안전하지 않아? 😊
3.2 강력한 캡슐화 🔒
모듈 시스템의 또 다른 큰 장점은 강력한 캡슐화야. 이전에는 public 클래스나 인터페이스는 모두 외부에서 접근 가능했지만, 이제는 모듈에서 명시적으로 export한 패키지만 외부에서 접근할 수 있어.
이게 어떤 의미일까? 예를 들어보자:
module com.myapp.core {
exports com.myapp.core.api;
// com.myapp.core.internal 패키지는 export하지 않았어
}
이 경우, com.myapp.core.api
패키지의 public 클래스들은 다른 모듈에서 사용할 수 있지만, com.myapp.core.internal
패키지의 클래스들은 아무리 public이어도 다른 모듈에서 접근할 수 없어. 이렇게 하면 내부 구현을 더 잘 숨길 수 있고, API의 안정성을 높일 수 있지.
💡 캡슐화의 이점
- 내부 구현의 변경이 외부에 영향을 미치지 않아
- API의 안정성과 일관성을 유지하기 쉬워져
- 보안성이 향상돼 (외부에서 접근하면 안 되는 코드를 숨길 수 있으니까)
- 코드의 재사용성이 높아져 (잘 정의된 인터페이스를 통해 모듈을 쉽게 재사용할 수 있으니까)
3.3 플랫폼의 모듈화와 최적화 🏗️
Java 9부터 JDK 자체가 모듈화되었어. 이게 무슨 의미일까? 이전에는 모든 JDK 클래스가 하나의 거대한 rt.jar 파일에 들어있었지만, 이제는 여러 개의 모듈로 나뉘어 있어. 이렇게 하면 어떤 장점이 있을까?
- 더 작은 런타임: 필요한 모듈만 포함해서 더 작은 Java 런타임을 만들 수 있어. 이건 특히 임베디드 시스템이나 IoT 디바이스에서 유용해.
- 보안 강화: 필요 없는 모듈을 제외하면 공격 표면을 줄일 수 있어.
- 성능 향상: 필요한 클래스만 로드하므로 시작 시간이 빨라지고 메모리 사용량도 줄어들어.
예를 들어, 이런 식으로 최소한의 Java 런타임을 만들 수 있어:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output minimal-java
이렇게 하면 java.base 모듈만 포함된 아주 작은 Java 런타임을 만들 수 있지. 멋지지 않아? 😎
3.4 서비스 지향 아키텍처 지원 🔧
모듈 시스템은 서비스 지향 아키텍처(SOA)를 더 쉽게 구현할 수 있게 해줘. provides
와 uses
키워드를 사용해서 서비스 제공자와 소비자를 명확하게 정의할 수 있거든.
예를 들어보자:
module com.myapp.core {
exports com.myapp.core.api;
uses com.myapp.core.spi.Plugin;
}
module com.myapp.plugin {
requires com.myapp.core;
provides com.myapp.core.spi.Plugin with com.myapp.plugin.MyPlugin;
}
이런 식으로 하면, com.myapp.core
모듈은 Plugin
서비스를 사용하고, com.myapp.plugin
모듈은 이 서비스의 구현체를 제공해. 이렇게 하면 플러그인 시스템 같은 걸 아주 쉽게 만들 수 있어!
3.5 컴파일 시간 개선 ⏱️
모듈 시스템을 사용하면 컴파일 시간도 개선될 수 있어. 어떻게 그럴 수 있을까?
- 의존성 그래프 최적화: 컴파일러가 모듈 간의 의존성을 더 잘 이해할 수 있어서, 필요한 부분만 컴파일할 수 있어.
- 병렬 컴파일: 모듈 간의 의존성이 명확하기 때문에, 독립적인 모듈들을 병렬로 컴파일할 수 있어.
- 증분 컴파일: 변경된 모듈만 다시 컴파일하면 되니까, 전체 프로젝트를 다시 컴파일할 필요가 없어져.
대규모 프로젝트에서는 이런 최적화가 엄청난 시간 절약으로 이어질 수 있어. 생산성이 크게 향상되는 거지!
3.6 더 나은 리플렉션 🔍
Java의 리플렉션 API도 모듈 시스템을 지원하도록 개선되었어. 이제 모듈의 정보를 조회하거나 모듈 내의 클래스에 접근할 때 모듈 시스템의 규칙을 따르게 돼. 이게 무슨 의미일까?
- 모듈이 export하지 않은 패키지의 클래스에는 리플렉션으로도 접근할 수 없어.
- 모듈의 메타데이터(이름, 의존성 등)를 프로그래밍 방식으로 조회할 수 있어.
- 런타임에 모듈 그래프를 동적으로 수정할 수 있어.
이런 기능들은 프레임워크 개발자들에게 특히 유용해. 예를 들어, 의존성 주입 프레임워크나 ORM 라이브러리 같은 걸 만들 때 이런 기능들을 활용할 수 있지.
4. 모듈 시스템 실전 활용: 어떻게 사용할까? 🛠️
자, 이제 모듈 시스템의 이론은 충분히 알았으니 실제로 어떻게 사용하는지 알아볼 차례야. 모듈 시스템을 활용한 프로젝트를 어떻게 구성 하고 빌드하는지, 그리고 실행할 때는 어떻게 해야 하는지 자세히 알아보자. 준비됐어? 그럼 시작해볼까! 🚀
4.1 모듈화된 프로젝트 구조 📁
모듈 시스템을 사용하는 프로젝트는 일반적으로 다음과 같은 구조를 가져:
my-app/
├── src/
│ ├── com.myapp.core/
│ │ ├── module-info.java
│ │ └── com/myapp/core/
│ │ └── ...
│ ├── com.myapp.ui/
│ │ ├── module-info.java
│ │ └── com/myapp/ui/
│ │ └── ...
│ └── com.myapp.util/
│ ├── module-info.java
│ └── com/myapp/util/
│ └── ...
└── build.gradle (or pom.xml)
각 모듈은 자신의 module-info.java
파일을 가지고 있어. 이 파일에서 모듈의 이름, 의존성, 그리고 외부에 공개할 패키지를 정의하지.
4.2 모듈 정의하기 📝
각 모듈의 module-info.java
파일을 어떻게 작성하는지 예제를 통해 알아보자:
// com.myapp.core/module-info.java
module com.myapp.core {
requires java.base; // 이건 기본적으로 포함되지만, 명시적으로 적어줄 수도 있어
requires com.myapp.util;
exports com.myapp.core.api;
uses com.myapp.core.spi.Plugin;
}
// com.myapp.ui/module-info.java
module com.myapp.ui {
requires com.myapp.core;
requires javafx.controls;
exports com.myapp.ui to javafx.graphics;
}
// com.myapp.util/module-info.java
module com.myapp.util {
exports com.myapp.util;
}
이렇게 각 모듈의 의존성과 공개할 패키지를 명확하게 정의할 수 있어.
4.3 모듈화된 프로젝트 빌드하기 🏗️
모듈화된 프로젝트를 빌드하는 방법은 사용하는 빌드 도구에 따라 조금씩 다를 수 있어. 여기서는 Gradle과 Maven의 예제를 살펴볼게.
Gradle을 사용한 빌드
Gradle에서는 build.gradle
파일에 다음과 같이 설정을 추가할 수 있어:
plugins {
id 'java'
}
sourceSets {
main {
java {
srcDirs = ['src/com.myapp.core', 'src/com.myapp.ui', 'src/com.myapp.util']
}
}
}
compileJava {
options.compilerArgs = [
'--module-path', classpath.asPath,
]
options.compilerArgs += [
'--module-source-path', sourceSets.main.java.srcDirs.join(File.pathSeparator)
]
}
Maven을 사용한 빌드
Maven에서는 pom.xml
파일에 다음과 같이 설정을 추가할 수 있어:
<project>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
...
</project>
4.4 모듈화된 애플리케이션 실행하기 ▶️
모듈화된 애플리케이션을 실행할 때는 --module-path
와 --module
옵션을 사용해. 예를 들면 이렇게:
java --module-path mods --module com.myapp.core/com.myapp.core.Main
여기서 mods
는 모듈 JAR 파일들이 있는 디렉토리야. com.myapp.core
는 메인 클래스가 있는 모듈의 이름이고, com.myapp.core.Main
은 메인 클래스의 전체 이름이야.
4.5 모듈 시스템과 레거시 코드의 통합 🔄
기존의 non-modular 코드를 모듈 시스템과 함께 사용해야 할 때도 있어. 이럴 때는 자동 모듈이라는 개념을 사용할 수 있어.
자동 모듈은 module-info.java
파일이 없는 일반 JAR 파일을 모듈 경로에 놓으면 자동으로 생성돼. 이 자동 모듈의 이름은 JAR 파일의 이름을 기반으로 생성되고, 모든 패키지를 자동으로 export해.
예를 들어, mylib-1.0.jar
라는 파일을 모듈 경로에 놓으면, mylib
라는 이름의 자동 모듈이 생성돼. 이 자동 모듈을 사용하려면 이렇게 하면 돼:
module com.myapp.core {
requires mylib;
...
}
이렇게 하면 레거시 라이브러리도 모듈 시스템과 함께 사용할 수 있어. 편리하지? 😊
5. 모듈 시스템의 모범 사례와 팁 💡
자, 이제 모듈 시스템을 어떻게 사용하는지 알았으니, 이를 효과적으로 활용하기 위한 몇 가지 팁을 알아볼까?
5.1 모듈 설계 원칙 📐
- 높은 응집도, 낮은 결합도: 각 모듈은 하나의 명확한 책임을 가져야 해. 모듈 간의 의존성은 최소화하고, 필요한 경우에만 패키지를 export해.
- 인터페이스와 구현의 분리: API를 정의하는 모듈과 구현을 제공하는 모듈을 분리해. 이렇게 하면 유연성이 높아져.
- 순환 의존성 피하기: 모듈 간의 순환 의존성은 피해야 해. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만들어.
- 최소 권한의 원칙: 꼭 필요한 패키지만 export하고, 꼭 필요한 모듈만 requires해. 이렇게 하면 보안성이 높아지고 의도치 않은 의존성을 방지할 수 있어.
5.2 모듈 이름 짓기 🏷️
모듈 이름을 지을 때는 다음과 같은 규칙을 따르는 것이 좋아:
- 역방향 도메인 이름 사용 (예:
com.mycompany.myapp
) - 간결하고 의미 있는 이름 사용
- 버전 번호는 포함하지 않기 (버전 관리는 빌드 시스템에 맡기자)
- Java 키워드 사용 피하기
5.3 서비스 사용하기 🔧
모듈 시스템의 서비스 기능을 활용하면 플러그인 아키텍처를 쉽게 구현할 수 있어. 다음과 같은 방식으로 사용할 수 있지:
// 서비스 제공자 모듈
module com.myapp.plugin {
requires com.myapp.core;
provides com.myapp.core.spi.Plugin with com.myapp.plugin.MyPlugin;
}
// 서비스 사용자 모듈
module com.myapp.core {
exports com.myapp.core.api;
uses com.myapp.core.spi.Plugin;
}
// 서비스 사용 예
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin plugin : loader) {
plugin.doSomething();
}
5.4 모듈 리팩토링 🔄
기존 프로젝트를 모듈화할 때는 다음과 같은 단계를 따르는 것이 좋아:
- 먼저 프로젝트의 구조를 분석하고 모듈 경계를 정의해.
- 각 모듈에 대한
module-info.java
파일을 생성해. - 필요한
requires
와exports
문을 추가해. - 컴파일 에러를 해결하면서 모듈 간의 의존성을 조정해.
- 점진적으로 리팩토링을 진행하고, 각 단계마다 테스트를 실행해.
5.5 모듈 테스트하기 🧪
모듈화된 프로젝트를 테스트할 때는 다음과 같은 점을 고려해야 해:
- 테스트 코드도 별도의 모듈로 만들 수 있어. 이 경우
module-info.java
에open module
을 사용해 리플렉션 접근을 허용해야 해. - 테스트 프레임워크가 모듈 시스템을 지원하는지 확인해. JUnit 5는 모듈 시스템을 잘 지원해.
- 통합 테스트를 위해 여러 모듈을 함께 테스트해야 할 수도 있어. 이 경우 모듈 경로 설정에 주의해야 해.
6. 모듈 시스템의 미래: 어떻게 발전할까? 🔮
Java 모듈 시스템은 계속해서 발전하고 있어. 앞으로 어떤 변화가 있을지 예측해볼까?
6.1 더 나은 도구 지원 🛠️
IDE와 빌드 도구들이 모듈 시스템을 더 잘 지원하게 될 거야. 예를 들어:
- 모듈 의존성 시각화 도구
- 모듈 리팩토링 자동화 도구
- 모듈 경계 위반 검사 도구
6.2 마이크로서비스와의 통합 🌐
모듈 시스템과 마이크로서비스 아키텍처를 결합하는 방법에 대한 연구가 진행될 거야. 모듈을 독립적인 마이크로서비스로 배포하는 방식이 더 보편화될 수 있어.