본문 바로가기
Java

[Java] 자바 Record 불변 데이터 객체 쉽게 만들기 (Java 14+)

by teamnova 2025. 4. 14.
728x90

레코드(Record)란?
Java 14부터 도입된 record는 불변(immutable) 데이터를 담기 위한 간결한 클래스 문법입니다.
우리가 흔히 사용하는 DTO(Data Transfer Object)나 VO(Value Object) 클래스보다 훨씬 간단하고 명확하게 데이터를 표현할 수 있죠.

 

불변(Immutable) 데이터란?
한 번 만들어진 이후, 내부 상태(값)를 바꿀 수 없는 객체를 의미.
즉, 객체가 생성되고 나면 필드 값을 변경할 수 없고 외부에서 setter로 값을 바꾸는 것도 불가능

 

불변 객체는 데이터를 바꿀 수 없으므로,
여러 스레드가 동시에 접근해도 동기화(synchronized) 처리 없이 사용할 수 있음.

 

예)  이 클래스는 setName()을 통해 객체의 상태(이름)를 바꿀 수 있어 → 가변

public class MutableUser {
    private String name;

    public MutableUser(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name; // 언제든 이름이 바뀔 수 있음
    }

    public String getName() {
        return name;
    }
}

 

예) name이 final이므로 한 번 값이 들어가면 바꿀 수 없음, setter도 없음 → 불변

public final class ImmutableUser {
    private final String name;

    public ImmutableUser(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

 

언제 레코드를 사용하면 좋을까요?

단순 데이터 전달 객체: DTO/VO 대체
JSON 응답 매핑: 값 변경 없이 읽기 전용으로 사용 가능

API 응답 데이터: 서버에서 클라이언트로 보내는 응답 데이터를 record로 정의하면, 응답 구조가 명확해지고 불변성을 보장할 수 있다.
데이터베이스 엔티티 객체: 데이터베이스에서 조회한 데이터를 전송할 때, 불변 객체로 정의하면 데이터 무결성을 보호할 수 있다.

컬렉션에 객체를 추가할 때: 두 개의 동일한 객체가 있는지 비교하고 싶을 때, record를 사용하면 자동으로 equals()와 hashCode()를 지원한다.
동일한 키로 여러 작업을 할 때: Map의 키로 객체를 사용할 때 record를 사용하면 성능이 향상되고 코드가 간결해진다.

 

시연코드

public class Main {
    // 🟦 불변 데이터 객체 User
    public record User(String name, int age) {}

    // 🟦 생성자 유효성 검증과 메소드가 있는 Product
    public record Product(String name, int price) {
        // 컴팩트 생성자: 가격은 0 이상만 허용
        public Product {
            if (price < 0) {
                throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
            }
        }

        // 사용자 정의 메소드
        public String label() {
            return name + " - " + price + "원";
        }
    }
    public static void main(String[] args) {
        // ✅ User 레코드 사용 예제
        User user = new User("Alice", 30);
        System.out.println("사용자 이름: " + user.name()); // getter 역할
        System.out.println("사용자 나이: " + user.age());
        System.out.println("User toString(): " + user); // 자동 생성된 toString()

        // ✅ Product 레코드 사용 예제
        Product coffee = new Product("커피", 4500);
        System.out.println("상품 라벨: " + coffee.label()); // 커피 - 4500원

        // ⚠️ 유효하지 않은 가격 (예외 발생)
        try {
            Product invalid = new Product("녹차", -1000);
        } catch (IllegalArgumentException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }

        // ✅ equals/hashCode 자동 동작 확인
        User user1 = new User("Alice", 30);
        User user2 = new User("Alice", 30);
        System.out.println("user1 equals user2? " + user1.equals(user2)); // true
    }
}