Java record 잘 사용하기

JINNI·2025년 4월 27일
0

[TIL] Java+Spring

목록 보기
16/20

1️⃣ Record가 뭘까?

record 는 특수한 클래스로, 일반 클래스보다 덜 복잡한 방식으로 일반 데이터를 모델링할 수 있는 친구다.

kotlin의 data class와 비슷하다고 생각하면 된다.

기존에 우리가 데이터를 만들기 위해서는 다음과 같이 구현해야 했다.

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // Implementation of equals() and hashCode(), which specify
    // that two record objects are equal if they
    // are of the same type and contain equal field values.
    public boolean equals...
    public int hashCode...

    // An implementation of toString() that returns a string
    // representation of all the record class's fields,
    // including their names.
    public String toString() {...}
}

생성자에 getter 메소드를 전부 다 구현해줘야 했다.

으!

그치만 record 의 등장부터는 이렇게 하지 않아도 된다는 사실.

record Rectangle(double length, double width) { }

record 는 이와 같이 header에 content에 대한 설명을 담는다.

→ 자동으로 적절한 접근자, 생성자, equals, hashCode, toString 메소드가 생성됨

→ 모든 record의 필드는 final 이다! 레코드는 정말 간단하게 “데이터 캐리어”의 역할만 하기 때문.

❗내부적으로 조금만 더 더 자세히 알아보자면..

record 는 이런 것들을 자동으로 선언한다.

  • Header의 각 구성요소에 대한 두 멤버
    1. record 구성 요소와 동일한 이름/타입을 가진 필드 (private final)
    2. record 구성 요소와 동일한 이름/타입을 갖는 접근자 메서드 (Rectangle::length()같은..)
  • 정식 생성자
    record Rectangle(double length, double width) {
        public Rectangle(double length, double width) {
            if (length <= 0 || width <= 0) {
                throw new java.lang.IllegalArgumentException(
                    String.format("Invalid dimensions: %f, %f", length, width));
            }
            this.length = length;
            this.width = width;
        }
    }
    기존에 위와 같이 주절주절 써줘야 했던 걸 암묵적인 간결한 생성자를 선언할 수 있음! 아래처 컴팩트한 생성자를 선언하면서 유효성 검사도 할 수 있다. (주의 : record 에서만 사용 가능!!)
    record Rectangle(double length, double width) {
        public Rectangle {
            if (length <= 0 || width <= 0) {
                throw new java.lang.IllegalArgumentException(
                    String.format("Invalid dimensions: %f, %f", length, width));
            }
        }
    }
  • toString(모든 필드 일치할 때 필드 이름 및 해당 값 포함)
  • equals(모든 필드 일치할 때 동일한 클래스의 객체에 대해 true 반환)
  • hashCode(모든 필드 일치할 때 동일한 값 반환)

2️⃣ 쓰는 법을 제대로 알아보자.

  • static 필드, static initializer, staic 메소드 선언 가능!
record Rectangle(double length, double width) {
    
    static double goldenRatio;

    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}
  • 레코드 객체라고 다르게 생성하는 것 아님!
Rectangle r = new Rectangle(4,5);
  • 기본적으로 Class이기 때문에 일반 클래스와 동일하게 동작함.
  • 다만 final 이기 대문에 명시적 확장은 불가함.
record Triangle<C extends Coordinate> (C top, C left, C right) { }

record Customer(...) implements Billable { }
  • 개별 요소에 annotation 붙일 수 있음
record Rectangle(
    @GreaterThanZero double length,
    @GreaterThanZero double width) { }

  • static이 아닌 필드나 initializer 선언 불가
record Rectangle(double length, double width) {

    BiFunction<Double, Double, Double> diagonal;

    {
        diagonal = (x, y) -> Math.sqrt(x*x + y*y);
    }
}
  • 이미 Record라는 추상 클래스를 상속받아서 구현된 아이기 때문에 다른 클래스를 상속받을 수 없음.
record Shape(
	String name
) { }

class Rectangle extends Shape{
 // 불가!
}

⚠️ 중첩된 record 도 가능합니다.

  • 자체 접근자 메소드 구현 여부와 관계 없이 인스턴스 메서드 선언 가능
  • 중첩 클래스와 인터페이스(암시적으로 static) 선언 가능
record Rectangle(double length, double width) {

    // Nested record class
    record RotationAngle(double angle) {
        public RotationAngle {
            angle = Math.toRadians(angle);
        }
    }
    
    // Public instance method
    public Rectangle getRotatedRectangleBoundingBox(double angle) {
        RotationAngle ra = new RotationAngle(angle);
        double x = Math.abs(length * Math.cos(ra.angle())) +
                   Math.abs(width * Math.sin(ra.angle()));
        double y = Math.abs(length * Math.sin(ra.angle())) +
                   Math.abs(width * Math.cos(ra.angle()));
        return new Rectangle(x, y);
    }
}

3️⃣ 근데 왜 쓰는 거에요?

  • 객체 간 Immutable한 데이터를 전달하는 것.. 보일러플레이트 필드와 메서드 발생 → record 를 사용하면 짧아집니다.
  • 데이터베이스 결과, 쿼리 결과, 서비스 정보 등 데이터 보관하기 위한 클래스를 작성하는데 대부분 이 데이터들은 변경 불가능함! → 동기화 없이도 데이터 유효성 보장 가능

⇒ 반복적인 데이터 클래스를 record 로 대체하자!

❓ 그렇다면 Kotlin의 Data Class와는 어떻게 다른가?

kotlin을 활용한 안드로이드 개발을 하면서 동시에 Java로 서버 공부를 하고 있는 입장에서..

record를 보자마자 ‘어? data class랑 똑같네?’ 라는 생각이 들었다.


// Kotlin
data class FriendCardModel(
    val friendId: Int,
    val friendName: String,
    val imageUrl: String,
    val state: BbangZipCardState,
    @DrawableRes val imgSrc: Int = 0,
)

// Java
record Rectangle(double length, double width) { }

코틀린

  • 클래스 내 다른 내용 없을 경우 { } 생략 가능
  • 프로퍼티 선언 시 val(불변), var(가변) 모두 사용 가능
  • 다른 클래스 상속받기 가능
  • copy() 가능

자바

  • 다른 내용 없어도 { } 필수
  • 묵시적으로 모든 필드가 final → Setter 존재 X, 불변성 어느정도 보장
  • 다른 클래스 상속하거나 받거나 둘다 안됨!
  • copy() 불가능

⇒ 코틀린에 비해 자바의 record 는 좀 더 불변성에 제약을 강제해둔 안정적인 Class


참고자료

Java Language Updates

이거 영문이랑 구글 자동번역 번갈아가면서 읽으면 이해가 쏙쏙 됩니다..

Java Record Keyword | Baeldung

[Java] Java 14부터 추가된 Record 타입과 Kotlin의 Data Class 비교

profile
천재 개발자 되기

0개의 댓글