Develop Study/Java
(25.06.05) record 클래스 & DTO 활용
WebSocket 기반의 대화방에서 채팅을 구현하는데 있어서,
Subscribe 한 클라이언트들에게 채팅을 보내는데 사용하는 responseDTO 클래스를 record 에 사용하는 것으로 참고자료에서 나타냈다.
하지만, DTO에서 사용되는 컴포넌트들을 그대로 축약했다고 생각한 클래스라고 치부하기 보다는
좀더 고려를 해야하는 부분이 많아서
Java 에 최신으로 등재된 record 클래스에 대해서 설펴보고,
중간중간 나의 궁금한 점들을 "주의 사항" 내지는 표로 정리했다.
record 클래스
- boilerplate 코드를 줄이기 위해서 자동으로 필요한 객체들과 여러 필요한 내용을 생성해주는 특수한 불변 데이터 클래스(immutable data class)
- Java 14 preview 버전, Java 16 정식 버전(standard feature)
- 모든 필드를 컴포넌트로 명시
- final 이기 때문에 당연히 다른 클래스 상속 또는 상속 받을 수 없음
- Setter가 없기 때문에 기존에 만들어진 객체의 필드값을 update 할 수 있는 설계와는 맞지 않음
- 기본 생성자 존재 X → 불변성과 필드 초기화를 한번 이뤄지는 설계 철학에 의해 = 모든 컴포넌트의 값이 존재해야한다는 것
- 모든 필드를 파라미터로 하는 생서자 = Canonical Constructor 만 존재
- 기존의 객체지향 설계의 POJO, Plain Old Java Object 구조에서 → 데이터 저장, 데이터 전달 목적의 클래스를 간결하게 표기 하기 위해 만들어진 구조
- DTO(Data Transfer Object)
- VO(Value Object)
- Config 설정 정보 등 불변 데이터 구조
- 자동 생성
- 모든 필드의 생성자
- 오버로딩 불가능
- getter 메서드
- equals(), hashCode()
- toString()
- 모든 필드의 생성자
example
// 기존의POJO 구성의 클래스
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
// record 클래스
public record Person(String name, int age) {}
- 기본적으로는 body는 빈칸으로 둠
- abstract 의 뜻이 아니라 자동으로 생성되기 때문에 남김
- 컴팩트 생성자를 추가로 작성 가능
POJO vs record
| 항목 | POJO | record |
| 목적 | 동적으로 데이터 저장/수정/조작 | 불변/고정 데이터 저장 |
| 필드 수정 | 가능 (setter 또는 update 메서드 등) | 불가능 (모든 필드는 final) |
| 코드 | getter/setter/생성자 등 수동으로 모두 작성 | 자동 생성 |
| 상속 | 가능 | 불가능 (final) |
| 사용 버전 | Java 1.0부터 | Java 14+ (정식은 Java 16+) |
| 생성자 유연성 | Overloading, default 등 다양하게 사용이 가능 | 모든 필드가 한번에 초기화 해야하므로 제한적 |
컴팩트 생성자, Compact constructor
- record 클래스의 클래스에서 자동으로 생성된 생성자와 메서드에서, 필드에 대해 생성 시 검증/로직을 수행하고 싶을 때 사용
- 일반 constructor와 다르게 필드 이름만으로 정의
기본 컴팩트 생성자 구조
public record Person(String name, int age) {
public Person {
if (age < 0) {
throw new IllegalArgumentException("나이는 0 이상이어야 합니다.");
}
name = name.strip(); // 공백으로 취급되는 모든 문자 제거
}
}
- 파라미터(매개변수)를 명시X
- Person(String name, int age) 대신 Person { ... }
- 컴파일러가 자동으로 파라미터를 필드에 할당하기 전, 또는 후에 원하는 로직 삽입 가능
- 특정 필드 값에 대해서의 조건을 설정 가능
- 내부적으로 this.name = name; 등의 기본 필드 초기화는 자동 수행
- Setter 구조가 아니기 때문에 다시 this.name = ... 하려고 하면 컴파일 오류 발생
static 팩토리 메서드를 통한 객체 생성 : overloading 대응
public record User(String name, String email) {
public User {
name = name.trim();
email = email.toLowerCase();
}
public static User fromRaw(String rawName, String rawEmail) {
return new User(rawName, rawEmail);
}
}
- record 설계 철학에 의해 new User() 과 같이 기본 생성자를 활용해서 새로운 객체를 만들 수 없기 때문에 지정할 수 있음
- Overloading 을 대체하는 역할
- 모든 컴포넌트 값들을 모두 기재를 할 경우 새로운 객체로 계속 만들어 낼 수 있음
record 직렬화
- record는 모든 필드가 생성자 인자로 한번에 들어가야 함
- 정확하게 데이터를 전달을 할 때 안정적으로 전달을 할 수 있음
- 무결성을 유지시킬 수 있음
- JSON 데이터에 필드가 누락되거나 타입이 맞지 않으면 역직렬화 실패
- 안정적인 특징으로 DTO 에 많이 사용가능
Request DTO 에 record 적용
- 응답 객체는 보통 가변적이고 점진적으로 완성, 동적으로 값이 계속 바뀌기 때문에, record에 부적합할 수 있음
validation 적용
POJO 방식
@Getter
public class ChatRoomRequestDTO {
@NotBlank(message = "대화방 제목은 필수 입력해주세요.")
private String title;
@NotBlank(message = "대화방 카테고리는 필수 입력해주세요.")
private String category;
private Boolean isPrivate;
private String password;
}
record
public record ChatRoomRequestDTO(
@NotBlank(message = "대화방 제목은 필수 입력해주세요.")
String title,
@NotBlank(message = "대화방 카테고리는 필수 입력해주세요.")
String category,
Boolean isPrivate,
String password
) {}
- Validation (@Valid) 까지 모두 포함해서 매개변수로 적용
주의
- @Setter와 기본 생성자가 없기 때문에, model.addAttribute() 를 사용하는 FORM 요청에서 사용 불가
- FORM 요청은 HTTP POST 방식으로 전송되며, Spring MVC에서는 보통 **@ModelAttribute**를 이용해서 기본 생성자 + setter로 객체를 구성
- AJAX 방식 = JSON을 사용하는 구조를 취급해야함
- 또는 POJO로 교체해야함
참고자료
https://docs.oracle.com/en/java/javase/21/language/records.html
Java Language Updates
Record classes, which are a special kind of class, help to model plain data aggregates with less ceremony than normal classes.
docs.oracle.com
AJAX 를 사용해서 FE와 데이터를 request, response 되는 환경, 그리고 Response DTO를 Setter로 계속 업데이트가 없을 경우 모든 DTO 는 record로 바꾸면서 보일러플레이트를 만들어도 효율적으로 작성할 수 있을 것이다.

'Develop Study > Java' 카테고리의 다른 글
| (25.06.26) equals() 와 hashCode() 의 contract에 관하여 (0) | 2025.06.26 |
|---|---|
| (25.05.07) NumberFormatException 와 오버플로우 (0) | 2025.05.07 |
| (25.02.19) Java Spring Boot 에서 파일 읽어오기 & 내보내기 (CSV) (1) | 2025.02.19 |
| (24.10.29) Comparator 인터페이스와 람다식 (0) | 2024.10.29 |
| (24.10.10) TDD & DDD / Filter & Interceptor & AOP 비교해보기 (0) | 2024.10.10 |