[iOS/Swift] 책임분리를 위한 설계 - DTO와 Entity에 대해

이전에 프로젝트를 진행하면서 '네트워크 응답값을 담는 struct 정도로 DTO라는 용어를 사용한 적이 있었다.

그러나, 공부를 하다보니 이는 DTO에 대한 이해가 없이 사용했던, 잘못된 표현이었다.

 

이번 글에서는 DTO 그리고 Entity에 대해서 간단히 알아보도록 하겠다.


DTO란?

DTO는 Data Transfer Objcet의 약자로, 우리말로 '데이터 전송 객체'라는 뜻을 가지고 있다.

그렇다면 데이터 전송 객체는 무엇인가?

찾아보니 조금씩 다른 말로 설명하고 있지만,

쉽게 말해 네트워크 통신에서 클라이언트와 서버간 데이터 전달을 위한 객체라고 생각하면 될것같다.

 

다음과 같은 식당 정보가 담긴 JSON 응답값이 온다고 가정해보자.

{
	"documents": [
		// ...
		{
			"address_name": "서울 강남구 역삼동 816-6",
			"category_group_code": "FD6",
			"category_group_name": "음식점",
			"category_name": "음식점 > 중식",
			"distance": "",
			"id": "981684729",
			"phone": "0507-0000-0000",
			"place_name": "팀호완 강남역점",
			"place_url": "http://place.map.kakao.com/981684729",
			"road_address_name": "서울 강남구 테헤란로1길 29",
			"x": "127.0272885648075",
			"y": "37.500108361339656"
		}
	],
	"meta": {
		"is_end": true,
		"pageable_count": 4,
		"same_name": {
			"keyword": "팀호완",
			"region": [],
			"selected_region": ""
		},
		"total_count": 4
	}
}

 

그럼 다음과 같은 struct를 만들어서 응답값을 받아올 수 있을것이다.

struct PlaceDTO: Decodable {
    let documents: [Document]
    let meta: Meta
}

struct Document: Decodable {
    let addressName: String
    let categoryGroupCode: String?
    let categoryGroupName: String?
    let categoryName, distance, id, phone: String
    let placeName: String
    let placeURL: String
    let roadAddressName, x, y: String

    enum CodingKeys: String, CodingKey {
        case addressName = "address_name"
        case categoryGroupCode = "category_group_code"
        case categoryGroupName = "category_group_name"
        case categoryName = "category_name"
        case distance, id, phone
        case placeName = "place_name"
        case placeURL = "place_url"
        case roadAddressName = "road_address_name"
        case x, y
    }
}

struct Meta: Decodable {
    let isEnd: Bool
    let pageableCount: Int
    let sameName: SameName
    let totalCount: Int

    enum CodingKeys: String, CodingKey {
        case isEnd = "is_end"
        case pageableCount = "pageable_count"
        case sameName = "same_name"
        case totalCount = "total_count"
    }
}

struct SameName: Decodable {
    let keyword: String
    let region: [String]
    let selectedRegion: String

    enum CodingKeys: String, CodingKey {
        case keyword, region
        case selectedRegion = "selected_region"
    }
}

즉, DTO는 서버로부터 오는 응답값을 담는(혹은 반대로 보내는) 구조체라고 보면 된다.


Entity가 필요한 이유

서버에서 오는 응답값을 DTO의 형태로 받아왔다면, View에서 그냥 해당 데이터를 가져다가 쓰면 된다!

라고 생각했었다.

 

하지만, 서버에서 내려주는 응답값이 변경되는 경우라면 어떨까?

응답값의 타입이 변경되거나, 새로운 데이터가 추가되거나, 원래 사용하던 데이터가 삭제되는 경우가 생길 수 있다.

 

그런데, DTO를 View에서 사용하는 경우, 예를 들어서

Repository가 있고, ViewModel에서 이 Repository를 사용하고 있고, ViewController에서 보여주고 있다면

서버의 응답값이 변경되는 순간 이 모든게 변경되어야 한다.

 

즉, View에서 사용하는 모델이 서버에 많이 의존하다보니, 이런 문제가 발생하게 된다.

 

그래서 View단에서 사용할 모델을 설정하곤 하는데, 그걸 Entity라고 한다.

struct PlaceEntity: Equatable {
    let details: [PlaceDetail]
    let info: SearchInfo
}

struct PlaceDetail: Equatable {
    let id: String
    let placeName: String
    let placeURL: String
    let roadAddressName: String
    let x: String
    let y: String
}

struct SearchInfo: Equatable {
    let isEnd: Bool
}

위의 PlaceDTO에서 View에서 사용할 데이터만 따로 받아올 모델로 위와 같은 PlaceEntity를 만들 수 있다.

 

그리고 아래와 같이 PlaceDTO -> PlaceEntity에 대응하는 메서드를 통해 이 둘을 연결시킬 수 있다.

extension PlaceDTO {
    func toEntity() -> PlaceEntity {
        return PlaceEntity(
            details: self.documents.map {
                PlaceDetail(
                    id: $0.id,
                    placeName: $0.placeName,
                    placeURL: $0.placeURL,
                    roadAddressName: $0.roadAddressName,
                    x: $0.x,
                    y: $0.y
                )
            },
            info: SearchInfo(isEnd: self.meta.isEnd))
    }
}

 

이제 View에서는 DTO가 아니라, Entity를 사용함으로써, 책임을 명확히 분리할 수 있고,

만약 서버에서 주는 데이터에 변화가 생긴다면 Entity는 그대로 두고 DTO만 수정하면 되고 View단에서는 그대로 Entity를 사용하면 되기 때문에 불필요한 수정 작업을 줄일 수 있다.

 

추가로 이 Entity로 Mock데이터를 만들어놓으면 서버에 문제가 생기거나, 서버가 완전히 구축되기 이전에도 View단에서의 테스트가 가능하다는 장점이 있다.

extension PlaceEntity {
    static var mockData: PlaceEntity {
        return PlaceEntity(
            details: [
                PlaceDetail(
                    id: "25037411",
                    placeName: "원조두꺼비집불오징어",
                    placeURL: "http://place.map.kakao.com/25037411",
                    roadAddressName: "서울 은평구 연서로28길 5",
                    x: "126.92100106119523",
                    y: "37.618100370960875"
                ),
                PlaceDetail(
                    id: "18508914",
                    placeName: "양지석쇠불고기백반",
                    placeURL: "http://place.map.kakao.com/18508914",
                    roadAddressName: "경기 용인시 처인구 양지로 116",
                    x: "127.28133163741273",
                    y: "37.23424037144186"
                ),
                PlaceDetail(
                    id: "2143580448",
                    placeName: "팔당불오징어불닭발",
                    placeURL: "http://place.map.kakao.com/2143580448",
                    roadAddressName: "경기 의정부시 망월로28번길 42",
                    x: "127.043186235008",
                    y: "37.7069097070703"
                ),
                PlaceDetail(
                    id: "333543684",
                    placeName: "문래동냉삼 본점",
                    placeURL: "http://place.map.kakao.com/333543684",
                    roadAddressName: "서울 영등포구 도림로147길 1",
                    x: "126.889387445627",
                    y: "37.5166012675922"
                ),
                PlaceDetail(
                    id: "14595758",
                    placeName: "이연국수",
                    placeURL: "http://place.map.kakao.com/14595758",
                    roadAddressName: "전북특별자치도 전주시 덕진구 견훤왕궁로 286-3",
                    x: "127.14457814284677",
                    y: "35.84356088529327"
                )
            ],
            info: SearchInfo(isEnd: false)
        )
    }
}

 

 

결론적으로, 

View단에서 사용하는 Model의 서버 의존성을 낮추고, DTO와 Entity간의 책임을 명확히 함으로써

값이 변경되었을때 불필요한 수정을 줄일 수 있다는 장점 때문에 Entity라는 별도의 모델을 사용한다고 보면 된다.


*참고자료

https://jife98.tistory.com/84

 

Entity, DTO, DAO, VO, Repository와 효율과 책임에 대한 고민

고민의 시작요새 프로젝트들 마다 클린 아키텍처를 적용하여, 진행중인데 문뜩 각 레이어에 대한 개념정리와 용어정리 그리고 혼자만의 고민이 있어 정리해야겠다는 생각이 들어, 해당 포스트

jife98.tistory.com