5. 삭제된 기능의 마이그레이션

김리원·2021년 9월 10일
1

Java 17로의 전환

목록 보기
5/6

Java의 호환성 정책

지금까지는 Java17까지의 기능 변화 및 추가 기능에 대해서 설명했습니다. 이 번에는 Java의 호환성에 대한 설명 및 기존 애플리케이션을 Java17로 이행시 처리하는 방법에 대해서 설명합니다.

기본은 하위호환성 유지!

Java의 큰 특징중 하나는 호환성입니다. 지금까지 사용하던 API와 기능이 갑자기 사리지는 것은 기본적으로는 없습니다. 반드시 사전에 비추천된 내역을 문서로 명시합니다. 컴파일시에는 동작시 경고를 주는 것이 대부분입니다.
Java의 메이저 버전 업그레이드시 주의해야할 정보는 각 메이저 버전의 마이그레이션 가이드에 정리하고 있습니다. 출시 직후 영어로만 제공되는 것이 아쉽지만 어쩔 수 없는 상황입니다. 앞에서 설명된 모듈 시스템을 포함한 Java 8에서 전화된 기준에서 최소로 알아야할 것도 포함하여 정리하기 때문에 작업을 할 때 읽어두면 도움이 됩니다.

비추천화 및 삭제 타이밍

Java 기능 삭제는 기본적으로 우선 비추천된 후, 삭제되는 단계로 갑니다. 신규 개발은 물론, 최신 Java로 전환할 경우에도 사용되지 않는 기능은 사용하지 않도록 하고 사용하던 경우라면 대체기능으로 전환하도록 합시다. 이때 힌트를 얻을 수 있는 것이 Javadoc 입니다.
예를 들어, String::getBytes(int, int, byte[],int)은 비추천 메소드입니다. 이를 Javadoc에서 확인할 때 예로 "getBytes()"를 보면,@Deprecated 어노테이션이 선언되어 있기 때문입니다. 이로 인해 그 기능을 사용되지 않다는 것을 알 수 있습니다. 이것은 Javadoc뿐만 아니라, 다양한 IDE의 자동완성 기능에서도 사용됩니다. 문서 또는 IDE에서뿐만 아니라, 아래 결과1처럼 컴파일시 경고가 발생하기 떄문에 다양한 단계에서 비추천 기능을 사용하는 것을 확인할 수 있습니다. 이 결과1은 -Xlint:deprecation을 추가하여 비추천 기능에 대한 상세정보를 출력하고 있습니다. 평소에 -Xlint: all처럼 모든 경고를 사용하여 빌드되도록 하는경우도 많지만, 여기에서는 deprecate도 포함됩니다.
참고로, @Deprecated는 애플리케이션 코드에서도 이용이 되기 떄문에 독자가 개발하는 제품에 사용하다가 오래된 API를 명시할 때도 사용됩니다.

결과1

$ javac -Xlint:deprecation Test.java
Test.java:4: warning: [deprecation]
getBytes(int,int,byte[],int) in String has been deprecated
args[0].getBytes(0, 1, bytes, 0);
^
1 warning

JEP참조 - 대안검토

대책을 검토하는데 큰 참조가 되는 것이 바로 JEP입니다. 기능에 대한 비추천, 삭제배경이 기대죄어 있으며, 그 중 대안에 대해 다루고 있습니다.
예를 들어, 다수의 JAR파일을 압축한 패키징 pack200이라는 도구를 비추천하고 있는 JEP 336에서는 처리결과에 차이가 있지만, 하나로 패키지화하고 싶다, 배포파일 크기를 작게하고 싶다라는 목적은 jlink, japackage로 대응할 수 있다고 기록되어 있습니다.

삭제되는 대표적인 기능

기존 Java 애플리케이션에서 사용하고 있는 기능이 삭제된 경우에 어떻게 대처해야하는지 구체적인 예를 살펴봅시다.

JAX-WS - SOAP 시대의 대명사

첫번째 예는 JAX-WS(Java API for XML Web Services)입니다. JAX-WS는 XML을 사용한 RPC(Remote Procedure Call, 원격 프로시저 호출)인 SOAP(Simple Object Access Protocol)을 처리하기 위한 사양입니다. SOAP는 HTTP를 통해 외부시스템과 통신할 수 있는 쉽고 간단하면서 W3C(World Wide Web COnsortium)이 권고하였하였으며, Java뿐만 아니라 폭넓게 사용되고 있었습니다. 현재는 JSON기반 REST와 gRPC를 이용한 시스템간의 연계가 주류이지만, 라이프사이클이 긴것을 보면 SOAP가 아직 사용중인 경우가 있습니다.

Java SE API에서만 사용되는 JAX-WS 코드

JAX-WS기능은 Java 6부터 Java SE API에 포함되어 제공되어 왔습니다. 즉, Java를 다운로드하면 JAX-WS를 사용할 수 있었습니다. 그러나, JAX-WS는 원래는 Java EE에 포함되어 있었습니다. 따라서 앞에서 설명한 모듈화 과정에서 JAX-WS는 비추천처리되었고 Java EE의 모듈로 자리잡은 것이 JEP 320에서 삭제되었습니다.

클아이언트 코드를 Java17로 전환

이는 Java 8동작을 전제로 한 JAX-WS이용 코드를 Java 18로의 마이그레이션 예를 생각해 봅시다. JAX-WS는 애플리케이션서버, 프레임워크 및 라이브러리 제공기능으로 실현하는 경우도 많지만, 여기에서는 아래와 같이 Java SE API로만 작성된 간단한 클라이언트 코드를 고려해 봅시다.

CountriesPortService service = new
CountriesPortService();
CountriesPort port = service.getCountriesPortSoap11();
ObjectFactory factory = new ObjectFactory();

GetCountryRequest req = factory.
createGetCountryRequest();
req.setName("Spain");
GetCountryResponse res = port.getCountry(req);

Country country = res.getCountry();

이 코드가 통신하는 것은 Spring Guides의 SOAP web services입니다. 클래스명에 Country와 있지만 국가를 요청하면 금액과 통화단위등에 대한 정보를 리턴하는 웹서비스입니다. 이 웹서비스는 WSDL(Web Services Description Language)를 제공하기 때문에, Java 8은 여기에서 wsimport 명령 스텁코드를 자동생성할 수 있었지만, Java 17에서는 wsimport가 미제공되어 사용할 수 없습니다. 어떻게 할 것인자라고 한다면 JAX-WS도구 클래스를 직접 실행합니다.

wsimport같은 JAX-WS도구는 Maven은 JAX WS RI Tools에서 확인이 가능합니다. 여기에서는 Ant를 위한 작업(com.sun.tools.ws.ant.WsImport)가 준비되어 있어 이를 활용하는 방법이 있지만, Maven이라면 다음과 같이 정의하는 것이 (maven-antrun-plugin에 의존하지 않도록 하기 위함) 간단합니다.

<dependency>
	<groupId>com.sun.xml.ws</groupId>
	<artifactId>jaxws-tools</artifactId>
	<scope>compile</scope>
	(생략)
	<plugin>
		<groupId>org.codehaus.mojo</groupId>
		<artifactId>exec-maven-plugin</artifactId>
		<version>3.0.0</version>
		<executions>
			<execution>
				<id>wsimport</id>
				<phase>generate-sources</phase>
				<goals>
					<goal>exec</goal>
				</goals>
				<configuration>
					<executable>java</executable>
					<arguments>
						<argument>-classpath</argument>
							<classpath/>
							<argument>com.sun.tools.ws.WsImport</argument> (1)
// 이후, wsimport 인수 < argumemnt>에서 열거함

(1)이 지정하고 있는 것이 wsimport의 기본클래스입니다. 여기에서는 wsimport에 전달할 인수를 < argument>에서 열거하고 있습니다. wsimport 메인클래스를 직접 실행하게 된 경우 exec-maven-pligin등을 사용합니다. wsimport코드 생성에 해당하므로 단계는 generate-sources를 설정합니다.
내용 정리하는 것떄문에 pom.xml의 일부만 기재하고 있지만 전체 내용은 따로 다운로드하십시오.

가장 중요한 주의점 - Jakarta EE

생성된 스텁코드를 보면, Java 8의 wsimport에서 생성한 것은 javax로 시작하는 패키지 클래스를 import하고 있는 반면에 위의 것은 jakarta로 시작합니다. 이는 Java EE가 Jakarta EE로 명칭이 변경된 것에 기인합니다. 즉, 이전 wsimport에서 생성한 새로운 JakartaEE의 JAXWS를 사용할 수 없습니다. 자동생성된 코드를 다시 생성하지 않고 사용하는 것이 유용하지만, JAXWS에 대한 스텁코드를 다시 생성하는 것을 추천합니다.

CMS - 유일하게 멈추지 않는 GC

Java 개발자라면 많이 고민하는 것이 GC(Garbage Collection, 불필요한 메모리 회수처리)에서 멈추지 않는 GC의 CMS(동시 마크스윕)이 삭제되었습니다. CMS는 G1이 나올때까지 유일한 GC였기 떄문에, STW(Stop The World, 완전 정지시간)을 허용하지 않는 시스템 채용하는 경우가 있지만, JEP 363에서 완전히 삭제되었습니다.

왜 CMS GC가 삭제된 것인가?

JEP 363에 따라면 2가지 이유가 있습니다.

  • G1, ZGC, Shenandoah와 같은 CMS 대체 GC가 나왔습니다.
  • GC의 관리자 부담 경감하였습니다.

사실 NUMA(Non Uniform Memory Access)아키텍쳐에 대한 대응과 같은 기능 개선으로 CMS만 방치하는 경우도 있었습니다. CMS는 개체의 생존기간에 회수대상이 정해져 세대별 GC에서 Old영역(수명이 긴 개체가 존재하는 영역) 회수를 동시 실행합니다.
그러나 회수 후, 여유 공간이 잘리지 않고 버그 상태가 될 것으로 보입니다. 사용 메모리가 적은 것이나, 그 상태를 개선하는 유일한 방법이 풀GC가 다중 스레드가 되지 않는등의 구조적인 문제가 이전부터 지적되어 왔습니다.
CMS를 사용자가 선택하는 GC는 무엇이 있는지 생각해 봅시다. 또한 이전에 GC튜닝은 다양한 옵션을 조정할 수 있었지만, GC알고리즘과 JVM의 인간공학도 발전하고 있어서 기본적으로 힙 초기 크기(-Xms)와 최대 크기(-Xmx)이외의 디테일한 조정을 별로 추천하지 않습니다. 오히려 성능이 악화될 수 있기 떄문입니다. 만약, 기본적인 성능이 나오지 않으면 애플리케이션의 메모리 사용을 개선하고 GC 튜닝 가이드를 참조해야 합니다.

대안1: G1GC

첫번째는 G1GC입니다. Java9부터 기본 GC 알고리즘이 되었습니다. CMS와 같은 형태의 GC입니다. Java 힙을 지역이라는 일정 크기로 분할하여 각각 Eden과 Old등의 역할로 합니다. 활용도가 높은 지역(로컬)을 선택하여 GC하기 때문에 높은 처리량을 기대할 수 있습니다. 또한, 정지목표시간을 -XX:MaxGCPauseMillis로 지정하여 JVM이 이를 지킬 수 있도록 자신의 동작을 최적화하는 것이 특징입니다.

대안2: ZGC

전환하는 타이밍에서 Java힙을 크게 넓히고 싶은 경우, ZGC도 선택지에 오릅니다. ZGC는 8MB에서 16TB까지 다양한 힙크기를 10ms 이하의 정지시간에 처리하는 것을 목표로 합니다. ZGC는 페이지라는 단위로 메모리를 관리합니다. 동시성 GC는 애플리케이션 스레드와 병렬로 작동하는 관계이기 때문에, Java 힙 용량이 설정한 상한까지 확장될 가능성이 있습니다. 이를 피하기 위해 -XX: SoftMaxHeapSize에서 가능한 이 설정한 값에 힙크기에 맞게 JVM에 처리할 수 있습니다.

대안3: Parallel GC

마미작으로 Java8까지 기본이었던 Parallel GC입니다. 처리량형이라고 하지만, 애플리케이션 스레드를 완전히 정지시키고 전속력으로 멀티스레드에서 GC합니다. 따라서, 가장 효율이 좋지만 오래된 기술입니다. 기계적 성능(CPU, 메모리 액세스속도)를 향상시킬 수 있다는 점에서 몇GB정도의 힙크기라면 Parallel GC도 충분합니다. 동시성 GC에 집착하지 않으면 좋은 결과를 얻을 수 있습니다.

Nashorn - JavaScript 엔진

Java에서는 Scripting API라는 스크립트 언어를 실행하는 API가 준비되어 있습니다. 스크립트 엔진을 직접 제공하여 다양한 언어에 대응할 수 있지만 완전히 JavaScript는 Nashorn이라는 엔진이 기본적으로 포함되어 있습니다. 그러나, JavaScript(ECMAScript)사양의 변화가 빠르고, 유지보수가 어렵다는 점에서 JEP 372에서 삭제되었습니다. 따라서 예제1와 같은 코드를 Java17에서 실행하려면 결과2와 같은 예외가 발생합니다.

예제1

var manager = new ScriptEngineManager();
var engine = manager.getEngineByName("JS");
engine.eval("print('from JS')");

결과2 - Nashorm삭제로 인한 NullpointerException

java.lang.NullPointerException: Cannot invoke "javax.
script.ScriptEngine.eval(String)" because "engine" is null at wdbpress.java17.examples.js.Test.main (Test.java:12)

그러나, Nashorn은 OpenJDK커뮤니티에서 계속해서 개발하도록 되었습니다. OpenJDK에 포함되어 있지는 않지만, 필요시 Maven에서 다운로드할 수 있도록 하고 있습니다. 2021년 8월 기준으로 15.3버전을 지원하는 ECMAScript버전5(일부 6기능 포함)이지만, 이는 지금까지 Java에 포함되어 있었던것과 크게 다르지는 않기 떄문에 Java 17로 전환하는 관점에서는 큰 문제는 없을 것입니다. pom.xml에 아래와 같이 종속성을 추가하여 사용할 수 있습니다.

<dependency>
	<groupId>org.openjdk.nashorn</groupId>
	<artifactId>nashorn-core</artifactId>
	<version>15.3</version>
</dependency>

참고: https://mvnrepository.com/artifact/org.openjdk.nashorn/nashorn-core

기타 사항

여기까지는 비교적 큰 변화를 살펴봤지만, 많은 변화 중에서 크게 영향이 없지만 경우에 따라서 문제가 될 수 있는 2가지를 소개합니다.

버전 표기 변경

최종 사용자에게 배포할 애플리케이션등은 자신이 어떤 Java로 실행하고 있는지 버전체크를 하는 경우가 있습니다. 이 때, java.version시스템 속성을 참조할 수 있지만, 이를 포함한 Java 버전 표기가 전반적으로 변경되었습니다.

  • Java 8: 1.8.0_295
  • Java 16: 16.0.1
    <기능(메이저버전>.<잠정적인 버전>.<업데이트버전>.<패치버전>으로 나열됩니다. 끝이 0일 경우 생략할 수 있습니다. 위 예에서는 16.0.1이지만, 16.0.1.0과 같습니다. 이런 버전의 비교등을 다루기 쉽게 하기 위해 Runtime.Version 클래스가 준비되어 있습니다. 파서를 가질뿐만 아니라, 비교가능한 클래스(Comparable)이기 때문에 버전번호에 대한 다양한 작업을 쉽게 할 수 있습니다. 이 클래스를 사용한 버전판정 예를 살펴봅시다.

버전 번호 인식

우선 필요한 것은 제대로 된 버전번호를 인식하고 Runtime.Version의 인스턴스를 얻을 수 있습니다. 자신이 어떤 버전의 Java로 실행하고 있을지를 Runtime.Version()으로 알 수 있습니다.

jshell> Runtime.version()
$1 ==> 17-ea+16-1315

어떤 버전을 얻으려면 문자열을 parse메소드로 파싱해야 합니다. 이때 문자열은 위 새 버전표시형식을 지켜야 합니다.

// 메이저버전만 체크
jshell> Runtime.Version.parse("17")
$2 ==> 17
// 형식이 안맞는 경우 IllegalArgumentException 발생
jshell> Runtime.Version.parse("17custom")
| Exception java.lang.IllegalArgumentException: Inva
lid version string: '17custom'
| at Runtime$Version.parse (Runtime.java:1014)
| at (#4:1)

특징버전과의 관계

버그 해결 및 삭제기능 이용을 위해 특정버전 이전 또는 이후를 인식하고 싶은 것입니다. 여기에서는 필자 환경에서 실행되고 있는 Java 17(OpenJDK 17의 Early Access버전)과 16.0.1과 비교해 봅니다.
앞에서 이야기한 것처럼 Runtime.Version은 비교가능하기 떄문에 Comparable인터페이스를 구현하고 있습니다. 따라서 compareTo()로 비교가 가능합니다. compareTo()는 값이 동일하면 0을 리턴하고 자신이 인수보다 크면 양수를 리턴하고, 작으면 음수를 리턴하는 메소드입니다. 이 메소드가 버전번호 비교에 어떻게 적용되는지 살펴 봅시다.

// 비교 대상이되는 Version 인스턴스얻기 (16.0.1)
jshell> var version16_0_1 = Runtime.Version.parse ("16.0.1")
version16_0_1 ==> 16.0.1
// 자동 버전 OpenJDK 17 EA
jshell> Runtime.version ()
$ 6 ==> 17-ea + 16-1315
// 자동 버전은 비교 대상에 비해 어떠한가?
jshell> Runtime.version (). compareTo (version16_0_1)
$ 7 ==> 1 큰 (0보다 크기 때문에)
// 비교 대상은 자동 버전에 비해 어떠한가?
jshell> version16_0_1.compareTo (Runtime.version ())
$ 8 ==> -1 작은 (0보다 작기 때문에)

Java 설치 디렉토리 변경

앞에서 설명한 모듈화에 따라 JDK디렉토리 구조도 Java 8에 비해 크게 변화가 있습니다. 경로와 설정파일 배치등 환경구축 및 사용자별 설정에 따라 내용이 변경될 가능성이 높아 설명합니다.

JRE삭제

Java8까지 사용자배포 등 순수한 Java실행환경만을 필요로 하는 독자를 위해 javac와 같은 개발환경을 제외한 JRE(Java Runtime Environment)가 제공되어 왔습니다.그러나 모듈화를 실현한 Java 9이후, JEP 220에 의해 JDK형식으로 모두 통합합니다. 이는 지금까지 JDK가 존재하고 있다고 알려주던 $JAVA_HOME/jre가 없으지며, 그 내용이 각 디렉토리별로 할당된 것을 의미합니다. 일부 오래된 Linux배포판에서 OpenJDK 경로설정 /usr/lib/.jvm/버전/jre/bin으로 설정되어 있지만, jre아래 경로를 설정하는 절차가 있는 경우 $JAVA_HOME/bin으로 변경해야 합니다.

설정파일 이동

Java는 다양한 설정파일을 가지고 있습니다. 위에서 이야기한 JRE삭제에 따라 지금까지 JRE아래 존재하는 설정파일은 $JAVA_HOME/conf로 이동합니다. 설정파일 업데이트나 변경하는 경우 사용자가 직접하는 경우 주의해야합니다.
conf에 배치되는 주요 파일은 아래와 같습니다.

  • logging.properties: 로깅API(java.util.logging)설정파일
  • net.properties: 네트워크 관련 설정파일
  • security: 보안정책 및 보안관련 설정파일
  • sound.properties: 사운드관련(javax.sound) 설정파일

tools.jar삭제

지금까지 JDK와 JRE각각 lib디렉토리를 가지고 있었지만, 모듈화에 따라 JDK디렉토리 구조 변경으로 $JAVA_HOME/lib로 일원화됩니다.이에 따라 기존 lib디렉토리에 배치된 라이브러리의 일부가 삭제되었습니다.
그중에서 가장 대표적인 것이 tools.jar입니다. 이는 이름 그대로 JDK도구인 Java클래스 파일을 정리한 것입니다. 이 라이브러리는 javac 구현과 클래스파일 작업을 위한 클래스가 포함되어 있기 때문에 IDE, 빌드도구, 분석도구등으로 이용되는 경우가 많습니다. 따라서, JDK가 버전업그레이드하는 기존 사용하던 도구가 작동되지 않는 경우가 있습니다. 즉, IDE포함한 주변도구가 Java17에 대응하고 있는지에 대한 여부도 중요한 확인포인트입니다.
쉘에서는 아래와 같이 Java버전을 확인하여 클래스경로에 tools.jar를 추가하거나 모듈을 export할 것인지 결정합니다.

// 버전 번호의 첫 번째 숫자얻음 
JAVA_VERSION =`$ JAVA_HOME / bin / java -version 2> & 1 | head-n 1 | sed -e 's /^.\+ "\ ([0-9] \ + \) \ \? * $ / \ 1 /'`
(생략)
// "1"인 경우 Java 8 이전 판단 (1.8.0 등)
if [$ JAVA_VERSION -eq 1]; then
	TOOLS_JAR = $ JAVA_HOME / lib / tools.jar
	if [! -e $ TOOLS_JAR]; then
		echo "$ TOOLS_JAR does not exist."
		exit 2
	fi
	$ JAVA_HOME / bin / java -cp $ BASEDIR / cfa.jar : $ TOOLS_JAR
$ MAIN_CLASS $ @
else
// "1"이 아닌 경우는 Java 9 이상 판단 (17 등)
$ JAVA_HOME / bin / java -p $ BASEDIR / cfa.jar \
				--add-modules cfa \
// 필요한 패키지를 자동 모듈로 내보내기
				--add-exports jdk.jdeps / com.sun.tools.classfile = cfa \
				$ MAIN_CLASS \
				$ @
 fi

정리

이 글은 Java8에서 Java17로 전환하기 위한 기준으로 Java의 큰 변화된 부분을 정리해 보았습니다. Java 개발주기가 빨라지고 있는지, 메이저 버전이 많이 올라와 있기 떄문에 큰 변화가 있었을 것입니다. 이런 경우 애플리케이션보다 깔끔하게 안정적으로 동작시키기 위한 것입니다. 이 글들은 독자가 Java의 버전 업그레이드 동기부여와 도움이 되길 바랍니다.

profile
개발자, IT강사, sage.riwon.kim@gmail.com

1개의 댓글

comment-user-thumbnail
2022년 6월 11일

본인이 작성한 글이 아닌데 왜 출처는 작성 안하시나요?

답글 달기