NYO_O

Java 11 본문

BackEnd/Java

Java 11

NYO_O 2026. 5. 29. 16:36
반응형

지난 시간, 우리는 Java 8이 왜 자바 역사에서 가장 큰 변화로 꼽히는지를 살펴보았습니다. Lambda와 Stream API, Optional, 새로운 날짜 API까지 함수형 프로그래밍을 자바에 녹여낸 버전이었습니다.

2026.05.29 - [BackEnd/Java] - Java 8, 함수형 언어

 

Java 8, 함수형 언어

지난 시간, 우리는 자바가 어떤 철학으로 설계되었는지, JVM이 어떻게 동작하는지를 살펴보았습니다. 자바라는 언어의 기반을 이해했다면, 이제 그 위에서 어떤 변화들이 있었는지를 살펴볼 차

ddangnyo.tistory.com

Java 8 이후 자바는 6개월마다 새 버전을 출시하는 빠른 릴리즈 사이클로 전환했습니다. 그 결과 Java 9, 10이 차례로 등장했지만, 실무에서는 대부분 Java 8 다음 LTS인 Java 11로 바로 넘어가는 경우가 많았습니다.

Java 11은 2018년에 출시된 LTS 버전입니다. Java 8만큼 드라마틱한 변화는 아니었지만, 개발자의 일상적인 코드를 더 간결하고 실용적으로 만들어 주는 변화들이 담겨 있습니다. 오늘은 "왜 이 기능이 필요했는가"라는 맥락을 중심으로 Java 11을 정리해 보겠습니다.

Java 9·10을 건너뛰고 11로 온 이유

Java 9와 10이 있었음에도 실무에서 Java 11을 첫 번째 선택지로 삼는 이유는 단순합니다. LTS(장기 지원) 버전이기 때문입니다.

Java 9는 모듈 시스템(Project Jigsaw)이라는 큰 변화를 담았지만, LTS가 아니었고 모듈 시스템 자체의 학습 비용과 기존 라이브러리와의 호환성 문제로 인해 실무 적용이 조심스러웠습니다. Java 10은 var 키워드를 도입했지만 역시 LTS가 아니었습니다.

Java 11은 Java 9와 10에서 도입된 기능들을 안정적으로 흡수하고, 추가적인 실용적 개선을 더한 LTS 버전입니다. Java 8 이후 처음 나온 LTS라는 점에서 많은 팀이 8에서 11로의 마이그레이션을 선택했습니다.

한 가지 더 알아두면 좋은 변화가 있습니다. Java 11부터 Oracle JDK의 상업적 사용이 유료화되었습니다. 이 시점을 기점으로 OpenJDK, Amazon Corretto, Adoptium(Eclipse Temurin) 같은 무료 배포판이 실무에서 더 널리 사용되기 시작했습니다.

var 키워드

사전 지식 — 타입 추론(Type Inference)이란? 컴파일러가 코드의 문맥을 보고 변수의 타입을 스스로 파악하는 기능입니다. 개발자가 타입을 직접 명시하지 않아도 컴파일러가 올바른 타입을 결정해 줍니다. 자바는 원래 강타입 언어라 모든 변수에 타입을 명시해야 했지만, var의 도입으로 지역 변수에 한해 이 의무가 완화되었습니다.

var는 Java 10에서 처음 도입되어 Java 11에서 Lambda 매개변수까지 확장된 기능입니다. 지역 변수를 선언할 때 타입 대신 var를 쓰면 컴파일러가 오른쪽 표현식을 보고 타입을 추론합니다.

// 기존 방식
String name = "Java";
List<String> names = new ArrayList<>();
Map<String, List<Integer>> map = new HashMap<>();

// var 사용
var name = "Java";              // String으로 추론
var names = new ArrayList<>();  // ArrayList로 추론
var map = new HashMap<>();      // HashMap으로 추론

타입 이름이 길어질수록 var의 효과가 두드러집니다.

// 기존 방식 — 타입명이 반복됨
Map<String, List<Integer>> scoreMap = new HashMap<String, List<Integer>>();

// var 사용 — 가독성 향상
var scoreMap = new HashMap<String, List<Integer>>();

var의 제약 조건

var는 편리하지만 사용할 수 있는 범위가 제한되어 있습니다. 이 제약이 생긴 이유를 이해하면 var의 동작 원리가 더 명확해집니다.

var는 지역 변수 선언 시에만 사용할 수 있습니다. 클래스 필드, 메서드 매개변수, 반환 타입에는 사용할 수 없습니다.

// 사용 가능
var count = 10;

// 사용 불가 — 클래스 필드
class MyClass {
    var name = "Java"; // 컴파일 오류
}

// 사용 불가 — 메서드 매개변수
void print(var message) { } // 컴파일 오류

// 사용 불가 — 초기값 없이 선언
var x; // 컴파일 오류 (추론할 문맥이 없음)

// 사용 불가 — null로만 초기화
var y = null; // 컴파일 오류 (타입 불분명)

면접에서 자주 나오는 질문

"var를 사용하면 동적 타입 언어가 되는 건가요?"

그렇지 않습니다. var는 어디까지나 컴파일 타임에 타입이 결정됩니다. 런타임에 타입이 바뀌는 Python, JavaScript의 동적 타이핑과는 다릅니다. 한 번 추론된 타입은 고정되며, 다른 타입의 값을 재할당하면 컴파일 오류가 발생합니다.

var count = 10;     // int로 추론
count = "열";       // 컴파일 오류 — String을 int에 할당할 수 없음

또한 var를 남용하면 코드를 읽는 사람이 타입을 파악하기 위해 추가적인 맥락을 확인해야 합니다. 타입이 명확하게 드러나는 경우에 사용하고, 타입 정보가 가독성에 중요한 역할을 하는 경우에는 기존 방식을 유지하는 것이 좋습니다.

모듈 시스템

사전 지식 — classpath란? JVM이 클래스 파일을 찾는 경로입니다. Java 9 이전에는 모든 클래스가 classpath에 일렬로 나열되어 있었기 때문에, 어떤 패키지가 어느 jar에서 왔는지, 서로 어떤 의존 관계인지를 코드만 보고 파악하기 어려웠습니다.

모듈 시스템(Java Platform Module System, JPMS)은 Java 9에서 처음 도입되어 Java 11에서 안정화된 기능입니다. 큰 애플리케이션을 명시적인 의존 관계를 가진 모듈 단위로 분리할 수 있게 해줍니다.

모듈 시스템이 등장하게 된 배경에는 두 가지 문제가 있었습니다.

첫째, 의존성의 불명확함이었습니다. 기존 classpath 방식에서는 어떤 코드가 어떤 패키지에 의존하는지 명시적으로 선언하는 수단이 없었습니다. 런타임에 가서야 ClassNotFoundException으로 문제를 발견하는 경우가 많았습니다.

둘째, 캡슐화의 한계였습니다. public으로 선언된 클래스는 같은 classpath에 있는 모든 코드에서 접근할 수 있었습니다. 내부 구현용으로 만든 클래스도 외부에서 마음대로 사용할 수 있었고, 이를 막을 방법이 없었습니다.

모듈은 module-info.java 파일로 정의합니다.

// module-info.java
module com.example.myapp {
    requires java.net.http;          // 이 모듈이 필요로 하는 모듈
    requires com.example.common;

    exports com.example.myapp.api;   // 외부에 공개할 패키지
    // 나머지 패키지는 외부에서 접근 불가
}

requires로 필요한 모듈을 명시하고, exports로 외부에 공개할 패키지를 선언합니다. exports에 포함되지 않은 패키지는 모듈 외부에서 접근할 수 없습니다. 같은 public 클래스라도 모듈 경계를 넘지 못합니다.

모듈 시스템의 현실

솔직히 말하면, 모듈 시스템은 기존 classpath 방식에 비해 설정이 복잡하고 기존 라이브러리와의 호환성 처리가 까다롭습니다. 실무에서 모듈 시스템을 처음부터 적용하는 경우는 많지 않고, Maven이나 Gradle 같은 빌드 도구가 의존성 관리를 이미 잘 해주고 있기도 합니다. 다만 Java 플랫폼 자체가 모듈화되면서 JVM의 경량 배포가 가능해졌고, 필요한 모듈만 포함하는 커스텀 런타임 이미지를 만들 수 있다는 점에서 의미가 있습니다.

새로운 HTTP Client API

사전 지식 — HttpURLConnection이란? Java 초창기부터 있던 HTTP 통신 클래스입니다. 기능은 동작하지만 설정이 장황하고, 비동기 처리를 지원하지 않아 외부 라이브러리(Apache HttpClient, OkHttp 등)에 의존하는 경우가 대부분이었습니다.

Java 11 이전, 표준 라이브러리만으로 HTTP 요청을 보내는 코드는 꽤 번거로웠습니다.

// 기존 HttpURLConnection 방식
URL url = new URL("https://api.example.com/data");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");

int responseCode = connection.getResponseCode();
BufferedReader reader = new BufferedReader(
    new InputStreamReader(connection.getInputStream())
);
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
    response.append(line);
}
reader.close();

이 때문에 현업에서는 Apache HttpClient, OkHttp, RestTemplate 같은 외부 라이브러리를 대신 사용하는 경우가 많았습니다.

Java 11은 java.net.http 패키지에 새로운 HTTP Client API를 정식 도입했습니다. 동기, 비동기 요청을 모두 지원하며, HTTP/2와 WebSocket도 기본으로 지원합니다.

// Java 11 HttpClient — 동기 요청
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .GET()
    .build();

HttpResponse response = client.send(
    request,
    HttpResponse.BodyHandlers.ofString()
);

System.out.println(response.statusCode()); // 200
System.out.println(response.body());       // 응답 본문

비동기 요청도 깔끔하게 처리할 수 있습니다.

// 비동기 요청 — CompletableFuture 반환
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println);

POST 요청과 헤더 설정도 직관적입니다.

HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"Java\"}"))
    .build();

표준 라이브러리만으로 HTTP 통신을 깔끔하게 처리할 수 있게 되었다는 점에서, 외부 의존성을 줄이고자 하는 프로젝트에 유용한 변화입니다. Spring 환경에서는 여전히 RestTemplate이나 WebClient를 더 많이 쓰지만, HttpClient의 등장 배경과 장점을 알고 있는 것이 좋습니다.

String API 개선

Java 11에서 String 클래스에 실용적인 메서드들이 여럿 추가되었습니다. 이전까지는 Apache Commons Lang 같은 외부 라이브러리나 직접 구현에 의존하던 기능들입니다.

// isBlank() — 공백 문자열 여부 확인 (trim().isEmpty()보다 간결)
"".isBlank();        // true
"  ".isBlank();      // true  (공백만 있어도 true)
"Java".isBlank();    // false

// strip() — 앞뒤 공백 제거 (trim()의 유니코드 버전)
"  Java  ".strip();        // "Java"
"  Java  ".stripLeading(); // "Java  "
"  Java  ".stripTrailing();// "  Java"

// repeat() — 문자열 반복
"ha".repeat(3);  // "hahaha"
"-".repeat(10);  // "----------"

// lines() — 줄 단위로 Stream 반환
"첫째 줄\n둘째 줄\n셋째 줄"
    .lines()
    .forEach(System.out::println);

strip()과 trim()의 차이

trim()은 ASCII 기준의 공백(코드값 32 이하)만 제거합니다. strip()은 유니코드 기준의 공백 문자를 모두 인식해서 제거합니다. 한국어, 일본어처럼 유니코드 공백 문자를 다루는 경우 strip()이 더 안전합니다. Java 11 이후에는 trim() 대신 strip()을 사용하는 것을 권장합니다.

컬렉션, 스트림 API 개선

Java 11에서 컬렉션과 스트림에도 편의 메서드가 추가되었습니다.

toArray() 개선

Stream에서 배열로 변환할 때 더 간결한 방식이 생겼습니다.

List<String> list = List.of("A", "B", "C");

// 기존 방식
String[] arr1 = list.toArray(new String[0]);

// Java 11 — 메서드 참조 방식
String[] arr2 = list.toArray(String[]::new);

takeWhile(), dropWhile() (Java 9 도입, 11에서 안정화)

Stream에서 조건이 참인 동안만 요소를 가져오거나 건너뛰는 메서드입니다.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);

// takeWhile — 조건이 거짓이 되는 순간 중단
numbers.stream()
    .takeWhile(n -> n < 4)
    .collect(Collectors.toList()); // [1, 2, 3]

// dropWhile — 조건이 거짓이 되는 순간부터 수집
numbers.stream()
    .dropWhile(n -> n < 4)
    .collect(Collectors.toList()); // [4, 5, 1, 2]

filter와의 차이점은, takeWhile과 dropWhile은 조건이 처음으로 거짓이 되는 시점을 기준으로 동작한다는 점입니다. 정렬된 데이터에서 구간을 잘라낼 때 유용합니다.

Optional 개선

Optional에도 유용한 메서드가 추가되었습니다.

// ifPresentOrElse() — 값이 있을 때와 없을 때 각각 처리
Optional<String> opt = Optional.of("Java");
opt.ifPresentOrElse(
    v -> System.out.println("값: " + v),
    () -> System.out.println("값 없음")
);

// or() — 비어있을 때 다른 Optional로 대체
Optional<String> result = Optional.<String>empty()
    .or(() -> Optional.of("기본값")); // Optional["기본값"]

// stream() — Optional을 Stream으로 변환
Optional.of("Java").stream()
    .map(String::toUpperCase)
    .forEach(System.out::println); // "JAVA"

Files API 개선

파일을 읽고 쓰는 작업도 더 간결해졌습니다.

// 파일 전체를 문자열로 읽기
Path path = Path.of("example.txt");
String content = Files.readString(path);
String contentUtf8 = Files.readString(path, StandardCharsets.UTF_8);

// 문자열을 파일에 쓰기
Files.writeString(path, "Hello, Java 11");
Files.writeString(path, "내용", StandardCharsets.UTF_8);

Java 11 이전에는 파일을 문자열로 읽으려면 Files.readAllBytes()로 바이트를 읽은 후 new String()으로 변환하거나, BufferedReader를 사용해야 했습니다. readString()과 writeString()은 이 과정을 한 줄로 줄여줍니다.

Lambda에서 var 사용 가능

앞서 살펴본 var가 Java 11에서 Lambda 매개변수에도 사용할 수 있게 확장되었습니다.

// Java 11 이전
(String s) -> s.toUpperCase()

// Java 11 — Lambda 매개변수에 var 사용
(var s) -> s.toUpperCase()

단순히 타입을 생략한 람다 표현식(s -> s.toUpperCase())과 비교했을 때, var를 사용하면 어노테이션을 함께 붙일 수 있다는 차이가 있습니다.

// 어노테이션이 필요한 경우
(@NonNull var s) -> s.toUpperCase()

마무리

오늘은 Java 11에서 달라진 것들을 배경과 의도를 중심으로 살펴보았습니다. Java 8처럼 패러다임을 바꾸는 큰 변화는 아니었지만, 매일 작성하는 코드의 군더더기를 줄이고 표준 라이브러리의 공백을 채우는 방향이었습니다. var로 장황한 타입 선언을 줄이고, HTTP Client로 외부 의존성을 줄이고, String과 Files API로 반복 코드를 걷어냈습니다.

반응형

'BackEnd > Java' 카테고리의 다른 글

Java 21  (1) 2026.05.29
Java 17  (0) 2026.05.29
Java 8  (0) 2026.05.29
Java란 무엇일까?  (0) 2026.05.29
JAR 파일의 종류(Plain JAR vs Boot JAR)  (0) 2026.05.29