DTO Data Transfer Object)는 애플리케이션 간에 테이터의 전달을 목적으로 하는 객체이다. 이는 프레젠테이션 계층과 비즈니스 계층, 혹은 비즈니스 계층과 데이터 액세스 계층, 더 나아가 애플리케이션 간에서 비즈니스 로직을 갖지 않고 순수하게 데이터 전송을 위해 사용된다.
클라이언트와 서버의 관계를 갖는 서비스를 설계/구축해 봤으면 누구나 DTO에 대한 개념을 알 것이다. 그러나 순수한 DTO의 의미와 의도를 정확히 파악하고 구현하는 것은 어려움이 있을 것이라 생각된다.
최근 서비스를 구현하면서 DTO/VO/Entity 등 데이터를 다루는 기본적인 객체에 대해 고민을 해보면서, 더 정확히 알고 쓰면 깔끔하면서 명확한 코드 작성이 가능할 것 같아 몇 가지 알아본 내용을 정리해보려고 한다.
이 포스트에서는 DTO를 설계하고 구현할 때 고려해야 할 10가지 DTO 모범 사례에 대해 작성한다.
1. 작게 유지하기 Keep them small
DTO가 거대하면 데이터를 한눈에 보기도 어려울뿐더러 이에 따라오는 유지 관리 및 업데이트의 어려움이 있을 수 있다. 또한 서비스 간에 전송해야 하는 데이터의 양이 증가하여 애플리케이션의 성능 저하를 가져올 수 있다.
DTO를 작게 유지하여 현재 서비스에서 필수적으로 필요한 필드만 추가하며 필요하지 않은 정보가 있으면 과감히 생략하는 것이 좋다. 이로써 DTO를 가볍게 활용하며 유지 관리에도 도움이 된다.
2. DTO를 만들 때는 Builder pattern을 이용하기 Use a builder pattern to create DTOs
빌더 패턴을 사용하면 체계적이고 효율적인 방식으로 DTO를 만들기 가능하다. 또한 전체 코드를 다시 작성하지 않고도 새로운 필드를 추가하거나 기존 필드를 수정하는 부분에서 더 용이하다. 이는 코드를 깔끔하게 유지 관리하기 쉽도록 할 수 있으며 DTO를 생성하는 로직은 별도로 작성하여 이를 제외한 비즈니스 로직에 충실한 코드를 작성하게 할 수 있다.
빌더 패턴을 사용한다면 DTO를 생성하기 전, 필요한 필드 데이터에 대해 존재 유무를 미리 확실하게 알기 편하다. 이는 정보 누락 혹은 런타임 상의 이슈를 방지할 수 있다. 마지막으로 필더 패턴은 다양한 유형의 DTO에 공통 코드를 재사용할 수 있으므로 코드의 양도 줄이며 깔끔하게 코드 관리를 할 수 있다.
예를 들어 DTO builder 패턴, mapper 패턴 등이 있다.
3. DTO에 상속을 사용하지 않기 Avoid using inheritance for DTOs
상속을 사용하게 되면 하위 클래스에서는 상위 클래스에서 필요가 없는 속성과 메서드를 포함하게 될 수도 있다. 이는 DTO 개체를 직렬화할 대 payload 크기가 늘어나게 되어 성능의 저하시킬 수 있다.
또한 상위 클래스의 변경이 있을 경우 모든 하위 클래스도 업데이터해야 하므로 유지 관리의 걸림돌이 된다. 이러한 이유로 DTO에 상속을 사용하지 않고 조합(Composition)을 사용하는 것이 좋다.
4. 불변 DTO를 사용하기 Use immutable DTO
immutable DTO는 생성 이후 내용을 변경할 수 없는 개체이다. 이는 DTO가 인스턴스화되면 해당 값을 수정할 수 없음을 의미한다. 보통 constructor를 이용해 초기화를 진행하며 setter가 없이 getter만을 갖는 패턴으로 사용된다.
변경할 수 없는 DTO를 사용하면 시스템 전체에서 데이터의 무결성과 일관성을 보장해 줄 수 있다. 또한 개체의 상태가 예기치 않게 데이터의 수정이 일어나지 않게 된다는 것을 알 수 있으므로 디버깅에 용이함이 있다. 이로써 객체에 액세스 하거나 수정할 때 스레드 안전에 대해 걱정할 필요가 없으므로 성능 향상에도 도움이 된다.
5. API의 파라미터로 DTO를 직접 사용하지 않기 Don't use DTOs as parameters in your API
API에서 DTO를 매개변수로 직접 사용하면 불필요하게 복잡한 구조의 문제가 발생할 수 있다. 이는 API에 전달해야 하는 데이터가 DTO의 구조와 정확히 일치해야 하기 때문이다. 즉, DTO를 변경하려면 해당 API도 변경해야 된다.
이 부분은 이해하기 쉽게 아래 코드로 작성하자면,
가령, 사용자 정보를 업데이트하는 API를 만든다고 가정해 보겠다. 우선적으로 DTO를 사용하는 경우는 아래와 같다.
{
"userId": 123,
"userName": "John Doe",
"userEmail": "john.doe@example.com"
}
API에서 DTO를 사용하는 경우
# API의 매개변수로 DTO를 직접 사용
function update_user_info(user_info_dto){
# user_info_dto의 구조와 일치하는 데이터를 받아 처리
const user_id = user_info_dto["userId"]
const user_name = user_info_dto["userName"]
const user_email = user_info_dto["userEmail"]
# 여기서부터는 업데이트 로직 수행
# ...
}
이 경우, 만약 DTO의 구조가 변경된다면 (API에 새로운 필드가 추가되거나 필드명이 변경된 경우), API의 매개변수를 사용하는 부분에서도 해당 변경에 대응할 수 있어야 된다.
반면, 개별 매개변수를 사용하는 경우
# API의 매개변수로 개별 변수를 사용하고, 내부에서 DTO에 매핑
function update_user_info(user_id, user_name, user_email):
# 여기서부터는 업데이트 로직 수행
# ...
이렇게 개별 매개변수를 사용하면 API 내에서 필요한 매개변수를 받아와서 DTO에 직접 매핑하는 작업을 수행할 수 있다. 이 경우에는 DTO가 변경되더라도 API의 매개변수 선언 부분은 건드릴 필요가 없어진다.
6. 꼭 필요한 경우에만 DTO를 사용하기 Use DTOs only when you need them
DTO는 애플리케이션의 계층 간에 데이터를 전송하는 좋은 방법이지만 불필요한 복잡성과 오버헤드를 초래할 수도 있다.
DTO가 제공하는 추가적인 추상화 계층이 필요하지 않다면 DTO를 피하는 것이 가장 좋다고 한다. 예를 들어서 정수나 문자열과 같은 간단한 데이터를 한 계층에서 다른 계층으로 전송하는 경우 DTO가 필요하지 않다. 이러한 경우 기본적인 type의 유형을 사용하거나 서비스 로직에서 자유롭게 사용이 가능한 entity를 정의하여 사용하는 것이 바람직해 보인다.
반면, 여러 필드가 포함된 복잡한 개체를 전송하는 경우 DTO를 사용하는 것이 좋다. 이러한 경우에는 코드를 깔끔하고 체계적으로 유지하는 동시에 필요한 데이터만 전송되도록 하여 데이터의 무결성과 보안적인 측면도 함께 가져갈 수 있다.
7. 입출력에 동일한 DTO 사용하기 Use the same DTO for input and output
입출력 모두에 동일한 DTO를 사용하는 방식은 코드의 일관을 보장할 수 있다. 즉, 사용자가 API에 데이터를 보낼 때 보낸 내용을 정확하게 돌려받을 수 있다. 또한 모든 데이터가 한 곳에 있기 때문에 이슈에 대해 디버깅하기가 더 쉬워진다.
동일한 DTO를 사용하면 DRY Don't Repeat Yourself 원칙을 지키는 데 도움이 된다.
8. 중첩된 DTO를 주의하기 Be careful with nested DTOs
중첩된 DTO는 코드를 복잡히게 하여 유지 관리의 어려움을 줄 수 있다. 중첩된 개체 수가 많으면 각 개체에서 어떤 필드가 사용되는지 추적이 어려우며 코드를 디버그 하거나 수정하려고 할 때 혼란을 줄 수 있다.
각 요청으로 전송되는 데이터의 양을 고려하는 것도 중요하다. 중첩된 DTO는 네트워크를 통해 전송해야 하는 데이터 양을 늘려 응답 시간을 느리게 만들 수 있다.
9. Entity 대신 DTO 사용하기 Use DTOs instead of Entities
Entity는 일반적으로 데이터베이스에 연결되어 있으며 비즈니스 로직에서 필요하지 않을 수 있는 많은 데이터를 하고 있을 수 있다. 반면 DTO는 특정 작업을 위해 특별히 설계 및 작성되어 있으며 해당 특정 작업에 필요한 데이터만을 가지고 있다. 이를 통해 더욱 효율적이고 유지 관리가 쉬운 코드 작성이 가능하다.
DTO를 사용하면 코드를 깔끔하고 체계적으로 유지하는 데에도 도움이 되니다. 데이터 전송 개체를 Entity에서 분리하면 각 작업에서 어떤 데이터 부분이 사용되는지 확인할 수 있다. 이렇게 하면 발생하는 문제에 대해 디버깅이 훨씬 쉬워진다.
10. DTO를 사용하여 쿼리에서 데이터를 반환하기 Use DTOs to return data from queries
데이터베이스를 쿼리 할 때 반환되는 데이터는 일반적으로 개체 또는 컬렉션 형식이다.
해당 개체나 컬렉션에서 특정 정보만 필요한 경우 이는 비효율적일 수 있다.
DTO를 사용하면 필요한 데이터만 반환할 수 있으므로 네트워크를 통해 전송되는 데이터의 양을 줄일 수 있으며 이에 따라 성능 향상에도 도움을 줄 수 있다. 또한 애블리케이션 안에서 서로 다른 계층 간의 개체를 매핑하는 것에 대해 걸정할 필요가 없으므로 코드를 더 쉽게 유지관리 할 수 있다.
+ DTO 생성 전 유효성 검사
+ null 값 사용하지 않기
+ 일관된 명명 규칙 사용하
참고:
https://climbtheladder.com/10-dto-best-practices/
https://www.baeldung.com/java-dto-pattern
https://medium.com/@samuelcatalano/best-practices-for-data-transfer-objects-dtos-d5007e3f2729
'Backend > 개발 방법론 & 디자인 패턴' 카테고리의 다른 글
[Rate Limiting] Token Bucket 알고리즘 (2) | 2024.10.27 |
---|---|
API 디자인 패턴 (0) | 2023.12.13 |
[Design Pattern] 싱글톤 패턴이란? (Singleton Pattern) (0) | 2023.03.11 |
[Methodologies] Monorepo(모노레포)란? (0) | 2023.02.24 |
[Design Pattern] Factory Pattern (0) | 2022.08.05 |