본문 바로가기
프로그래밍/Java

[DDD/Kotlin] DTO와 Command

by 물고기고기 2023. 9. 21.

DTO라는건 뭘까?

개발을 전혀 모르는 사람을 기준으로 설명하자면,

데이터베이스에 데이터가 저장되기 전까지 클라이언트로부터 받은 데이터를 저장하고, 서비스로직을 수행한 뒤 Entity(테이블)에 매핑해줄때까지 해당 데이터를 가지고 있는 역할을 수행하는 객체이다.

그래서 나는 컨트롤러에서(맨처음 데이터를 받아주는 계층) 데이터를 DTO에 저장한 뒤 Entity로 매핑한 뒤 바로 저장해주었으나

최근 사이드 프로젝트를 개발하면서 command라는 개념을 처음 접하게 되었다.

command 객체란 컨트롤러에서 데이터를 dto로 받아준 뒤 그 다음 계층으로 갈때 데이터를 매핑해주는 객체를 한 단계 더 생성하여 역할을 분리한다.

 

예제코드로 설명하자면

  • DTO 클래스
data class UserDTO(val id: Long, val username: String, val email: String)
  •  Command 클래스
data class UserCommand(val username: String, val email: String)
  • Entity
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

@Entity
data class UserEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val username: String,
    val email: String
)
  • 데이터 매핑 함수
    DTO에서 Command로, Command에서 Entity로 데이터를 매핑하는 함수를 정의
fun mapDTOToCommand(userDTO: UserDTO): UserCommand {
    return UserCommand(userDTO.username, userDTO.email)
}

fun mapCommandToEntity(userCommand: UserCommand): UserEntity {
    return UserEntity(username = userCommand.username, email = userCommand.email)
}
  • 사용 예제
fun main() {
    // DTO 생성
    val userDTO = UserDTO(id = 1, username = "johndoe", email = "johndoe@example.com")

    // DTO에서 Command로 매핑
    val userCommand = mapDTOToCommand(userDTO)

    // Command에서 Entity로 매핑
    val userEntity = mapCommandToEntity(userCommand)

    // 결과 확인
    println("User ID: ${userEntity.id}")
    println("Username: ${userEntity.username}")
    println("Email: ${userEntity.email}")
}

그런데 한가지 의문이 생기는건 dto의 요구사항이 수정되면 command도 추가로 작업해줘야해서 더 번거로워지는거아닌가?

그래서 gpt에게 물어봤다

→ DTO의 요구사항이 수정될 때 Command 객체에도 수정이 필요하게 될 수 있습니다. 이는 데이터 구조가 변경될 때 발생하는 작업으로, 두 객체 간의 일관성을 유지하기 위해 필요한 작업입니다. 번거로움은 발생할 수 있지만, 이 작업은 필수적인 것이며 적절한 설계와 관리로 최소화될 수 있습니다.

 

두 객체 간의 일관성을 유지하는 방법을 요약하자면 이렇다.

  1. 자동화된 매핑 도구 사용: 자동화된 매핑 도구나 라이브러리를 사용하여 DTO와 Command 객체 간의 매핑을 자동화할 수 있습니다. 예를 들어, MapStruct, ModelMapper, Dozer 등의 라이브러리를 사용하면 객체 간의 데이터 매핑을 자동화하고 변경이 발생할 때 자동으로 매핑 코드를 업데이트할 수 있습니다.
  2. 프로젝트 구조 및 패턴: DTO와 Command 객체를 수정할 때 변경이 반영되도록 프로젝트 구조와 패턴을 잘 설계하세요. 예를 들어, DTO와 Command 객체 각각을 별도의 패키지로 유지하고, 매핑 코드는 해당 패키지 내에 위치시킵니다. 이렇게 하면 수정이 필요한 경우 해당 패키지 내에서만 작업하면 되므로 유지 보수가 용이해집니다.

 

한편 충분히 DTO만으로도 서비스단에서 충분히 작업하고 매핑할 수 있는데 일관성 관리까지 해가자면서 서비스단에서만 사용하는 데이터 매핑 객체(command)가 필요한 이유가 뭐냐고 할 수도 있다.

그래서 도대체 command가 필요한 이유가 뭘까?

  1. 역할 분리 및 관심사의 분리: DTO는 주로 외부 시스템 또는 레이어 간 데이터 전송을 위한 객체로 사용됩니다. 반면에 Command 객체는 내부 로직을 처리하기 위한 객체로 사용됩니다. 이 두 역할을 분리함으로써 코드베이스가 더 간결해지고 관심사를 분리할 수 있습니다. DTO는 외부와의 통신을 위한 것이므로 외부 요청에 따라 구조가 바뀔 수 있으며, Command 객체는 내부 로직을 처리하므로 내부 구조에 영향을 미치지 않습니다.
  2. 보안 및 데이터 무결성: 외부로부터 전달되는 데이터는 보안 및 무결성 검사가 필요할 수 있습니다. DTO를 사용하여 외부 입력 데이터를 받고, Command 객체를 생성할 때 검증 및 보안 처리를 수행할 수 있습니다. 이를 통해 안전한 데이터 처리를 보장할 수 있습니다.
  3. 데이터 가공 및 변환: DTO는 외부 요청과 내부 데이터 모델 간에 데이터 변환을 담당할 수 있습니다. Command 객체는 이러한 변환된 데이터를 사용하여 내부 로직을 수행할 수 있습니다. 이를 통해 데이터 변환이나 가공 로직을 중앙 집중화할 수 있습니다.
  4. 확장성: DTO와 Command 객체를 분리하면 두 객체의 구조가 독립적으로 확장 가능합니다. 외부 요청이나 내부 로직의 변경이 각각의 객체에 영향을 미치지 않고 확장할 수 있습니다.

 

gpt에게 물어본 결과 이런 답이 나왔다. 나머지는 이해가 가는데 잘 와닿지 않는다.

특정 상황을 예로 들어보자.

우리는 사용자(User) 정보를 다루는 서비스를 가지고 있고, 외부 클라이언트로부터 사용자 정보를 받아서 내부 데이터베이스에 저장하고 있다. 현재는 이름(name)과 이메일(email) 정보만 다루고 있다. 이때 사용자 정보에 phone이라는 항목이 추가되는 상황이다.

 

DTO 확장

data class UserDTO(val name: String, val email: String, val phone: String)

그러나 Command 확장 X 의 경우

data class UserCommand(val name: String, val email: String)

 

그런데 이때 DTO에서 phone 항목은 전화번호 인증을 위해 사용되고 Command는 그대로 DB에 저장하기 위해 사용된다면 이런 형태로도 활용이 가능해지는 것이다.

class UserService {
    fun verifyPhoneNumber(userDTO: UserDTO): Boolean {
        // 전화번호 인증 로직을 수행
        val phone = userDTO.phone
        // ...
        return true // 인증 성공 여부 반환
    }

    fun createUser(userCommand: UserCommand): UserEntity {
        // Command 객체를 Entity로 매핑하고 저장
        val userEntity = mapCommandToEntity(userCommand)
        userRepository.save(userEntity)
        return userEntity
    }
}

위의 코드에서 verifyPhoneNumber 함수는 전화번호 인증을 위한 로직을 담당하고, 이 함수에서는 DTO를 사용한다. 반면에 createUser 함수는 사용자를 생성하고 DB에 저장하는 로직을 담당하며, 이 함수에서는 Command 객체를 사용한다.

이렇게 phone 필드는 인증 로직에서만 사용되고, DB에 저장할 때는 고려하지 않을 수 있다.

이러한 설계는 데이터 전송과 비즈니스 로직을 분리하고, 필요한 데이터만 처리한다.

댓글