대부분의 Java 개발자에게 JAR 파일과 그 특별한 사촌인 WAR 및 EAR은 단순히 장기적인 Ant 또는 Maven 프로세스의 최종 결과에 불과하다. 일반적으로 JAR을 서버(또는 드물게 사용자의 시스템)의 올바른 위치에 복사한 다음 잊어버린다.
실제로 JAR은 소스 코드를 저장하는 것 이상의 작업을 수행할 수 있지만 사용자가 가능한 작업과 작업 요청 방법을 알아야 한다. 5가지 사항 시리즈의 이 기사에서 제공하는 팁에서는 Java Archive 파일(및 WAR와 EAR)을 특히, 배치 시간에 최대한으로 활용하는 방법에 대해 설명한다.
많은 Java 개발자가 Spring을 사용하고 있고 Spring 프레임워크에서 일반적인 JAR 사용 방법이 문제가 되는 경우가 있기 때문에 일부 팁에서는 특별히 Spring 애플리케이션에서 JAR을 사용하는 방법에 대해 설명한다.
먼저 이후 팁에서 기본적으로 사용할 표준 Java Archive 파일 프로시저를 보여 주는 간단한 예제를 살펴보자.
일반적으로 코드 소스를 컴파일한 후 jar 명령행 유틸리티 또는 더 일반적으로
Ant jar 태스크를 통해 Java 코드(패키지별로 구분됨)를 단일 콜렉션으로 수집하여
JAR 파일을 빌드한다. 이 프로세스는 매우 간단하기 때문에 여기에서는 다루지 않겠지만 이 기사의 뒷부분에서
JAR의 생성 방법에 대한 주제를 다시 살펴볼 것이다. 지금은 단지 메시지를 콘솔에 인쇄하는 매우 유용한 태스크를
수행하는 독립형 콘솔 유틸리티인 Hello를 아카이브해야 한다(Listing 1 참조).
Listing 1. 콘솔 유틸리티 아카이브하기
package com.tedneward.jars;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Howdy!");
}
}
|
Hello 유틸리티는 거창하지는 않지만 코드를 실행하여 JAR 파일을 탐색하기에 유용한 골격이다.
.NET, C++ 등의 언어는 역사적으로 OS 친화적인 장점을 가지고 있다. 즉, 명령행에서 단순히 해당 이름을
참조하거나(helloWorld.exe) GUI 쉘에서 해당 아이콘을 두 번 클릭하여 애플리케이션을
실행할 수 있다. 하지만 Java 프로그래밍에서는 실행 애플리케이션인 java가 JVM을
프로세스로 부트스트랩하고, 사용자가 실행할 main() 메소드가 있는 클래스를 나타내는
명령행 인수(com.tedneward.Hello)를 전달해야 한다.
이러한 추가 단계로 인해 Java에서는 사용자 친화적인 애플리케이션을 작성하기가 더 어렵다. 일반 사용자가 이러한 모든 요소를 명령행에 입력해야 한다. 하지만 이 작업은 많은 일반 사용자가 원하지 않는 것이다. 게다가 키보드를 잘못 눌러서 모호한 오류가 발생할 수도 있다.
이 문제를 해결하는 방법은 JAR 파일을 "실행 파일"로 만드는 것이다. 즉, Java 실행 프로그램이 JAR 파일을 실행할
때 시작할 클래스를 자동으로 인식할 수 있도록 만들면 된다. 이를 위해서는 다음과 같이 JAR 파일의 매니페스트(JAR의 META-INF 서브디렉토리에 있는 MANIFEST.MF)에 항목을 추가하면
된다.
Listing 2. 시작점 알려주기
Main-Class: com.tedneward.jars.Hello |
매니페스트는 이름/값 쌍으로 구성된 세트이다. 매니페스트에서는 캐리지 리턴과 공백으로 인해 문제가 발생할 수도 있기
때문에 JAR을 빌드할 때 Ant를 사용하여 매니페스트를 생성하는 방법이 가장 쉬운 방법이다. Listing 3에서는 Ant jar
태스크의 manifest 요소를 사용하여 매니페스트를 지정한다.
Listing 3. 시작점 빌드하기
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
</manifest>
</jar>
</target>
|
이제 사용자는 java -jar outapp.jar을 통해 명령행에서 파일 이름을 지정하는 것만으로도
JAR 파일을 실행할 수 있다. 일부 GUI 쉘에서는 JAR 파일을 두 번 클릭하여 실행할 수도 있다.
Hello 유틸리티의 단어가 너무 많이 사용되고 있기 때문에 구현을
변경할 필요성이 제기되고 있다. Spring 또는 Guice와 같은 DI(Dependency Injection) 컨테이너가
많은 세부 사항을 자동으로 처리하기는 하지만 DI 컨테이너의 사용을 포함하도록 코드를 수정할 경우
Listing 4와 같은 결과가 발생할 수 있다는 문제점이 아직도 남아 있다.
Listing 4. Hello, Spring world!
package com.tedneward.jars;
import org.springframework.context.*;
import org.springframework.context.support.*;
public class Hello
{
public static void main(String[] args)
{
ApplicationContext appContext =
new FileSystemXmlApplicationContext("./app.xml");
ISpeak speaker = (ISpeak) appContext.getBean("speaker");
System.out.println(speaker.sayHello());
}
}
|
실행 프로그램에 대한 -jar 옵션은 -classpath
명령행 옵션의 내용을 겹쳐쓰기 때문에 이 코드를 실행할 때 Spring이 CLASSPATH
및 환경 변수에 있어야 한다. 다행스럽게도 JAR에서는 매니페스트에 표시할 다른 JAR 종속성을
선언할 수 있다. 이렇게 하면 사용자가 선언하지 않아도 CLASSPATH가 암묵적으로 작성된다(Listing 5 참조).
Listing 5. Hello, Spring CLASSPATH!
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
<attribute name="Class-Path"
value="./lib/org.springframework.context-3.0.1.RELEASE-A.jar
./lib/org.springframework.core-3.0.1.RELEASE-A.jar
./lib/org.springframework.asm-3.0.1.RELEASE-A.jar
./lib/org.springframework.beans-3.0.1.RELEASE-A.jar
./lib/org.springframework.expression-3.0.1.RELEASE-A.jar
./lib/commons-logging-1.0.4.jar" />
</manifest>
</jar>
</target>
|
Class-Path 속성에는 애플리케이션에서 사용하는 JAR 파일에 대한
상대 참조가 있다. 이 참조는 절대 참조로 작성하거나 접두부 없이 작성할 수 있으며, 이 경우 JAR
파일은 애플리케이션 JAR과 같은 디렉토리에 있는 것으로 간주된다.
아쉽게도 Ant Class-Path 속성에 대한 value
속성은 한 행으로 표시되어야 한다. 왜냐하면 JAR 매니페스트가 여러 Class-Path
속성을 처리하지 못하기 때문이다. 따라서 이러한 모든 종속성은 매니페스트 파일에 한 행으로 표시되어야
한다. 물론 보기에 좋지는 않지만 java -jar outapp.jar을 사용할 수 있기에
그 가치는 충분하다고 할 것이다.
Spring 프레임워크를 사용하는 여러 다양한 명령행 유틸리티(또는 기타 애플리케이션)가 있다면 모든
유틸리티에서 참조할 수 있는 공통 위치에 Spring JAR 파일을 저장하여 효율을 높일 수 있다. 이렇게
하면 전체 파일 시스템 내에서 JAR 사본을 여러 개 유지하지 않아도 된다. Java 런타임에서 JAR에 대한
공통 위치로 사용하는 "확장 디렉토리"는 기본적으로 설치된 JRE 경로 아래의 lib/ext
서브디렉토리에 있다.
JRE는 사용자 정의할 수 있는 위치이지만 지정된 Java 환경 내에서 사용자 정의되는 경우가 거의 없기
때문에 lib/ext가 JAR을 저장하기에 안전한 위치이고 Java 환경의 CLASSPATH에서
JAR을 암묵적으로 사용할 수 있다고 생각해도 무방하다.
4. Java 6에서는 클래스 경로 와일드카드를 사용할 수 있다.
CLASSPATH 환경 변수(몇 년 전에 Java 개발자가 남겨 두어야 했던)와 명령행
-classpath 매개변수가 많아지는 것을 피하기 위해 Java 6에서는 클래스 경로
와일드카드라는 개념이 도입되었다. 클래스 경로 와일드카드를 사용하면 인수에 명시적으로 나열된 모든
JAR 파일을 하나하나 실행할 필요 없이 lib/*와 해당 디렉토리에 나열된 모든 JAR
파일(재귀적이지 않음)을 클래스 경로에 지정할 수 있다.
아쉽게도 클래스 경로 와일드카드는 앞에서 설명한 Class-Path 속성 매니페스트 항목에
적용되지 않는다. 하지만 코드 생성 도구 또는 분석 도구와 같은 개발자 태스크를 위한 Java 애플리케이션(서버 포함)을
쉽게 실행할 수 있다.
Java 에코시스템의 많은 부분과 마찬가지로 Spring도 환경의 설정 방법을 설명하는 구성 파일을 사용한다. 즉, Spring에서는 JAR 파일과 같은 디렉토리에 있는 app.xml 파일을 사용한다. 하지만 대부분의 개발자는 구성 파일을 JAR 파일과 함께 복사하는 것을 잊어버린다.
일부 구성 파일은 시스템 관리자가 편집할 수 있지만 하이버네이트 맵핑과 같은 상당 수의 구성 파일은 시스템 관리자
도메인의 외부에 있기 때문에 배치 작업 중에 발생하는 문제의 원인이 되고 있다. 합리적인 해결 방법은 구성 파일과
코드를 함께 패키지하는 것이다. 이는 JAR이 기본적으로 외형 상 ZIP 형식으로 되어 있기 때문에 가능한 방법이다. JAR을
빌드할 때 구성 파일을 Ant 태스크나 jar 명령행에 포함시키면 된다.
JAR은 구성 파일뿐만 아니라 다른 유형의 파일도 포함할 수 있다. 예를 들어, 필자는 특성 파일에 액세스할 필요가
있는 SpeakEnglish 컴포넌트를 Listing 6과 같이 설정했다.
Listing 6. 임의로 응답하기
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
Random random = new Random();
public String sayHello()
{
// Pick a response at random
int which = random.nextInt(5);
return responses.getProperty("response." + which);
}
} |
responses.properties를 JAR 파일에 넣는다는 것은
JAR 파일과 함께 배치할 파일이 하나 이상 있다는 것을 의미한다. 이 작업을 수행하려면
JAR 단계 동안 responses.properties 파일을 포함시켜야 한다.
특성을 JAR에 저장한 후 특성을 다시 가져오는 방법이 궁금할 수도 있을 것이다. 원하는
데이터가 동일한 JAR 파일 내에 함께 있는 경우에는 이전 예제에서 보았듯이 JAR 파일의 위치를
확인한 후 JarFile 오브젝트를 사용하여 JAR 파일을 열려고 시도하지
않아도 된다. 대신 ClassLoader getResourceAsStream() 메소드를
사용하여 클래스의 ClassLoader를 통해 JAR 파일 내에서 특성을 "자원"으로
찾는다(Listing 7 참조).
Listing 7. ClassLoader가 자원을 찾는다.
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
// ...
public SpeakEnglish()
{
try
{
ClassLoader myCL = SpeakEnglish.class.getClassLoader();
responses.load(
myCL.getResourceAsStream(
"com/tedneward/jars/responses.properties"));
}
catch (Exception x)
{
x.printStackTrace();
}
}
// ...
}
|
구성 파일, 오디오 파일, 그래픽 파일 등을 포함한 모든 유형의 자원에 대해 이 프로시저를
수행할 수 있다. 실제로 모든 파일 유형을 JAR로 묶고, InputStream으로 가져온(ClassLoader를
통해) 다음 원하는 방식으로 사용할 수 있다.
이 기사에서는 적어도 역사와 필자의 경험을 바탕으로 대부분의 Java 개발자가 JAR에 대해 모르고
있던 5가지 주요 사항을 살펴보았다. 이러한 JAR 관련 팁은 모두 WAR에도 동일하게 적용된다. WAR의
경우에는 서블릿 환경이 디렉토리의 전체 내용을 선택하고 사전 정의된 시작점이 있기 때문에 일부
팁(특히, Class-Path 및 Main-Class 속성)의
중요성이 낮을 수 있다. 하지만 전체적인 관점에서 이러한 팁은 "좋아, 이 디렉토리의 모든 항목을
복사하는 작업부터 시작하자..."라는 패러다임을 극복할 수 있도록 도와 준다. 결과적으로 Java 애플리케이션을
훨씬 더 간단하게 배치할 수도 있다.
이 시리즈의 다음 기사에서는 Java 애플리케이션의 성능 모니터링에 대해 모르고 있던 5가지 사항에 대해 살펴본다.
| 설명 | 이름 | 크기 | 다운로드 방식 |
|---|---|---|---|
| Sample code for this article | j-5things6-src.zip | 10KB | HTTP |
교육
- 5
things you didn't know about ...(Ted Neward 저, developerworks): 이 새 시리즈는 Java의 사소한 기능에 큰 가치를 부여한다.
- "JAR 파일"(Pagadala
Suresh 및 Palaniyappan Thiagarajan 저, developerWorks, 2003년 10월): 패키징, 실행 가능한 JAR 파일, 보안 및 색인을 포함한 Java Archive
형식의 특징과 장점에 대해 소개한다.
- Packaging programs
in JAR files(The Java Tutorials trail): JAR에 대한 기본적인 정보를 볼 수 있다.
- Spring: 소스를 통해 직접 이 강력하고 유연하며 유명한
프레임워크에 대한 정보를 볼 수 있다.
- "Dependency
injection with Guice"(Nicholas Lesiecki 저, developerworks, 2008년 12월): DI는 관리성, 테스트 가능성 및 유연성을
높여 주며, Guice는 DI를 쉽게 사용할 수 있도록 지원한다.
- "Setting
multiple jars in java classpath"(StackOverflow Q&A, 마지막 갱신 날짜: 2010년 3월): 클래스 경로 와일드카드에 대한 자세한 정보를 볼 수 있다.
- "See JARs
run"(Shawn Silverman 저, JavaWorld.com, 2002년 5월): 이 튜토리얼에서는 실행 가능한 JAR 파일을 작성하는 방법에 대해 자세히 설명한다.
- "Advanced
topics in programming languages series: JSR 277: Java Module System"(Google Tech Talks, 2007년 5월): JAR에는 클래스
경로, JAR 파일, 확장 등과 관련된 단점이 있다. 이 Google Tech Talk에서는 JSR 277이 이러한 단점을 해결하는 방법에 대해 설명한다.
- developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
토론
- My developerWorks 커뮤니티에 참여하자.