2103 단어
11 분
[ Java ] Record

이번 포스팅에서는 자바 17버전에 정식으로 들어오게 된 record 클래스를 살펴보려 한다.

record 클래스란?#

record 클래스는 데이터를 보다 간결하게 표현하기 위해 도입된 Java의 특별한 클래스다. 일반적인 클래스를 작성할 때는 필드, 생성자, equals(), hashCode(), toString() 등의 메서드를 직접 정의해야 하지만, record 클래스를 사용하면 이러한 과정을 생략하고도 동일한 기능을 갖춘 클래스를 선언할 수 있다.

예를 들어, Rectangle(double length, double width)라는 레코드 클래스를 선언하면, Java 컴파일러가 lengthwidth를 필드로 가지는 private final 변수와, 이를 반환하는 접근자 메서드, 그리고 equals(), hashCode(), toString()을 자동으로 생성해준다.

예를 들어, 다음과 같은 record 클래스가 있다고 하자.

record Rectangle(double length, double width) { }

이 클래스는 다음 코드와 동일하다.

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // equals() 및 hashCode() 메서드는 동일한 타입이고
    // 같은 값을 가진 필드를 포함하는 경우 객체를 동일하다고 판단한다.
    public boolean equals(...) { ... }
    public int hashCode(...) { ... }

    // toString() 메서드는 모든 필드 이름과 값을 포함한 문자열을 반환한다.
    public String toString() { ... }
}

record 클래스는 기본적으로 “데이터를 담는 용도”로 설계되었기 때문에 불변(immutable)하게 동작하며, 모든 필드는 final로 선언된다. 따라서 한 번 생성된 record 객체의 필드는 변경할 수 없다. 이를 통해 불변성을 보장하면서도 간결한 데이터 모델을 만들 수 있다.

record 클래스의 주요 특징#

record 클래스의 선언 방식은 일반적인 클래스와 다르다. 기본적으로 다음과 같은 구성 요소를 포함한다.

  • 클래스 이름
  • (선택 사항) 제네릭 타입 매개변수
  • 헤더(레코드 컴포넌트 목록)
  • (선택 사항) 본문

record 클래스를 선언하면 다음과 같은 요소가 자동으로 생성된다.

  1. 각 컴포넌트에 대한 멤버
    • private final 필드가 생성된다.
    • 필드 값을 반환하는 public 접근자 메서드가 자동 생성된다.
  2. 자동 생성되는 메서드
    • 컴포넌트 값을 필드에 할당하는 정식 생성자(Canonical Constructor)
    • 같은 타입이며 값이 동일한 두 객체를 동일하다고 판단하는 equals()
    • 객체를 해시 코드 값으로 변환하는 hashCode()
    • 객체의 필드 값을 문자열로 변환하는 toString()

record 클래스는 final#

record 클래스는 암묵적으로 final로 선언되기 때문에 다른 클래스가 이를 상속할 수 없다. 이는 record 클래스의 불변성을 유지하고 데이터 모델을 단순하게 유지하기 위한 설계다. 예를 들어, 다음과 같은 코드는 허용되지 않는다.

record Parent(int value) { }
class Child extends Parent { } // 오류 발생

record 클래스의 생성자#

record 클래스의 생성자는 자동으로 생성되지만, 필요에 따라 직접 선언할 수도 있다. 예를 들어, lengthwidth가 0 이하일 경우 예외를 발생시키도록 생성자를 선언할 수 있다.

record Rectangle(double length, double width) {
    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
        this.length = length;
        this.width = width;
    }
}

하지만 record 클래스에서는 보다 간결한 형태의 **압축 생성자(Compact Constructor)**를 사용할 수도 있다. 이 경우 생성자의 매개변수를 명시적으로 선언하지 않아도 된다.

record Rectangle(double length, double width) {
    public Rectangle {
        if (length <= 0 || width <= 0) {
            throw new IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
    }
}

압축 생성자를 사용하면 필드 값이 자동으로 초기화되므로 this.length = length;와 같은 코드가 필요 없다.

record 클래스에서 명시적인 메서드 정의#

자동으로 생성되는 접근자 메서드나 equals(), hashCode(), toString() 등의 메서드를 직접 재정의할 수도 있다. 예를 들어, length() 메서드를 직접 구현하여 값을 출력하는 기능을 추가할 수도 있다.

record Rectangle(double length, double width) {
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}

만약 equals(), hashCode(), toString()을 직접 구현하는 경우, Java에서 기본 제공하는 것과 동일한 기능을 유지하도록 작성하는 것이 좋다.

record 클래스의 정적(static) 요소#

record 클래스에서는 정적 필드, 정적 메서드, 정적 초기화 블록을 사용할 수 있다. 예를 들어, Rectangle 클래스에 황금 비율(Golden Ratio)을 사용하는 정적 필드를 추가하고, 이를 활용하는 정적 메서드를 구현할 수 있다.

record Rectangle(double length, double width) {
    
    static double goldenRatio;

    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}

하지만 record 클래스에서는 인스턴스 변수(비-정적 필드)나 인스턴스 초기화 블록을 사용할 수 없다.

record Rectangle(double length, double width) {
    BiFunction<Double, Double, Double> diagonal; // 오류 발생

    {
        diagonal = (x, y) -> Math.sqrt(x * x + y * y); // 오류 발생
    }
}

record 클래스에서 인터페이스 구현#

record 클래스는 인터페이스를 구현할 수 있다.

record Customer(String name) implements Billable { }

중첩 record 클래스와 로컬 record 클래스#

record 클래스 내부에는 중첩된 record 클래스를 선언할 수도 있다.

record Rectangle(double length, double width) {
    record RotationAngle(double angle) {
        public RotationAngle {
            angle = Math.toRadians(angle);
        }
    }
}

또한, 메서드 내부에서 로컬 record 클래스를 선언하여 사용할 수도 있다. 예를 들어, 특정 월의 판매량을 집계하는 로컬 record 클래스를 선언하여 코드의 가독성을 높일 수 있다.

record Merchant(String name) { }
record Sale(Merchant merchant, LocalDate date, double value) { }

public class MerchantExample {
    List<Merchant> findTopMerchants(List<Sale> sales, List<Merchant> merchants, int year, Month month) {

        record MerchantSales(Merchant merchant, double sales) {}

        return merchants.stream()
            .map(merchant -> new MerchantSales(
                merchant, this.computeSales(sales, merchant, year, month)))
            .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
            .map(MerchantSales::merchant)
            .collect(Collectors.toList());
    }
}

로컬 record 클래스는 자동으로 static이므로 메서드 내부의 지역 변수에 접근할 수 없다.

record 클래스와 JavaBeans 패턴 비교#

기존 JavaBeans 패턴에서는 필드 값을 저장하기 위해 getter/setter 메서드를 직접 구현해야 했다. 하지만 레코드 클래스는 이를 자동으로 생성하며, 불변성을 유지한다.

  • JavaBeans 패턴 예시
public class Rectangle {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    public double getLength() { return length; }
    public void setLength(double length) { this.length = length; }
}
  • record 클래스 사용 시
record Rectangle(double length, double width) { }

위와 같이 getter/setter 없이도 데이터를 간결하게 관리할 수 있다.

record 클래스의 직렬화(Serialization)#

record 클래스는 Serializable 인터페이스를 구현하면 직렬화할 수 있지만, 기존 클래스에서 writeObject() 또는 readObject() 메서드를 직접 정의하여 커스터마이징하는 것은 불가능하다. 대신, 역직렬화할 때는 record 클래스의 정식 생성자(canonical constructor) 가 자동으로 호출된다.

import java.io.*;

record Rectangle(double length, double width) implements Serializable {}

public class RecordSerializationDemo {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(5.0, 10.0);

        // 객체 직렬화 (Serialization)
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangle.ser"))) {
            oos.writeObject(rect);
            System.out.println("Rectangle 객체가 직렬화되었습니다.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 객체 역직렬화 (Deserialization)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rectangle.ser"))) {
            Rectangle deserializedRect = (Rectangle) ois.readObject();
            System.out.println("Deserialized Rectangle: " + deserializedRect);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

위 코드에서는 Rectangle record 클래스를 Serializable로 선언한 후, ObjectOutputStream을 사용하여 객체를 파일(rectangle.ser)에 직렬화한다. 이후, ObjectInputStream을 이용해 해당 파일에서 객체를 읽어와 역직렬화한다. 이 과정에서 기존의 writeObject()readObject() 메서드를 정의하지 않아도, record 컴포넌트 값을 기반으로 한 정식 생성자가 자동으로 실행되어 원래 객체가 복원된다.

출처#

[ Java ] Record
https://blog-full-of-desire-v3.vercel.app/posts/java/record/java-record/
저자
SpeculatingWook
게시일
2025-03-09
라이선스
CC BY-NC-SA 4.0