Java함수형과 현대 Java

record와 instanceof 패턴 매칭

`record`가 어떻게 값 객체 보일러플레이트를 없애는지, `instanceof`의 패턴 매칭이 cast 코드를 어떻게 줄이는지 정리합니다.

마지막 수정 2026년 3월 27일

기본 패턴

java
// record — 한 줄로 불변 값 객체
record Point(int x, int y) {}

Point p = new Point(3, 4);
System.out.println(p.x());        // 3 — 접근자 자동 생성
System.out.println(p);            // Point[x=3, y=4] — toString 자동

// instanceof 패턴 매칭 — 타입 검사 + 변수 선언 한 번에
Object obj = "hello";
if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // cast 없이 바로 사용
}

설명

record — 보일러플레이트 없는 불변 값 객체

record는 데이터를 담기 위한 클래스를 만들 때 생성자, 접근자, equals, hashCode, toString을 자동으로 생성합니다. DTO, 좌표, 범위처럼 "데이터 묶음"이 목적인 클래스에서 반복적인 코드를 없애줍니다.

record의 필드는 모두 final이므로 생성 이후 변경할 수 없습니다. 불변이기 때문에 다중 스레드 환경에서 안전하게 공유할 수 있습니다.

java
// ❌ 전통 방식 — 보일러플레이트 반복
class Point {
    private final int x;
    private final int y;
    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) { ... }
    @Override public int hashCode() { ... }
    @Override public String toString() { ... }
}

// ✅ record — 동일한 기능을 한 줄로
record Point(int x, int y) {}

// 컴팩트 생성자로 유효성 검사 추가 가능
record Range(int min, int max) {
    Range {
        if (min > max) throw new IllegalArgumentException("min > max");
    }
}

instanceof 패턴 매칭 — 타입 검사와 cast를 한 번에

Java 16 이전에는 instanceof로 타입을 확인한 뒤 별도로 cast해야 했습니다. 패턴 매칭은 타입 검사와 변수 선언을 한 문장으로 합칩니다. if 블록 안에서 cast 없이 바로 타입별 메서드를 쓸 수 있습니다.

java
// ❌ 전통 방식 — instanceof + 별도 cast
Object obj = getShape();
if (obj instanceof Circle) {
    Circle c = (Circle) obj; // 이미 확인했는데 또 cast
    System.out.println(c.radius());
}

// ✅ 패턴 매칭 — 한 번에 처리
if (obj instanceof Circle c) {
    System.out.println(c.radius()); // c는 Circle 타입으로 바로 사용
}

// switch 패턴 매칭 (Java 21+) — 타입별 분기
String describe = switch (obj) {
    case Circle c    -> "원, 반지름 " + c.radius();
    case Rectangle r -> "사각형, 넓이 " + r.area();
    case null        -> "null";
    default          -> "알 수 없는 도형";
};

record의 제약 — 상속 불가, 불변만

record는 암묵적으로 java.lang.Record를 상속하므로 다른 클래스를 extends할 수 없습니다. 또 모든 필드가 final이므로 가변 상태를 담을 수 없습니다. 이 두 제약이 불편하면 일반 클래스를 사용해야 합니다.

java
// ❌ record는 상속 불가
record Child(int value) extends Parent {} // 컴파일 오류

// ❌ record는 가변 필드 불가
record Counter(int count) {
    void increment() { this.count++; } // 컴파일 오류 — final
}

// ✅ 가변 상태가 필요하면 일반 클래스
class Counter {
    private int count;
    void increment() { count++; }
}

// ✅ record는 인터페이스 구현 가능
record Point(int x, int y) implements Comparable<Point> {
    @Override
    public int compareTo(Point other) {
        return Integer.compare(this.x, other.x);
    }
}

빠른 정리

상황선택
불변 데이터 묶음 (DTO, 값 객체)record
가변 상태 필요일반 class
상속이 필요일반 class
타입 검사 후 해당 타입 메서드 사용instanceof 패턴 매칭
여러 타입 분기switch 패턴 매칭 (Java 21+)

주의할 점

record의 접근자는 필드 이름과 동일한 메서드입니다. getX()가 아니라 x()입니다. JavaBeans 규약(getX())을 기대하는 프레임워크(일부 직렬화 라이브러리, 구버전 Spring 등)에서 호환 문제가 생길 수 있습니다.

java
record User(String name, int age) {}

User u = new User("Kim", 30);

// ✅ record 접근자 — 필드명 그대로
String name = u.name(); // "Kim"
int age = u.age();      // 30

// ❌ JavaBeans 스타일은 자동 생성되지 않음
u.getName(); // 컴파일 오류 — 메서드 없음

참고 링크

2 sources