최근 기존의 MVVM 구조로 되어있던 회사 프로젝트에 clean architecture 를 적용하여 코드 구조를 개선하고 있다. 적용하기 위해 공부하다가 DTO라는것을 알게되고, 내 나름대로 내용을 정리하고, 프로젝트에서 적용하기 위해 고민했던 내용을 작성해보려고 한다.
데이터 전송 객체(data transfer object)는 프로세스 간에 데이터를 전달하는 객체이다.
비즈니스 로직을 포함할 필요가 없는 단순한 객체이지만, 데이터 전송을 위해 직렬화와 역직렬화 메커니즘을 포함할 수 있다.
- 출처: 위키백과
언제나 느끼는 거지만, 사전적 정의는 항상 잘 와닫지 않는다. 여러 글들을 찾아본 결과, 좀더 쉽게 설명하면,
데이터 전달을 위한 객체이며, 주로 계층 간(서버 <-> 클라이언트) 데이터를 주고받을때 사용한다고 이해하면 된다.
단순히 넘어오는 데이터의 호출 결과를 의미하며, 추가적인 로직이 포함되지 않는다.
우리는 사실 이미 DTO를 너무도 잘 사용하고 있다. iOS 개발을 하면서 서버와의 네트워킹을 진행할때, 서버에서 전달받은 값을 변환하기 위해 Codable을 채택한 모델을 선언하여 데이터를 전달받을때 사용한다. 그동한 사용해왔던 모델들이 DTO 였던 거다.
그렇다면 DTO를 사용하지 않으면 어떻게 되는지 궁금해졌다. 그래서, 간단한 JSON을 응답으로 돌려주는 API를 사용해 그 차이점을 확인해보려고 한다.
간단하게 3개의 필드를 가지고 있는 JSON이다.
{
"field1": "one",
"field1": "two",
"field1": "three"
}
이 JSON 객체를 가져올때, DTO를 사용하지 않는다면 어떻게 될까?
func DTO없이데이터가져오기() async throws {
let url = URL(string: "https://dummyjson.com/c/ca69-acc4-4bc3-a740")
let urlRequest = URLRequest(url: url!)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let field1Value = json?["field1"] as? String
let field2Value = json?["field2"] as? String
let field3Value = json?["field3"] as? String
print(field1Value, field2Value, field3Value) // Optional("one") Optional("two") Optional("three")
}
응답으로 넘어오는 데이터의 구조를 선언하지 않고, 디코딩 되기 전 데이터를 그대로 가져오기 때문에, 실제로 우리가 받은 데이터를 사용하기 위해서는 여러 과정을 거처 해당 필드명으로 접근하여야 비로소 데이터를 사용할수 있다.
필드가 3개밖에 되지 않기 때문에 코드가 길지 않지만, 이거보다 필드가 조금만 더 많아진다고 하면, 코드의 양은 매우 많이 늘어날 것이다. 그리고 해당 API를 여러군데에서 반복적으로 호출한다면, 해당 API를 호출할때마다 일일이 필드값에 다 접근해야하기 때문에, 여간 귀찮은 일이 아닐 수 없다.
struct JsonDTO: Codable {
let field1: String
let field2: String
let field3: String
}
func DTO사용해서데이터가져오기() async throws {
let url = URL(string: "https://dummyjson.com/c/ca69-acc4-4bc3-a740")
let urlRequest = URLRequest(url: url!)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
let dto = try JSONDecoder().decode(JsonDTO.self, from: data)
print(dto.field1, dto.field2, dto.field3)
}
사용하지 않은 경우와 비교해봤을때, 코드의 길이가 짧아졌다. Codable를 채택한 구조체로 미리 넘어오는 데이터의 구조롤 정의해놓았기 때문에, 받아온 데이터를 일일이 변환할 필요 없이, JSONDecoder를 사용해서 선언한 타입으로 바로 디코딩 시켜준다.
필드값에 접근시에도 객체의 프로퍼티로 접근할 수 있어 사용하기에도 더 편리하다.
앞서 설명한것 처럼 사용방법은 넘어오는 응답 구조를 잘 분석해서 선언한 객체에 Codable만 채택하면 간단하게 사용할 수 있다.
내가 좀더 고민했던 부분은 네이밍에 있었다.
나의 경우나, 다른 분들도 일반적으로는 DTO 네이밍을 할때 해당 객체의 접미사로 dto를 붙여 네이밍을 한다.
거기에 나의 경우 DTO의 네이밍이 구체적인 단어의 조합으로 명확하게 되지 않을때(ex: 로그인 응답, 간단하게 넘어오는 응답 등등...) 끝에 Response를 붙이고 맨 마지막에 접미사로 DTO를 붙여 네이밍을 하는 편이다.
로그인 응답의 경우 (LoginResponseDTO) 같은 방식으로 네이밍을 진행한다.
그런데 Response 뒤에 DTO 접미사까지 붙으니까 이게 생각보다 네이밍이 길어져서, 보기 불편할때가 많다.
그래서 관습적으로 접미사로 DTO를 붙이는 방법 대신, 다른 방법이 있을까 고민해보게 되었다.
간단하다. DTO의 경우 일반적으로 Codable을 채택하기 때문에, 프로토콜을 확장 기본구현을 사용해서, DTO 타입을 만들었다.
// MARK: - AccessTokenRequestResponseDTO
struct AccessTokenRequestResponseDTO: Codable {
let code: Int?
let message: String?
let response: Self.Response?
// MARK: - Response
struct Response: Codable {
let accessToken: String?
let now, expiredAt: Int?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case now
case expiredAt = "expired_at"
}
}
}
protocol DTO: Codable {
}
// MARK: - AccessTokenRequestResponse
struct AccessTokenRequestResponse: DTO {
let code: Int?
let message: String?
let response: Self.Response?
// MARK: - Response
struct Response: Codable {
let accessToken: String?
let now, expiredAt: Int?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case now
case expiredAt = "expired_at"
}
}
}
오! 해보니까 너무 좋았다. 타입의 이름으로 DTO라는것을 표현하면서도, 선언부의 코드 알파벳 갯수가 줄어서 더 읽기 편했다.
하지만 또 다른 문제가 발생했다. 바로 DTO을 Entity로 바꾸는 부분에서 네이밍이 겹치는 것이었다.
기존에는 Entity로 변환시, 끝에 접미사로 들어간 DTO민 제거해서 Entity의 이름을 지었었는데, 접미사가 삭제되니까 Entity의 이름을 다 바꿔야 하는 상황이 발생했다.
여러가지로 생각해봤었다. 그중에 가장 괜찮았다고 했던 생각은, DTO, Entity를 실질적으로 사용하는 상위 개념(Data, Domain)의 이름으로 중첩 구조를 사용해서 네이밍을 가져갈가 싶기도 했다.
Data.AccessTokenRequestResponse, Domain.AccessTokenRequestResponse // 이런식으로....
근데 결국엔 간결함을 추구하기 위해 시작했는데, 앞에 Data, Domain이라는 접두사가 새로 붙어 결국 제자리걸음 하는 느낌이 많이 들었다...
그래서 결국엔 원래대로 DTO를 접미사로 사용하여 네이밍을 짓는것으로 프로젝트에 적용시키고 있다. ㅋㅋㅋㅋㅋ
글을 제대로 쓰기로 다짐글을 쓰고, 첫 글을 작성해봤는데, 나름 글의 구조를 미리 작성해놓고 글을 써봤는데도 글을 이쁘게 쓰는게 쉽지가 않다...
또한 dto 개념에 대해 설명하기 위해 다시 정리하면서 확실히 머리에 더 남고 공부도 많이 되었던거 같다.
항상 개발을 하면서 개선할 여지가 있다고 생각이 들면 파고드는 편인데, 이번에도 dto를 사용하는 과정에서 네이밍이 너무 형식적으로 길어지는것 같아 간결하게 선언해보고자 이래저래 생각을 많이 해봤는데, 기존에 방식대로 사용하는 이유가 있었구나 싶었다.