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
파일을 어떻게 작성하는지 예제를 통해 알아보자: