본문 바로가기
공부/이펙티브코틀린

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

by 띵커베르 2024. 7. 14.
728x90
  • 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를 오버라이드해서 순환 참조를 방지한다. 또는 한쪽에서 삭제한다.
  • Hibernate의 Lazy Loading을 사용하기 위해서는 데이터 클래스를 사용할 수 없다. (프록시 객체 문제)

 

 

갓현호님b 감사감사! 

 
728x90

댓글