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) {}
이 한 줄로 다음과 같은 작업이 자동으로 처리됩니다:
name
과age
라는 두 개의 필드가 생성됩니다(private final
로 선언).- 모든 필드를 초기화하는 생성자가 만들어집니다.
- 각 필드에 대한 getter 메서드(
name()
,age()
)가 생성됩니다. 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
로 선언되기 때문인데요, 상속을 허용하지 않는 이유는 다음과 같습니다:
- 불변성 보장 상속을 허용하면 하위 클래스에서 필드를 추가하거나 메서드를 오버라이드하여 불변성을 깨뜨릴 가능성이 생깁니다. 예를 들어, 하위 클래스에서
toString()
이나equals()
를 오버라이드하면record
의 데이터 중심 설계가 훼손될 수 있습니다. - 자동 생성 메서드의 일관성 유지
record
는 생성자,toString()
,equals()
,hashCode()
메서드를 자동으로 생성합니다. 상속을 허용하면 하위 클래스에서 이러한 메서드를 오버라이드하거나 변경할 수 있으므로,record
의 일관성이 깨질 수 있습니다. - 단순성과 명확성
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
는 단순한 데이터 구조와 불변 객체에 적합하지만, 프로젝트의 복잡성, 데이터 변경 요구사항, 또는 환경에 따라 적절히 선택해야 합니다.