1. record 클래스 확장: 추가 메서드
record에 메서드를 추가할 수 있나요?
물론입니다! Record는 마치 인테리어가 완료된 아파트 같습니다: 벽과 바닥은 이미 완성되어 바꿀 수 없지만, 가구 배치는 마음대로 할 수 있죠. record 내부에는 일반 메서드와 정적 메서드를 선언할 수 있고, 상수를 보관할 수도 있습니다. 즉, 비즈니스 로직을 꼭 별도의 “유틸리티” 클래스에 분리하지 않아도 되며 — record 자체에 깔끔하게 녹여 넣을 수 있습니다.
예: 두 점 사이 거리 계산 메서드
예를 들어, 평면의 점을 나타하는 record가 있다고 합시다:
public record Point(int x, int y) {
// 추가 메서드
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
이제 이렇게 사용할 수 있습니다:
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceTo(p2)); // 5.0
보시다시피, record 클래스는 자체 메서드로 “풍성하게” 만들 수 있으며 — 매우 편리합니다!
예: 정적 메서드
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
public static Rectangle square(int size) {
return new Rectangle(size, size);
}
}
이제 한 번의 호출로 “정사각형”을 만들 수 있습니다:
Rectangle r = Rectangle.square(5);
System.out.println(r.area()); // 25
2. 컴팩트 생성자와 데이터 검증
왜 “컴팩트” 생성자가 필요한가요?
정식 record 생성자는 자동으로 만들어지며, 매개변수를 필드에 할당합니다. 하지만 때로는 입력 데이터에 대한 검사(예: 음수 좌표를 금지)가 필요합니다.
일반 클래스에서는 다음과 같이 작성합니다:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
// ...
}
record 클래스에서는 컴팩트 생성자를 선언할 수 있습니다 — 매개변수 목록을 반복해서 쓰지 않아도 되고, 필드에 대한 명시적 할당도 필요 없습니다(이는 컴파일러가 처리합니다).
컴팩트 생성자의 문법
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
// 작성할 필요가 없습니다: this.x = x; this.y = y;
}
}
- 생성자의 매개변수는 record 컴포넌트와 자동으로 일치합니다.
- this.x = x 및 this.y = y 할당은 생성자 본문 실행 후(또는 성공적으로 빠져나온 후) 컴파일러가 자동으로 수행합니다.
- 예외를 던지면 객체는 생성되지 않습니다.
검증 예시
Point p1 = new Point(3, 5); // OK
Point p2 = new Point(-1, 2); // IllegalArgumentException을 던집니다!
“일반” 생성자를 선언할 수 있나요?
네, 가능합니다! 다른 매개변수 목록이나 추가 로직이 필요하다면 명시적으로 선언하세요:
public record Range(int from, int to) {
public Range(int size) {
this(0, size); // 기본 생성자를 호출함
}
}
3. record 클래스의 제약
일반 클래스와 달리, record 클래스에는 몇 가지 제약이 있습니다. 이는 컴파일 오류에 놀라지 않도록 기억해야 합니다.
컴포넌트만 — 추가적인 비정적 필드는 금지
record 클래스에서는 새로운 비정적 필드를 선언할 수 없습니다:
public record Person(String name, int age) {
// int id; // 컴파일 오류! 비정적 필드를 추가할 수 없습니다.
}
정적 필드와 메서드는 선언할 수 있습니다:
public record Person(String name, int age) {
public static final String SPECIES = "Homo sapiens";
}
Record는 항상 final
Record 클래스는 상속의 부모가 될 수 없고(즉, 다른 클래스가 이를 상속할 수 없으며), 스스로도 다른 클래스를 명시적으로 상속할 수 없습니다(암시적인 java.lang.Record 상속만 허용). 즉, record 클래스는 항상 “최종” 구조입니다.
public record User(String login) { }
// public class Admin extends User {} // 오류: record로부터 상속할 수 없습니다!
인터페이스는 구현할 수 있습니다
Record 클래스는 인터페이스를 구현할 수 있습니다:
public interface Printable {
void print();
}
public record Invoice(int amount) implements Printable {
@Override
public void print() {
System.out.println("금액: " + amount);
}
}
4. 예제: 실제 과제에서의 확장된 records
추가 메서드와 컴팩트 생성자가 record 클래스를 얼마나 유용하게 만드는지, 몇 가지 실용적인 예제를 살펴봅시다.
계산 메서드를 가진 Record
public record Circle(double x, double y, double radius) {
public double area() {
return Math.PI * radius * radius;
}
public double distanceTo(Circle other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
검증을 수행하는 Record
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("잘못된 이메일: " + value);
}
}
}
이제 잘못된 이메일을 만들 수 없습니다:
Email e1 = new Email("test@example.com"); // OK
Email e2 = new Email("not-an-email"); // IllegalArgumentException 발생
추가 정적 메서드를 가진 Record
public record Temperature(double celsius) {
public static Temperature fromFahrenheit(double fahrenheit) {
return new Temperature((fahrenheit - 32) * 5 / 9);
}
public double toFahrenheit() {
return celsius * 9 / 5 + 32;
}
}
사용 예:
Temperature t = Temperature.fromFahrenheit(98.6);
System.out.println(t.celsius()); // 37.0
System.out.println(t.toFahrenheit()); // 98.6
5. 컴팩트 생성자: 요령과 제약
언제 컴팩트 생성자를 사용할까요?
- 데이터의 올바름을 검증해야 할 때.
- 저장 전에 값을 조정해야 할 때(예: 수를 반올림하거나 문자열을 대문자로 변환).
- 매개변수 목록의 중복을 피하고 싶을 때.
동작상의 특징
- 컴팩트 생성자에서는 컴포넌트에 명시적으로 값을 할당할 수 없습니다(this.x = ...). 생성자 본문 이후 할당은 컴파일러가 수행하므로, 시도하면 컴파일 오류가 발생합니다.
- 컴팩트 생성자에서는 매개변수 이름을 바꿀 수 없습니다 — 항상 record 컴포넌트 이름과 동일합니다.
예: 자동 반올림
public record Money(double amount) {
public Money {
amount = Math.round(amount * 100) / 100.0; // 소수점 둘째 자리까지 반올림
}
}
6. 실습: 학습용 애플리케이션 확장
학습용 애플리케이션에서 은행 거래 기능을 만든다고 가정해 봅시다. record 클래스 Transaction이 금액, 보낸 사람, 받는 사람을 저장합니다.
public record Transaction(String from, String to, double amount) {
public Transaction {
if (amount <= 0) throw new IllegalArgumentException("금액은 양수여야 합니다");
if (from == null || to == null) throw new IllegalArgumentException("필드는 null일 수 없습니다");
}
public String description() {
return String.format("이체 %.2f: %s에서 %s로", amount, from, to);
}
}
사용 예:
Transaction t = new Transaction("Alice", "Bob", 150.0);
System.out.println(t.description()); // 이체 150.00: Alice에서 Bob으로
잘못된 트랜잭션을 만들려고 하면 오류가 발생합니다:
Transaction t2 = new Transaction("Alice", "Bob", -10.0); // IllegalArgumentException
표: record 클래스에서 가능한 것과 불가능한 것
| record 클래스에서 가능한 것 | record 클래스에서 불가능한 것 |
|---|---|
| 일반 메서드 | 새로운 비정적 필드 |
| 정적 메서드와 필드 | 다른 클래스로부터 상속 |
| 인터페이스 구현 | 다른 클래스의 슈퍼클래스가 되기 |
| 컴팩트 및 일반 생성자 | 생성 후 컴포넌트 변경 |
| 메서드 재정의 | 세터 사용 |
7. 비표준 본문을 가진 record 클래스를 다룰 때의 흔한 실수
오류 № 1: 비정적 필드를 추가하려는 시도.
입문자는 종종 record 클래스에 “내부 로직을 위한 또 다른 필드” — 예를 들어, 카운터나 캐시 — 를 추가하려고 합니다. 이는 동작하지 않습니다. 컴파일러가 즉시 오류를 냅니다. 추가 상태를 보관해야 한다면, 아마도 record 클래스는 적합하지 않습니다.
오류 № 2: 컴팩트 생성자에서의 검증 누락.
객체가 항상 유효하도록 하려면, 검증을 컴팩트 생성자에서 수행하세요. “사용자가 엉뚱한 값을 넣지는 않겠지”라고 기대하지 마세요.
오류 № 3: 생성 후 컴포넌트를 변경하려는 시도.
record 클래스의 필드는 final입니다 — 직접적으로나 메서드를 통해서나 변경할 수 없습니다. 변경 가능한 구조가 필요하다면 일반 클래스를 사용하세요.
오류 № 4: 메서드와 생성자에 로직을 중복.
때로는 검증 로직과 계산 로직을 메서드와 생성자 양쪽에 중복해서 넣습니다. 검증은 생성자에서 모두 처리하고, 메서드는 “순수한” 비즈니스 로직을 위해 남겨두는 것이 좋습니다.
오류 № 5: 상속 제약을 잊음.
Record 클래스는 항상 final입니다 — 이로부터 하위 클래스를 만들 수 없습니다. 하위 클래스가 필요한 계층을 설계한다면 일반 클래스를 사용하세요.
GO TO FULL VERSION