Java 9 모듈 시스템: Jigsaw 프로젝트 🧩

콘텐츠 대표 이미지 - 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의 주요 목표

  1. Java SE 플랫폼의 모듈화
  2. 애플리케이션의 확장성과 유지보수성 개선
  3. 보안성 강화
  4. 성능 향상
  5. 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.basejava.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)를 더 쉽게 구현할 수 있게 해줘. providesuses 키워드를 사용해서 서비스 제공자와 소비자를 명확하게 정의할 수 있거든.

예를 들어보자:

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 모듈 설계 원칙 📐

  1. 높은 응집도, 낮은 결합도: 각 모듈은 하나의 명확한 책임을 가져야 해. 모듈 간의 의존성은 최소화하고, 필요한 경우에만 패키지를 export해.
  2. 인터페이스와 구현의 분리: API를 정의하는 모듈과 구현을 제공하는 모듈을 분리해. 이렇게 하면 유연성이 높아져.
  3. 순환 의존성 피하기: 모듈 간의 순환 의존성은 피해야 해. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만들어.
  4. 최소 권한의 원칙: 꼭 필요한 패키지만 export하고, 꼭 필요한 모듈만 requires해. 이렇게 하면 보안성이 높아지고 의도치 않은 의존성을 방지할 수 있어.

5.2 모듈 이름 짓기 🏷️

모듈 이름을 지을 때는 다음과 같은 규칙을 따르는 것이 좋아:

  • 역방향 도메인 이름 사용 (예: com.mycompany.myapp)
  • 간결하고 의미 있는 이름 사용
  • 버전 번호는 포함하지 않기 (버전 관리는 빌드 시스템에 맡기자)
  • Java 키워드 사용 피하기