[Java] Record

Woomin Wang ·2025년 7월 14일

[Java]

목록 보기
10/10
post-thumbnail

Record에 대해 본격적으로 학습하기 전에, 먼저 Record가 왜 필요하지 이해하기 위해 EntityDTO라는 두 가지 핵심 개념부터 알아보자.

Entity

애플리케이션에서 가장 중요한 데이터는 Entity로 표현된다.
Entity는 데이터베이스 테이블의 '원본 데이터'와 일대일로 매핑되는 객체이다.
예를 들어 User Entity는 데이터베이스의 user 테이블과 연결되어 사용자의 모든 정보를 담고 있다.

Entity는 단순히 데이터를 저장하는 것을 넘어, 비즈니스 규칙에 따라 상태가 변해야 할 필요가 있다.
주문 Entity가 결제 완료에서 배송 중으로 상태를 바꾸거나, 상품 Entity의 재고 수량이 변하는 것처럼 말이다.
이러한 이유 때문에 Entity는 가변적으로 설계되며, 직접적인 비즈니스 로직을 포함하는 경우가 많다.



DTO (Data Transfer Object)

애플리케이션은 여러 계층으로 나뉘어 있다. 이 계층들 사이에서 데이터를 주고받을 때, Entity 원본을 그대로 전달하면 보안이나 불필요한 정보 노출 문제가 발생할 수 있다.

이때 사용되는 것이 바로 DTO(Data Transfer Object)이다. DTO는 계층 간에 데이터를 안전하게 전달하기 위한 '복사본' 역할을 한다.
DTO는 클라이언트에게 필요한 정보만 선별적으로 담고, 민감한 정보는 제외한다.
이처럼 DTO는 순수한 데이터 전달용이므로 비즈니스 로직을 포함하지 않으며, 데이터의 안정성을 위해 상태가 변하지 않는 불변 객체로 설계되는 것이 일반적이다.



Record

DTO는 불변 객체여야 하지만, 기존 자바 클래스로 불변 DTO를 만들려면 필드, 생성자, getter, equals(), hashCode(), toString() 등 반복적인 코드를 직접 작성해야 하는 번거로움이 있었다.

이처럼 반복적으로 작성해야 하는 코드를 보일러플레이트 코드라고 하며,
자바 14에서 도입된 Record는 이 문제를 해결하기 위해 등장했다.

💡 보일러 플레이트 코드

소프트웨어 개발에서 반복적으로 작성되지만, 특별한 의미나 로직을 담고 있지 않은 코드를 말한다.

예시: DTO를 만들 때 작성하는 getter, setter, equals(), hashCode(), toString() 메서드가 대표적인 보일러플레이트 코드에 해당한다.

다음과 같은 특성 덕분에 Record는 DTO의 모든 요구사항을 완벽하게 충족시킨다.

  • 불변성 자동 보장: Record는 선언과 동시에 모든 컴포넌트가 final로 설정되어 불변성이 보장된다.

  • 코드 간소화: Record는 컴포넌트만 정의하면 생성자와 핵심 메서드(equals(), hashCode(), toString())를 자동으로 생성해준다.

결론적으로, Record는 불변적이어야 하는 DTO를 위한 완벽한 도구이며, 가변적이어야 하는 Entity와는 역할과 목적이 명확히 구분된다.

따라서, Entity는 class로, DTO는 record로 구현하는 것이 현대 자바 개발의 모범 사례이다.



Record 사용 예제

기존 class와 Record를 비교하며 Record의 간결함을 살펴보자.

기존 User 클래스

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

    // 1. 모든 필드를 받는 생성자 (정규 생성자)
    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 2. 접근자(getter) 메서드
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // 3. equals() 메서드 오버라이드
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserDto userDto = (UserDto) o;
        return age == userDto.age && Objects.equals(name, userDto.name);
    }

    // 4. hashCode() 메서드 오버라이드
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    // 5. toString() 메서드 오버라이드
    @Override
    public String toString() {
        return "UserDto[name=" + name + ", age=" + age + "]";
    }
}

위 코드를 보면, 단순한 데이터 객체임에도 불구하고 상당한 양의 코드를 작성해야 한다.


Record를 활용한 User 클래스

import java.util.Objects;

// name과 age를 컴포넌트로 가지는 Record 선언
public record UserRecord(String name, int age) {
    // Record는 정규 생성자를 자동으로 생성하지만, 필요시 추가 로직을 넣을 수 있습니다.
    // 예를 들어, 유효성 검사를 추가할 수 있습니다.
    public UserRecord {
        Objects.requireNonNull(name, "이름은 null일 수 없습니다.");
        if (age < 0) {
            throw new IllegalArgumentException("나이는 0 미만일 수 없습니다.");
        }
    }

    // 컴포넌트에 대한 접근자 메서드
    // String name() { return this.name; }
    // int age() { return this.age; }
    // 이 메서드들은 자동으로 생성되므로 직접 작성할 필요가 없습니다.
}

Record는 헤더(header)에 컴포넌트를 선언하는 것만으로 모든 핵심 기능을 제공한다.

💡 Record의 컴팩트 생성자

유효성 검사와 같이 불변 데이터를 초기화하기 전 필요한 추가 로직을 처리하는 데 사용된다.

정규 생성자와 달리 매개변수 목록을 생략하며, 개발자가 직접 필드를 초기화하지 않아도 로직 실행 후 자동으로 초기화가 이루어진다.


Record 인스턴스 생성

UserRecord user = new UserRecord("홍길동", 20);

Record에서 생성자는 유효성 검사를 위한 수단이며, 실제 객체의 생성과 컴포넌트의 초기화는 헤더에 정의된 컴포넌트를 기반으로 컴파일러가 자동으로 처리한다.


Record의 주요 특징

1. 불변성 (Immutability)

  • Record는 불변 객체로, 한 번 생성되면 값을 변경할 수 없다.

  • 모든 컴포넌트는 자동으로 private final로 정의되며, setter 메서드가 존재하지 않는다.

2. 간결한 선언

  • Record는 암묵적으로 final이므로 상속할 수 없다.(extends 불가)

  • 하지만 인터페이스는 구현할 수 있다.(implements 가능)

  • abstract로 선언할 수 없다.

3. 내부 구조의 제약

  • Record는 인스턴스 필드를 추가로 선언할 수 없다. 모든 필드는 헤더에서 정의된 컴포넌트들로만 구성된다.

  • 하지만 static 필드나 static 메서드, 중첩 클래스는 선언할 수 있다.

4. 자동 생성되는 코드

  • Record는 컴포넌트만 정의하면 생성자, getter, equals(), hashCode(), toString() 메서드를 자동으로 제공합니다.


참고 문서

profile
Backend Developer

0개의 댓글