Java Record: 간결하고 깔끔한 데이터 모델링의 새로운 클래스


Java 14에서 등장한 record는 데이터를 다루는 방식을 한 단계 업그레이드해주는 멋진 기능입니다. 데이터를 표현하기 위해 매번 반복적으로 작성하던 코드들(생성자, getter, toString(), equals() 등)을 자동으로 처리해주기 때문에, 개발자는 데이터의 본질에만 집중할 수 있게 해줍니다.

하지만 record가 모든 상황에서 완벽한 해결책은 아닙니다. Redis 캐싱과 같은 특정 상황에서는 문제가 발생할 수 있고, 상속을 지원하지 않는다는 점도 알아두어야 합니다. 이번 글에서는 record의 장점과 단점, 그리고 Redis 캐싱 문제와 상속 제한에 대해 부드럽게 풀어보겠습니다.


1. Java Record란?

record는 데이터를 표현하기 위해 설계된 특별한 클래스입니다. 기존에는 데이터를 저장하려면 필드, 생성자, getter, toString(), equals(), hashCode() 같은 메서드를 일일이 작성해야 했지만 record를 사용하면 이런 반복 작업을 모두 생략할 수 있습니다. 간단히 말해, 데이터를 깔끔하게 표현하기 위한 데이터 중심 클래스라고 할 수 있습니다.

Record의 특징

  • 불변성(Immutable): record로 만든 객체는 생성 후 값을 변경할 수 없습니다. 모든 필드는 final로 선언됩니다.
  • 자동 생성 메서드: 생성자, toString(), equals(), hashCode() 같은 메서드가 자동으로 만들어집니다.
  • 간결한 문법: 데이터를 표현하는 데 필요한 코드가 최소화됩니다.

2. Record의 기본 사용법

record를 정의하는 방법은 정말 간단합니다. 예를 들어, 사람(Person)을 표현하는 클래스를 만들어보죠.

public record Person(String name, int age) {}

이 한 줄로 다음과 같은 작업이 자동으로 처리됩니다:

  1. nameage라는 두 개의 필드가 생성됩니다(private final로 선언).
  2. 모든 필드를 초기화하는 생성자가 만들어집니다.
  3. 각 필드에 대한 getter 메서드(name(), age())가 생성됩니다.
  4. toString(), equals(), hashCode() 메서드도 자동으로 생성됩니다.

사용 예제

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 25);

        // Getter 메서드 호출
        System.out.println(person.name()); // Alice
        System.out.println(person.age());  // 25

        // toString() 호출
        System.out.println(person); // Person[name=Alice, age=25]

        // 불변성 확인
        // person.name = "Bob"; // 컴파일 에러: 필드는 final로 선언됨
    }
}


3. Record의 장점

3.1. 보일러플레이트 코드 제거

기존에는 데이터를 표현하기 위해 필드, 생성자, getter, toString(), equals(), hashCode() 등을 직접 작성해야 했습니다. 하지만 record는 이런 반복 작업을 자동으로 처리해주기 때문에, 코드가 훨씬 간결해지고 가독성이 좋아집니다.

기존 클래스와 Record 비교

기존 클래스:

public class Person {
    private final String name;
    private final int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + 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 && Objects.equals(name, person.name);
    }

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

Record:

public record Person(String name, int age) {}

어떤가요? 훨씬 깔끔하죠?

3.2. 불변성 보장

record는 모든 필드를 final로 선언하므로, 객체가 생성된 이후에는 값을 변경할 수 없습니다. 이는 데이터 무결성을 유지하는 데 유리하며, 멀티스레드 환경에서도 안전합니다.

3.3. 간결한 데이터 모델링

record는 데이터를 저장하고 전달하는 데 초점이 맞춰져 있습니다. 복잡한 비즈니스 로직이 없는 단순한 데이터 구조를 표현하기에 딱 적합합니다.


4. Record의 단점

4.1. 상속 불가: 왜 Record는 상속을 지원하지 않을까?

record는 상속을 지원하지 않습니다. 이는 record가 암묵적으로 final로 선언되기 때문인데요, 상속을 허용하지 않는 이유는 다음과 같습니다:

  1. 불변성 보장 상속을 허용하면 하위 클래스에서 필드를 추가하거나 메서드를 오버라이드하여 불변성을 깨뜨릴 가능성이 생깁니다. 예를 들어, 하위 클래스에서 toString()이나 equals()를 오버라이드하면 record의 데이터 중심 설계가 훼손될 수 있습니다.
  2. 자동 생성 메서드의 일관성 유지 record는 생성자, toString(), equals(), hashCode() 메서드를 자동으로 생성합니다. 상속을 허용하면 하위 클래스에서 이러한 메서드를 오버라이드하거나 변경할 수 있으므로, record의 일관성이 깨질 수 있습니다.
  3. 단순성과 명확성 record는 데이터를 표현하기 위한 단순하고 명확한 구조를 제공하는 데 초점이 맞춰져 있습니다. 상속을 허용하면 복잡한 계층 구조가 생길 수 있고, 이는 record의 설계 철학과 어긋납니다.

4.2. Redis 캐싱 문제

record를 사용할 때 Redis 캐싱에서 직렬화/역직렬화 문제가 발생할 수 있습니다. Redis와 같은 캐싱 라이브러리는 객체를 역직렬화할 때 기본 생성자를 사용하는데, record는 기본 생성자를 제공하지 않기 때문입니다.

문제 상황

다음과 같은 record를 Redis 캐싱에 사용한다고 가정해봅시다:

public record PostDto(
        long postId,
        long userId,
        String title,
        String content,
        int likes
) {
}

이 경우, Redis는 기본 생성자가 없다는 이유로 직렬화/역직렬화 과정에서 에러를 발생시킬 수 있습니다:

Could not read JSON: Unexpected token (START_OBJECT), expected START_ARRAY: need Array value to contain `As.WRAPPER_ARRAY` type information for class java.lang.Object

해결 방법

  • 일반 클래스 사용 record 대신 일반 클래스를 사용하고, Lombok을 활용해 보일러플레이트 코드를 줄이는 방법이 있습니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostDto {
    private long id;
    private long postId;
    private String title;
    private String content;
    private int likes;
}

5. 결론

Java의 record는 데이터를 표현하기 위한 간결하고 강력한 도구입니다. 불변성을 기본으로 제공하며, 보일러플레이트 코드를 제거해 코드의 가독성과 유지보수성을 크게 향상시킵니다. 특히, record를 사용하면 해당 클래스가 불변 객체라는 것을 직관적으로 알 수 있어 코드의 의도를 명확히 전달할 수 있습니다. 또한, 코드가 간결해지기 때문에 작성과 유지보수가 훨씬 수월해지는 장점이 있습니다.

하지만 상속을 지원하지 않으며, Redis 캐싱과 같은 특정 상황에서는 문제가 발생할 수 있습니다.

record는 데이터 중심의 단순한 구조를 간결하게 표현할 때 유용하며, 불변 객체를 쉽게 정의할 수 있는 장점이 있습니다. 하지만 프로젝트 요구사항에 따라 일반 클래스나 Lombok을 사용하는 것이 더 적합한 경우도 있습니다.

예를 들어, 상속이 필요한 경우 record는 사용할 수 없으며, 복잡한 도메인 모델에서는 일반 클래스가 더 적합합니다. 또한, 비즈니스 로직 상에서 중간에 데이터를 변경(set)해야 하는 경우에도 record는 불변 객체이기 때문에 데이터 변경이 불가능해 데이터를 변경해야 하는 상황에서는 일반 클래스나 Lombok을 사용하는 것이 더 유리합니다.

추가로, 대기업이나 레거시 시스템에서는 Java 1.8이나 11을 여전히 많이 사용하기 때문에, Java 16 이상에서만 지원되는 record를 사용할 수 없는 환경도 많습니다.

결론적으로, record는 단순한 데이터 구조와 불변 객체에 적합하지만, 프로젝트의 복잡성, 데이터 변경 요구사항, 또는 환경에 따라 적절히 선택해야 합니다.

jeewoo jung 아바타