본문 바로가기

Java

Java Record (레코드)

참고:
https://www.baeldung.com/java-15-new

https://www.digitalocean.com/community/tutorials/java-records-class

 

Record는 왜 만들어졌나?

Record는 java 14에서 처음 소개된 새로운 클래스 타입이며, 변경불가(immutable) 데이터 객체를 쉽게 만들수 있게 한다.

Record 타입이 생기기전에는 값이 변경이 불가능한 테이터 객체를 정의해서 사용했다.

변경 불가 데이터 객체를 만들기위 해서는 매번 아래 내용들을 작성해야 했다.

  1. 필드를 private final 로 정의
  2. getter 메소드 작성
  3. 생성자 생성
  4. hashCode, equals, toString 함수 재정의
  5. 클래스의 상속을 막고 싶다면 final class 로 정의

하지만 Record 타입이 생기면서 훨씬더 간단한 방법으로 변경이 불가능한 데이터 객체를 생성할 수 있게 되었다.

// Record 이전
public class PersonClass {
    private final String name;
    private final int age;

    public PersonClass(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

// Record 이후
public record PersonRecord(String name, int age) {
}

 

Record 클래스는...

final 클래스 이기 때문에 상속이 불가능 하다.

암시적으로 java.lang.Record 클래스를 상속한다.

Record에 정의된 모든 필드는 final 속성이다.

Record의 필드는 얕은 변경불가(immutrable)이다. 필드값은 변경이 불가 하지만 필드에 정의된 필드가 속성을 가지는 객체 타입인 경우 필드의 속성값은 변경이 가능하다.

레코드의 모든 필드를 인자로 받는 생성자가 자동으로 생성된다.

Record의 필드는 필드 이름이 함수명인 함수로 접근이 가능하다. (필드에 직접 접근도 가능하다.)

hashCode, equals, toString 함수를 기본으로 제공한다.

@Test
@DisplayName("필드값은 변경이 불가 하지만 필드에 정의된 필드가 속성을 가지는 객체 타입인 경우 필드의 속성값은 변경이 가능하다.")
void recordFieldsAreShallowImmutables() {
    PersonRecord person = new PersonRecord("John", 20, new Contact("02", "010"));

    // person.name = "John2"; // 컴파일 에러
    person.contact.setHome("070");
    assertEquals("070", person.contact.getHome());
}

@Test
@DisplayName("Record의 필드는 필드 이름이 함수명인 함수로 접근이 가능하다. (필드에 직접 접근도 가능하다.)")
void canAcesseRecordFieldsByFieldName() {
    PersonRecord person = new PersonRecord("John", 20, new Contact("02", "010"));

    // 필드명으로 바로 접근 가능 - 가급적이면 함수를 이용하자
    assertEquals("John", person.name);

    // 필드명과 동일한 함수로 접근 - 캡슐화를 위해 함수를 이용하자
    assertEquals(20, person.age());

    assertEquals("02", person.contact().getOffice());
    assertEquals("010", person.contact().getHome());
}

@Test
@DisplayName("toString 테스트")
void toStringTest() {
    SimplePersonRecord person = new SimplePersonRecord("John", 20);
    assertEquals("SimplePersonRecord[name=John, age=20]", person.toString());
}


@Test
@DisplayName("equals, hashCode 테스트")
void equalsAndHashCodeTest() {
    SimplePersonRecord simplePerson = new SimplePersonRecord("John", 20);
    SimplePersonRecord simplePerson2 = new SimplePersonRecord("John", 20);
    assertEquals(simplePerson, simplePerson2);

    Contact defaultContact = new Contact("02", "010");
    PersonRecord person = new PersonRecord("John", 20, defaultContact);
    PersonRecord person2 = new PersonRecord("John", 20, new Contact("02", "010"));

    // person과 person2는 서로 다른 Contact 가진다.
    assertNotEquals(person, person2);

    // person과 person3는 같은 Contact를 가진다.
    PersonRecord person3 = new PersonRecord("John", 20, defaultContact);

    assertEquals(person, person3);
    assertTrue(person.equals(person3));

    assertEquals(person.hashCode(), person3.hashCode());
}

 

생성자

Record 타입이 생성될때 생성자에 유효성 검증이나 로깅 같은 기능을 추가 할 수 있다. 생성자 기능 추가는 아래와 같이 compact 생성자 를 작성 함 으로서 가능해 지며, 이렇게 작성한 compact 생성자는 자동을 생성되는 생성자보다 먼저 수행 된다.

public record SimplePersonRecord(String name, int age) {
    public SimplePersonRecord {
        if (age < 0) {
            throw new IllegalArgumentException("age must be positive");
        }
    }
}
@Test
@DisplayName("Constructor 에 validation 추가")
void constructorValidationTest() {
    assertThrows(IllegalArgumentException.class, () -> new SimplePersonRecord("John", -1));
}

 

함수

Record는 함수를 가질수도 있다. 하지만 Record의 역할은 데이터를 전달하는데 있으므로, 편의기능을 위한 메소드는 레코드에 작성하지 않는 것이 좋다. 혹시 꼭 Record에 함수를 작성해야 하는 상황 이라면, Record를 사용하는 것이 맞는지 생각 해 보는 것이 좋다.

 

반응형