avatar
Octoping Blog

record에서 배열 사용하지 않기

Java
3 months ago
·
5 min read

이전에 Java에서 불변 데이터 객체를 만들기 위해서는 여러 보일러플레이트들이 많이 필요했다. 그리고 이런 불편을 해소하기 위해 JDK 14부터 record라는 녀석이 추가되었다.

getter, equals, hashcode, toString 등을 자동으로 만들어주어, 불변 데이터 객체를 쉽게 만들 수 있게 해주는 효자같은 존재다.

하지만 이런 record를 사용할 때 조심해야 할 점이 있다.

record에 배열을 쓸 때 조심해야한다

public record Person(
    String[] names,
    int age
) {
}

바로 다음과 같이 레코드의 구성요소에 배열(array)이 있을 경우이다.

레코드의 구성요소에 배열이 존재할 경우, 레코드가 보일러플레이트들을 자동으로 생성해줌에도 불구하고 직접 equals, hashcode, toString을 재정의해주어야 한다.

https://rules.sonarsource.com/java/?search=Equals%20method%20should%20be%20overridden%20in%20records%20containing%20array%20fields

실제로 SonarQube에도 관련된 이슈가 major 단계로 등록되어 있다.

그렇다면 이제 왜 이런 이슈가 존재하는지 그 이유를 알아보자.

record에 배열을 쓸 때 조심해야 하는 이유

record의 equals 메소드 작동 방식

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Record.html

오라클의 Record 명세를 살펴보자.

until-1346

레코드의 equals 메소드는 해당 레코드의 모든 매개변수 인스턴스가 상대 인스턴스와 동등할 때 true를 반환한다.

레코드 클래스의 동등성(Equality)을 비교하는 방법은 다음과 같다.

  • 레코드 구성요소 c가 참조 타입 (reference type)일 경우, Objects.equals(this.c, r.c)의 결과로 판단한다

  • 레코드 구성요소 c가 원시 타입일 경우, 래퍼 클래스로 바꿔서 Integer.compare(this.c, r.c)와 같이 compare의 결과로 판단한다.

until-1347

배열(array)은 참조 타입이므로 Objects::equals의 결과로 동등성을 판단하게 되고, 배열은 참조가 같을 경우에만 true를 반환하기 때문에 레코드의 equals 연산은 우리가 바라던 대로 흘러가지 않게 된다.

그리고 이는 hashcode, toString에도 똑같은 논리로 적용되어 원치 않는 결과를 만들어낼 수 있다..

해결법

1. 직접 equals, hashcode, toString을 재정의한다

문제에 대한 직관적인 해답이다. 직접 배열에 대응하도록 이 셋을 재정의해주어야 한다.

public record Person(
    String[] names,
    int age
) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Arrays.equals(names, person.names);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(age);
        result = 31 * result + Arrays.hashCode(names);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "names=" + Arrays.toString(names) +
                ", age=" + age +
                '}';
    }
}

하지만 이렇게 되면 코드가 굉장히 복잡해진다. 또, 자동으로 생성해주는 함수들을 직접 또 만들어야 하니 사실상 Record를 사용하는 맛이 없다.

2. 배열 대신 List를 사용한다

훨씬 간단하고 영리한 방법이다.

Objects::equals를 제대로 지원하지 않는 배열 대신 List를 사용하면 된다.

public record Person(
    List<String> names,
    int age
) {
}
until-1348


List 최고!







반갑습니다 😄