자바

[모던 자바 인 액션] 자바 모듈 시스템

__Minnie_ 2025. 5. 9. 00:19
추론하기 쉬운 소프트웨어

 

유지보수를 하기 쉬운 코드는 추론이 쉬운 코드라고 할 수 있다. 우리는 관심사 분리와 정보 은닉을 통해서 추론하기 쉬운 소프트웨어를 만들 수 있다.

 

1. 관심사 분리

컴퓨터 프로그램을 고유의 기능으로 나누는 것을 권장하는 원칙이다. 예를 들어서 파일, URL 등 다양한 형식으로 구성된 지출을 파싱하고, 분석한 후 결과를 고객에게 보고하는 프로그램이 있을 때, 데이터를 읽어오는 부분, 파싱하는 부분, 모델링하는 부분, 분석하는 부분으로 관심사를 분리할 수 있다. 

 

2. 정보 은닉

세부 구현을 숨기도록 장려하는 원칙이다. 세부 구현을 숨김으로써 요구 사항이 변하여 동작이 변경되어도 다른 부분에 영향을 미칠 가능성을 낮출 수 있다. 

 

모듈 시스템의 설계 이유

 

1. 기존 모듈화의 한계 

자바는 클래스, 패키지, JAR 세가지 수준의 코드 그룹화를 제공한다. 클래스에서는 접근 제한자를 제공하지만 패키지와 JAR에서는 거의 지원하지 않았다. 예를 들어서 하나의 패키지의 기능을 다른 특정 패키지에게만 공개하고자 한다면 public으로 선언해야 하고, 그렇게 되면 의도와는 다르게 모든 사람에게 공개가 되어야 했다. 

 

2. 클래스 경로

클래스 경로에는 버전을 구분하는 개념이 없었다. 따라서 하나의 프로젝트 내에서 동일한 라이브러리의 다른 버전을 사용해도 이를 명확하게 설정할 수 없었다. 또한 클래스 경로는 명시적인 의존성을 제공하지 않았다. 따라서 특정 클래스가 다른 클래스의 의존성을 가지고 있어도 이를 표현할 수가 없었다. 

 

3. 거대한 자바 JDK

JDK는 자바 프로그램을 만들고 실행하는데 도움을 주는 도구의 집합이다. 시간이 흐르면서 JDK의 덩치도 많이 커졌다. 그래서 특정 도구가 필요하지 않는 환경에서도 모두 포함되어 문제를 발생시키는 경우가 있었다. 이런 문제를 컴팩트 프로파일, 관련 분야에 따라 JDK 라이브러리의 프로파일을 분리하는 방식으로 해결하고자 하였다. 또한 JDK 라이브러리들은 공개되지 않아야 하지만, 자바의 낮은 접근 제어 설정과 캡슐화 지원 때문에 내부 API가 외부에 공개되는 문제점도 있었다. 

 

 

자바 모듈

 

자바 모듈은 새로운 자바 프로그램 구조 단위를 의미한다. 모듈은 module이라는 키워드에 이름과 바디를 추가해서 정의한다.

module 모듈명 {
	requires 모듈명
	exports 패키지명
}

이 내용은 모듈 디스크립터라고 부르며, 해당 내용은 module-info.java라는 파일에 보관된다. 모듈 디스크립터는 보통 패키지와 같은 폴더에 위치한다. 

 

 

세부적인 모듈화와 거친 모듈화

 

세부적인 모듈화란 모든 패키지가 자신의 모듈을 갖는 것을 의미한다. 반대로 거친 모듈화는 하나의 모듈이 모든 패키지를 포함하고 있는 것을 의미한다. 세부적인 모듈화는 설계 비용이 높아진다는 단점이 있고, 거친 모듈화는 모듈화의 장점이 없어진다는 단점이 있다. 따라서, 상황에 맞게 적절하게 모듈화하는 것이 중요하다. 

 

 

모듈명 규칙

 

패키지명처럼 인터넷 도메인명을 역순으로 모듈의 이름을 정하도록 권고한다. 

 

 

모듈 디스크립터에서 사용되는 여러 구문들

 

1. exports

module expenses.readers {
	exports com.example.expenses.readers;
}

exports는 위와 같은 문법으로 사용되며, exports 키워드와 함께 패키지 경로명을 작성한다. 기본적으로 모듈 내의 모든 것은 캡슐화 되는데, exports를 통해 선언된 것들만 다른 모듈에 공개된다. 즉 화이트 리스트 개념을 사용한다. 

 

2. requires

module expenses.readers {
	requires java.base;
}

requires는 위와 같은 문법으로 사용된다. requires의 키워와 함께 모듈명을 작성한다. (참고, exports는 패키지명)  requires는 의존하는 모듈을 지정한다. 해당 모듈에서 사용하는 모듈들을 임포트할 때 사용한다.

 

3. requires transitive

module com.interatrlearning.ui {
	requires transitive com.iteratrlearning.core;
}

requires transitive의 키워드와 함께 모듈명을 작성한다. 다른 모듈이 제공하는 공개 형식을 한 모듈에서 사용할 수 있다고 지정한다. 이렇게 사용하게 되면 core모듈에서 공개한 모든 겂에 접근할 수 있다. 

 

4. exports to

exports는 모든 모듈에 공개를 시키는 것이었다면 exports to는 특정 모듈에게만 공개하도록 대상을 특정할 수 있다.

 

5. open, opens

모듈 시스템에서는 기본적으로 반사적인 접근 권한을 막는데, open과 opens는 반사적인 접근 권한을 부여하는 구문이다. open을 사용하게 되면 모든 모듈 내부의 패키지에 대해서 반서적 접근 권한을 허용하게 되고 opens to를 사용하게 되면 해당 접근 권한을 특정 대상에게만 허용할 수 있다. 

 

반사적 접근이란?? 런타임에 내부를 까서 조작하는 방식을 의미한다. 

Class<?> cls = Class.forName("com.example.Person");
Field f = cls.getDeclaredField("name");
f.setAccessible(true);
f.set(obj, "Alice");

 

6. uses, providers

uses는 서비스 소비자를 지정하고, provider는 서비스 제공자를 지정한다. 

 

 

컴파일과 패키징

 

메이븐으로 컴파일을 한다고 하였을 때, 각 모듈마다 pom.xml을 추가해야 한다. 또한 부모 모듈에도 pom.xml을 설정해야 한다. pom.xml에는 모듈에 대한 정보, 부모 모듈에 대한 정보, 의존성 등에 대한 정보가 포함된다. 

 

이렇게 pom.xml을 모두 작성한 후에는 mvn clean package라는 명령어를 통해서 JAR파일을 생성할 수 있고, 생성된 jar를 --module-path 옵션을 통해서 다른 모듈 실행시 포함하여 실행하도록 할 수 있다. 

 

 

자동 모듈

 

우리가 모듈 내에서 httpClient처럼 자바 모듈이 아닌 외부 라이브러리를 사용한다면 어떻게 될까?? 자바는 이런 경우 JAR를 자동 모듈이라는 형태로 적절하게 변환한다. 즉, 모듈 디스크립터를 가지지 않은 모든 JAR는 자동 모듈이 된다. 자동 모듈은 암묵적으로 자신의 모든 패키지를 노출시킨다.