내가 자바를 배우며 제일 신기했던 점은 데이터 전송하는 방식이 다양했다는 점이었다.
(다른 언어에도 있는데 굳이 신경쓰지 않았던 것일 수도..)
Java애플리케이션의 다양한 계층간, 서비스간에 데이터를 전송해야할 때 DTO를 사용하는데, Java14부터 새로운 기능인 Record가 도입되었다.
이 둘의 차이점과 사용법을 알아보고 어떤 것이 더 적합한지 알아보자.
이름 그대로 데이터를 전송하는 객체이다.
복잡한 동작이나 로직 없이 데이터를 담기 위한 간단한 객체이며,
이 객체의 역할은 데이터를 묶어서 필요한 곳에 전달하는 것이다.
toString(), hashCode(), equals()메서드 재정의(Override) Lombok(롬복)같은 도구를 사용하면 반복적인 코드 작성 않고도 완전한 기능을 갖춘 DTO를 만들 수 있다.
하지만 Record는 기본적으로 불변성(immutability)를 가지며, 반복적인 코드를 제거하는 또 다른 방식을 제공한다.
import java.util.Objects;
public class UserDTO {
private String username;
private String password;
//생성자
public UserDTO(String username, String password) {
this.username = username;
this.password = password;
}
//getter, setter
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//equals재정의
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserDTO userDTO = (UserDTO) o;
return Objects.equals(username, userDTO.username);
}
//toString재정의
@Override
public String toString() {
return "UserDTO{" +
"name='"+ username +
"password = " + password +'}';
}
//hashCode()재정의
@Override
public int hashCode() {
return Objects.hash(username);
}
}
public static void main(String[] args) {
UserDTO userDTO = new UserDTO("nana", "password123");
//데이터 접근
System.out.println(userDTO.getUsername());
System.out.println(userDTO.getPassword());
//toString()
System.out.println(userDTO);
//비교
UserDTO userDTO2 = new UserDTO("nana", "password1234");
System.out.println(userDTO.equals(userDTO2));
}
DTO에서는 데이터 유효성 검사, 데이터 변환 메서드 또는 비즈니스 로직을 추가할 수 있다.
예를 들어, password필드가 유효한 형식인지 확인하는 검증 메서드를 추가할 수 있다.
Record는 커스터마이징이 제한적이다.
가볍고 불변성을 유지하도록 설계되었기 때문에 내부 상태를 수정하거나 복잡한 로직을 쉽게 추가할 수 없다.
데이터 객체에 커스터마이징된 동작이나 로직이 필요하다면 DTO가 더 유연한 선택이다.
public record UserRecord(String userName, String password){
public UserRecord{
validate(userName, password);
}
}
public static void main(String[] args) {
UserRecord userRecord = new UserRecord("nana", "password123");
//데이터 접근
System.out.println(userRecord.userName());
System.out.println(userRecord.password());
//toString()사용
System.out.println(userRecord);
//객체 비교
UserRecord userRecord1 = new UserRecord("nana", "password123");
System.out.println(userRecord.equals(userRecord1));
}
Boilerplate Code) 감소Boilerplate Code란?
컴퓨터 프로그래밍에서 보일러플레이트 또는 보일러플레이트 코드라고 부르는 것은 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다.
객체 생성 이후 데이터 변경을 못하므로 다중 스레드 환경에서 더 안전하게 사용할 수 있다.
Record를 사용하면 해당 객체가 추가적인 동작이나 로직 없이 데이터를 전달하기 위한 것임을 분명하게 알 수 있다.
데이터를 간결하고 직관적으로 표현한다. Record 선언에는 필드만 포함되므로 코드가 더 깔끔하고 읽기 쉽다. 특히 데이터 모델이 많은 프로젝트에서 유지보수가 용이하다
함수형 프로그래밍의 핵심 원칙 중 하나인 불변성(Immutability)과 잘 맞다.
불변 데이터 객체를 사용하고자 하는 시스템에 적합하다.
객체의 수명 주기 동안 데이터가 계속해서 업데이트 되는 상황
ex) 웹앱에서 사용자 등록 양식을 통해 일부 필드가 비어있는 상태로 UserDTO가 생성됨.
사용자가 프로필을 업데이트 하면서 UserDTO들의 값이 변경되어야 하는 경우.
데이터 객체가 단순히 데이터를 전달하는 것 이상으로 동작해야할 때, 예를들어 이메일 형식을 확인하거나 입력을 정제하는 로직이 필요할 떄.
=> DTO는 검증, 변환, 추가 메서드 등 맞춤형 동작을 추가하는 데 더 유연하다.
Java16 이전 버전의 프로젝트는 Record를 사용할 수 없다.
이런 경우에는 DTO나 Lombok과 같은 대안으로 코드를 간소화 해야한다.
Record는 가볍고 불변성을 가진 객체로 데이터를 전달해야할 때 이상적이다.
마이크로 서비스 아키텍쳐(MSA)에서 서비스 간 데이터를 전달할 때 데이터를 수정할 필요가 없다면 Record사용이 유의미하다.
데이터를 전달하기만 하고 수정할 필요가 없을 경우.
불변성을 보장하여 데이터 일관성을 유지하므로, 데이터베이스에서 서비스 계층으로 또는 서비스간 데이터를 전달하는 데 적합하다.
최신Java 애플리케이션에서 데이터 표현을 간소화 하도록 설계되었기 때문에 기존 DTO가 가지고 있던 불필요한 반복적인 코드를 줄이는 데 도움이 된다.
확장이 어렵다.
extends를 사용하여 다른 클래스를 상속할 수 없고, 필드가 final로 선언되기 때문.
주로 데이터를 전달하려는 목적으로 설계되었기 때문에 비즈니스 로직을 포함하기에 적절하지 않다.
Java14, 16 이전 버전에서는 호환이 불가능하다.
Record와 VO 모두 객체의 상태가 변경되지 않는 것을 보장한다.
또 데이터를 캡슐화하여 표현하는 데 초점을 맞춘다.
VO는 값 기반의 동등성을 가지며, Record도 동일한 필드 값을 가지면 동일한 객체로 간주된다는 점이 공톰점이다.
VO는 도메인 모델내에서 특정 개념을 표현하고, 도메인 로직과 밀접하게 관련이 있다.
VO는 비즈니스 로직이나 규칙을 가질 수 있으나 Record는 단순히 데이터를 캡슐화하여 전달하는데 의미가 있다.
=> Record는 VO를 구현하는 데 적합하지만, VO의 모든 특성을 완벽히 대체하지는 않는다.
VO는 더 넓은 도메인 맥락에서 사용되고, 비즈니스 로직을 포함할 수 있다.
Record가 설계상 간결하므로 DTO보다 메모리를 조금 덜 사용할 수 있다.
Record는 불변이기 때문에 스레드 간에 공유될 때 동기화나 잠금(locking)메커니즈이 필요하지 않다.
스레드 간 경쟁(thread contention)으로 성능이 저하되는 상황에서 성능을 향상할 수 있다.
반면, 가변(mutable) DTO를 멀티 스레드 환경에서 사용할 경우, 스레드 안정성을 보장하기 위해 접근을 동기화하거나 다른 메커니즘을 사용해야 하므로 성능에 추가적인 부담이 생기고 애플리케이션이 느려질 수 있다.
DTO와 Record 모두 일반적인 Java객체(POJO, Plain old Java Object)이므로 동일한 가비지 컬렉션 처리에 따라 관리된다.
하지만 Record가 더 간결하니 메모리에 적은 객체가 생성, 유지 될 수 있어 가비지 컬렉션이 조금 더 빠르게 이루어질 수 있다.
=> 대량의 데이터 객체를 처리하는 장기 실행 애플리케이션에서 성능 향상에 기여할 수 있다.
Record는 컴파일러에 의해 자동 생성 되며, 성능을 최적화하도록 설계되어 있어, 객체 생성, 메서드 호출, 비교작업에서 CPU성능이 조금 더 향상될 수 있다.
복잡한 DTO는 수동으로 구현된 메서드에서 비효율성이 발생할 수 있으나, Record는 일관되고 최적화된 방식으로 작업을 처리하므로 더 효율적이다.
DTO와 Record간의 성능 차이는 대부분 애플리케이션에서 매우 적거나 무시할만한 수준이다.
대용량 데이터 처리, 높은 처리량을 요구하는 애플리케이션, 또는 리소스가 제한된 환경에서만 눈에 띌 정도의 성능 차이를 경험할 수 있다.
레코드는 불변 데이터를 전달하기 위한 캐리어이기 때문에 단순한 값의 집합을 표현하는 객체 지향적인 구성을 고안한다.
개발자가 확장 가능한 동작보다는 불변의 데이터 모델링에 집중할 수 있도록 도와준다.
과제를 하면서 레코드 사용한 부분에 대해 좋은 피드백을 받았던 적이 있는데 사실 왜 잘 사용한건지 몰랐다.
근데 해당 내용을 공부하면서 왜 DTO가 아닌 Record사용으로 좋은 피드백을 받았던 것인지 이해가 되었다 :)
DTO vs Record in Java: Which Should You Use?
자바 DTO vs Record, 무엇을 사용해야 할까?
[10분 테코톡] 타칸의 Record