JDK 11 — 두 번째 LTS
2018년 9월 출시된 JDK 11은 JDK 8 이후 두 번째 LTS(Long-Term Support) 릴리스입니다. JDK 8에서 현대 Java로 마이그레이션할 때 가장 많이 선택되는 버전이며, 많은 기업 환경에서 프로덕션 기준 버전으로 사용되고 있습니다.
JDK 9, 10에서 추가된 모듈 시스템, var 등의 기능을 모두 포함하면서, HTTP Client API 정식 도입, String/Files 유틸리티 강화 등 실용적인 개선이 추가되었습니다.
HTTP Client API
JDK 9에서 인큐베이터 모듈로 도입된 HTTP Client가 JDK 11에서 정식 API(java.net.http)로 확정되었습니다. 기존 HttpURLConnection의 복잡하고 저수준인 API를 대체하며, HTTP/2, 비동기 요청, WebSocket을 기본 지원합니다.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientExample {
public static void main(String[] args) throws Exception {
// HttpClient 생성 — HTTP/2 기본, 타임아웃 설정
var client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
// GET 요청 생성
var request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.header("Accept", "application/json")
.GET()
.build();
// 동기 요청: send() — 응답이 올 때까지 블로킹
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("상태 코드: " + response.statusCode());
System.out.println("프로토콜: " + response.version());
System.out.println("본문 길이: " + response.body().length() + "자");
// 출력:
// 상태 코드: 200
// 프로토콜: HTTP_2
// 본문 길이: 292자
// 비동기 요청: sendAsync() — CompletableFuture 반환
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("비동기 응답 수신: " + body.length() + "자"))
.join(); // 메인 스레드에서 완료 대기
// 출력: 비동기 응답 수신: 292자
// POST 요청
var postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"title\": \"테스트\", \"body\": \"본문\", \"userId\": 1}"
))
.build();
var postResponse = client.send(postRequest, HttpResponse.BodyHandlers.ofString());
System.out.println("POST 상태 코드: " + postResponse.statusCode());
// 출력: POST 상태 코드: 201
}
}
HttpClient는 스레드 안전하므로 하나의 인스턴스를 애플리케이션 전체에서 공유할 수 있습니다. 요청마다 새로 생성할 필요가 없습니다.
새로운 String 메서드
JDK 11에서 String 클래스에 실무에서 자주 필요한 유틸리티 메서드가 추가되었습니다.
import java.util.stream.Collectors;
public class StringMethods {
public static void main(String[] args) {
// isBlank(): 빈 문자열 또는 공백만 있는지 확인
System.out.println(" ".isBlank()); // true
System.out.println(" ".isEmpty()); // false — 공백 문자가 있으므로
System.out.println("hello".isBlank()); // false
// isBlank()는 isEmpty()와 달리 공백, 탭, 줄바꿈도 "빈 것"으로 판단
// strip(): 앞뒤 공백 제거 (유니코드 인식)
String text = " \u2000Hello Java\u2000 ";
System.out.println("trim: [" + text.trim() + "]");
// 출력: trim: [ Hello Java ] — 유니코드 공백 미처리
System.out.println("strip: [" + text.strip() + "]");
// 출력: strip: [Hello Java] — 유니코드 공백까지 제거
System.out.println("stripLeading: [" + text.stripLeading() + "]");
// 출력: stripLeading: [Hello Java ]
System.out.println("stripTrailing: [" + text.stripTrailing() + "]");
// 출력: stripTrailing: [ Hello Java]
// lines(): 문자열을 줄 단위로 Stream 변환
String multiline = "첫 번째 줄\n두 번째 줄\n세 번째 줄";
long lineCount = multiline.lines().count();
System.out.println("줄 수: " + lineCount);
// 출력: 줄 수: 3
// 빈 줄 필터링 예시
String withBlanks = "Hello\n\n \nWorld\n";
var nonBlank = withBlanks.lines()
.filter(line -> !line.isBlank())
.collect(Collectors.toList());
System.out.println("비어있지 않은 줄: " + nonBlank);
// 출력: 비어있지 않은 줄: [Hello, World]
// repeat(): 문자열 반복
String separator = "-".repeat(30);
System.out.println(separator);
// 출력: ------------------------------
System.out.println("★".repeat(5));
// 출력: ★★★★★
}
}
strip()과 trim()의 차이는 유니코드 공백 처리입니다. trim()은 ASCII 공백(코드 32 이하)만 제거하지만, strip()은 Character.isWhitespace()가 true인 모든 문자를 제거합니다. 다국어 환경에서는 strip()이 더 안전합니다.
Files 유틸리티
파일 전체를 문자열로 읽거나 쓰는 작업이 한 줄로 가능해졌습니다.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FilesUtility {
public static void main(String[] args) throws IOException {
// 임시 파일 생성
Path tempFile = Files.createTempFile("jdk11-demo", ".txt");
// writeString(): 문자열을 파일에 바로 쓰기
Files.writeString(tempFile, "JDK 11에서 파일 쓰기가 간단해졌습니다.\n두 번째 줄입니다.");
// readString(): 파일 전체를 문자열로 읽기
String content = Files.readString(tempFile);
System.out.println("파일 내용:");
System.out.println(content);
// 출력:
// 파일 내용:
// JDK 11에서 파일 쓰기가 간단해졌습니다.
// 두 번째 줄입니다.
System.out.println("파일 크기: " + Files.size(tempFile) + " bytes");
// 출력: 파일 크기: 67 bytes
// 정리: 임시 파일 삭제
Files.delete(tempFile);
System.out.println("임시 파일 삭제 완료");
// 출력: 임시 파일 삭제 완료
}
}
기존에는 BufferedReader로 줄 단위로 읽고 StringBuilder로 조합하거나, 외부 라이브러리(Apache Commons IO, Guava)를 사용해야 했던 작업이 표준 API 한 줄로 해결됩니다.
람다에서 var 사용
JDK 10에서 도입된 var가 JDK 11에서 람다 파라미터에도 사용 가능해졌습니다(JEP 323). 타입 추론이 가능한 상황에서 var를 명시하는 이유는 어노테이션을 붙이기 위해서입니다.
import java.util.List;
import java.util.stream.Collectors;
public class LambdaVar {
// @interface 어노테이션 정의 (예시)
@interface NonNull {}
public static void main(String[] args) {
var names = List.of("Java", "Kotlin", "Scala", "Groovy");
// JDK 10 이전: 타입 명시 또는 생략
var result1 = names.stream()
.filter(name -> name.length() > 4)
.collect(Collectors.toList());
// JDK 11: var를 사용하여 어노테이션 적용 가능
var result2 = names.stream()
.filter((@NonNull var name) -> name.length() > 4)
.collect(Collectors.toList());
System.out.println("5글자 초과: " + result2);
// 출력: 5글자 초과: [Kotlin, Scala, Groovy]
}
}
java 명령으로 소스 파일 직접 실행
JDK 11부터 .java 소스 파일을 컴파일 없이 바로 실행할 수 있습니다(JEP 330). 간단한 스크립트나 테스트 코드를 실행할 때 javac 단계를 건너뛸 수 있습니다.
# JDK 10 이전: 컴파일 → 실행 (2단계)
javac Hello.java
java Hello
# JDK 11: 소스 파일 직접 실행 (1단계)
java Hello.java
단일 소스 파일에서만 동작하며, 여러 파일로 구성된 프로젝트에서는 기존처럼 javac를 사용해야 합니다. Unix/Linux에서는 shebang(#!/usr/bin/java --source 11)을 추가하면 셸 스크립트처럼 실행할 수도 있습니다.
성능 관련 변화
ZGC (실험적 도입): JDK 11에서 ZGC(JEP 333)가 실험적으로 도입되었습니다. Stop-the-World 시간을 10ms 이하로 유지하는 것을 목표로 설계된 초저지연 GC입니다. 테라바이트급 힙에서도 짧은 일시 정지를 보장합니다. 이후 JDK 15에서 프로덕션 준비 완료(Production Ready) 상태가 되었습니다.
Epsilon GC: 아무것도 하지 않는 “no-op” GC입니다. 메모리 할당만 하고 회수는 하지 않으며, 힙이 가득 차면 JVM이 종료됩니다. 용도는 성능 벤치마크에서 GC 오버헤드를 측정하거나, 수명이 짧은 애플리케이션(배치 작업 등)에서 GC 비용을 완전히 제거하는 것입니다.
Nest-Based Access Control: 중첩 클래스(inner class)가 외부 클래스의 private 멤버에 접근할 때, 기존에는 컴파일러가 합성 브릿지 메서드를 생성했습니다. JDK 11부터 JVM이 “nest” 관계를 직접 인식하여 브릿지 메서드 없이 직접 접근합니다. 리플렉션에서도 동일하게 동작합니다.
JDK 8에서 11로 마이그레이션 체크포인트
JDK 8에서 11로 업그레이드할 때 확인해야 할 핵심 사항을 정리합니다.
| 항목 | 확인 사항 |
|---|---|
| 모듈 시스템 | --add-opens, --add-modules 옵션 필요 여부 확인 |
| 제거된 API | Java EE 모듈(javax.xml.bind, javax.annotation 등) 별도 의존성 추가 |
| 내부 API | sun.misc.Unsafe 등 내부 API 사용 시 경고 또는 오류 |
| 빌드 도구 | Maven/Gradle 플러그인 버전 업데이트 |
| 라이브러리 | 주요 프레임워크(Spring, Hibernate 등) JDK 11 호환 버전 확인 |
| GC 옵션 | PermGen 관련 옵션(-XX:PermSize) 제거, Metaspace 옵션으로 교체 |
특히 javax → jakarta 네임스페이스 변경이 가장 큰 장벽입니다. javax.xml.bind(JAXB), javax.annotation 등이 JDK에서 제거되었으므로, Maven/Gradle 의존성으로 별도 추가해야 합니다.
정리
| 기능 | 핵심 가치 |
|---|---|
| HTTP Client API | 현대적 HTTP 통신, HTTP/2, 비동기 기본 지원 |
| String 메서드 | isBlank, strip, lines, repeat로 문자열 처리 간소화 |
| Files 유틸리티 | readString/writeString 한 줄 파일 I/O |
| 람다 var | 람다 파라미터에 어노테이션 적용 가능 |
| 소스 직접 실행 | java Hello.java로 컴파일 없이 실행 |
| ZGC | 10ms 이하 초저지연 GC (실험적) |
| Epsilon GC | 벤치마크/단수명 앱용 no-op GC |
JDK 11은 JDK 8의 안정성과 JDK 9~10의 혁신을 결합한 LTS 릴리스입니다. JDK 8에서 마이그레이션한다면 모듈 시스템 호환성과 제거된 Java EE API를 먼저 점검하고, HTTP Client API와 새로운 String 메서드를 적극 도입하는 것을 권장합니다. 다음 마이그레이션 목표는 JDK 17(세 번째 LTS) 또는 JDK 21(네 번째 LTS, Virtual Threads 포함)이 될 것입니다.