[Java] Record란? feat. Lombok

Record란?
record는 Java 16에서 정식 도입된 문법으로, 데이터를 담기만 하는 불변(immutable) 객체를 단 한 줄로 만들어 주는 기능입니다.
public record User(String name, int age) { }
이 한 줄만 쓰면 생성자, 접근자(getter()), equals(), hashCode(), toString() 을 컴파일러가 전부 자동으로 만들어 줍니다. 예전에는 수십 줄을 직접 써야 했던 일을 자바 문법 하나로 끝내는 거죠. 그래서 DTO나 VO처럼 "값을 표현하거나, 전달하는 객체" 를 만들 때 가장 많이 쓰입니다.
1. record의 등장 배경
자바를 처음 배우면 이런 코드를 자주 만나게 됩니다. "이름과 나이만 담는 객체 하나" 를 만들고 싶을 뿐인데, 막상 제대로 쓰려면 이렇게 길어집니다.
public class User {
private final String name;
private final int age;
public User(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) { /* 한참... */ }
@Override
public int hashCode() { /* 한참... */ }
@Override
public String toString() { /* 한참... */ }
}
본질은 "필드 2개" 인데 코드는 주석 부분을 고려하면 실제로 수십 줄이 됩니다. 이런 장황함(verbosity)을 자바는 오랫동안 비판받아 왔어요.
이 문제를 라이브러리로 먼저 해결한 게 Lombok 이고, 라이브러리에 의존하지 않도록 언어 차원에서 해결한 게 record 입니다. record는 "데이터를 담기만 하는 객체"를 자바 문법 자체로 간결하게 표현하기 위해 등장했습니다.
💡 버전 이야기
record는 Java 14·15에서 프리뷰로 나왔다가 Java 16에서 정식 기능(JEP 395) 으로 확정됐습니다. 다만 Java 17이 LTS(장기 지원 버전)이다 보니 "17부터 쓰는 기능"으로 기억하는 사람도 있습니다.
2. record 기본 문법
위에서 본 길었던 User 클래스를 record로 바꾸면 딱 한 줄 입니다.
public record User(String name, int age) { }
이 한 줄로 컴파일러가 아래의 것들을 자동으로 만들어 줍니다.
// 실제로는 눈에 보이지 않지만, 컴파일러가 만들어 주는 것들
private final String name; // 모든 필드는 final (불변)
private final int age;
public User(String name, int age) // 생성자 (canonical constructor)
public String name() // 접근자 — 주의! getName()이 아니라 name()
public int age()
public boolean equals(Object o) // equals
public int hashCode() // hashCode
public String toString() // "User[name=홍길동, age=30]"
여기서 가장 많이 헷갈리는 포인트 하나. record의 접근자는 getName()이 아니라 name() 입니다. 기존 자바빈(JavaBean) 규약을 따르지 않아요. 이로 인해 record를 사용했을때 동작하지 않는 오래된 버전의 라이브러리들도 종종 있습니다.
검증 로직 넣기. Compact Constructor
생성자에 값 검증을 넣고 싶을 때는 compact constructor 를 사용합니다. 파라미터 선언과 this.x = x 할당을 생략할 수 있어 깔끔합니다.
public record User(String name, int age) {
public User { // {} 괄호 안에 파라미터를 안 씀!
if (age < 0) {
throw new IllegalArgumentException("나이는 0 이상이어야 합니다");
}
name = name.trim(); // 값 정규화도 가능
// this.name = name; ← 이 할당은 컴파일러가 자동으로 해줌
}
}
편의 생성자 추가하기
다른 형태의 생성자가 필요하면 추가할 수 있는데, 반드시 기본 생성자에게 위임(this(...))해야 합니다.
public record User(String name, int age) {
public User(String name) {
this(name, 0); // 기본 생성자에게 위임 필수
}
}
메서드도 추가
record는 단순한 데이터 그릇이지만 메서드를 넣는 것은 가능합니다.
public record Circle(double radius) {
public double area() { // 인스턴스 메서드 OK
return Math.PI * radius * radius;
}
public static Circle unit() { // static 메서드 OK
return new Circle(1.0);
}
// private String memo; ← ❌ 인스턴스 필드 추가는 불가!
}
record가 할 수 없는 것 (제약사항)
| 항목 | 가능 여부 |
|---|---|
| 다른 클래스 상속 | ❌ 불가 |
| 인스턴스 필드 추가 | ❌ 불가 |
| 필드 값 변경 (가변) | ❌ 불가 (모두 final) |
| 인터페이스 구현 | ✅ 가능 |
| 제네릭 | ✅ 가능 |
| 메서드 추가 | ✅ 가능 |
요약하면 record는 "불변(immutable)이고, 상속할 수 없는, 데이터 전용 객체" 입니다.
3. record 사용 OR Lombok을 사용한 DTO 클래스
같은 DTO를 두 방식으로 만들어 비교해 봅시다.
Lombok 방식
@Getter
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class UserDto {
private final String name;
private final int age;
}
record 방식
public record UserDto(String name, int age) { }
겉보기엔 둘 다 보일러플레이트를 줄여주지만, 속을 들여다보면 차이가 분명합니다.
| 구분 | record | Lombok |
|---|---|---|
| 정체 | 언어 문법 (컴파일러가 보장) | 외부 라이브러리 |
| 불변성 | 항상 불변 | @Data는 가변, @Value는 불변 |
| 접근자 이름 | name() |
getName() |
| 상속 | 불가 | 자유롭게 가능 |
| 빌더 | 없음 | @Builder 제공 |
| IDE/플러그인 | 불필요 | 플러그인 필요 |
| 새 자바 버전 | 항상 안전 | 깨질 위험 존재 |
핵심 차이를 정리하면 이렇습니다.
- record는 언어의 일부라 컴파일러가 동작을 보장합니다. 반면 Lombok은 컴파일러 내부 API에 의존하는 라이브러리라, 새 자바 버전이 나오면 깨질 수 있고 IDE 플러그인이 따로 필요합니다.
- record는 강제로 불변 입니다. 모든 필드가
final이라 값을 바꿀 수 없습니다. - record는 상속이 안 됩니다. 대신 인터페이스 구현은 가능합니다.
그렇다고 record가 무조건 좋은 건 아닙니다. 필드가 많을 때 @Builder가 주는 편리함이나, 상속·가변성이 필요한 경우는 Lombok이 여전히 유리합니다.
4. record와 찰떡궁합 — Record Pattern (Java 21)
record의 진짜 강력함은 Java 21의 record pattern (JEP 440) 과 만났을 때 드러납니다. record를 분해(deconstruct) 해서 안에 든 값을 바로 꺼낼 수 있는 기능입니다. Lombok으로는 절대 흉내 낼 수 없는, 언어 차원의 이점이에요.
기존 방식 vs record pattern
record Point(int x, int y) { }
record Circle(Point center, double radius) { }
Object obj = new Circle(new Point(1, 2), 3.0);
// 기존 방식 — 타입 확인 → 캐스팅 → getter 호출, 3단계
if (obj instanceof Circle c) {
Point center = c.center();
int x = center.x();
}
// record pattern — 중첩 구조까지 한 번에 분해!
if (obj instanceof Circle(Point(int x, int y), double radius)) {
System.out.println("x=" + x + ", y=" + y + ", r=" + radius);
}
record를 switch 문법과 함께 사용시
sealed interface(상속 가능한 클래스를 못박아두는 기능)와 함께 쓰면 컴파일러가 "모든 경우를 다 처리했는지" 검사까지 해줍니다.
sealed interface Shape permits Circle, Rectangle, Triangle { }
record Circle(Point center, double radius) implements Shape { }
record Rectangle(Point topLeft, int width, int height) implements Shape { }
record Triangle(Point a, Point b, Point c) implements Shape { }
double area(Shape shape) {
return switch (shape) {
case Circle(_, double r) -> Math.PI * r * r;
case Rectangle(_, int w, int h) -> (double) w * h;
case Triangle(var a, var b, var c) -> calcArea(a, b, c);
// default가 없어도 됨! 컴파일러가 모든 경우를 확인했기 때문
};
}
여기서
_(언더스코어)는 Java 21의 unnamed pattern 으로, "이 값은 안 쓴다"는 명시적 표현입니다.
조건을 더하는 Guarded Pattern
when 키워드로 추가 조건을 걸 수도 있습니다.
switch (obj) {
case Point(int x, int y) when x == 0 -> System.out.println("Y축 위의 점: " + y);
case Point(int x, int y) when y == 0 -> System.out.println("X축 위의 점: " + x);
case Point(int x, int y) -> System.out.println("일반 점");
default -> System.out.println("Point가 아님");
}
5. 기존 여러 라이브러리, 프레임워크와의 조합
실무에서 가장 궁금한 부분입니다. record를 Lombok, Spring, JPA 같은 익숙한 기술들과 함께 쓸 수 있을까요? 결론부터 표로 정리하면 이렇습니다.
| 기술 | 조합 | 한 줄 설명 |
|---|---|---|
Lombok @Builder |
✅ | 필드 많을 때 유용 |
Lombok @Getter/@ToString 등 |
⚠️ | record가 이미 제공 → 불필요 |
Spring @RequestBody |
✅ | DTO로 가장 흔하게 사용 |
Spring @ModelAttribute |
✅ | Spring Boot 3+ 공식 지원 |
Spring @ConfigurationProperties |
✅ | 불변 설정값에 이상적 |
Bean Validation (@Valid) |
✅ | 필드에 직접 붙임 |
| JPA 엔티티 | ❌ | JPA 스펙상 근본적으로 충돌 |
| JPA 조회용 프로젝션 | ✅ | 매우 유용 |
| Jackson (JSON) | ✅ | 별도 설정 불필요 |
Lombok과 조합
record는 이미 getter, toString 등을 제공하므로 그런 애너테이션은 필요 없습니다. 다만 record에 없는 @Builder 는 함께 쓰면 유용합니다.
@Builder
public record CreateUserRequest(String name, String email, int age) { }
// 사용
CreateUserRequest req = CreateUserRequest.builder()
.name("홍길동")
.email("hong@example.com")
.age(30)
.build();
Spring 프레임워크와 조합시
특히 컨트롤러의 요청/응답 DTO로 매우 잘 어울립니다. Spring 6 (Boot 3) 부터 record를 @ModelAttribute 대상으로 공식 지원합니다.
// 요청 DTO + 검증을 한 번에
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(0) int age
) { }
@RestController
public class UserController {
@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserRequest req) {
// ...
}
}
설정값을 담는 @ConfigurationProperties에도 잘 맞습니다. 설정값은 보통 바뀌지 않으니 불변인 record와 궁합이 좋습니다.
@ConfigurationProperties(prefix = "app")
public record AppConfig(String apiKey, int timeout) { }
JPA와 조합 — 엔티티는 ❌, 프로젝션은 ✅
여기는 꼭 알아둬야 할 중요한 부분입니다. record는 JPA 엔티티로 쓸 수 없습니다. 이유는 둘의 설계 철학이 정반대이기 때문입니다.
| JPA가 요구하는 것(스펙) | record의 현실 |
|---|---|
| 기본 생성자(파라미터 없는 생성자) | 없음. 지원불가 |
| 값을 바꿀 수 있는 가변 필드 | 모두 final (불변) |
| 프록시를 위한 상속 가능성 | 상속 불가로 인해 미지원 |
그래서 엔티티는 여전히 일반 클래스 + Lombok 으로 만듭니다.
// ❌ 이렇게 하면 안 됨
@Entity
public record User(Long id, String name) { }
// ✅ 엔티티는 기존 방식대로
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class User {
@Id @GeneratedValue
private Long id;
private String name;
}
대신 조회 결과를 담는 프로젝션(projection) 으로는 record가 아주 유용합니다.
// 조회 결과 전용 record
public record UserSummary(Long id, String name) { }
// Spring Data JPA — 반환 타입을 record로 지정하면 끝
interface UserRepository extends JpaRepository<User, Long> {
List<UserSummary> findByName(String name);
}
Jackson (JSON 변환) — 자동 동작
JSON ↔ 객체 변환도 별도 설정 없이 잘 됩니다. (Jackson 2.12+ 별도 설정 없이 바로 동작)
public record UserDto(String name, int age) { }
// {"name":"홍길동","age":30} ↔ UserDto 자동 변환'자바[Java]' 카테고리의 다른 글
| 불변 객체(Immutable Object)란 (0) | 2026.06.04 |
|---|---|
| [JAVA] Lombok이란? Lombok 적용하는 방법 (3) | 2020.02.13 |
| [Java] Java의 문자열(String) 객체가 저장되는 String Pool에 대하여 (0) | 2019.07.18 |
| [Effective Java] 인스턴스화가 필요없는 Utility 클래스 등은 private 생성자를 사용하자 (0) | 2019.07.16 |
| [Effective Java] 문자열 String 영어 대소문자 무시하여 비교시에는 equalsIgnoreCase() 메서드를 사용하자. (0) | 2019.07.11 |
도로락
WriterIT, 프로그래밍, 컴퓨터 활용 정보 등을 위한 블로그