NYO_O

Java 17 본문

BackEnd/Java

Java 17

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

지난 시간, 우리는 Java 11이 일상적인 코드의 군더더기를 줄이고 표준 라이브러리의 공백을 채운 버전이었음을 살펴보았습니다. var, HTTP Client, String API 개선 등 실용적인 변화들이 중심이었습니다.

2026.05.29 - [BackEnd/Java] - Java 11

 

Java 11

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

ddangnyo.tistory.com

Java 17은 2021년에 출시된 LTS 버전으로 편의 메서드를 추가하는 수준을 넘어, 언어의 표현력 자체를 한 단계 끌어올리는 변화들이 담겨 있습니다.

Records, Sealed Classes, Pattern Matching for instanceof, Text Blocks — 이 기능들은 도메인 모델을 설계하고 분기 로직을 작성하는 방식에 직접적인 영향을 줍니다. Java 17은 Spring Boot 3.x의 최소 요구 버전이기도 해서, 현재 새로운 프로젝트의 사실상 기본 선택지가 되어 있습니다. 오늘도 각 기능이 왜 필요했는지를 중심으로 살펴보겠습니다.

Java 17이 주목받는 이유

Java 17이 실무에서 빠르게 표준으로 자리잡은 데는 몇 가지 배경이 있습니다.

가장 직접적인 이유는 Spring Boot 3.0의 최소 요구 버전이라는 점입니다. 2022년 말 출시된 Spring Boot 3.0은 Jakarta EE 9와 Java 17을 기준으로 설계되었습니다. Spring Boot 기반으로 새 프로젝트를 시작한다면 Java 17 이상을 선택하는 것이 자연스러운 흐름이 되었습니다.

또한 Java 11 대비 성능 개선도 있었습니다. ZGC, G1GC 등 가비지 컬렉터가 지속적으로 개선되었고, JVM 내부 최적화도 누적된 버전입니다.

마지막으로 언어 표현력의 실질적 향상입니다. Java 11까지는 불변 데이터 클래스 하나를 만들기 위해 수십 줄의 보일러플레이트를 작성해야 했습니다. Java 17에서는 그 불편함이 언어 수준에서 해소되었습니다.

Records

사전 지식 — 보일러플레이트(Boilerplate)란? 반복적으로 작성해야 하지만 실제 비즈니스 로직과는 무관한 코드를 말합니다. Java에서 데이터를 담는 클래스를 만들 때 필요한 생성자, getter, equals(), hashCode(), toString()이 대표적입니다.

Java에서 단순히 데이터를 담는 클래스를 만들려면 얼마나 많은 코드가 필요했는지 생각해 보겠습니다.

// Java 17 이전 — 데이터 클래스 하나를 만들기 위한 코드
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

실제로 담고 싶은 데이터는 int x와 int y 두 개뿐인데, 그것을 위해 30줄 이상의 코드가 필요합니다. Lombok 같은 외부 라이브러리를 쓰거나 IDE의 자동 생성 기능에 의존해야 했습니다.

Java 17의 Record는 이 문제를 언어 수준에서 해결합니다.

// Java 17 — Record
record Point(int x, int y) { }

단 한 줄로 위의 30줄짜리 클래스와 동일한 기능을 합니다. 컴파일러가 생성자, 접근자(getter), equals(), hashCode(), toString()을 자동으로 만들어 줍니다.

Point p1 = new Point(3, 5);
Point p2 = new Point(3, 5);

p1.x();           // 3
p1.y();           // 5
p1.equals(p2);    // true
p1.toString();    // "Point[x=3, y=5]"

Record는 불변(Immutable) 입니다. 모든 필드는 자동으로 final이 되어, 생성 이후 값을 변경할 수 없습니다. 또한 다른 클래스를 상속할 수 없습니다. Record 자체가 java.lang.Record를 암묵적으로 상속하기 때문입니다.

커스텀 유효성 검사가 필요하다면 컴팩트 생성자(Compact Constructor)를 활용할 수 있습니다.

record Range(int min, int max) {
    // 컴팩트 생성자 — 매개변수 목록을 생략하고 검증 로직만 작성
    Range {
        if (min > max) {
            throw new IllegalArgumentException("min은 max보다 클 수 없습니다.");
        }
    }
}

면접에서 자주 나오는 질문

"Record와 Lombok의 @Value는 어떻게 다른가요?"

기능적으로는 비슷해 보이지만 중요한 차이가 있습니다. Lombok은 외부 라이브러리이고 컴파일 시 어노테이션 프로세서를 통해 코드를 생성합니다. Record는 자바 언어 자체의 기능이기 때문에 외부 의존성 없이 동작하며, IDE, 리플렉션, 직렬화 등에서 언어 수준의 지원을 받습니다. 또한 Record는 instanceof 패턴 매칭, Record Patterns(Java 21) 등 이후 기능들과 자연스럽게 연결됩니다.

Sealed Classes

사전 지식 — 상속(Inheritance)이란? 기존 클래스의 속성과 동작을 물려받아 새로운 클래스를 만드는 객체지향의 핵심 개념입니다. Java에서는 extends 키워드로 상속을 표현합니다. 기존에는 final 클래스(상속 자체를 금지)와 일반 클래스(누구나 상속 가능) 두 가지 선택지만 있었습니다.

도형(Shape)을 표현하는 클래스 계층을 설계한다고 가정해 보겠습니다. 이 시스템에서 도형은 원(Circle), 직사각형(Rectangle), 삼각형(Triangle) 세 가지만 존재해야 한다고 명세가 정해져 있습니다.

Java 17 이전에는 이 의도를 코드로 표현할 방법이 없었습니다. 일반 클래스나 인터페이스로 선언하면 누구든 상속해서 새로운 도형을 만들 수 있었습니다.

// 기존 방식 — 상속을 제한할 수 없음
public abstract class Shape { }

// 외부에서 얼마든지 새로운 하위 클래스를 만들 수 있음
class Pentagon extends Shape { } // 개발자가 의도하지 않은 확장

Sealed Class는 어떤 클래스가 자신을 상속할 수 있는지를 명시적으로 선언합니다.

// Sealed Class 선언
public sealed class Shape
    permits Circle, Rectangle, Triangle { }

// 허용된 하위 클래스들 — 반드시 sealed, non-sealed, final 중 하나를 선언해야 함
public final class Circle extends Shape {
    private final double radius;
    public Circle(double radius) { this.radius = radius; }
    public double radius() { return radius; }
}

public final class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public non-sealed class Triangle extends Shape {
    // non-sealed — Triangle의 하위 클래스는 자유롭게 만들 수 있음
}

permits에 명시되지 않은 클래스가 Shape를 상속하려 하면 컴파일 오류가 발생합니다.

Sealed Class의 진가는 다음에 살펴볼 Pattern Matching for switch와 결합할 때 나타납니다. 컴파일러가 Shape의 하위 타입이 정확히 세 가지임을 알고 있기 때문에, switch에서 모든 경우를 처리했는지 검증해 줄 수 있습니다.

"Sealed Class와 enum의 차이가 무엇인가요?"

enum은 각 상수가 동일한 타입의 인스턴스이고, 상태를 가지더라도 구조가 동일합니다. Sealed Class는 각 하위 클래스가 서로 다른 필드와 동작을 가질 수 있습니다. Circle은 radius를, Rectangle은 width와 height를 별도로 가질 수 있는 것처럼, 타입 계층 자체를 닫힌 집합으로 설계할 때 Sealed Class가 적합합니다.

Pattern Matching for instanceof

Java에서 타입을 확인하고 해당 타입으로 캐스팅하는 코드는 오랫동안 불필요한 반복을 요구했습니다.

// Java 17 이전 — 타입 확인 후 별도 캐스팅 필요
if (obj instanceof String) {
    String s = (String) obj; // instanceof로 확인했는데 또 캐스팅
    System.out.println(s.toUpperCase());
}

instanceof로 이미 타입을 확인했음에도 별도로 캐스팅을 해야 했습니다. Pattern Matching for instanceof는 이 두 단계를 하나로 합칩니다.

// Java 17 — 타입 확인과 동시에 변수 바인딩
if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // 바로 s 사용 가능
}

조건식 안에서 바로 사용할 수도 있습니다.

if (obj instanceof String s && s.length() > 5) {
    System.out.println("긴 문자열: " + s);
}

Sealed Class와 함께 사용하면 더욱 강력해집니다. Java 21에서 정식화된 Pattern Matching for switch의 기반이 되는 기능입니다.

// Sealed Class + Pattern Matching
double area(Shape shape) {
    if (shape instanceof Circle c) {
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle r) {
        return r.width() * r.height();
    } else if (shape instanceof Triangle t) {
        return t.base() * t.height() / 2;
    }
    throw new IllegalArgumentException("알 수 없는 도형");
}

이 패턴은 Java 21의 Pattern Matching for switch에서 더욱 간결하게 표현할 수 있게 됩니다. Java 17의 Pattern Matching for instanceof는 그 출발점이 되는 기능입니다.

면접에서 자주 나오는 질문

"바인딩 변수의 스코프는 어떻게 되나요?"

바인딩 변수 s는 패턴이 매칭된 블록 안에서만 유효합니다. 또한 &&로 조건을 추가할 경우, 그 조건 안에서도 사용할 수 있습니다. 반면 ||로 연결하면 한쪽 조건이 실패했을 때 바인딩이 보장되지 않으므로 컴파일 오류가 발생합니다.

Text Blocks

사전 지식 — 이스케이프 시퀀스란? 문자열 안에서 특수 문자를 표현하기 위한 표기법입니다. \n은 줄바꿈, \t는 탭, \"는 큰따옴표를 나타냅니다. 여러 줄 문자열을 기존 방식으로 작성하면 이스케이프 문자가 가득한 읽기 어려운 코드가 됩니다.

JSON, SQL, HTML처럼 여러 줄에 걸친 문자열을 Java 17 이전에 작성하면 다음과 같았습니다.

// 기존 방식 — 가독성이 나쁨
String json = "{\n" +
    "    \"name\": \"Java\",\n" +
    "    \"version\": 17\n" +
    "}";

String sql = "SELECT *\n" +
    "FROM users\n" +
    "WHERE age > 20\n" +
    "ORDER BY name";

Text Block은 """ 세 개로 시작하고 끝나는 여러 줄 문자열 리터럴입니다. Java 13에서 Preview로 도입되어 Java 15에서 정식화되었고, Java 17에 포함되어 있습니다.

// Java 17 Text Block
String json = """
    {
        "name": "Java",
        "version": 17
    }
    """;

String sql = """
    SELECT *
    FROM users
    WHERE age > 20
    ORDER BY name
    """;

String html = """
    <html>
        <body>
            <h1>Hello, Java 17</h1>
        </body>
    </html>
    """;

이스케이프 없이 실제 문자 그대로 작성할 수 있어서, 특히 SQL 쿼리나 JSON 템플릿, HTML 조각을 다룰 때 가독성이 크게 향상됩니다.

들여쓰기 처리

Text Block은 공통 들여쓰기를 자동으로 제거합니다. 위 예시에서 닫는 """의 위치가 들여쓰기 기준이 됩니다. 닫는 """를 앞으로 당기면 더 많은 들여쓰기가 제거됩니다.

String text = """
        첫째 줄
        둘째 줄
    """; // 닫는 """ 기준으로 공통 들여쓰기 제거

switch 표현식 정식화 — 값을 반환하는 switch

switch 표현식은 Java 14에서 정식화되었고 Java 17에 포함되어 있습니다. 기존 switch 문(Statement)과 달리 값을 반환하는 표현식(Expression)으로 사용할 수 있습니다.

// 기존 switch 문 — 장황하고 break를 빠뜨리면 fall-through 발생
String result;
switch (day) {
    case MONDAY:
    case FRIDAY:
        result = "주중";
        break;
    case SATURDAY:
    case SUNDAY:
        result = "주말";
        break;
    default:
        result = "기타";
}

// Java 17 switch 표현식 — 간결하고 fall-through 없음
String result = switch (day) {
    case MONDAY, FRIDAY -> "주중";
    case SATURDAY, SUNDAY -> "주말";
    default -> "기타";
};

-> 화살표 문법을 사용하면 fall-through가 발생하지 않습니다. 여러 case를 쉼표로 묶을 수 있어 중복이 줄어듭니다.

여러 줄의 처리가 필요할 때는 yield로 값을 반환합니다.

int score = switch (grade) {
    case "A" -> 100;
    case "B" -> 80;
    case "C" -> {
        System.out.println("평균 점수입니다.");
        yield 60; // 블록에서 값을 반환할 때 yield 사용
    }
    default -> 0;
};

 

"yield와 return의 차이가 무엇인가요?"

return은 메서드 전체를 빠져나갑니다. yield는 switch 표현식의 블록에서만 값을 반환하고, 메서드는 계속 실행됩니다. switch 표현식의 블록 안에서 값을 반환할 때만 사용하는 키워드입니다.

그 외 주목할 변화들

난수 생성 API 개선

기존의 Random, SecureRandom, SplittableRandom 등 난수 생성 클래스들이 공통 인터페이스 RandomGenerator를 통해 다룰 수 있게 되었습니다.

RandomGenerator random = RandomGeneratorFactory.of("Xoshiro256PlusPlus").create();
int value = random.nextInt(100);

알고리즘을 문자열로 지정해 교체할 수 있어, 테스트나 성능 비교 시 유연하게 활용할 수 있습니다.

강화된 캡슐화

Java 17에서는 JDK 내부 API에 대한 접근이 기본적으로 차단되었습니다. Java 8~16까지는 리플렉션으로 JDK 내부 클래스에 접근하는 것이 가능했지만, Java 17부터는 --add-opens 옵션 없이는 접근이 제한됩니다. 일부 오래된 라이브러리가 내부 API에 의존하는 경우 마이그레이션 시 주의가 필요합니다.

macOS 렌더링 파이프라인 개선

Java 17에서 macOS에 새로운 Metal 기반 렌더링 파이프라인이 도입되었습니다. 데스크탑 애플리케이션(Swing, AWT)을 macOS에서 실행할 때 성능이 개선됩니다.

마무리

오늘은 Java 17의 주요 변화를 살펴보았습니다. Records는 불변 데이터 클래스 작성의 부담을 없애주었고, Sealed Classes는 타입 계층을 닫힌 집합으로 설계할 수 있는 수단을 주었습니다. Pattern Matching for instanceof는 타입 확인과 변환을 하나로 합쳤고, Text Block은 여러 줄 문자열을 읽기 좋게 만들었습니다.

이 기능들은 단독으로도 유용하지만, Java 21의 Record Patterns, Pattern Matching for switch와 조합될 때 더 강력해집니다.

반응형

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

Java 25  (0) 2026.05.29
Java 21  (1) 2026.05.29
Java 11  (0) 2026.05.29
Java 8  (0) 2026.05.29
Java란 무엇일까?  (0) 2026.05.29