[자바] 17. record 패턴 : 구조 분해로 코드 줄이기 (Java 21)

자바 버전 여행 17편 - 구조 분해로 코드 줄이기

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(자바 버전 여행 ⑰)

자바 21은 지금 시점의 최신 LTS다. 여기서 패턴 매칭이 완성되고, 가상 스레드라는 큰 변화가 정식이 된다. 이 모듈에서 하나씩 본다. 첫 편은 record와 패턴 매칭이 만나는 record 패턴이다.

검사하고, 또 꺼내던 일

15편에서 instanceof 패턴으로 캐스팅을 없앴다. 그런데 record라면 한 걸음 더 갈 수 있다. 타입을 확인하는 동시에 그 안의 값까지 분해해서 바로 변수로 받는 것이다.

record Point(int x, int y) {}

// Java 16 패턴 — 타입만 바인딩, 값은 또 꺼냄
if (obj instanceof Point p) {
    int x = p.x(), y = p.y();
    System.out.println(x + y);
}

// Java 21 record 패턴 — x, y로 바로 분해
if (obj instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

중첩도 한 번에 풀린다

record 안에 record가 있으면, 패턴도 그만큼 중첩해서 가장 안쪽 값까지 한 번에 꺼낸다.

record Point(int x, int y) {}
record Line(Point from, Point to) {}

if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
    double len = Math.hypot(x2 - x1, y2 - y1);
    System.out.println("길이 " + len);
}

switch와 만나면 더 강력하다. 타입별로 분기하면서 동시에 값을 분해한다.

String desc = switch (shape) {
    case Circle(double r)           -> "원 반지름 " + r;
    case Rectangle(double w, double h) -> "사각형 " + (w * h);
};

직접 해보기 — 중첩 record를 한 줄로 분해

직접 분해해 보면 이게 왜 편한지 바로 안다.

  1. Point와 Line record를 만든다.
  2. instanceof record 패턴으로 Line의 네 좌표를 한 번에 꺼내 거리(길이)를 구한다.
  3. switch에 record 패턴을 써서 도형 넓이를 계산해 본다.
record Point(int x, int y) {}
record Line(Point from, Point to) {}

static double length(Object o) {
    if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2)))
        return Math.hypot(x2 - x1, y2 - y1);
    return -1;
}
// length(new Line(new Point(0,0), new Point(3,4))) -> 5.0

실행해 보면 이렇게 나온다.

5.0

여기서 조금씩 바꿔 보자.

  • var 대신 구체 타입(int x1)으로 바꿔도 되는지 확인한다.
  • 안쪽 패턴 하나를 타입이 안 맞게 바꿔(예: Point를 String) 컴파일러가 잡는지 본다.

한 걸음 더 — switch에서 record 패턴과 when 가드(다음 편)를 섞으면 어떻게 되는지 미리 상상해 보자.

✅ 확인 — record 패턴이 ‘검사+분해’를 한 번에 한다는 것과, 중첩 분해가 가능하다는 걸 설명할 수 있으면 통과다.

switch와 만나면 더 강력하다

record 패턴은 switch 패턴 매칭과 결합할 때 진가를 낸다. 도형 계층 같은 sealed 타입의 각 경우를 ‘타입 확인 + 값 분해’로 한 번에 처리한다. sealed면 컴파일러가 모든 경우를 다뤘는지까지 검사해준다.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}

double area = switch (shape) {
    case Circle(double r)              -> Math.PI * r * r;
    case Rectangle(double w, double h) -> w * h;
};   // sealed라 default 없이도 컴파일러가 망라성 보장

var로 간결하게, 타입 명시로 안전하게

분해할 때 각 요소의 타입을 int x처럼 명시하거나 var로 추론시킬 수 있다. var는 간결하고, 명시는 의도가 분명하다. 그리고 분해 패턴 안에서 타입 검사도 함께 된다 — case Point(int x, int y)는 x·y가 int일 때만 매치한다. 확인과 추출이 문법 하나에 녹아 있는 셈이다.

실무형으로 — 이벤트 분기와, 컴파일러가 잡아주는 순간

좌표 예제를 실무 모양으로 바꿔보자. 웹훅 이벤트처럼 ‘종류가 정해진 메시지’를 처리하는 코드다.

sealed interface Event permits OrderPlaced, OrderCanceled, PaymentFailed {}
record OrderPlaced(String orderId, int amount) implements Event {}
record OrderCanceled(String orderId, String reason) implements Event {}
record PaymentFailed(String orderId, String code) implements Event {}

String handle(Event e) {
    return switch (e) {
        case OrderPlaced(String id, int amt)    -> notifyNew(id, amt);
        case OrderCanceled(String id, var why)  -> refund(id, why);
        case PaymentFailed(String id, var code) -> retryOrAlert(id, code);
    };
}

여기에 넉 달 뒤 record OrderRefunded(...)를 permits에 추가하면, 위 handle은 그 자리에서 이런 컴파일 에러가 난다.

error: the switch expression does not cover all possible input values
    return switch (e) {
           ^— case OrderRefunded 누락

if-else 사다리였다면 새 이벤트는 조용히 무시되고 운영에서야 발견됐을 것이다. ‘경우를 늘리면 컴파일이 깨진다’는 이 성질이, 이벤트·상태 머신류 코드에서 record 패턴을 쓰는 실질적 이유다.

모양이 정해진 데이터에 특히 강하다

record + sealed + 패턴 매칭은 요즘 자바가 미는 데이터 중심 스타일의 삼각형이다. 데이터를 record로 표현하고, sealed로 경우를 닫고, 패턴으로 분해·분기한다. JSON·이벤트·AST처럼 구조가 확정된 데이터를 다룰 때 특히 깔끔하다. 상속·다형성으로 풀던 걸, 데이터와 그 처리를 분리하는 방식으로 바꾸는 것이다. 이 셋을 함께 익히면 자바 21의 문법이 하나로 꿰인다.

분해가 편해졌으니, 이걸 제대로 쓰는 무대인 switch 패턴 매칭이 다음 편이다. 타입으로 분기하는 완성형 switch를 본다.

자주 묻는 질문 (FAQ)

record 패턴이 뭔가요?

record를 검사하면서 동시에 그 안의 값을 분해해 변수로 받는 기능입니다. if (o instanceof Point(int x, int y))처럼 씁니다.

중첩된 record도 한 번에 분해되나요?

네, 패턴을 중첩하면 가장 안쪽 값까지 한 번에 꺼낼 수 있습니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다