top bar

글 목록

2015년 11월 22일 일요일

[JAVA] Executable(or Runnable) JAR 파일 생성

개요


JAR(Java Archive, 자바 아카이브)는 소프트웨어에서 수많은 자바 클래스 파일과 연관 메타데이터, 리소스(텍스트, 그림 등)을 하나의 파일로 모아서 자바 플랫폼에 응용 어플리케이션이나 라이브러리를 배포하기 위한 패키지 파일 포맷이다.(출처: 위키 백과)

하지만 'main' 메소드를 구동하여 하나의 자바 어플리케이션 으로서 동작하게 하려면 특별한 설정이 필요하다. 이제 자바 'Manifest' 파일을 통해 'Runnable JAR' 파일을 생성하는 법을 알아 보고자 한다.


JAR 파일 생성


먼저 아래와 같은 기본적인 'HelloWorld' 클래스를 컴파일하고, 'jar' 파일을 생성해보자.
package test;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}
컴파일한다
> javac -d . HelloWorld.java
시험삼아 구동해보자
> java test.HelloWorld
Hello world!
이제 jar 파일을 생성해본다. 이는 jdk bin 폴더 아래에 있는 'jar' 실행파일을 사용한다.
환경변수를 설정했다면 어디서든 'jar' 명령어를 사용 할 수 있다.
> jar -cf helloworld.jar test\*.class
참고로 아무것도 없이 'jar' 명령어만 치면 usage 목록이 쫙 나오고 각 옵션에 대한 설명도 나온다. 일단 위에서는 'c'와 'f' 옵션을 사용했는데, 직관적으로 보이겠지만 'c'는 새로운 아카이브 파일을 생성하는 것이고 'f'는 파일의 이름을 지정하는 것이다.

'-cf' 옵션뒤에 첫번째 파라미터는 jar파일의 이름, 둘째는 아카이빙될 .class 파일을 입력하는데, 여기서는 와일드카드(*)를 사용하였다.

이제 생성된 jar파일을 구동해볼까나?
> java -jar helloworld.jar
helloworld.jar에 기본 Manifest 속성이 없습니다.
이것이 필자가 이 글을 쓰고 있는 이유이다. 실무에서도 실행가능한 jar파일을 생성하지 못하여 위와 같은 메시지를 자주 접했었다. 이제 JAVA Manifest 파일을 통해서 실행가능한 jar파일을 생성해 보도록 하자.


JAVA Manifest 파일


JAVA Manifest 파일은 해당 자바 어플리케이션의 정보 즉, 일종의 메타정보를 담고 있는 파일이다. 아래에 가면 아주 자세한 Tutorial을 볼 수 있다.

https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html

일단 jar 명령어로 jar파일을 생성할때, META-INF/MANIFEST.MF 파일이 자동으로 jar 패키지에 포함된다. 일단 위에서 생성된 'helloworld.jar' 파일을 jar 명령어로 압축을 풀어보자.
> jar -xvf helloworld.jar
생성됨: META-INF/
증가됨: META-INF/MANIFEST.MF
증가됨: test/HelloWorld.class
위에서 볼 수 있는 것처럼 MANIFEST.MF 파일이 포함되어 패키지가 구성되어 있다는 것을 알 수 있다. 자동적으로 생성되는 default MANIFEST.MF 파일의 내용은 아래와 같다.
> type META-INF\MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.7.0_67 (Oracle Corporation)
Manifest-Version과 파일을 생성한 주체가 표시 되어 있다. 위 예에서 볼수 있듯이 Manifest파일의 기본 포맷은 'key: value' 의 포맷이다.

자동적으로 Manifest 파일이 생성되어 패키지에 포함되어 있는데 왜 기본 Manifest 속성이 없습니다. 라는 오류 메시지가 노출된 것일까?


Entry Point 설정


기본적으로 JAVA 어플리케이션은 'main' 메소드로 구동되는건 알 것이다. 이를 'Entry Point' 라고도 하나보다. 이제 Manifest 파일에 'Entry Point'를 설정해보자.

아래와 같은 내용으로 'Manifest.txt'를 생성한다
Main-Class: test.HelloWorld
그냥 한줄만 입력했다. 말그대로 'main' 메소드를 구동시키는 'Main-Class'를 지정한 것이다. 간단하지 않은가? 이 Manifest.txt 파일을 이용해 다시한번 jar 파일을 생성해보자.
> jar -cfm helloworld.jar Manifest.txt test\*.class
최초 '-cf' 옵션만을 사용했었는데, 여기다 'm' 옵션이 추가되었다. 말그대로 Manifest 파일을 포함해 jar파일을 생성하겠다는 의미 이다.

이제 다시 'helloworld.jar'파일을 구동해보자.
> java -jar helloworld.jar
Hello world!
이제 잘 구동된다.

하지만 Manifest 파일을 이용하지 않고도 Main-Class 지정, 즉 Entry Point를 설정하는 더 간단한 방법이 있다.
> jar -cfe helloworld.jar test.HelloWorld test\*.class
바로 위처럼 'e' 옵션을 사용하는 것이다. '-cfe' 옵션뒤에 jar 파일 이름, Main클래스 경로, jar 아카이빙될 클래스파일 경로를 지정한다. 위와 같이 하면 Manifest 파일을 사용하지 않고 'main' 메소드를 구동할 Main클래스를 지정할 수 있다!


Eclipse에서 'Runnable JAR' 생성하기


1) Project 우클릭 > Export






















2) JAVA > Runnable JAR file






















3) Launch Configuration 및 Export destination 선택 후 Finish























이때, Launch configuration은 해당 main 클래스의 실행 이력이 있다면 자동으로
생성되어 있을 것이다. 만약 select box에 아무것도 안뜬다면 이클립스의 'Run configuration' 에서 새로운 실행 설정을 추가해야 한다.


마치며..


이 topic에 대해서 포스팅을 하려고 마음먹었던 이유는 저 위의 기본 Manifest 속성이 없습니다. 와 같은 빌어먹을 오류 때문이었다. 보통 jar 파일 패키지 산출은 eclipse나 maven과 같은 빌드 도구가 알아서 해주다 보니, 저런 오류를 만나면 어디서부터 문제를 해결해야 할지 답답했다.

정말 요즘은 IDE나 갖가지 개발업무를 도와주는 Tool들 덕분에 개발자들이 해야할 일이 많이 줄어든건 사실이지만, 자신이 하는 개발업무 자체에 대한 깊이는 그만큼 얕아진것 같다. 물론 여기서 다루지 않은 JAVA 'Manifest' 파일의 스펙은 무척 다양하다.

하지만 오늘 포스팅을 하면서, 기본적으로 JAVA 어플리케이션이 어떻게 Main 메소드가 있는 클래스를 찾아서 프로그램을 실행 시키는지 이해 할 수 있었던 것 같다.

2015년 11월 13일 금요일

[JAVA] 우선순위(PriorityQueue) 큐

1. 개요


일반적으로 Queue라는 자료구조는 '선입선출'(First-In, First-Out)의 대기열 규칙(queuing discipline)을 가지고 있다. 말그대로 먼저들어온 놈이 먼저 나간다는 것이다.

하지만 JAVA에서 제공하는 'PriorityQueue'는 우선순위를 결정하여 들어온 순서와 상관없이 그 우선순위가 높은 엘리먼트가 나가게 된다. 이제부터 간단한 예제를 통해 알아보겠다.


2. 예제


아래와 같은 Prisoner(죄수) 클래스가 있다.
class Prisoner {

    String name;
    int weight; // 형량

    public Prisoner(String name, int weight) {
        super();
        this.name = name;
        this.weight = weight;
    }
}
이 클래스는 'name' 와 'weight(형량)' 의 2가지 필드가 있다. 이 Prisoner 클래스를 PriorityQueue에 넣고, 형량에 따라 큐에서 나오게(출소하게) 하려고 한다.

물론 일반적으로, 형량이 낮으면 먼저 출소하는 것이 인지상정. 이제 이 Prisoner 클래스에 Comparable 인터페이스를 구현 해보자.
class Prisoner implements Comparable<Prisoner> {

    String name;
    int weight; // 형량

    public Prisoner(String name, int weight) {
        super();
        this.name = name;
        this.weight = weight;
    }

    @Override
    public int compareTo(Prisoner target) {
        if (this.weight > target.weight) {
            return 1;
        } else if (this.weight < target.weight) {
            return -1;
        }
        return 0;
    }
}
위에 'Comparable' 인터페이스를 구현한 Prisoner 클래스에 있다. 형량이 낮은 Prisoner 객체를 먼저 꺼내기 위해 compareTo 메소드를 오름차순으로 정렬 되도록 구현하였다.

* PriorityQueue 사용

먼저 테스트 클래스에 'getPriorityQueue' 메소드를 구현한다. 이 메소드는 5개의 'Prisoner' 객체를 생성하여 'PriorityQueue'에 넣고 해당 'PriorityQueue' 객체를 반환한다.
private static PriorityQueue<Prisoner> getPriorityQueue() {

    Prisoner prisoner1 = new Prisoner("james", 5);
    Prisoner prisoner2 = new Prisoner("schofield", 99);
    Prisoner prisoner3 = new Prisoner("alex", 14);
    Prisoner prisoner4 = new Prisoner("silvia", 10);
    Prisoner prisoner5 = new Prisoner("thomson", 1);

    PriorityQueue<Prisoner> priorityQueue = new PriorityQueue<Prisoner>();

    priorityQueue.offer(prisoner1);
    priorityQueue.offer(prisoner2);
    priorityQueue.offer(prisoner3);
    priorityQueue.offer(prisoner4);
    priorityQueue.offer(prisoner5);
    
    return priorityQueue;
}
5명의 죄수 객체를 생성하여 우선순위 큐에 넣었다. 이 죄수 객체들은 각각 형량이 다르다.

만약 'Prisoner' 클래스에 Comparable 인터페이스를 구현하지 않은채 PriorityQueue에 집어넣으면 어떻게 될까? 우선순위 큐의 'offer'는 큐 한쪽 끝에 엘리먼트를 저장하는데, 이때 '다형성'을 이용하여 추가 되는 엘리먼트 객체를 'Comparable' 인터페이스로 'Up Casting' 한다. 하지만 'Comparable' 인터페이스를 구현한 객체가 아니라면 당연히 아래와 같은 에러메시지가 노출 될 것이다.
Exception in thread "main" java.lang.ClassCastException: Prisoner cannot be cast to java.lang.Comparable

이제 main 메소드를 구현하여 돌려보자.
public static void main(String[] args) {

        PriorityQueue<Prisoner> priorityQueue = getPriorityQueue();

        System.out.println("=============== Normal Order");

        while (!priorityQueue.isEmpty()) {
            Prisoner prisoner = priorityQueue.poll();
            System.out.println(prisoner.name);
        }
}
결과 >
=============== Normal Order
thomson
james
silvia
alex
schofield
형량이 낮은 'thomson' 이라는 녀석부터 출소한다. 형량이 '99년'인 schofield는 제일 나중에 나올 것이다.

이것이 우선순위 큐(PriorityQueue) 이다.

* Reversed Order

갑자기 교도소의 정책이 바뀌었다. 교도소장 생일을 맞이하여(.. 억지스럽더라고 걍 넘어가자) 형량이 가장 높은 죄수부터 차례로 출소하게 된것이다. 이럴때는 어떻게 해야할까? 'compareTo' 메소드를 오름차순에서 내림차순으로 바꿔서 구현해야 할까?

기존의 코드를 그대로 두고 Order를 뒤집는 방법이 있다. 아래와 같다.
public static void main(String[] args) {

        PriorityQueue<Prisoner> priorityQueue = getPriorityQueue();

        PriorityQueue<Prisoner> reversedPriorityQueue = 
         new PriorityQueue<Prisoner>(priorityQueue.size(), Collections.reverseOrder());
        reversedPriorityQueue.addAll(priorityQueue);

        System.out.println("=============== Reversed Order!");

        while (!reversedPriorityQueue.isEmpty()) {
            Prisoner prisoner = reversedPriorityQueue.poll();
            System.out.println(prisoner.name);
        }
}
결과 >
=============== Reversed Order!
schofield
alex
silvia
james
thomson
'Collections.reverseOrder()' 메소드를 살펴보면 알겠지만, 두 Camparable 객체를 비교하는 순서를 뒤집은 'Comparator' 객체를 리턴한다. 이로서 'reversedPriorityQueue' 컨테이너가 생성이 되고, 바로 위에 생성한 오름차순의 'priorityQueue' 컨테이너를 'addAll' 하게되면 'reversedPriorityQqueue'의 우선순위 정책으로 인해서 컨테이너 안의 엘리먼트의 우선순위가 뒤바뀐다.

그래서 위와 같은 결과를 도출 하는 것이다.

2015년 11월 11일 수요일

[MAVEN] Sonatype Nexus + Maven + Jenkins 배포환경 구성

1. 개요


 유지보수 업무를 하는 중, 여러 컴포넌트들이 공통적으로 쓰는 core 클래스들을 라이브러리로 묶어, 사내 maven repository에 등록하여 사용할 필요성을 느꼈다. 하지만 사내 repository에 jar를 등록하는 방법은 아이디를 발급받아야 하는 등 절차가 어려웠다.. 기보다는 귀찮았다. 그래서 직접 'sonatype nexus' 를 활용하여 maven repository를 구축해보기로 하였다.

Let the working begin~


2. Sonatype Nexus 설치


 이 작업은 소름돋을 정도로 쉽다. 예전엔 Nexus webapp을 다운받아, 따로 톰캣에 올려야했는데 요즘엔 jetty 기반의 WAS가 built-in 되어있는 컴포넌트로 릴리즈 되는것 같다.

아래 공식 웹사이트를 참고하자

http://www.sonatype.org/nexus/

참고로 nexus는 oss 와 pro 버전이 있는데, pro는 비용을 지불해야 하는 상용 버전이다. (pro도 기능에 따라 종류가 세분화 되는 듯 하다) 하지만 구지 pro 를 사용할 필요없이 oss 버전도 기본적인 repository management를 제공하기때문에 여기서는 oss 버전으로 진행한다.

1) download
$ wget --no-check-certificate https://sonatype-download.global.ssl.fastly.net/nexus/oss/nexus-2.11.1-01-bundle.tar.gz
wget으로 먼저 'tar' 파일을 다운받는다. 'https' 프로토콜 이기 때문에 '--no-check-certificate' 옵션을 잊지 말자

2) 압축해제
$ tar -xvzf nexus-2.11.1-01-bundle.tar.gz

3) 구동
bin]$ ./nexus start
Starting Nexus OSS...
Started Nexus OSS.

4) 접속

기본포트는 '8081'이며, /conf/nexus.properties 파일에서 변경할 수 있다
아래처럼 http://[server ip]:8081/nexus 로 접속한다

























5) 둘러보기

우측 상단에 'Log In' 버튼을 눌러 로그인할 수 있는데, 기본적으로 관리자 계정으로 'admin' (비번 admin123) 을 제공한다.

























왼쪽 메뉴의 'Repositories' 를 클릭하면 각종 저장소가 보인다.

저장소에는 3가지 유형이 있는데, 각각 아래와 같다.


  • 프록시 저장소 (proxy) : 프록시 저장소는 외부에 있는 메이븐 공개 저장소에 대한 프록시 역할을 하는 저장소이다.  위 사진에서는 'Central' 이라는 이름으로 메이븐 중앙 저장소가 이미 추가가 되어있으며, 대략적인 개념은 아래와 같다. 말그대로 '대리자'인 셈이다.








  • 호스티드 저장소(hosted) : 필자가 이 삽질을 하고 있는 이유다. 말그대로 사내에서 사용하는 라이브러리 관리 또는 3rd Party 라이브러리를 관리하기 위한 용도이다. 보면 알겠지만 기본적으로 'Release' , 'Snapshots' , '3rd party' 라는 저장소가 이미 추가되어있다.
  • 버추얼 저장소(virtual) : 넥서스에 이미 설정되어 있는 저장소에 대하여 다른 URL로 접근 할 수 있도록 지원하기 위한 논리적인 저장소이다.
  • 저장소 그룹(group) : 넥서스에 설정한 저장소의 그룹이다. 프로젝트가 진행되면서 의존 관계에 있는 라이브러리가 증가하면서 외부 저장소도 증가하는데, 이 저장소 그룹에다 추가되는 외부 저장소를 추가하면 메이븐 설정파일 변경 없이 의존 관계를 확장할 수 있다.

3. Maven 설정


이 작업 또한 매우 간단하다

1) .m2/settings.xml 파일 변경

아래처럼 <servers> 설정에 nexus 사용자 아이디와 비번을 설정한다.
      .
      .
      .
      <server>
            <id>releases</id>
            <username>admin</username>
            <password>admin123</password>
      </server>
</servers>

2) project pom.xml 파일 설정 추가
<distributionManagement>
        <repository>
            <id>releases</id>
            <url>http://[server ip]:8081/nexus/content/repositories/releases</url>
        </repository>
</distributionManagement>

이때 주의해야 할점은 settings.xml 파일에 설정한 <server><id> 와 pom.xml 에 설정한 <repository><id> 값이 일치해야 한다는 점이다.


4. Jenkins 설정 및 Nexus로 배포하기


1) Jenkins 설정

Jenkins에서 해당 프로젝트를 빌드할때 maven goal을 'deploy'로 설정한다. 아래와 같다.











'profile' 매개변수는 빌드 환경에따라 다른 저장소로 (release/alpha) 배포하기 위해 설정한다. 'deploy' 를 입력하면 deploy phase에 메이븐에 기본적으로 내장된 'maven-deploy-plugin' 이 수행되면서 <distributionManagement> 에 설정된 저장소로 배포하게 된다.

이때 profile 마다 <distributionManagement> 설정을 바꿔 다른 저장소로 배포하게 하면 개발 환경에 따라 저장소를 달리해서 구분하여 관리할 수 있다.

2) 배포 하기 

Jenkins Build를 수행한다. output console 로그를 보면 아래와 같이 해당 Nexus로 Uploaded 되었다는 내용이 출력 될 것이다.







3) Nexus에서 확인


해당 repository의 'Browse Index' 탭을 열어보면 방금 빌드한 라이브러리가 배포된 것을 볼 수 있다.

이것으로 완료 되었다!