equals(), hashCode(), toString()같은 보일러플레이트 코드를 컴파일러가 자동으로 생성해 줌.class 키워드 대신 record 키워드를 사용하여 '데이터 캐리어(data carrier)'를 간결하게 선언하는 방법임. 클래스명 옆에 소괄호로 정의된 헤더(header)를 통해 상태를 구성할 컴포넌트(component)를 선언함.
컴파일러는 이 헤더를 기반으로 다음 요소를 자동으로 생성함.
private final로 선언됨: 불변성을 보장하는 핵심.public 생성자.get 접두사 없이 필드명과 동일한 이름(예: user.username()).equals(): 모든 필드의 값이 같은지 비교.hashCode(): 모든 필드의 hashCode를 조합하여 생성.toString(): 클래스명과 각 필드 및 값을 UserRecord[username=testuser, email=... ] 형태로 출력.모든 Record는 암묵적으로 java.lang.Record 클래스를 상속함.
// ✅ Record 버전 (Java 16+)
public record UserRecord(String username, String email) { }
// 모든 필드는 private final로 자동 선언됨
// Canonical Constructor, equals, hashCode, toString, getter 자동 생성
// getter는 get 접두사 없이 username(), email() 형태로 제공됨
public class MainRecordExample {
public static void main(String[] args) {
UserRecord user = new UserRecord("testUser", "test@example.com"); // 자동 생성자 호출
System.out.println(user.username()); // "testUser" (get 접두사 없음)
System.out.println(user); // UserRecord[username=testUser, email=test@example.com]
}
}
Record의 모든 필드는 final이므로 얕은 불변성(Shallow Immutability)을 가짐.
⚠️ 주의: 얕은 불변성 (Shallow Immutability)
Record의 필드가List나 다른 객체와 같은 참조 타입일 경우, 그 참조 자체는 불변이지만 참조가 가리키는 객체 내부의 상태는 변경될 수 있음. 완전한 불변성을 원한다면List.copyOf()등을 사용해 방어적 복사를 해야 함.
public record Order(long id, List<String> items) {}
order.items().add("new item");// 컴파일 오류는 없으나, 객체 상태가 변경됨.
DTO는 일반적으로 데이터와 접근자 메서드만 가지는 순수 데이터 객체임. 기존에는 Lombok 같은 라이브러리의 도움 없이는 많은 상용구 코드가 필요했으나, Record는 이를 언어 차원에서 해결함.
record 키워드 자체로 '이것은 데이터를 담는 객체'라는 의도를 명확하게 전달.Record는 필드 초기화(this.x = x) 코드가 없는 컴팩트 생성자를 지원함. 주로 매개변수 유효성 검증이나 정규화에 사용됨.
import java.util.Objects;
// 컴팩트 생성자로 email 필드의 null 여부를 검증하는 Record
public record UserRecord(String username, String email) {
// 이것이 컴팩트 생성자
public UserRecord {
Objects.requireNonNull(username);
Objects.requireNonNull(email);
if (username.isBlank()) {
throw new IllegalArgumentException("사용자 이름은 비어 있을 수 없습니다.");
}
}
}
// 사용 예시
// UserRecord user = new UserRecord(null, "test@test.com"); // NullPointerException 발생
// UserRecord user = new UserRecord(" ", "test@test.com"); // IllegalArgumentException 발생
Record도 일반 클래스처럼 인터페이스를 구현하거나, 정적(static) 필드/메서드를 가질 수 있음. (단, 다른 클래스 상속은 불가)
public interface JsonSerializable {
String toJson(); // 간단한 JSON 변환 메서드를 가진 인터페이스
}
public record UserRecord(String username, String email) implements JsonSerializable {
// 정적 팩토리 메서드
public static UserRecord createGuest() {
return new UserRecord("guest", "guest@example.com");
}
// 인터페이스 메서드 구현
@Override
public String toJson() {
return """
{
"username": "%s",
"email": "%s"
}
""".formatted(username, email);
}
}
// 사용 예시
UserRecord guest = UserRecord.createGuest();
System.out.println(guest.toJson());
Spring Boot 2.5+, Jackson 2.12+ 환경에서 Record를 DTO로 완벽하게 지원함.
// /src/main/java/com/example/demo/dto/UserDto.java
// DTO는 별도의 패키지에서 top-level record로 관리하는 것이 일반적
public record UserDto(String username, String email, int age) {}
// /src/main/java/com/example/demo/controller/UserController.java
import com.example.demo.dto.UserDto;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
// Jackson 라이브러리가 HTTP Message Body(JSON)를 UserDto Record로 역직렬화(@RequestBody)
@PostMapping
public UserDto createUser(@RequestBody UserDto user) {
System.out.println("생성된 사용자: " + user);
// ... 비즈니스 로직 처리 (예: service.join(user)) ...
// 처리 결과를 다시 Record 객체에 담아 반환
// Jackson이 Record를 JSON으로 직렬화(@ResponseBody)하여 응답
return user;
}
@GetMapping("/{username}")
public UserDto getUser(@PathVariable String username) {
// ... 사용자 조회 로직 ...
return new UserDto(username, username + "@example.com", 30);
}
}
@Value와 가장 유사함. 하지만 언어 내장 기능이므로 별도 의존성이나 플러그인 설정이 필요 없음.| 구분 | record (Java 16+) | @Value (Lombok) | @Data (Lombok) |
|---|---|---|---|
| 불변성 | 기본 (모든 필드 private final) | 기본 (모든 필드 private final) | 선택 (변경 가능, setter 생성) |
| 접근자 | field() (get 접두사 없음) | getField() | getField(), setField() |
| 빌더 | 지원 안 함 (직접 구현 필요) | @Builder 추가 가능 | @Builder 추가 가능 |
| 의존성 | 없음 (언어 내장) | Lombok 라이브러리 필요 | Lombok 라이브러리 필요 |
| 상속 | java.lang.Record 암묵적 상속, 다른 클래스 상속 불가 | 상속 가능 | 상속 가능 |
Pattern Matching for instanceof (Java 16+): Record와 함께 사용하면 타입 체크와 동시에 컴포넌트를 변수로 추출하여 코드를 더욱 간결하게 만듦.
// 이전 방식
if (obj instanceof UserRecord) {
UserRecord user = (UserRecord) obj;
System.out.println("사용자 이름: " + user.username());
}
// Pattern Matching 적용
if (obj instanceof UserRecord(String username, String email)) {
System.out.println("사용자 이름: " + username); // 바로 변수 사용 가능
}
Sealed Classes (Java 17+): 특정 클래스나 인터페이스를 상속/구현할 수 있는 타입을 명시적으로 제한하는 기능. Record와 함께 사용하면, 특정 인터페이스를 구현하는 타입이 오직 지정된 Record들 뿐임을 보장하여 더욱 정교하고 안정적인 도메인 모델 설계가 가능함.