CodeGym /행동 /JAVA 25 SELF /Record: 문법, 장점

Record: 문법, 장점

JAVA 25 SELF
레벨 22 , 레슨 0
사용 가능

1. DTO 클래스의 문제: 왜 record 클래스를 써야 할까?

솔직히 말해 봅시다. 이런 클래스를 몇 번이나 작성해 보셨나요?


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

그리 어렵진 않아 보여도, 줄 수를 세어 보세요! 이제 이런 클래스가 10개 있고, 각 클래스마다 필드가 5–6개라고 상상해 보세요. IDE조차 이 보일러플레이트를 생성하느라 지칩니다. 새 필드를 추가하기로 했다면 — 생성자, 게터, equals, hashCode, toString까지 일일이 수정해야 하죠... 지루하고 반복적이며 오류의 원인이 됩니다.

이런 클래스는 DTO(Data Transfer Object) 또는 Value Object라고 부릅니다. 그저 데이터를 담기만 합니다 — 그것뿐이죠. 하지만 보일러플레이트 때문에 유지 보수가 힘듭니다.

이게 문제가 아니라고 느껴지면, 이런 클래스 50개를 한 번에 바꿔야 할 때를 기다려 보세요. 그때 record 클래스를 각별히 그리워하게 될 겁니다!

2. record 입문: Java 16+의 문법과 마법

Java 16부터 모든 것이 달라졌습니다. 새로운 종류의 클래스 — record가 등장했죠. 데이터 묶음을 단순히 보관해야 하는 경우를 위해 만들어졌습니다. 문법은 다른 언어의 튜플과 거의 비슷합니다.

record를 어떻게 선언할까?


public record Point(int x, int y) { }

그리고... 끝! 지금 막 두 개의 필드, 생성자, 게터, equals, hashCode, toString을 가진 불변 클래스를 만들었습니다. 쓸데없는 장황함 없이요.

Java가 내부적으로 무엇을 해 주나?

  • 필드 xyprivate final이 됩니다.
  • 게터가 자동으로 생성됩니다: int x()int y().
  • 생성자: public Point(int x, int y).
  • equals/hashCode: 모든 필드를 값으로 비교합니다.
  • toString: "Point[x=1, y=2]" 형태의 문자열을 반환합니다.

요컨대, record는 “스테로이드를 맞은 DTO”입니다: 코드는 적고, 보장은 더 많고, 버그는 더 적죠.

불변성 (immutability)

record 클래스의 모든 필드는 자동으로 final입니다. 객체를 생성한 뒤에는 수정할 수 없으며 — 컴파일러가 이를 보장합니다.

세터를 추가하거나 필드를 final이 아니게 만들려고 하면 — 컴파일러가 막습니다. 이런 친절한 안전장치는 흔치 않죠!

3. record 클래스 사용 예

일반 클래스(코드 많음):


public class Client {
    private final String name;
    private final int id;

    public Client(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() { return name; }
    public int getId() { return id; }

    // equals, hashCode, toString ...
}

record 클래스(한 줄!):


public record Client(String name, int id) { }

사용 예:


public class Main {
    public static void main(String[] args) {
        Client client = new Client("이반", 123);
        System.out.println(client.name()); // 이반
        System.out.println(client.id());   // 123
        System.out.println(client);        // Client[name=이반, id=123]
    }
}

주의하세요:
- 필드 접근 메서드의 이름은 필드 이름과 같습니다: name(), id().
- setName()이나 setId()가 없습니다 — 객체는 생성 후 변경할 수 없습니다.

4. record 클래스의 장점: 코드도 적고, 오류도 적다

코드는 적을수록 행복하다

40줄을 왜 쓰나요? 한 줄이면 됩니다. record 클래스는 특히 DTO와 value 객체가 많은 대규모 프로젝트에서 시간과 노력을 절약해 줍니다.

“계약에 의한” 불변성

  • record 클래스는 항상 final이며 불변입니다.
  • 객체를 위조하거나 실수로 변경할 수 없습니다.
  • 예상치 못한 위치에서 객체 상태가 바뀌어 생기는 “이상한” 버그가 없습니다.
  • 모든 필드도 immutable이라면 멀티스레드 프로그램에서 안전하게 사용할 수 있습니다.

equals/hashCode/toString 자동 생성

비교, 해시 계산, 예쁜 출력 메서드를 손으로 작성할 필요가 없습니다. 자동으로, 그리고 올바르게 처리됩니다.


Client c1 = new Client("안나", 42);
Client c2 = new Client("안나", 42);

System.out.println(c1.equals(c2));                 // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1);                             // Client[name=안나, id=42]

컬렉션과 키에 제격

record 객체는 HashMap의 키, HashSet의 요소 등으로 사용할 수 있습니다 — equalshashCode가 모든 필드를 반영하므로 올바르게 동작합니다.


import java.util.HashMap;
import java.util.Map;

Map<Client, String> clients = new HashMap<>();
clients.put(new Client("안나", 42), "VIP");

System.out.println(clients.get(new Client("안나", 42))); // VIP

데이터를 명시적으로 표현

record 클래스의 문법을 보면 어떤 데이터를 담고 있으며 객체가 불변이라는 점이 즉시 드러납니다. 이는 다른 개발자(그리고 6개월 뒤의 여러분)에게 코드를 더 이해하기 쉽게 만듭니다.

5. 표: 일반 클래스와 record 클래스 비교

일반 클래스 record 클래스
문법 코드 많음 한 줄
불변성 직접 구현해야 함 컴파일러가 보장
메서드 자동 생성 아니오 예 (equals, hashCode, toString)
필드 추가 가능 record 컴포넌트만 가능
상속 상속 가능 항상 final, 상속 불가
컬렉션에서의 사용 메서드를 올바르게 구현해야 함 바로 동작함

6. 학습용 애플리케이션 확장: record 클래스 예시

예를 들어, 학습용 뱅킹 애플리케이션에서 계좌 거래를 저장해야 한다고 합시다: 날짜, 금액, 거래 유형(예: “입금” 또는 “출금”).

Java 16 이전:


public class Transaction {
    private final LocalDate date;
    private final double amount;
    private final String type;

    public Transaction(LocalDate date, double amount, String type) {
        this.date = date;
        this.amount = amount;
        this.type = type;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getType() { return type; }

    // equals, hashCode, toString ...
}

Java 16과 record 사용:


import java.time.LocalDate;

public record Transaction(LocalDate date, double amount, String type) { }

사용 예:


Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t);         // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0

7. 시각화: record가 생성해 주는 것

“펼쳐진” record 클래스를 살펴봅시다(컴파일러가 대략적으로 생성하는 것):


public final class Point extends java.lang.Record {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) { /* 필드 기준 비교 */ }

    @Override
    public int hashCode() { /* 필드 기준 계산 */ }

    @Override
    public String toString() { /* 보기 좋은 출력 */ }
}

요약: record를 언제 쓰면 좋을까

  • 불변 데이터 묶음이 필요할 때.
  • 수작업 없이도 “정직한” equals/hashCode/toString이 필요할 때.
  • DTO, Value Object, 쌍/삼중쌍, 색상, 점, 구간, 컬렉션 키 등과 같은 용도를 만들 때.

8. record 클래스를 사용할 때 흔한 실수

오류 №1: 세터를 추가하거나 생성 후 필드를 변경하려는 시도.
record 클래스는 자신의 필드를 변경할 수 없습니다. setX(int x) 같은 메서드를 추가하려 하면 컴파일러가 곧바로 “안 됩니다”라고 말합니다. 필드를 직접 변경하려는 시도도 마찬가지입니다.

오류 №2: 비정적 필드를 추가하려는 시도.
record 클래스에서는 컴포넌트(레코드 이름 뒤 괄호 안에 지정된 필드)와 정적 필드만 선언할 수 있습니다. 일반 비정적 필드는 추가할 수 없으며 — 컴파일러가 허용하지 않습니다.

오류 №3: mutable 로직에 record를 사용하는 것.
record 클래스는 상태가 변하는 객체를 위한 것이 아닙니다. 생성 후 무언가를 변경해야 한다면 — 일반 클래스를 사용하세요.

오류 №4: record는 항상 final이라는 점을 잊는 것.
record 클래스는 상속할 수 없고, 슈퍼클래스로 만들 수도 없습니다. 이 제한을 어기려 하면 컴파일 오류가 발생합니다. 핵심 포인트 — record를 “확장”하려 하지 마세요: 완결된 불변 타입으로 설계되었습니다.

오류 №5: 자동 생성되는 메서드를 무시하는 것.
equals, hashCode 또는 toString을 재정의한다면 주의하세요 — 그 계약을 깨지 말아야 컬렉션과 비교가 올바르게 동작합니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION