Java 재활 훈련 7일차 - Library와 Module

0

java

목록 보기
7/18

라이브러리와 모듈

라이브러리는 프로그램 개발 시 활용할 수 있는 클래스와 인터페이스들을 모아놓은 것을 말한다. 일반적으로 JAR(java archive) 압축 파일인(~.jar) 형태로 존재한다. JAR 파일에는 클래스와 인터페이스의 바이트 코드(~.class)들이 압축되어있다.

   (~.jar)
---------------
|pack1        | ---import---> application1
|  |- A.class |
|pack2        | ---import---> application2
|  |- B.class |
---------------

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

  • java 명령어 실행할 때 -classpath로 제공
  • CLASSPATH 환경 변수에 경로를 추가
  • IDE를 활용

라이브러리 프로젝트 생성

my_lib 라이브러리 프로젝트를 생성하고, pack1, pack2 package를 생성하자. 그리고 다음의 코드를 넣어주도록 하자.

  • pack1/A.java
package pack1;

public class A {
    public void method() {
        System.out.println("A-method 실행");
    }
}
  • pack2/B.java
package pack2;

public class B {
    public void method() {
        System.out.println("B-method 실행");
    }
}

다음으로 jar파일 결과를 넣을 dist 디렉터리를 만들도록 하자.

src
  pack1
  - A.java
  pack2
  - B.java
dist

이렇게 구성된 것이다. jar를 만드는 방법은 다음과 같다.

  1. javac를 통해서 .java 파일들을 .class 파일로 컴파일하기
  2. jar 명령어를 통해서 jar 생성

intellij에서는 최상위 tab의 Build -> Build Project를 누르면 out 디렉터리에 .class 결과가 나온다.

javac로 complie하는 방법은 다음과 같다.

javac -d ./bin ./src/**/*.java

./src/**/*.javasrc에 있는 모든 패키지들에 .java파일들을 .class로 컴파일하겠다는 것이다. 그 결과는 ./bin 디렉터리에 넣도록하겠다는 것이다.

이제 jar를 통해서 이 .class파일을 jar로 만들어보도록 하자.

jar cvf ./dist/my_lib.jar -C ./bin/ .
  • c: jar파일을 생성
  • v: log 출력
  • f: 파일 이름을 지정

-C ./bin/ .bin에 있는 모든 .class 파일들을 묶어서 jar로 만들겠다는 것이다.

결과가 다음과 같이 /dist/my_lib.jar로 나올 것이다.

이제, 새로운 project를 만들고 my_lib.jar를 적용해보도록 하자. 이를 적용하기 위해서, intellij에서는 다음과 같이 설정할 수 있다.

새로운 project에 lib 디렉터리를 만들고 my_lib.jar를 넣도록 하자.

src
  - app.Main.java
lib
  - my_lib.jar

File -> Project Structure -> 왼쪽의 Modules -> + 버튼 -> JARs or Dictionaries -> my_lib.jar 선택

잘 설정되었다면 intellij에서 해당 jar를 읽고 package들을 불러올 수 있다.

  • app.Main.java
import pack1.A;
import pack2.B;

public class app.Main {
    public static void main(String[] args) {
        A a = new A();
        a.method();

        B b = new B();
        b.method();
    }
}

이제 실행시켜보도록 하자.

java -classpath ./lib/my_lib.jar:. ./src/app.Main.java

-classpath로 현재 my_lib.jar 파일을 포함시키도록 하자. 또한, ./bin으로 우리의 app.Main.class의 경로도 포함시켜야한다. 이때, 경로의 구분자는 :으로 my_lib.jar./bin 경로를 :로 구분한 것이다. app.Mainapp.Main.class이다.

결과는 다음과 같다.

A-method 실행
B-method 실행

우리의 my_lib.jar 라이브러리가 잘 적용된 것을 볼 수 있다.

모듈

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

----A module----
| package1---------사용----->
| package2---------사용----->
| package3(은닉)|
----------------

또 다른 차이는 모듈은 자신이 실행할 때 필요로 하는 의존 모듈을 module-info.java에 기술할 수 있기 대문에 모듈 간의 의존 관계를 쉽게 파악할 수 있다는 것이다. 다음은 모듈 A가 모듈 B에 의존하여 실행할 수 있고, 모듈 B는 모듈 C에 의존하여 실행할 수 있는 모습을 보여준다.

Module A ---> Module B ---> Module C

모듈도 라이브러리이므로 jar파일 형태로 배포할 수 있다. 대규모 응용 프로그램은 기능별 모듈화를 해서 개발할 수도 있다.

      ------------------
      |my_application_2|
      ------------------
      |                |
---my_module_a---   ---my_module_b---
|   package     |   |   package     |
-----------------   -----------------

다음의 형식으로 만들어보도록 하자.

새로운 프로젝트인 my_module_a을 만들어, 모듈화를 해보도록 하자.

  • my_module_a
src
  pack1
    - A.java
  pack2
    - B.java
  - module-info.java

다음의 코드를 입력하도록 하자.

  • A.java
package pack1;

public class A {
    public void method() {
        System.out.println("A-method 실행");
    }
}
  • B.java
package pack2;

public class B {
    public void method() {
        System.out.println("B-method 실행");
    }
}
  • module-info.java
module my.module.a {
    exports pack1;
    exports pack2;
}

pack1pack2는 외부에서 사용할 때 import를 허용하는 것이다.

my_module_a에 가서 다음의 명령어를 입력하여 liba.jar를 만들도록 하자.

javac -d ./bin ./src/module-info.java ./src/**/*.java
jar cvf ./dist/liba.jar -C ./bin/ .

distliba.jar가 만들어졌을 것이다.

새로운 프로젝트인 my_module_b을 만들어, 모듈화를 해보도록 하자.

  • my_module_b
src
  pack3
    - C.java
  pack4
    - D.java
  - module-info.java
  • C.java
package pack3;

public class C {
    public void method() {
        System.out.println("C-method 실행");
    }
}
  • D.java
package pack4;

public class D {
    public void method() {
        System.out.println("D-method 실행");
    }
}
  • module-info.java
module my.module.b {
    exports pack3;
}

pack3은 외부에 노출하지만, pack4은 외부에 노출하지 않도록 하는 것이다.

my_module_b에 가서 다음의 명령어를 입력하여 liba.jar를 만들도록 하자.

javac -d ./bin ./src/module-info.java ./src/**/*.java
jar cvf ./dist/libb.jar -C ./bin/ .

distlibb.jar가 만들어졌을 것이다.

새로운 project를 만들어서 libliba.jarlibb.jar를 넣주도록 하자.

lib
  - liba.jar
  - libb.jar
src
  app
    - Main.java
  - module-info.java

src에 app package를 만들고, Main.java를 쓰는 것이다. module system을 쓰기 위해서는 application code도 module을 지정해야하기 때문에 모듈명을 쓰는 것이다.

이제 코드를 넣어보도록 하자.

  • app/Main.java
package app;

import pack1.A;
import pack2.B;
import pack3.C;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.method();

        B b = new B();
        b.method();

        C c = new C();
        c.method();

        // 불가 D d = new D();
    }
}

pack4는 exports하지 않았으므로 D는 불러오지 못한다.

  • module-info.java
module JavaGrammer {
    requires my.module.a;
    requires my.module.b;
}

실행해보도록 하자.

java --module-path ./lib:. ./src/app/Main.java 
A-method 실행
B-method 실행
C-method 실행

module은 java로 실행할 때 --module-path를 써야한다.

전이 의존

다음과 같이 모듈의 의존관계가 형성될 수 있다.

app ---> my_module_a --> my_module_b

이런 경우, appmy_module_a을 사용할 때 my_module_b도 필요하다는 것이다.

가령, appmodule-info.java가 아래와 같다고 하자.

  • module-info.java
module JavaGrammer {
    requires my.module.a;
}

그리고 my_module_amodule-info.java는 아래와 같다고 하자.

  • module-info.java
module my.module.a {
    exports pack1;
    requires my_module_b;
}

이러한 경우 appmy_module_a만 의존했지만, my_module_b도 의존해야하는 상황이 나온다. 만약, my_module_b를 의존해주지 않으면 에러가 발생하게 되는 것이다. 이러한 문제를 해결하기 위해서, java에서는 '전이 의존'이라는 것을 만들었는데, my_module_amy_module_b를 의존하므로 my_module_a를 의존하는 모듈은 my_module_b도 자동으로 가지도록 하는 것이다. 이를 다음과 같이 쓸 수 있다.

  • module-info.java
module my.module.a {
    exports pack1;
    requires transitive my_module_b;
}

의존관계가 다음과 같이 된다.

 ----------------------------
 |                          |
 |                          v
app ---> my_module_a --> my_module_b

transitive 덕분에 my_module_a를 의존하는 시스템도 자동으로 my_module_b도 의존할 수 있게 된 것이다.

리플렉션 허용

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

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

  • 모듈 전체를 리플렉션 허용
open module 모듈명 {
    ...
}
  • 지정된 패키지에 대해 리플렉션 허용
module 모듈명 {
    ...
    opens package1;
    opens package2;
}

exports된 패키지는 언제나 리플렉션이 불가능하므로 opens를 지정해야 리플렉션이 가능하다.

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

외부모듈은 해당 패키지를 리플렉션으로 사용하고 싶을 client를 말한다. 즉, 특정 client에게만 리플레션을 open하겠다는 것이다. 특정 모듈에 제한하여 사용하는 것이 안전한 사용 방법이다.

0개의 댓글