VO, DTO, Record 뭘 써야 할까?

GEONNY·2024년 9월 1일
0
post-thumbnail

Java 에서 데이터 객체를 다룰 때는 VO, DTO를 사용합니다. 그리고 Java 14 이후로는 이런 데이터 객체를 다루기 위한 Record 가 추가되었습니다. 이것들이 무엇이며 어떠한 특징을 가지고 있는지 알아보겠습니다.

📌VO

VO (Value Object)는 값(Value)을 표현하는 객체로, 동일한 값을 가지는 두 객체는 동일한 것으로 간주됩니다. VO는 주로 불변(immutable) 객체로 설계되며, 값의 동일성을 비교할 때 사용됩니다.

📍등장배경

VO는 복잡한 비즈니스 로직에서 특정 속성 값에 대한 신뢰성과 일관성을 유지하기 위해 도입되었습니다. 값의 무결성을 보장하고, 동일한 값을 가진 객체들이 동일하다고 간주되도록 하기 위한 목적이 있습니다.

📍주요특징

🎈불변성 (Immutable)

VO는 생성 시점 이후에 상태가 변경되지 않도록 설계됩니다. 이는 코드의 안전성을 높여줍니다.

🎈동등성

VO는 객체의 주소가 아닌, 객체가 담고 있는 값으로 동등성을 비교합니다. 두 VO가 동일한 값을 가지면, 두 객체는 동일한 것으로 간주됩니다.

📍사용 예

@Getter
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class PersonVO {
    private String firstName;
    private String lastName;
    private Integer age;
}
PersonVO personNoArgs = new PersonVO();
PersonVO personAllArgs = new PersonVO("건", "이", 20);
PersonVO personBuilder = PersonVO.builder()
       			                 .firstName("건")
			                     .lastName("이")
			                     .age(20)
			                     .build();

VO 의 동등성을 위해 @EqualsAndHashCode 를 추가해 주었습니다. 불변성을 위해 Setter 는 존재하지 않습니다.

📌DTO

DTO (Data Transfer Object)는 계층 간 데이터를 전송하기 위한 객체입니다. Database에서 조회한 데이터를 서비스 계층으로 전달하거나, 클라이언트로부터 받은 데이터를 서비스 계층에 전달할 때 사용됩니다.

📍등장배경

엔터프라이즈 애플리케이션에서 계층 간 데이터 전송을 간단하게 하기 위한 필요성에서 등장했습니다. 복잡한 엔티티나 도메인 모델 대신, 필요한 데이터만을 담아 간단하게 전달하기 위해 사용됩니다.

📍주요특징

🎈가변성

DTO는 데이터를 전송하기 위한 용도로, 필드의 값이 자유롭게 변경될 수 있습니다.

🎈단순성

DTO는 주로 데이터를 담는 필드와 해당 필드에 접근하기 위한 getter/setter 메서드로 구성됩니다.

📍사용 예

@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class PersonDTO {
    private String firstName;
    private String lastName;
    private Integer age;
}
PersonDTO personNoArgs = new PersonDTO();
personNoArgs.setFirstName("건");
personNoArgs.setLastName("이");
personNoArgs.setAge(20);
PersonDTO personAllArgs = new PersonDTO("건", "이", 20);
PersonDTO personBuilder = PersonDTO.builder()
                                   .firstName("건")
                                   .lastName("이")
                                   .age(20)
                                   .build();

VO와 비슷하지만 Setter 가 포함되어 있고, @EqualsAndHashCode 는 제외되었습니다.

📌Record

VO와 DTO의 장점을 흡수하여 더 간결하고 안전한 데이터 클래스를 제공하기 위해 Java 14에서 처음 도입되었습니다.

📍등장배경

Java에서 VO나 DTO와 같은 클래스를 정의할 때, 데이터 보관을 목적으로 하는 클래스에 대해 필드, 생성자, getter, equals, hashCode, toString 메서드를 매번 작성해야 하는 번거로움이 있었습니다. 이러한 반복 작업을 줄이기 위해 record가 도입되었습니다.

📍주요특징

🎈간결성

record는 자동으로 생성자, getter, equals, hashCode, toString 메서드를 제공합니다. 이로 인해 반복되는 코드를 최소화할 수 있습니다.

🎈불변성

record는 기본적으로 불변이며, 모든 필드는 final로 선언됩니다.

🎈데이터 구조의 명확성

record는 순수한 데이터 보관 목적으로 사용되므로, 클래스의 목적을 명확하게 드러냅니다.

📍사용 예

@Builder
public record Person(
        String firstName,
        String lastName,
        int age
) {
}
Person personRecord = new Person("건", "이", 20);
Person personRecordBuilder = Person.builder()
								   .firstName("건")
								   .lastName("이")
								   .age(20)
								   .build();

Record는 생성 편의성을 위해 @Builder 만 있을 뿐 다른 Annotation 들을 제외되었습니다.

📌VO/DTO와 Record 비교

📍코드 간결성

record는 VO나 DTO에 비해 훨씬 더 간결한 코드를 제공합니다. 같은 기능을 구현할 때 수많은 보일러플레이트 코드를 줄일 수 있습니다.

📍불변성

VO와 마찬가지로 record는 불변성을 기본으로 합니다. 반면, 전통적인 DTO는 가변적으로 설계되곤 했습니다.

📍기능적 차이

record는 주로 데이터 저장 및 전달을 위한 용도로 사용되며, 복잡한 비즈니스 로직을 포함하지 않습니다. 이는 VO의 목적과 유사하지만, 간결한 문법과 편리한 메서드 제공이 큰 차이입니다.

📌Conclusion

현업에서는 VO와 DTO를 명확히 구분하지 않고 사용하는 경우가 많습니다. 실무에서는 코드의 간결성과 생산성을 중시하기 때문에, 이론적으로 구분되는 개념들이 실질적으로는 혼용되는 경우가 많습니다. 그러나 시스템이 복잡해질수록, 특히 도메인 주도 설계(DDD)를 채택하는 프로젝트에서는 VO와 DTO의 구분이 중요해질 수 있습니다. 그리고 VO나 DTO 를 Record로 사용하는 것이 간결한 코드작성과 불변성을 보장하는 측면에서 유리할 수 있습니다. 하지만 Java 14 이상이어야 하고, 상속을 받지 못하는 단점도 존재합니다.
결국, 상황에 따라 유연하게 접근하는 것이 중요합니다. 기본 정책은 Record 를 사용하고, 복잡한 로직이나 상속 구조가 필요할 때는 전통적인 클래스를 사용하는 것이 적절할 수 있습니다.

📚참고

📕Immutable 객체의 장단점

📖장점

안전한 멀티스레딩 - 동시성 문제 회피
불변 객체는 상태가 변하지 않기 때문에, 여러 스레드에서 동시에 접근해도 안전합니다. 이를 통해 동기화(synchronization) 문제를 피할 수 있으며, 멀티스레드 환경에서 안전한 프로그램을 작성하는 데 큰 도움이 됩니다.

예측 가능성 및 디버깅 용이성 - 변경 불가능
객체의 상태가 변하지 않으므로, 특정 시점의 객체 상태를 예측하기 쉽습니다. 이는 코드의 가독성을 높이고, 디버깅을 단순하게 만듭니다. 상태 변화를 추적할 필요가 없기 때문에, 어떤 상태에서 문제가 발생했는지 쉽게 파악할 수 있습니다.

객체 캐싱 및 재사용 가능성 - 캐싱 가능
불변 객체는 상태가 변하지 않으므로, 동일한 객체를 여러 곳에서 안전하게 공유할 수 있습니다. 이는 메모리 사용량을 줄이고, 성능을 향상시킬 수 있습니다. 예를 들어, 같은 값을 가진 불변 객체를 여러 번 생성하는 대신, 하나의 객체를 캐싱하여 재사용할 수 있습니다.

방어적 복사 불필요 - 안전한 참조 전달
: 불변 객체를 외부에 전달할 때, 객체가 수정될 염려가 없으므로 방어적 복사(defensive copy)가 필요하지 않습니다. 이는 코드의 간결성을 높이고, 성능도 향상시킵니다.

코드 안정성 - 신뢰성 있는 코드
불변 객체는 예상치 못한 상태 변화로 인해 발생하는 버그를 예방할 수 있습니다. 이는 특히 대규모 시스템에서 코드의 신뢰성을 높이는 데 기여합니다.

📖단점

객체 생성 비용 증가 - 새로운 객체 생성 필요
불변 객체는 상태를 변경할 수 없기 때문에, 객체의 값을 수정하려면 새로운 객체를 생성해야 합니다. 이는 메모리 사용량을 증가시키고, 빈번한 객체 생성이 필요한 경우 성능에 영향을 미칠 수 있습니다. 예를 들어, 객체의 상태를 자주 변경해야 하는 상황에서는 새로운 객체를 반복적으로 생성해야 하므로, 가변 객체에 비해 성능이 저하될 수 있습니다.

메모리 사용 증가 - 중복된 객체 생성
불변 객체의 특성상, 객체의 상태를 변경할 때마다 새로운 객체를 생성해야 합니다. 이로 인해 메모리 사용량이 증가할 수 있습니다. 특히, 큰 데이터를 가진 불변 객체를 자주 생성하는 경우 메모리 낭비가 발생할 수 있습니다.

복잡한 변경 작업 - 객체 생성과정의 복잡성
불변 객체의 상태를 변경하려면 새로운 객체를 만들어야 하므로, 많은 필드를 가진 불변 객체를 수정할 때 모든 필드를 복사해야 하는 번거로움이 발생할 수 있습니다. 이는 코드의 복잡성을 증가시키고, 가독성을 저하시킬 수 있습니다.

컬렉션과의 호환성 - 컬렉션과의 사용 제약
불변 객체를 포함한 컬렉션을 다룰 때, 컬렉션의 요소를 변경하려면 새로운 불변 객체를 생성해야 합니다. 이는 가변 객체에 비해 작업이 더 복잡하고 비효율적일 수 있습니다. 예를 들어, 리스트에 불변 객체를 추가하거나 수정할 때마다 새로운 리스트를 생성해야 합니다.

📕Override Equals And HashCode

equals 와 hashCode 는 Object 에 구현되어있는 Method이고 객체 비교와 해시 기반 자료구조에서 매우 중요한 역할을 합니다.

📖equals

quals method는 두 객체가 논리적으로 동일한지를 비교하는 데 사용됩니다. 기본적으로 Object 클래스의 equals 메서드는 두 객체의 참조를 비교하여, 동일한 객체인지 여부를 판단합니다. 즉, 두 객체의 메모리 주소가 같은지를 비교합니다.

public boolean equals(Object obj) {
    return (this == obj);
}

📖hashCode

hashCode method는 객체의 해시 코드를 반환합니다. 해시 코드는 HashMap, HashSet 등 해시 기반 자료구조에서 객체를 빠르게 검색하거나 삽입하는 데 사용됩니다. 기본적으로 Object 클래스의 hashCode 메서드는 객체의 메모리 주소를 기반으로 해시 코드를 생성합니다.

📖왜 두 Method 를 Override 하나?

equals와 hashCode method는 반드시 일관성을 가져야 합니다. 즉, 두 객체가 equals 메서드로 같다고 판단되면, 그 두 객체는 동일한 hashCode를 반환해야 합니다. 그렇지 않으면 해시 기반 컬렉션에서 예상치 못한 동작이 발생할 수 있습니다.

📖Example

public class Person {
    private String firstName;
    private String lastname
    private int age;

    @Override
    public boolean equals(Object o) {
    	// 같은 객체 참조 확인
        if (this == o) return true;
        // 객체 타입 확인
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        //객체 내 모든 값을 비교
        return Objects.equals(firstName, person.firstName) 
        	&& Objects.equals(lastName, person.lastName)
            && age == person.age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }
}
profile
Back-end developer

0개의 댓글