불변 데이터 클래스 - Record로 간결한 코드 작성하기
Java 14+ (Preview), Java 16+ (Standard)
// ❌ Record 이전 (Java 15 이전)
class PersonOld {
private final String name;
private final int age;
public PersonOld(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonOld person = (PersonOld) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return java.util.Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonOld{name='" + name + "', age=" + age + "}";
}
}
// ✅ Record 사용 (Java 16+)
record Person(String name, int age) {}
public class WhyRecord {
public static void main(String[] args) {
System.out.println("=== Record의 장점 ===\n");
System.out.println("📌 간결성");
System.out.println("50줄 → 1줄");
System.out.println("보일러플레이트 코드 제거\n");
System.out.println("📌 자동 생성");
System.out.println("- 생성자");
System.out.println("- getter");
System.out.println("- equals/hashCode");
System.out.println("- toString\n");
System.out.println("📌 불변성");
System.out.println("- final 필드");
System.out.println("- setter 없음");
System.out.println("- 스레드 안전\n");
// 사용
Person person = new Person("Alice", 25);
System.out.println(person);
System.out.println("이름: " + person.name());
System.out.println("나이: " + person.age());
}
}record Point(int x, int y) {}
public class RecordCharacteristics {
public static void main(String[] args) {
System.out.println("=== Record 특징 ===\n");
System.out.println("📌 클래스");
System.out.println("- final class");
System.out.println("- 상속 불가");
System.out.println("- 암묵적으로 java.lang.Record 상속\n");
System.out.println("📌 필드");
System.out.println("- private final");
System.out.println("- 자동 생성");
System.out.println("- 불변\n");
System.out.println("📌 메서드");
System.out.println("- 접근자 (getter) 자동");
System.out.println("- equals/hashCode 자동");
System.out.println("- toString 자동\n");
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(3, 4);
System.out.println("=== 동작 확인 ===");
System.out.println("p1: " + p1);
System.out.println("p1.x(): " + p1.x());
System.out.println("p1 equals p2: " + p1.equals(p2));
System.out.println("p1 equals p3: " + p1.equals(p3));
}
}// 기본 Record
record User(String username, String email) {}
record Product(String name, double price, int stock) {}
record Coordinate(double latitude, double longitude) {}
public class BasicRecord {
public static void main(String[] args) {
System.out.println("=== 기본 Record ===\n");
// User
User user = new User("alice", "alice@example.com");
System.out.println("사용자: " + user);
System.out.println("이름: " + user.username());
System.out.println("이메일: " + user.email());
// Product
System.out.println("\n=== 상품 ===");
Product product = new Product("노트북", 1_500_000, 10);
System.out.println(product);
System.out.printf("가격: %,d원%n", (int)product.price());
System.out.println("재고: " + product.stock());
// Coordinate
System.out.println("\n=== 좌표 ===");
Coordinate seoul = new Coordinate(37.5665, 126.9780);
System.out.println(seoul);
}
}// Compact Constructor (유효성 검증)
record Person(String name, int age) {
// Compact Constructor
public Person {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수입니다");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("나이가 유효하지 않습니다");
}
// 정규화
name = name.trim();
}
}
record Email(String address) {
public Email {
if (address == null || !address.contains("@")) {
throw new IllegalArgumentException("유효하지 않은 이메일");
}
address = address.toLowerCase();
}
}
public class CompactConstructor {
public static void main(String[] args) {
System.out.println("=== Compact Constructor ===\n");
// 정상 생성
Person person = new Person(" Alice ", 25);
System.out.println("이름: '" + person.name() + "'"); // trim됨
// 유효성 검증
System.out.println("\n=== 유효성 검증 ===");
try {
Person invalid = new Person("", 25);
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
try {
Person invalid = new Person("Bob", -5);
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
// Email
System.out.println("\n=== Email ===");
Email email = new Email("Alice@Example.Com");
System.out.println("정규화: " + email.address()); // 소문자
}
}record Point(int x, int y) {
// Canonical Constructor (명시적)
public Point(int x, int y) {
this.x = x;
this.y = y;
System.out.println("Point 생성: (" + x + ", " + y + ")");
}
}
record Range(int start, int end) {
public Range(int start, int end) {
if (start > end) {
throw new IllegalArgumentException("start > end");
}
this.start = start;
this.end = end;
}
}
public class CanonicalConstructor {
public static void main(String[] args) {
System.out.println("=== Canonical Constructor ===\n");
Point p = new Point(10, 20);
System.out.println("\n=== Range 검증 ===");
Range valid = new Range(1, 10);
System.out.println("유효: " + valid);
try {
Range invalid = new Range(10, 1);
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
}
}record Person(String name, int age, String email) {
// Compact Constructor
public Person {
if (name == null) throw new IllegalArgumentException("name null");
if (age < 0) throw new IllegalArgumentException("age < 0");
}
// 추가 생성자 - 이메일 없이
public Person(String name, int age) {
this(name, age, "");
}
// 추가 생성자 - 나이 기본값
public Person(String name) {
this(name, 0, "");
}
}
record Product(String name, double price, int stock) {
// 추가 생성자들
public Product(String name, double price) {
this(name, price, 0);
}
public Product(String name) {
this(name, 0.0, 0);
}
}
public class AdditionalConstructors {
public static void main(String[] args) {
System.out.println("=== 추가 생성자 ===\n");
Person p1 = new Person("Alice", 25, "alice@example.com");
Person p2 = new Person("Bob", 30);
Person p3 = new Person("Charlie");
System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
System.out.println("\n=== Product ===");
Product prod1 = new Product("노트북", 1_000_000, 10);
Product prod2 = new Product("마우스", 30_000);
Product prod3 = new Product("키보드");
System.out.println(prod1);
System.out.println(prod2);
System.out.println(prod3);
}
}record Rectangle(double width, double height) {
// 인스턴스 메서드
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
public Rectangle scale(double factor) {
return new Rectangle(width * factor, height * factor);
}
}
record Money(double amount, String currency) {
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화가 다릅니다");
}
return new Money(amount + other.amount, currency);
}
public Money multiply(double factor) {
return new Money(amount * factor, currency);
}
public String format() {
return String.format("%.2f %s", amount, currency);
}
}
public class InstanceMethods {
public static void main(String[] args) {
System.out.println("=== 인스턴스 메서드 ===\n");
// Rectangle
Rectangle rect = new Rectangle(10, 5);
System.out.println("면적: " + rect.area());
System.out.println("둘레: " + rect.perimeter());
System.out.println("정사각형? " + rect.isSquare());
Rectangle scaled = rect.scale(2);
System.out.println("2배 확대: " + scaled);
// Money
System.out.println("\n=== Money ===");
Money m1 = new Money(1000, "USD");
Money m2 = new Money(500, "USD");
Money sum = m1.add(m2);
System.out.println("합계: " + sum.format());
Money doubled = m1.multiply(2);
System.out.println("2배: " + doubled.format());
}
}record Point(int x, int y) {
// 정적 팩토리 메서드
public static Point origin() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
// 정적 유틸리티 메서드
public static double distance(Point p1, Point p2) {
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
record Temperature(double value, char unit) {
public static Temperature celsius(double value) {
return new Temperature(value, 'C');
}
public static Temperature fahrenheit(double value) {
return new Temperature(value, 'F');
}
public Temperature toCelsius() {
if (unit == 'C') return this;
return celsius((value - 32) * 5 / 9);
}
public Temperature toFahrenheit() {
if (unit == 'F') return this;
return fahrenheit(value * 9 / 5 + 32);
}
}
public class StaticMethods {
public static void main(String[] args) {
System.out.println("=== 정적 메서드 ===\n");
// 팩토리 메서드
Point origin = Point.origin();
Point p1 = Point.of(3, 4);
System.out.println("원점: " + origin);
System.out.println("점: " + p1);
System.out.println("거리: " + Point.distance(origin, p1));
// Temperature
System.out.println("\n=== Temperature ===");
Temperature c = Temperature.celsius(100);
Temperature f = c.toFahrenheit();
System.out.println("섭씨: " + c);
System.out.println("화씨: " + f);
}
}interface Drawable {
void draw();
}
record Circle(double radius) implements Drawable {
@Override
public void draw() {
System.out.println("원 그리기 (반지름: " + radius + ")");
}
public double area() {
return Math.PI * radius * radius;
}
}
interface Comparable<T> {
int compareTo(T other);
}
record Student(String name, int score) implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return Integer.compare(this.score, other.score);
}
}
public class RecordWithInterface {
public static void main(String[] args) {
System.out.println("=== Record 인터페이스 ===\n");
// Drawable
Circle circle = new Circle(5);
circle.draw();
System.out.println("면적: " + circle.area());
// Comparable
System.out.println("\n=== Student 비교 ===");
Student s1 = new Student("Alice", 90);
Student s2 = new Student("Bob", 85);
int result = s1.compareTo(s2);
System.out.println(s1 + " vs " + s2);
System.out.println("결과: " + (result > 0 ? "Alice 점수 높음" : "Bob 점수 높음"));
// 정렬
java.util.List<Student> students = new java.util.ArrayList<>();
students.add(new Student("Charlie", 75));
students.add(s1);
students.add(s2);
java.util.Collections.sort(students);
System.out.println("\n정렬됨:");
students.forEach(System.out::println);
}
}interface Identifiable {
long getId();
}
interface Timestamped {
java.time.Instant getCreatedAt();
}
record Post(long id, String title, String content, java.time.Instant createdAt)
implements Identifiable, Timestamped {
@Override
public long getId() {
return id;
}
@Override
public java.time.Instant getCreatedAt() {
return createdAt;
}
public String summary() {
return title + " (" + content.substring(0, Math.min(50, content.length())) + "...)";
}
}
public class MultipleInterfaces {
public static void main(String[] args) {
System.out.println("=== 여러 인터페이스 ===\n");
Post post = new Post(
1L,
"Record 소개",
"Record는 Java 16에서 정식으로 도입된 불변 데이터 클래스입니다.",
java.time.Instant.now()
);
System.out.println("ID: " + post.getId());
System.out.println("제목: " + post.title());
System.out.println("요약: " + post.summary());
System.out.println("작성: " + post.getCreatedAt());
}
}public class RecordConstraints {
public static void main(String[] args) {
System.out.println("=== Record 제약사항 ===\n");
System.out.println("❌ 불가능");
System.out.println("1. 다른 클래스 상속");
System.out.println(" - java.lang.Record 상속 고정");
System.out.println(" - extends 사용 불가\n");
System.out.println("2. 인스턴스 필드 추가");
System.out.println(" - 컴포넌트 외 필드 선언 불가");
System.out.println(" - 모든 필드는 final\n");
System.out.println("3. abstract 선언");
System.out.println(" - final class 고정");
System.out.println(" - 상속 불가\n");
System.out.println("4. native 메서드");
System.out.println(" - JNI 메서드 선언 불가\n");
System.out.println("✅ 가능");
System.out.println("1. 인터페이스 구현 (여러 개)");
System.out.println("2. 정적 필드/메서드");
System.out.println("3. 인스턴스 메서드");
System.out.println("4. 중첩 클래스/Record");
System.out.println("5. Generic 사용");
}
}// 가변 필드가 필요한 경우 → 일반 클래스 사용
class MutablePoint {
private int x, y;
public void setX(int x) { this.x = x; }
public void setY(int y) { this.y = y; }
}
// 상속이 필요한 경우 → 일반 클래스 사용
class Shape {
protected String color;
}
class Circle extends Shape {
private double radius;
}
// Record는 불변 데이터에만 사용
record ImmutablePoint(int x, int y) {}
public class RecordWorkarounds {
public static void main(String[] args) {
System.out.println("=== Record 우회 ===\n");
System.out.println("📌 언제 Record를 사용하나?");
System.out.println("✅ 불변 데이터");
System.out.println("✅ DTO (Data Transfer Object)");
System.out.println("✅ Value Object");
System.out.println("✅ Key (Map)");
System.out.println("✅ 튜플\n");
System.out.println("📌 언제 일반 클래스를 사용하나?");
System.out.println("✅ 가변 상태");
System.out.println("✅ 상속 필요");
System.out.println("✅ 복잡한 로직");
System.out.println("✅ 초기화 후 변경");
}
}// API 요청/응답
record LoginRequest(String username, String password) {
public LoginRequest {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("username 필수");
}
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("password는 8자 이상");
}
}
}
record LoginResponse(boolean success, String token, String message) {
public static LoginResponse success(String token) {
return new LoginResponse(true, token, "로그인 성공");
}
public static LoginResponse failure(String message) {
return new LoginResponse(false, null, message);
}
}
record UserDto(long id, String username, String email, java.time.LocalDateTime createdAt) {}
public class DTOPattern {
public static void main(String[] args) {
System.out.println("=== DTO 패턴 ===\n");
// 로그인 요청
LoginRequest request = new LoginRequest("alice", "password123");
System.out.println("요청: " + request);
// 로그인 응답
LoginResponse response = LoginResponse.success("abc123token");
System.out.println("응답: " + response);
LoginResponse failure = LoginResponse.failure("비밀번호 오류");
System.out.println("실패: " + failure);
// User DTO
System.out.println("\n=== User DTO ===");
UserDto user = new UserDto(1L, "alice", "alice@example.com",
java.time.LocalDateTime.now());
System.out.println(user);
}
}record Money(double amount, String currency) {
public Money {
if (amount < 0) {
throw new IllegalArgumentException("금액은 음수 불가");
}
if (currency == null || currency.length() != 3) {
throw new IllegalArgumentException("통화 코드는 3자");
}
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 불일치");
}
return new Money(amount + other.amount, currency);
}
public Money subtract(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 불일치");
}
return new Money(amount - other.amount, currency);
}
}
record Email(String value) {
public Email {
if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("유효하지 않은 이메일");
}
value = value.toLowerCase();
}
public String domain() {
return value.substring(value.indexOf('@') + 1);
}
}
public class ValueObjectPattern {
public static void main(String[] args) {
System.out.println("=== Value Object ===\n");
// Money
Money m1 = new Money(1000, "USD");
Money m2 = new Money(500, "USD");
Money sum = m1.add(m2);
System.out.println("합계: " + sum);
// Email
System.out.println("\n=== Email ===");
Email email = new Email("Alice@Example.Com");
System.out.println("이메일: " + email.value());
System.out.println("도메인: " + email.domain());
}
}record Pair<T, U>(T first, U second) {
public static <T, U> Pair<T, U> of(T first, U second) {
return new Pair<>(first, second);
}
}
record Triple<A, B, C>(A first, B second, C third) {
public static <A, B, C> Triple<A, B, C> of(A first, B second, C third) {
return new Triple<>(first, second, third);
}
}
public class TuplePattern {
// 여러 값을 반환
public static Pair<Integer, Integer> divMod(int a, int b) {
return Pair.of(a / b, a % b);
}
public static Triple<String, Integer, String> getUserInfo(long id) {
// DB 조회 시뮬레이션
return Triple.of("Alice", 25, "alice@example.com");
}
public static void main(String[] args) {
System.out.println("=== 튜플 패턴 ===\n");
// 몫과 나머지
Pair<Integer, Integer> result = divMod(17, 5);
System.out.println("17 / 5 = " + result.first() + " ... " + result.second());
// 여러 값 반환
System.out.println("\n=== 사용자 정보 ===");
Triple<String, Integer, String> user = getUserInfo(1L);
System.out.println("이름: " + user.first());
System.out.println("나이: " + user.second());
System.out.println("이메일: " + user.third());
}
}record DatabaseConfig(
String host,
int port,
String database,
String username,
String password,
int maxConnections
) {
public DatabaseConfig {
if (host == null || host.isBlank()) {
throw new IllegalArgumentException("host 필수");
}
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("포트 범위: 1-65535");
}
if (maxConnections < 1) {
throw new IllegalArgumentException("최소 연결 수: 1");
}
}
public static DatabaseConfig local() {
return new DatabaseConfig(
"localhost",
5432,
"mydb",
"root",
"",
10
);
}
public static DatabaseConfig production(String host, String database) {
return new DatabaseConfig(
host,
5432,
database,
System.getenv("DB_USER"),
System.getenv("DB_PASSWORD"),
100
);
}
public String connectionString() {
return String.format("jdbc:postgresql://%s:%d/%s", host, port, database);
}
}
public class ConfigExample {
public static void main(String[] args) {
System.out.println("=== 설정 관리 ===\n");
DatabaseConfig localConfig = DatabaseConfig.local();
System.out.println("로컬: " + localConfig.connectionString());
System.out.println("최대 연결: " + localConfig.maxConnections());
}
}import java.time.Instant;
import java.util.*;
sealed interface Event permits OrderCreated, OrderShipped, OrderDelivered, OrderCancelled {}
record OrderCreated(String orderId, String customerId, List<String> items, Instant timestamp) implements Event {}
record OrderShipped(String orderId, String trackingNumber, Instant timestamp) implements Event {}
record OrderDelivered(String orderId, Instant timestamp) implements Event {}
record OrderCancelled(String orderId, String reason, Instant timestamp) implements Event {}
class EventStore {
private List<Event> events = new ArrayList<>();
public void save(Event event) {
events.add(event);
}
public List<Event> getEvents() {
return List.copyOf(events);
}
}
public class EventSourcingExample {
public static void main(String[] args) {
System.out.println("=== 이벤트 소싱 ===\n");
EventStore store = new EventStore();
// 주문 생성
OrderCreated created = new OrderCreated(
"ORD-001",
"CUST-001",
List.of("상품A", "상품B"),
Instant.now()
);
store.save(created);
System.out.println("주문 생성: " + created);
// 배송 시작
OrderShipped shipped = new OrderShipped(
"ORD-001",
"TRACK-123",
Instant.now()
);
store.save(shipped);
System.out.println("배송 시작: " + shipped);
// 이벤트 조회
System.out.println("\n=== 이벤트 히스토리 ===");
for (Event event : store.getEvents()) {
System.out.println(event);
}
}
}// Point Record 완성
record Point(int x, int y) {
// 두 점 사이 거리 계산
public double distanceTo(Point other) {
// 구현
return 0;
}
// 원점으로부터 거리
public double distanceFromOrigin() {
// 구현
return 0;
}
}
public class Problem1 {
public static void main(String[] args) {
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println("거리: " + p1.distanceTo(p2)); // 5.0
System.out.println("원점 거리: " + p2.distanceFromOrigin()); // 5.0
}
}정답:
정답 보기
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
public double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}// Range Record 완성
record Range(int start, int end) {
// Compact Constructor로 검증
public Range {
// 구현
}
// 값이 범위 안에 있는지
public boolean contains(int value) {
// 구현
return false;
}
// 범위 길이
public int length() {
// 구현
return 0;
}
}
public class Problem2 {
public static void main(String[] args) {
Range range = new Range(1, 10);
System.out.println(range.contains(5)); // true
System.out.println(range.contains(15)); // false
System.out.println(range.length()); // 9
}
}정답:
정답 보기
record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException("start > end");
}
}
public boolean contains(int value) {
return value >= start && value <= end;
}
public int length() {
return end - start + 1;
}
}// Task Record 완성
record Task(String title, List<String> tags) {
// Compact Constructor로 방어적 복사
public Task {
// 구현
}
// 태그 추가한 새 Task 반환
public Task addTag(String tag) {
// 구현
return null;
}
}
public class Problem3 {
public static void main(String[] args) {
Task task = new Task("Study", List.of("Java", "Record"));
Task updated = task.addTag("Modern");
System.out.println("원본: " + task.tags()); // [Java, Record]
System.out.println("수정: " + updated.tags()); // [Java, Record, Modern]
}
}정답:
정답 보기
record Task(String title, List<String> tags) {
public Task {
tags = List.copyOf(tags); // 방어적 복사
}
public Task addTag(String tag) {
List<String> newTags = new ArrayList<>(tags);
newTags.add(tag);
return new Task(title, newTags);
}
}record Person(String name, int age) {}record Person(String name, int age) {
public Person {
if (age < 0) throw new IllegalArgumentException();
name = name.trim();
}
}record Point(int x, int y) {
// 인스턴스 메서드
public double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
// 정적 메서드
public static Point origin() {
return new Point(0, 0);
}
}record Circle(double radius) implements Drawable {
@Override
public void draw() { }
}✅ DTO (Data Transfer Object)
✅ Value Object
✅ 튜플
✅ 불변 설정
✅ 이벤트
❌ 다른 클래스 상속 불가
❌ 인스턴스 필드 추가 불가
❌ abstract 불가
✅ 인터페이스 구현
✅ 정적 필드/메서드