Java Record로 DTO 구현하기
불변성과 간결성을 동시에 잡는 Java Record
Java Record로 DTO 구현하기
📌개요
DTO는 계층 간 데이터 전송을 위한 객체로 Java 개발에서 빈번히 사용된다.
기존에는 Class로 DTO를 구현했지만 Java 14부터 등장한 Record를 활용해보면 불변성, 간결성, 명확성을 모두 확보할 수 있다.
주문 시스템을 예시로 Record를 활용해 DTO를 효과적으로 구현하는 방법을 알아보자.
- DTO: Data Transfer Object
- 불변성(Immutability): 생성 후 수정 불가
- 간결성(Simplicity): 반복 코드 자동 생성
- 명확성(Clarity): 순수 데이터 객체임을 명시
📌내용
DTO란?
Data Transfer Object는 다음과 같은 특징을 가진다.
- 데이터 전송 전용 객체
- 비즈니스 로직 없이 순수 데이터만 포함한다.
- 계층 간 데이터 변환
- 예: Controller <-> Service 계층 연결
- 선택적 데이터 노출
- 민감 정보 필터링 또는 필요한 데이터만 전송
DTO 사용 Flow
sequenceDiagram
participant Client as 클라이언트
participant Controller as 컨트롤러
participant Service as 서비스
participant Repository as 리포지토리
Client->>Controller: Request DTO 전송
Controller->>Service: Record DTO 변환 전달
Service->>Repository: Entity 변환 저장
Repository->>Service: Entity 반환
Service->>Controller: Record DTO 변환
Controller->>Client: Response DTO 반환
Class DTO VS Record DTO
Class DTO (기존 방식)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ProductDTO {
private final Long id;
private final String name;
private final double price;
// 생성자
public ProductDTO(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// 수동 작성 필요
public Long getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
// getter, setter, toString(), equals(), hashCode() 등 ...
}
Record DTO (Java 16+)
Record는 Constructor
, getter
, equals()
, hashCode()
, toString()
을 자동 생성한다. 그 외에도 추가적인 기능을 제공하지만 직접 구현해야 하는 부분도 있다.
1
public record ProductDTO(Long id, String name, double price) {}
Record로 DTO 구현하기
간단히 어떻게 동작하는지 알아본다.
테스트 프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
12
├─main
│ ├─java
│ │ └─org
│ │ └─b9f1
│ │ │ Main.java
│ │ │
│ │ ├─dto
│ │ │ ProductDTO.java
│ │ │ UserDTO.java
│ │ │
│ │ └─service
│ │ UserService.java
UserDTO.java
1
2
3
4
5
6
public record UserDTO(
String id,
String userName,
String email,
LocalDateTime createdAt
) {}
ProductDTO.java
1
2
3
4
5
6
public record ProductDTO(
String id,
String name,
int price,
int stock
) {}
UserService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserService {
private final Map<String, UserDTO> userStorage = new HashMap<>();
public UserDTO createUser(String userName, String email) {
String id = UUID.randomUUID().toString();
UserDTO newUser = new UserDTO(
id,
userName,
email,
LocalDateTime.now()
);
userStorage.put(id, newUser);
return newUser;
}
public UserDTO getUser(String id) {
return userStorage.get(id);
}
}
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
// 1. UserService 테스트
UserService userService = new UserService();
UserDTO user = userService.createUser("길동쓰", "길동@길동쓰.com");
System.out.println("생성된 사용자: " + user);
// Java Record의 getter는 전통적인 Java Beans 스타일(getXXX()) 과 다르게 동작한다.
// user.getEmail() 이게 아니고 필드명과 동일한 메서드를 사용한다.
System.out.println("생성된 사용자 이메일: " + user.email());
// 2. ProductDTO 직접 사용
ProductDTO product = new ProductDTO("LT1", "Laptop", 1000, 10);
System.out.println("상품 정보: " + product);
}
}
Record DTO의 적절한 사용처
✅ Record DTO를 사용하면 좋은 경우
성능 팁 Record는 일반 클래스보다 메모리 사용량이 20~30% 적고 생성 속도가 약 15% 빠른 것으로 측정된다고 한다. (JMH 벤치마크 기준)
- 간단한 데이터 전송 객체
- API 요청/응답 모델
- 계층 간 데이터 전달
- 데이터베이스 조회 결과 매핑
- 값 객체(Value Object)
- 좌표(Coordinate), 금액(Money) 등 도메인 원시값 래핑
- 임시 데이터 그룹핑
- 다중 반환값 처리
1
public record Pair<A, B>(A first, B second) {}
- 다중 반환값 처리
❌ RecordDTO가 부적합한 경우
- 가변 객체가 필요한 경우
- 상태 변경이 빈번한 도메인 모델
- 복잡한 비즈니스 로직 포함
- 유효성 검사 외 추가 로직이 필요한 경우
- 상속이 필요한 구조
주의사항
- 불변성 유지
- 값 변경이 필요하면 새 객체 생성
1 2 3 4 5
ProductResponse updated = new ProductResponse( original.id(), "새 이름", // 변경 original.price() );
- 값 변경이 필요하면 새 객체 생성
- Jackson 직렬화
- 기본 지원되지만 커스텀 설정 필요시
@JsonCreator
사용1 2 3 4
public record ProductRequest(String name, double price) { @JsonCreator public ProductRequest { /* ... */ } }
- 기본 지원되지만 커스텀 설정 필요시
- 상속 불가
final
클래스이므로 상속할 수 없음
⚙️EndNote
사전 지식
- Java 16+ (Record 정식 기능)
- DTO 패턴
- 불변 객체(Immutable Object)
더 알아보기
This post is licensed under CC BY 4.0 by the author.