1. 레코드 (record)란?
자바에서 record
는 자바 16부터 정식 기능으로 도입되어, 자바 17에서 계속해서 사용되고 있는 기능입니다. record
는 불변의 데이터 운반 객체를 간단하게 생성할 수 있는 방법을 제공합니다. 이는 주로 데이터를 담기 위한 클래스에 매우 유용하며, 클래스를 정의할 때 필요한 상용구 코드(boilerplate code)의 양을 대폭 줄여줍니다.
2. record의 주요 특징들
2.1 불변성 (Immutability)
레코드는 불변 객체입니다. 레코드의 모든 필드는 final
로 선언되며, 객체가 생성된 후에는 필드 값을 변경할 수 없습니다. 즉, 레코드는 객체의 상태가 절대 바뀌지 않도록 보장합니다.
public record Person(String name, int age) {
// 필드 name과 age는 final로 자동 선언되며, 불변임
// Person 객체가 생성된 후에는 이름과 나이를 변경할 수 없음
}
Person person = new Person("Alice", 25);
// person.name = "Bob"; // 컴파일 에러! 필드를 변경할 수 없음
Person
레코드 객체는 생성된 후 값을 수정할 수 없습니다.- 이런 불변성은 동시성 문제를 방지하고 데이터 무결성을 유지하는 데 도움을 줍니다.
2.2 데이터 운반 (Data Carrier)
레코드는 주로 데이터를 운반하는 역할에 초점이 맞추어져 있습니다. 데이터를 저장하고 전달하기 위해 만들어졌으며, 복잡한 비즈니스 로직보다는 데이터의 저장 및 운반에 집중합니다.
public record Car(String model, String manufacturer) {
// Car 레코드는 데이터를 저장하는 용도로만 사용됨
}
Car car = new Car("Model S", "Tesla");
System.out.println("Car Model: " + car.model()); // Car Model: Model S
- 이 레코드는 단순히 데이터를 담는 역할을 하며, 로직 없이 데이터만 운반하는 객체입니다.
2.3 상용구 코드 감소 (Boilerplate Reduction)
레코드는 자동으로 equals()
, hashCode()
, toString()
메서드를 생성합니다. 이는 개발자가 일일이 메서드를 구현하지 않아도 되도록 상용구 코드를 줄여줍니다.
public record Product(String name, double price) {
// equals(), hashCode(), toString() 메서드가 자동으로 생성됨
}
Product p1 = new Product("Laptop", 1500.0);
Product p2 = new Product("Laptop", 1500.0);
// equals() 자동 비교
System.out.println(p1.equals(p2)); // true, 두 객체는 값이 동일하므로 같음
// toString() 자동 출력
System.out.println(p1); // Product[name=Laptop, price=1500.0]
equals()
,hashCode()
,toString()
메서드를 자동으로 제공하여, 클래스 정의를 단순화합니다.
2.4 패턴 매칭과 호환 (Pattern Matching Compatibility)
레코드는 패턴 매칭 기능과 잘 어울립니다. 패턴 매칭은 주로 switch
구문에서 사용되며, 특정 객체의 타입과 속성을 검사해 그에 따라 행동할 수 있게 해줍니다. 레코드는 이를 더 간결하게 작성할 수 있도록 도와줍니다.
public record Circle(double radius) {}
public record Rectangle(double length, double width) {}
public class ShapeTest {
public static void printShapeInfo(Object shape) {
// switch 구문에서 레코드 패턴 매칭 사용
switch (shape) {
case Circle(double radius) -> System.out.println("Circle with radius: " + radius);
case Rectangle(double length, double width) ->
System.out.println("Rectangle with length: " + length + " and width: " + width);
default -> System.out.println("Unknown shape");
}
}
public static void main(String[] args) {
Circle circle = new Circle(5.0);
Rectangle rectangle = new Rectangle(4.0, 6.0);
printShapeInfo(circle); // Circle with radius: 5.0
printShapeInfo(rectangle); // Rectangle with length: 4.0 and width: 6.0
}
}
- 위 코드는
switch
구문에서 레코드의 필드를 직접 패턴 매칭하여, 각각의 모양에 맞는 값을 추출하고 처리합니다. 레코드는 이러한 패턴 매칭 기능과 결합될 때 코드 가독성과 간결성을 크게 높일 수 있습니다.
2.5 컴팩트한 구문 (Compact Syntax)
레코드를 정의할 때, 클래스 본문을 생략할 수 있으며 필드를 레코드의 헤더에서 선언합니다. 이는 레코드가 데이터만을 다루기 때문에 클래스 본문이 간결하게 유지됩니다.
public record Book(String title, String author) {
// 별도의 메서드나 로직이 필요 없으면 본문을 생략할 수 있음
}
Book book = new Book("Effective Java", "Joshua Bloch");
System.out.println(book); // Book[title=Effective Java, author=Joshua Bloch]
- 클래스 본문을 생략하여 코드를 더 간결하게 만들 수 있습니다.
2.6 Getter 및 Setter 방식의 차이
레코드에서는 getter
가 일반 DTO와는 조금 다릅니다. 일반 클래스에서 getter
는 getName()
과 같은 형태지만, 레코드에서는 필드 이름이 그대로 getter
메서드가 됩니다. 반면, Setter
는 레코드에서 지원하지 않습니다. 레코드는 불변 객체이기 때문에 값을 변경할 수 없으므로 Setter
는 제공되지 않습니다.
public record Employee(String name, int id) {
}
Employee emp = new Employee("John", 1001);
System.out.println(emp.name()); // 일반 DTO에서 getName()에 해당
System.out.println(emp.id()); // 일반 DTO에서 getId()에 해당
// emp.setName("Jane"); // 컴파일 에러! 레코드에는 Setter가 존재하지 않음
- 레코드에서는
name()
과id()
메서드가 자동으로 생성되어,getter
역할을 합니다. Setter
는 불변성을 유지하기 위해 제공되지 않습니다.
3. 불변 데이터 객체 예시
3.1 불변 데이터 객체 구현의 문제점
기존 클래스를 사용하여 불변 데이터 객체를 구현할 때, 불변성을 유지하고 equals()
, hashCode()
, toString()
등의 메서드를 올바르게 처리하기 위해 많은 상용구(boilerplate) 코드를 작성해야 합니다. 이런 문제들은 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만들 수 있습니다.
// 클래스에 final을 붙여 상속을 방지, 불변 객체로 만듦
public final class Person {
// final 필드: 불변 객체를 위해 모든 필드를 final로 선언
private final String name;
private final String address;
// 필드를 초기화하는 생성자
public Person(String name, String address) {
this.name = name;
this.address = address;
}
// equals() 메서드: 객체가 같은지 비교
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true; // 동일한 객체일 경우 true
}
if (!(obj instanceof Person)) {
return false; // 타입이 다르면 false
}
Person other = (Person) obj;
// 모든 필드의 값을 비교하여 동등성 체크
return Objects.equals(name, other.name) && Objects.equals(address, other.address);
}
// hashCode() 메서드: 객체를 고유하게 식별할 수 있는 해시 코드 생성
@Override
public int hashCode() {
return Objects.hash(name, address);
}
// toString() 메서드: 객체의 내용을 출력하는 문자열 반환
@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}
// Getter 메서드: 불변성을 유지하면서 필드에 접근
public String getName() {
return name;
}
public String getAddress() {
return address;
}
}
1) 상용구 코드의 과다
equals()
,hashCode()
,toString()
메서드를 직접 작성해야 하며, 이로 인해 코드의 양이 늘어납니다.- 클래스 목적이 단순히 데이터를 담는 것임에도 불구하고 불필요한 코드가 많아집니다.
2) 자동 업데이트의 부재
- 필드를 추가할 때마다
equals()
,hashCode()
,toString()
등을 수동으로 수정해야 하며, 누락 시 오류가 발생할 수 있습니다.
3) 클래스 목적의 모호성
- 클래스가 데이터 전송 목적임에도 불구하고, 여러 부가적인 메서드들이 포함되면서 핵심 기능이 흐려질 수 있습니다.
3.2 record로 불변 데이터 객체 구현시 특징
위의 문제를 해결하기 위해, Java record
를 사용하면 상용구 코드를 줄이고, 데이터 운반의 목적이 더 명확해집니다. Record
는 자동으로 equals()
, hashCode()
, toString()
메서드를 생성해주며, 불변성을 보장합니다.
public record Person(String name, String address) {
// Record는 자동으로 equals(), hashCode(), toString() 메서드를 생성합니다.
}
1) 간결한 코드
record
는 클래스 본문에 불필요한 상용구 코드를 포함하지 않고, 필드만 선언해도 모든 필요한 메서드가 자동으로 생성됩니다.
2) 자동 업데이트
- 필드를 추가하거나 제거할 경우
equals()
,hashCode()
등은 자동으로 처리되므로, 수동으로 수정할 필요가 없습니다.
3) 클래스 목적의 명확성
- 클래스가 오직 데이터 운반 용도임이 더 명확해집니다. Record는 본질적으로 DTO(데이터 전송 객체)를 대체하는 역할을 합니다.
3.3 일반 클래스와 record
의 equals()
동작 차이
일반 클래스에서는 equals()
메서드를 직접 구현해야 하며, 객체의 필드 값을 비교하도록 코드를 작성해야 합니다. 기본적으로 일반 클래스의 equals()
메서드는 객체의 메모리 주소(참조값)를 기준으로 동등성을 비교하기 때문에, 우리가 원하는 필드 값의 비교를 위해서는 이를 오버라이드하여 커스터마이징 해야 합니다.
1) 일반 클래스의 equals()
동작 방식
public final class ExampleClass { // final 클래스: 상속을 방지하여 불변성을 강화
private final String data; // final 필드: 필드를 변경할 수 없도록 설정
public ExampleClass(String data) {
this.data = data;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true; // 동일한 객체일 때 true
}
if (obj == null || getClass() != obj.getClass()) {
return false; // 타입이 다르거나 null이면 false
}
ExampleClass other = (ExampleClass) obj;
// 필드 값을 비교하여 동등성을 판단
return Objects.equals(this.data, other.data);
}
// hashCode()와 toString()도 필요하지만 생략
}
public class Main {
public static void main(String[] args) {
ExampleClass class1 = new ExampleClass("data1");
ExampleClass class2 = new ExampleClass("data1");
// 두 객체는 다른 메모리 주소에 존재하므로, 기본 equals()는 false를 반환
// 그러나 오버라이드된 equals()는 필드 값이 같으면 true를 반환함
System.out.println(class1.equals(class2)); // true (오버라이드 덕분에 필드 비교)
}
}
- 두
ExampleClass
객체의data
필드는 같지만, 객체는 서로 다른 메모리 주소에 존재합니다. 그래서 기본equals()
메서드를 사용하면 false를 반환합니다. - 그러나, 우리가
equals()
메서드를 오버라이드해서 필드 값을 비교하도록 구현했기 때문에 두 객체는 동등하다고 판단되어true
를 반환합니다.
2) record의 eqauls()
동작 방식
record
에서는 equals()
메서드가 자동으로 오버라이드되며, 모든 필드를 비교하여 동등성을 확인합니다. 개발자는 따로 equals()
메서드를 구현할 필요가 없으며, Record의 모든 필드 값이 자동으로 비교됩니다.
public record ExampleRecord(String data) {
// equals()는 자동으로 필드 data를 비교하는 방식으로 동작
}
public class Main {
public static void main(String[] args) {
ExampleRecord record1 = new ExampleRecord("data1");
ExampleRecord record2 = new ExampleRecord("data1");
// 필드 값이 동일하면 두 객체는 동일하다고 판단함
System.out.println(record1.equals(record2)); // true
}
}
Record
는 필드의 값을 비교하는equals()
메서드를 자동으로 생성해줍니다.- 즉, 개발자가 따로
equals()
메서드를 오버라이드할 필요가 없습니다. 필드 값이 동일한지 자동으로 확인하여 동등성을 비교합니다. - 두
ExampleRecord
객체의data
필드가 동일하므로true
를 반환합니다.
4. 결론
record
는 자바에서 불변 객체를 간결하고 효율적으로 정의할 수 있는 현대적인 기능입니다. 주로 데이터 전달 객체(Data Transfer Object, DTO)나 값 객체(Value Object)와 같이 데이터를 운반하고, 전달하는 데 최적화된 클래스를 만들 때 매우 유용합니다.
4.1 record
의 주요 장점
- 간결한 코드
- 레코드는 자바에서 상용구 코드(boilerplate code)를 대폭 줄여줍니다. 생성자,
equals()
,hashCode()
,toString()
메서드, 그리고 getter 메서드가 자동으로 생성되므로, 개발자는 데이터 필드 정의에만 집중할 수 있습니다.
- 레코드는 자바에서 상용구 코드(boilerplate code)를 대폭 줄여줍니다. 생성자,
- 불변성 보장
- 레코드의 모든 필드는
private final
로 자동으로 선언되며, 객체 생성 이후 값이 변경되지 않는 불변 객체(Immutable Object)를 쉽게 구현할 수 있습니다. 이를 통해 데이터의 안전성과 신뢰성을 보장합니다.
- 레코드의 모든 필드는
- DTO에 적합
- 레코드는 주로 DTO(Data Transfer Object)로 많이 사용됩니다. DTO는 계층 간 데이터 전송에 사용되는 객체로, 데이터를 안전하게 전달하고 명확한 의미를 갖는 객체를 만들기 위해 불변성이 중요합니다. 레코드는 이를 간단하게 구현할 수 있습니다.
- 값 객체(Value Object)에 적합
- 레코드는 데이터를 비교하거나 해시 처리해야 할 때 유용한 값 객체(Value Object)로 사용할 수 있습니다. 자동으로 생성되는
equals()
및hashCode()
메서드 덕분에 레코드를 이용하면 값 비교와 같은 연산이 간결해집니다.
- 레코드는 데이터를 비교하거나 해시 처리해야 할 때 유용한 값 객체(Value Object)로 사용할 수 있습니다. 자동으로 생성되는
- 데이터 중심의 설계
- 레코드는 데이터를 운반하는 데 최적화된 구조로, 복잡한 로직을 구현하기보다는 데이터의 보관 및 전달에 중점을 둔 설계를 가능하게 합니다. 이를 통해 클래스의 목적이 명확해지고, 애플리케이션의 유지보수성과 가독성이 높아집니다.
- Thread-safe
- 레코드 객체는 불변성이 보장되기 때문에 동시성 문제에 안전(Thread-safe)하며, 멀티스레드 환경에서 데이터를 보호하는 데 유리합니다.
4.2 레코드를 사용할 때의 적합한 상황
- 간단한 데이터 객체: 복잡한 로직을 구현할 필요 없이 단순히 데이터를 전달하거나 저장해야 할 때
- 불변성 요구: 객체 생성 후 값이 변경되면 안 되는 상황, 예를 들어 설정 값이나 특정 상태 값을 전달할 때
- 값 객체: 두 객체를 값으로 비교해야 하거나 해시맵의 키로 사용해야 할 때
- DTO: 데이터 계층 간 전송을 위해 객체의 변형을 막고 안전하게 전송해야 할 때
4.3 레코드를 피해야 할 경우
- 복잡한 로직이 필요한 클래스: 레코드는 주로 데이터 저장과 관련된 객체에 적합하므로, 복잡한 비즈니스 로직이나 상태 변화를 관리해야 하는 경우에는 사용하지 않는 것이 좋습니다.
- 상속이 필요한 경우: 레코드는 암시적으로
final
이므로 상속을 지원하지 않습니다. 상속이 필요한 경우는 전통적인 클래스를 사용해야 합니다.