Chapter 10 - 라이브러리와 모듈

김태원·2023년 1월 18일
0
post-custom-banner

라이브러리

라이브러리(library)는 프로그램 개발 시 활용할 수 있는 클래스와 인터페이스들을 모아놓은 것을 말한다.

일반적으로 JAR(Java Archive) 압축 파일(~.jar) 형태로 존재한다. JAR 파일에는 클래스와 인터페이스의 바이트코드 파일(~.class)들이 압축되어 있다.

특정 클래스와 인터페이스가 여러 응용 프로그램을 개발할 떄 공통으로 자주 사용된다면 JAR 파일로 압축해서 라이브러리로 관리하는 것이 좋다.

프로그램 개발 시 라이브러리를 이용하려면 라이브러리 JAR 파일을 ClassPath에 추가해야 한다.
ClassPath란 말 그대로 클래스를 찾기 위한 경로이다. ClassPath에 라이브러리를 추가하는 방법은 다음과 같다.

  • 콘솔(명령 프롬프트 또는 터미널)에서 프로그램을 실행할 경우
    • java 명령어를 실행할 때 -classpath로 제공
    • CLASSPATH 환경 변수에 경로를 추가
  • IDE에서 실행할 경우
    • IDE 프로젝트의 Build Path에 추가

모듈

Java 9부터 지원하는 모듈(module)은 패키지 관리 기능까지 포함된 라이브러리이다.

일반 라이브러리는 내부에 포함된 모든 패키지에 외부 프로그램에서의 접근이 가능하지만, 모듈은 다음과 같이 일부 패키지를 은닉하여 접근할 수 없게끔 할 수 있다.

또 다른 차이점은 모듈은 자신이 실행할 때 필요로 하는 의존 모듈을 모듈 기술자(module-info.java)에 기술할 수 있기 때문에 모듈 간의 의존 관계를 쉽게 파악할 수 있다는 것이다.

아래 그림은 A 모듈은 B 모듈이 있어야 실행할 수 있고, B 모듈은 C 모듈이 있어야 실행할 수 있다는 의존 관계를 보여준다.

모듈도 라이브러리이므로 JAR 파일 형태로 배포할 수 있다. 응용 프로그램을 개발할 때 원하는 기느의 모듈(JAR) 파일을 다운로드해서 이용하면 된다.

대규모 응용 프로그램은 기능별로 모듈화(Modulization)해서 개발할 수도 있다.
모듈별로 개발하고 조립하는 방식을 사용하면 재사용성 및 유지보수에 유리하기 때문이다.

응용프로그램 모듈화

응용 프로그램은 하나의 프로젝트로도 개발이 가능하지만, 이것을 기능별로 서브 프로젝트(모듈)로 쪼갠 다음 조합해서 개발할 수도 있다.

다음 그림처럼 my_application_2 응용프로그램은 2개의 서브 프로젝트(모듈)인 my_module_a와 my_module_b로 쪼개서 개발하고, 이들을 조합해서 완성할 수 있다.

응용프로그램의 규모가 커질수록 협업과 유지보수 측면에서 서브 모듈로 쪼개서 개발하는 것이 유리하며, 이렇게 개발된 모듈들은 다른 응용프로그램에서도 재사용이 가능하다.

모듈 배포용 JAR 파일

모듈 개발을 완료했다면 다른 모듈에서 쉽게 사용할 수 있도록 바이트코드 파일(.class)로 구성된 배포용 JAR 파일을 생성할 수 있다.

배포용 JAR 파일은 모듈별로 배포용 JAR 파일을 각각 생성한다.

패키지 은닉

모듈은 모듈 기술자(module_info.java)에서 exports 키워드를 사용해 내부 패키지 중 외부에서 사용할 패키지를 지정한다.

exports 되지 않는 패키지는 자동적으로 은닉된다. 다음 그림을 보면 exports된 pack1만 외부에서 사용할 수 있고, pack2와 pack3은 은닉된다.

모듈이 일부 패키지를 은닉하는 이유는 다음과 같다.

  • 모듈 사용 방법 통일
    • 모듈 외부에서 패키지2와 3을 사용하지 못하도록 막고, 패키지1로 사용 방법을 통일한다.
  • 쉬운 수정
    • 모듈 성능 향상을 위해 패키지2와 3을 수정하더라도 모듈 사용 방법(패키지1)이 달라지지 않기 때문에 외부에 영향을 주지 않는다.

전이 의존

my_application_2 프로젝트와 my_module_a, my_module_b 모듈의 의존 관계는 다음과 같이 표현할 수 있다. my_application_2는 직접적으로 두 모듈을 requires하고 있기 때문이다.


다음과 같이 의존 관계를 변경한다고 가정해보자.

module my_application_2 {
    requries my_module_a;
}
module my_module_a {
    exports pack1;
    requries my_module_b;
}

이렇게 작성하면 my_application_2의 Main 클래스는 my_module_b 모듈을 사용할 수 없기 때문에 컴파일 오류가 발생한다.
my_application_2의 모듈 기술자에서 requires my_module_b가 빠졌기 때문이다.
Main 클래스에서 my_module_b 패키지 코드를 모두 제거하면 되겠지만, 제거할 수 없는 경우도 있다.
다음과 같이 my_module_a 소속의 A 클래스가 my_module_b 소속의 C 타입의 객체를 리턴하는 경우이다.

A a = new A();
c c = a.method();

my_application_2에서 이 코드를 사용해야 한다면 C 타입이 있기 때문에 my_application_2의 모듈 기술자에 requires my_module_b를 반드시 추가해야 한다. my_application_2는 단지 my_module_a만 사용하고 싶었는데도 말이다.

이 문제를 해결할 방법은 my_module_a가 가지고 있다. my_module_a의 모듈 기술자에게 transitive 키워드와 함께 my_module_b를 의존 설정하면 된다.

그러면 my_application_2에서도 my_module_b를 사용할 수 있게 된다. 의존 설정이 전이되기 때문이다.

module my_application_2 {
    requires my_module_a;
}
module my_module_a {
    exports pack1;
    requires transitive my_module_b;
}

집합 모듈

집합 모듈은 여러 모듈을 모아놓은 모듈을 말한다. 자주 사용되는 모듈들을 일일이 requires하는 번거로움을 피하고 싶을 때 집합 모듈을 생성하면 편리하다.

집합 모듈은 자체적인 패키지를 가지지 안혹, 모듈 기술자에 전이 의존 설정만 한다.

예를 들어 my_module은 my_module_a와 my_module_b를 제공하는 집합 모듈이라고 가정해보자.
my_module의 모듈 기술자는 다음과 같이 작성할 수 있다.

module my_module {
    requires transitive my_module_a;
    requires transitive my_module_b;
}

이때 다른 프로젝트에서 my_module만 requires하게 되면 my_module_a와 my_module_b 모듈 둘 다 사용할 수 있게 된다.

리플렉션 허용

은닉된 패키지는 기본적으로 다른 모듈에 의해 리플렉션을 허용하지 않는다. 리플렉션(reflection)이란 실행 도중에 타입(클래스, 인터페이스 등)을 검사하고 구성 멤버를 조사하는 것을 말한다.

경우에 따라서는 은닉된 패키지도 리플렉션을 허용해야 할 때가 있다. 모듈은 모듈 기술자를 통해 모듈 전체 또는 지정된 패키지에 대해 리플렉션을 허용할 수 있고, 특정 외부 모듈에서만 리플렉션을 허용할 수도 있다.

// 모듈 전체를 리플렉션 허용
open module 모듈명 {
    ...
}
// 지정된 패키지에 대해 리플렉션 허용
module 모듈명 {
    ...
    opens 패키지1;
    opens 패키지2;
}
// 지정된 패키지에 대해 특정 외부 모듈에서만 리플렉션 허용
module 모듈명 {
    ...
    opens 패키지1 to 외부모듈명, 외부모둘명, ...;
    opens 패키지2 to 외부모듈명;
}

export된 패키지는 언제든지 리플렉션이 가능하므로 opens로 지정할 필요가 없다.
opens는 은닉된 패키지 중에서 특정 패키지에 대한 리플렉션을 허용한다.

자바 표준 모듈

자바 프로그램이라면 반드시 활용해야 하는 라이브러리가 있다. 바로 JDK가 제공하는 표준 라이브러리이다.

표준 라이브러리는 Java 9부터 모듈화가 되어 다음 그림처럼 Java 17 표준 모듈이 완성되었다. 화살표는 모듈간의 의존 관계를 표시한다.

지면상 일부만 담았으며, Java 17의 전체 모듈 그래프는 다음 URL에서 자세히 볼 수 있다.
https://docs.oracle.com/en/java/javase/17/docs/api/java.se/module-summary.html
그림에서 보듯이 java.base는 모든 모듈이 의존하는 기본 모듈이다. java.base 모듈은 requires하지 않아도 사용할 수 있지만, 다른 모듈들은 모듈 기술자에 requires를 명시하고 사용해야 한다.

java.base 모듈에는 java.lang, java.util, java.io 등의 핵심 패키지가 있으며, java.lang을 제외하고 import해서 사용할 수 있다.

왜 java.lang은 예외일까?
java.lang 패키지 내 클래스들은 다른 패키지들보다 사용 빈도가 높기 때문에 모든 소스 파일에서 암시적으로(implicitly) 다음과 같은 import 문이 선언되어 있기 때문

import java.lang.*;

java.se는 JDK가 제공하는 모든 모듈을 제공하는 집합 모듈이다. Java 8 이전 버전과 같이 자바 표준 라이브러리를 제한 없이 사용하고 싶을 경우에는 이 java.se를 requires하면 된다.

module my_application {
    requries java.se;
}

또 다른 방법은 모듈 기술자가 없는 프로젝트를 만드는 것이다.
모듈 기술자가 없으면 모듈로 인식되지 않기 때문에 자바 표준 라이브러리를 제한 없이 사용할 수 있다.

Java 8 이전 버전까지는 응용 프로그램이 표준 라이브러리의 5%만 사용하는데도 불구하고 응용 프로그램을 실행하려면 전체 표준 라이브러리가 갖추어진 자바 실행 환경(JRE)이 필요했었다.

표준 라이브러리를 모듈화한 이유는 응용 프로그램을 실행하는데 필요한 모듈만으로 구성된 작은 사이즈의 자바 실행 환경(JRE - Java Runtime Environment)을 만들기 위해서이다.

작은 사이즈의 자바 환경이 필요한 경우는 다음과 같다.

  • 독립된 실행형(응용 프로그램 + 표준 라이브러리)으로 배포할 경우 표준 라이브러리 크기가 작을수록 배포 사이즈가 줄어든다.
  • 제한된 자원만 가지고 있는 소형(임베디드) 기기에는 사이즈가 작은 자바 실행 환경이 필요하다.

다음 그림을 보면 자바 표준 모듈은 모듈 A에서 모듈 D까지 제공하지만, 프로젝트를 실행하는 데는 모듈 A와 B만 있으면 된다.
따라서 모듈 C와 D를 제외하고 프로젝트만 실행할 수 있는 작은 실행 환경을 jlink 명령어로 생성할 수 있다.

profile
개발이 재밌어서 하는 Junior Backend Developer
post-custom-banner

0개의 댓글