[Tistory] 아이템 37 – 데이터 집합 표현에 data 한정자를 사용하라

원글 페이지 : 바로가기

Kotlin에서는 데이터 집합을 표현할 때 data 클래스를 사용하면 여러 가지 편리한 기능을 자동으로 제공받을 수 있습니다. data 클래스를 사용하면 코드가 간결해지고, 데이터 객체의 비교, 복사, 문자열 표현 등을 쉽게 처리할 수 있습니다. equals: 객체의 내용을 비교합니다. hashCode: 객체의 해시 코드를 생성합니다. toString: 객체의 문자열 표현을 제공합니다. copy: 객체를 복사할 수 있습니다.(얕은 복사) componentN: 객체의 각 속성에 접근할 수 있습니다. // 얕은복사
data class Address(var street: String, var city: String)

data class Person(var name: String, var address: Address)

fun main() {
val originalAddress = Address(“Main St”, “Springfield”)
val person1 = Person(“John”, originalAddress)
val person2 = person1.copy()

// 두 객체가 동일한 내부 객체를 참조
println(person1.address === person2.address) // true

// person2의 주소를 변경
person2.address.street = “Elm St”

// person1의 주소도 변경됨
println(person1.address.street) // Elm St
println(person2.address.street) // Elm St
}

// 깊은복사
data class Address(var street: String, var city: String)

data class Person(var name: String, var address: Address) {
fun deepCopy(): Person {
return Person(name, Address(address.street, address.city))
}
}

fun main() {
val originalAddress = Address(“Main St”, “Springfield”)
val person1 = Person(“John”, originalAddress)
val person2 = person1.deepCopy()

// 두 객체가 동일한 내부 객체를 참조하지 않음
println(person1.address === person2.address) // false

// person2의 주소를 변경
person2.address.street = “Elm St”

// person1의 주소는 변경되지 않음
println(person1.address.street) // Main St
println(person2.address.street) // Elm St
} 명확성: 데이터 클래스는 각 필드에 의미 있는 이름을 부여할 수 있어 코드의 가독성을 높입니다. 자동 생성 메서드: 데이터 클래스는 equals, hashCode, toString, copy, componentN 메서드를 자동으로 생성합니다. 이를 통해 객체 비교, 해시 코드 생성, 문자열 표현, 객체 복사, 구조 분해 선언 등이 간편해집니다. 구조 분해 선언: 데이터 클래스는 구조 분해 선언을 통해 객체의 각 필드를 개별 변수로 쉽게 추출할 수 있습니다.(아이템 2) 불변성: 데이터를 불변 객체로 쉽게 만들 수 있어 데이터의 일관성을 유지하고, 예기치 않은 변경으로부터 보호할 수 있습니다. 아래는 data class 를 상속받아 사용하고싶은 니즈와, response 가 대부분 비슷한데, 한두개만 달랐을때 어떻게 사용하면 좋을까 라는 동료의 질문에 궁금해서 찾아본 코드들.. @GetMapping(“/response1”)
fun getResponse1(): Any {
val commonFields = CurationRecCommonFields(
recType = “Type1”,
iids = “Item IDs 1”,
cids = “Category IDs 1”,
exiids = “Excluded Item IDs 1”,
excids = “Excluded Category IDs 1”,
someCommonField = “Some common data”
)
val result = CurationRecApiResponseDto(common = commonFields, additionalField1 = “Additional data 1”)
return try {
return responseEntityOk(BAD_REQUEST, null, result, null)
} catch (e: Exception) {
logger.error(“getResponse1 error”, e)
return responseEntityOk(BAD_REQUEST, null, null, null)
}
}

@GetMapping(“/response2”)
fun getResponse2(): Any {
val commonFields = CurationRecCommonFields(
recType = “Type2”,
iids = “Item IDs 2”,
cids = “Category IDs 2”,
exiids = “Excluded Item IDs 2”,
excids = “Excluded Category IDs 2”,
someCommonField = “Some other common data”
)
val result = CurationRecApiResponseDto2(common = commonFields, aa = “Different field”)
return try {
return responseEntityOk(BAD_REQUEST, null, result, null)
} catch (e: Exception) {
logger.error(“getResponse2 error”, e)
return responseEntityOk(BAD_REQUEST, null, null, null)
}
}

data class CurationRecCommonFields(
var recType: String? = null,
var iids: String? = null,
var cids: String? = null,
var exiids: String? = null,
var excids: String? = null,
var someCommonField: String? = null // 임시로 추가한 필드
)

data class CurationRecApiResponseDto(
@JsonUnwrapped
val common: CurationRecCommonFields,
val additionalField1: String? = null
)

data class CurationRecApiResponseDto2(
@JsonUnwrapped
val common: CurationRecCommonFields,
val aa: String? = null
) 위 호출내역 // http://localhost:8080/discovery/api/v1/live-shop/response1

{
“status”: “SUCCESS”,
“code”: 200,
“message”: null,
“data”: {
“recType”: “Type1”,
“iids”: “Item IDs 1”,
“cids”: “Category IDs 1”,
“exiids”: “Excluded Item IDs 1”,
“excids”: “Excluded Category IDs 1”,
“someCommonField”: “Some common data”,
“additionalField1”: “Additional data 1”
},
“pagination”: null
}

—————

// http://localhost:8080/discovery/api/v1/live-shop/response2

{
“status”: “SUCCESS”,
“code”: 200,
“message”: null,
“data”: {
“recType”: “Type2”,
“iids”: “Item IDs 2”,
“cids”: “Category IDs 2”,
“exiids”: “Excluded Item IDs 2”,
“excids”: “Excluded Category IDs 2”,
“someCommonField”: “Some other common data”,
“aa”: “Different field”
},
“pagination”: null
} 좋은방법인지는 모르겠으나, 보일러 플레이트 코드가 너무 과하게 많을때, 단순하게 DTO 를 반환할대는 괜찮아보인다. 데이터 클래스의 문제점 엔티티 클래스로는 사용안하는 것을 권장 1:N 관계에서 순환 참조로 인해 StackOverFlow Error 발생할 수 있다. 해결 방안: toString, equals, hashCode를 오버라이드해서 순환 참조를 방지한다. 또는 한쪽에서 삭제한다. https://stackoverflow.com/questions/48926704/kotlin-data-class-entity-throws-stackoverflowerror copy의 얕은 복사로 인해 참조가 복사되어 문제가 발생할 수 있다. Hibernate의 Lazy Loading을 사용하기 위해서는 데이터 클래스를 사용할 수 없다. (프록시 객체 문제) https://velog.io/@donghyunele/kotlin-jpa-2-%EC%9A%B0%EC%95%84%ED%95%9C%ED%98%95%EC%A0%9C%EB%93%A4-%EA%B8%B0%EC%88%A0%EB%B8%94%EB%A1%9C%EA%B7%B8 자동으로 만들어주는 함수 때문에 용량이 크다. (App의 경우는 용량에 대한 이슈가 있을 수 있다.) 갓현호님b 감사감사!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다