본문 바로가기
Java

[Java] Java14 레코드(Record)를 알아보자

by 곰민 2023. 2. 19.
728x90

Java 8 , 11 버전만 사용하다 보니 이후 버전에 추가된 것들에 대해서 학습을 하려고 합니다.
Java 14에서 추가된 레코드(Record)에 대해서 레코드의 목적, 자동생성 항목 등 레코드의 기본 사항에 대해 살펴보고 제약사항은 또 어떤 부분이 있는지 알아보도록 하겠습니다.

baeldung에 상당 부분 잘 정리가 되어있기 때문에 해당글을 대부분 참조하였으며 추가적인 정보를 더해 글을 작성했습니다.

 

java record

 

 

객체 간에 변경 불가능한 데이터를 전달 시 Java 14 이전에는 boilerplate field와 메서드가 포함된 클래스를 생성해야 했기 때문에 사소한 실수가 발생하거나, 의도가 혼동되기 쉬웠습니다.

Java 14가 출시되면서 이제 레코드를 사용하여 이러한 문제를 해결할 수 있습니다.

레코드는 Java14에서 preview로 나왔고 Java16에서 정식 기능으로 포함되었습니다.

 

불변 데이터 객체 예시

Person Class 

public class Person {

    private final String name;
    private final String address;

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Person)) {
            return false;
        } else {
            Person other = (Person) obj;
            return Objects.equals(name, other.name)
              && Objects.equals(address, other.address);
        }
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", address=" + address + "]";
    }

    // standard getters
}

 

문제점

 

두 가지 문제가 있습니다

 

1. boilerplate code가 많습니다.

각 데이터 클래스에 대해 동일한 지루한 프로세스를 반복해야 합니다
(equals, hashCode 및 toString 메서드를 생성하고, 각 필드를 받아들이는 생성자를 생성해야 합니다.)

IDE는 이러한 많은 클래스를 자동으로 생성할 수 있지만, 새 필드를 추가할 때 클래스를 자동으로 업데이트하지는 못합니다.

ex) 새 필드를 추가하면 이 필드를 통합하기 위해 equals 메서드를 업데이트해야 합니다.

 

2. 이름과 주소가 있는 사람을 표현한다는 클래스의 목적이 모호해집니다.

추가 코드로 인해 클래스가 단순히 이름과 주소라는 두 개의 String 필드가 있는 데이터 클래스라는 사실이 모호해집니다.

 

더 나은 방법은 클래스가 데이터 클래스임을 명시적으로 선언하는 것입니다.

JDK 14부터는 반복적인 데이터 클래스를 레코드로 대체할 수 있습니다.

레코드는 필드 유형과 이름만 필요한 불변 데이터 클래스입니다.

equals, hashcode, tostring 메서드와 private, final field, public constructor는 Java 컴파일러에 의해 생성됩니다.

 

Person record를 생성하기 위해서는 record keyword만 사용하면 됩니다.

public record Person (String name, String address) {}

 

Constructor


record를 생성하면 각 필드에 argument가 있는 public constructor가 생성됩니다.

public Person(String name, String address) {
    this.name = name;
    this.address = address;
}

 

이 생성자는 클래스와 같은 방식으로 레코드로부터 객체를 인스턴스화하는 데 사용할 수 있습니다

Person person = new Person("John Doe", "100 Linda Ln.");

 

Getter


또한 필드 이름과 이름이 일치하는 Getter 메서드도 사용할 수 있습니다.

Person 레코드에서는 name() 및  address() getter를 의미합니다

@Test
public void givenValidNameAndAddress_whenGetNameAndAddress_thenExpectedValuesReturned() {
    String name = "Gom";
    String address = "55 GomeHouse";

    Person person = new Person(name, address);

    assertEquals(name, person.name());
    assertEquals(address, person.address());
}

 

equals()


또한 equals 메서드가 생성됩니다.

이 메서드는 제공된 객체의 유형이 동일하고 모든 필드 값이 일치하는 경우 true를 반환합니다

@Test
public void givenSameNameAndAddress_whenEquals_thenPersonsEqual() {
    String name = "Gome";
    String address = "50 GomeHouse";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertTrue(person1.equals(person2));
}

 

두 Person 인스턴스 간에 필드 중 하나라도 다른 경우 같음 메서드는 false를 반환합니다.

 

hashcode


같음 메서드와 유사하게 해당 해시코드 메서드도 생성됩니다.

두 개체의 필드 값이 모두 일치하는 경우 두 Person 개체에 대해 동일한 값을 반환하는 hashCode 메서드를 생성합니다

@Test
public void givenSameNameAndAddress_whenHashCode_thenPersonsEqual() {
    String name = "Gome";
    String address = "50 GomeHouse";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertEquals(person1.hashCode(), person2.hashCode());
}

필드 값 중 하나라도 다르면 해시코드 값이 달라집니다.

 

toString


마지막으로, 레코드의 이름과 각 필드의 이름 및 대괄호 안의 해당 값이 포함된 문자열을 반환하는 toString 메서드도 수신합니다.

따라서 이름이 "Gom"이고 주소가 "50 GomeHouse"인 Person을 인스턴스화하면 다음과 같은 toString 결과가 반환됩니다.

Person[name=Gom, address=50 GomeHouse.]

 

컴팩트 생성자 (Compact Constructor)


🤔컴팩트 생성자는 무엇일까요?

If you want your record's constructor to do more than initialize its private fields, you can define a custom constructor for the record. However, unlike a class constructor, a record constructor doesn't have a formal parameter list; this is called a compact constructor.

 

oracle에서는 이렇게 말합니다.

레코드의 생성자가 비공개 필드를 초기화하는 것 이상의 작업을 수행하도록 하려면 레코드에 대한 사용자 지정 생성자를 정의할 수 있습니다. 

그러나 클래스 생성자와 달리 레코드 생성자에는 공식적인 매개변수 목록이 없으며, 이를 컴팩트 생성자라고 합니다.

이 사용자 정의는 validation에 사용하기 위한 것이며 가능한 한 간단하게 유지해야 합니다.

예를 들어 다음 생성자를 구현하여 개인 레코드에 제공된 이름과 주소가 null이 아닌지 확인할 수 있습니다.

 

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
}

 

다른 argument 목록을 제공하여 다른 argument를 가진 새로운 생성자를 생성할 수도 있습니다

public record Person(String name, String address) {
    public Person(String name) {
        this(name, "Unknown");
    }
}

 

클래스 생성자와 마찬가지로 필드를 참조할 수 있습니다.

생성된 public 생성자와 동일한 argument를 사용하여 생성자를 생성하는 것도 유효하지만
각 필드를 수동으로 초기화해야 합니다.

public record Person(String name, String address) {
    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

 

컴팩트한 생성자와 생성된 생성자와 일치하는 인수 목록을 가진 생성자를 선언하면 컴파일 오류가 발생합니다.

 

public record CompactCon(int component1, String component2) {
    public CompactCon {
        // Body of the constructor goes here
    }
}

 

따라서 다음은 컴파일되지 않습니다

 

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
    
    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

 

Static Variables & Method


일반 Java 클래스와 마찬가지로 정적 변수와 메서드도 레코드에 포함할 수 있습니다.

정적 변수는 클래스와 동일한 구문을 사용하여 선언합니다

 

public record Person(String name, String address) {
    public static String UNKNOWN_ADDRESS = "Unknown";
}

 

마찬가지로 클래스와 동일한 구문을 사용하여 정적 메서드를 선언합니다

 

public record Person(String name, String address) {
    public static Person unnamed(String address) {
        return new Person("Unnamed", address);
    }
}

 

그런 다음 레코드 이름을 사용하여 정적 변수와 정적 메서드를 모두 참조할 수 있습니다

 

Person.UNKNOWN_ADDRESS
Person.unnamed("50 GomeHouse");

 

Record  제약 사항


 

Record는 그렇다면 어떤 제약 사항들을 갖고 있을까요?

JEP359에서는

 

Records cannot extend any other class, and cannot declare instance fields other than the private final fields which correspond to components of the state description. Any other fields which are declared must be static. These restrictions ensure that the state description alone defines the representation.
Records are implicitly final, and cannot be abstract. These restrictions emphasize that the API of a record is defined solely by its state description, and cannot be enhanced later by another class or record.
The components of a record are implicitly final. This restriction embodies an immutable by default policy that is widely applicable for data aggregates.
Beyond the restrictions above, records behave like normal classes: they can be declared top level or nested, they can be generic, they can implement interfaces, and they are instantiated via the new keyword. The record's body may declare static methods, static fields, static initializers, constructors, instance methods, and nested types. The record, and the individual components in a state description, may be annotated. If a record is nested, then it is implicitly static; this avoids an immediately enclosing instance which would silently add state to the record.

 

레코드는 다른 클래스를 상속받을 수 없으며,  private final fields 이외의 인스턴스 필드를 선언할 수 없습니다.
선언되는 다른 모든 필드는 static 이어야 합니다.

이러한 제약 사항은 상태 설명만으로 표현을 정의할 수 있도록 보장해 줍니다.

레코드는 암시적으로 fianl이며 abstract 일 수 없습니다.

이러한 제약 사항은 레코드의 API가 상태 설명에 의해서만 정의되며 나중에 다른 클래스나 레코드에 의해 변경될 or 나아질 수 없음을 강조합니다.

위의 제한 사항 외에도 레코드는 최상위 레벨로 선언되거나 중첩될 수 있고, 제네릭이 될 수 있으며, 인터페이스를 구현할 수 있고, 새 키워드를 통해 인스턴스화되는 등 일반 클래스처럼 작동합니다. 

레코드 body는 static 메서드, static 필드, static 이니셜라이저, 생성자, 인스턴스 메서드 및 중첩 타입을 선언할 수 있습니다. 
중첩 타입 - static nested class, inner class

레코드가 중첩된 경우 암시적으로 static 이므로 레코드에 상태를 자동으로 추가하는 인스턴스를 즉시 둘러싸는 것을 방지할 수 있습니다.

 

참조


Java 14 Record Keyword | Baeldung

What are Java Records and How to Use them Alongside Constructors and Methods? - GeeksforGeeks

tutorials/core-java-modules/core-java-14 at master · eugenp/tutorials

https://docs.oracle.com/en/java/javase/14/language/records.html

https://openjdk.org/jeps/359

728x90

댓글