Devlog/Kotlin

[Kotlin] 두 객체의 μ„œλ‘œ λ‹€λ₯Έ ν•„λ“œμ˜ κ°’ μ°ΎκΈ°

Jwhy 2024. 2. 7. 22:27

πŸ› οΈ 개발 ν™˜κ²½

  • kotlin : 1.8.22

πŸ’¬ 상황 μ„€λͺ…

νšŒμ‚¬μ—μ„œ κ°œλ°œμ„ ν•˜λ˜ 쀑, λ™μΌν•œ ν΄λž˜μŠ€μ— λŒ€ν•œ 두 μΈμŠ€ν„΄μŠ€μ˜ 값을 비ꡐ해 λ‹€λ₯Έ 뢀뢄을 μ°Ύμ•„ ν”„λ‘ νŠΈμ—μ„œ λ³΄μ—¬μ£ΌλŠ” 둜직이 ν•„μš”ν–ˆλ‹€.

enum class PostCategory(val title: String) {  
    C1("자유 κ²Œμ‹œνŒ"), C2("정보 κ²Œμ‹œνŒ")

    override fun toString(): String {  
        return title  
    }  
}

class Post() {  
    var title: String = ""  
    var content: String = ""  
    var isDeleted: Boolean = false
    var category: PostCategory = PostCategory.C1
    var writer: Member
}

예λ₯Ό λ“€μ–΄, μœ„μ™€ 같이 μžˆμ„ λ•Œ, 제λͺ©μ΄ λ³€κ²½λ˜μ—ˆλ‹€λ©΄ (제λͺ© : κΈ°μ‘΄ λ‚΄μš© -> 바뀐 λ‚΄μš©) 이런 μ‹μœΌλ‘œ λ³΄μ—¬μ€˜μ•Όν•˜λŠ” 것이닀. μš°μ„  νšŒμ‚¬μ—μ„œλŠ” κΈ‰ν•˜κ²Œ κ°œλ°œμ„ ν•˜λ‹€λ³΄λ‹ˆ λ‹€μŒκ³Ό 같이 κ΅¬ν˜„ν–ˆλ‹€.

/**
* λ³€ν™”λœ 뢀뢄을 μ°Ύμ•„ 리슀트둜 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜
*/
fun findChanges(
    origin: Post, 
    target: Post
): List<Triple<String, String, String>> {  
    val list = mutableListOf<Triple<String, String, String>>()  

    if (origin.title != target.title) {  
        list += Triple("제λͺ©", origin.title, target.title)  
    }  

    if (origin.content != target.content) {  
        list += Triple("λ‚΄μš©", origin.content, target.content)  
    }  

    if (origin.isDeleted != target.isDeleted) {  
        val t1 = if(origin.isDeleted) "μ‚­μ œλ¨" else "μ‚­μ œλ˜μ§€ μ•ŠμŒ"  
        val t2 = if(target.isDeleted) "μ‚­μ œλ¨" else "μ‚­μ œλ˜μ§€ μ•ŠμŒ"    
        list += Triple("μ‚­μ œ μ—¬λΆ€", t1, t2)  
    }

    if (origin.category != target.category) {  
        list += Triple("μΉ΄ν…Œκ³ λ¦¬", origin.category.title, target.category.title)  
    }

    return list  
}

μ˜ˆμ œμ—μ„œλŠ” ν•„λ“œκ°€ 4개인 κ°„λ‹¨ν•œ Postλ₯Ό μ˜ˆμ‹œλ‘œ μž‘μ„±ν–ˆμ§€λ§Œ, μ‹€μ œ μ½”λ“œμ—μ„œ λΉ„κ΅ν•œ ν•„λ“œλŠ” 8κ°€μ§€ 정도 λœλ‹€. μ΄λ ‡κ²Œ μž‘μ„±μ„ ν•˜λ‹€λ³΄λ‹ˆ λͺ¨λ“  ν•„λ“œλ₯Ό ν•˜λ‚˜ν•˜λ‚˜ 직접 λΉ„κ΅ν•΄μ•Όν•˜λŠ” λ²ˆκ±°λ‘œμ›€μ΄ μžˆμ—ˆκ³ , λΉ„κ΅ν•˜λŠ” κ³Όμ •μ—μ„œ μ‹€μˆ˜λ„ 일어날 수 μžˆμ–΄ μœ„ν—˜ν•œ μ½”λ“œλΌκ³  μƒκ°ν–ˆλ‹€.

βœ… ν•΄κ²° κ³Όμ •

사싀 ν˜„μž¬ ν΄λž˜μŠ€μ—μ„œλ§Œ 이런 κΈ°λŠ₯이 μ‘΄μž¬ν•œλ‹€λ©΄ μœ„ λ°©μ‹μœΌλ‘œ ꡬ성해도 크게 λ¬Έμ œκ°€ μ—†λ‹€. ν•˜μ§€λ§Œ λ‚˜λ¦„ κ°œλ°œμžλ‘œμ„œ λ‹€λ₯Έ κ³³μ—μ„œλ„ μ‚¬μš©ν•  수 μžˆλ„λ‘ λ§Œλ“€κ³  싢은 μš•μ‹¬μ΄ 생겼고, 더 효율적으둜 μ²˜λ¦¬ν•  수 μ—†μ„κΉŒμ— λŒ€ν•œ 고민을 ν•˜κ²Œ λ˜μ—ˆλ‹€.

1. μš”κ΅¬ 뢄석

ν˜„μž¬ λ‚΄κ°€ λ§Œλ“œλ €κ³  ν•˜λŠ” κΈ°λŠ₯이 무엇을 ν•„μš”λ‘œ ν•˜λŠ”μ§€ λΆ„μ„ν–ˆλ‹€.

  • λ³€κ²½λœ ν”„λ‘œνΌν‹°μ˜ λ ˆμ΄λΈ”
    • 예) title -> 제λͺ©
  • λ³€κ²½λœ ν”„λ‘œνΌν‹°μ˜ κ°’
    • 예) Boolean -> μ‚¬μš© 함 / μ‚¬μš©ν•˜μ§€ μ•ŠμŒ || μ‚­μ œλ¨ / μ‚­μ œλ˜μ§€ μ•ŠμŒ

μ΄κ²ƒλ§Œ 봐도 벌써 λ³΅μž‘ν•˜λ‹€. Boolean νƒ€μž…μ΄λ”λΌλ„ λ™μΌν•œ λ ˆμ΄λΈ”μ„ μ‚¬μš©ν•˜μ§€ λͺ»ν•˜κ³ , ν΄λΌμ΄μ–ΈνŠΈκ°€ μ›ν•˜λŠ” 값을 넣을 수 μžˆλ„λ‘ ν•΄μ•Ό ν•œλ‹€. λ˜ν•œ, 기획 μ˜λ„μ— μ˜ν•΄ λ³€κ²½λœ 값듀이 λ³΄μ—¬μ§ˆ μˆœμ„œλ„ 지정해쀄 수 μžˆμ–΄μ•Ό ν•œλ‹€.

2. ν‹€ λ§Œλ“€κΈ°

μš°μ„ , ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ μ•„λž˜ ν΄λž˜μŠ€μ— λŒ€ν•œ 리슀트λ₯Ό 보내쀀닀고 κ°€μ •ν•˜μž.

data class Item(  
    val order: Int,  
    val propertyName: String,  
    val label: String,  
    val v1: String = "",  
    val v2: String = ""  
)
val items = listOf(  
    Item(1, "title", "제λͺ©"),  
    Item(2, "writer", "μž‘μ„±μž", origin.writer.username, changed.writer.username), 
    Item(3, "category", "μΉ΄ν…Œκ³ λ¦¬"),  
    Item(4, "isDeleted", "μ‚­μ œμ—¬λΆ€", "μ‚­μ œλ¨", "μ‚­μ œλ˜μ§€ μ•ŠμŒ")
)

findChanges(items)

ν•΄λ‹Ή 정보λ₯Ό ν† λŒ€λ‘œ λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•΄ μ œλ„€λ¦­μœΌλ‘œ λ“€μ–΄μ˜¨ ν΄λž˜μŠ€μ— λŒ€ν•œ 정보λ₯Ό λ°›κ³ , ν•΄λ‹Ή ν΄λž˜μŠ€κ°€ κ°–κ³  μžˆλŠ” 각각의 ν•„λ“œλ₯Ό μˆœνšŒν•˜λ©°, ν΄λΌμ΄μ–ΈνŠΈκ°€ 보내쀀 ν•„λ“œμ™€ μΌμΉ˜ν•˜λŠ”μ§€ 비ꡐ할 것이닀.

이제 클래슀 내뢀에 λ‹€λ₯Έ 객체λ₯Ό μ°Έμ‘°ν•˜κ³  μžˆλŠ” κ²½μš°λ„ μžˆμœΌλ‹ˆ, μ •ν™•ν•œ 비ꡐλ₯Ό ν•  수 μžˆλ„λ‘ equals, hashCodeλ₯Ό κ΅¬ν˜„ν•˜κ²Œ κ°•μ œν•΄λ³΄μž.

interface Detectable {  
    override fun hashCode(): Int  
    override fun equals(other: Any?): Boolean  
}
class Post(  
    val title: String = "",  
    val content: String = "",  
    val isDeleted: Boolean = false,  
    val category: PostCategory = PostCategory.C1,  
    val writer: Member  
) : Detectable {

    override fun equals(other: Any?): Boolean {  
        ...
    }  

    override fun hashCode(): Int {  
        ...
    }  

}

μœ„μ™€ 같이 Detectable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λ„λ‘ ν•˜λ©΄, 내뢀에 λ‹€λ₯Έ 객체λ₯Ό μ°Έμ‘°ν•˜κ³  μžˆλ”λΌλ„ μ •ν™•ν•œ 비ꡐλ₯Ό ν•  수 있게 λœλ‹€.

3. κ΅¬ν˜„

μ•žμ„œ μ„€λͺ…ν•œ 것과 같이 λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•΄ μ œλ„€λ¦­μ— λŒ€ν•œ 클래슀λ₯Ό λ°›μ•„μ˜¬ 것이닀. 그러기 μœ„ν•΄μ„œλŠ” λŸ°νƒ€μž„μ—λ„ νƒ€μž… 정보가 μ‚΄μ•„μžˆμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— inline ν•¨μˆ˜μ™€ reifiedλ₯Ό μ‚¬μš©ν•  것이닀.

/**  
* 두 객체λ₯Ό 비ꡐ해 λ³€κ²½λœ 뢀뢄을 μ°ΎλŠ” ν•¨μˆ˜  
* @param origin 기쑴 객체  
* @param target 비ꡐ λŒ€μƒ 객체  
* @param itemList 변경을 감지할 μ•„μ΄ν…œ 리슀트  
* */  
inline fun <reified T : Detectable> findChanges(  
    origin: T,  
    target: T,  
    itemList: List<Item>  
): List<Item> { ... }

inline ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ©΄, ν•¨μˆ˜ ν˜ΈμΆœλΆ€κ°€ ν•¨μˆ˜ 본문으둜 λŒ€μΉ˜λœλ‹€. 덕뢄에 reifiedλ₯Ό μ‚¬μš©ν•΄ λŸ°νƒ€μž„μ—λ„ μ œλ„€λ¦­ νƒ€μž… 정보λ₯Ό κ°–κ³  μžˆμ„ 수 있게 λœλ‹€. λ§Œμ•½ 일반 ν•¨μˆ˜μ™€ reified ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ„ 경우, λ‹€μŒκ³Ό 같은 μ—λŸ¬κ°€ λ°œμƒν•œλ‹€.

Cannot use 'T' as reified type parameter. Use a class instead.

μ œλ„€λ¦­μ€ λŸ°νƒ€μž„μ— νƒ€μž… 정보가 μ†Œκ±°λ˜κΈ° λ•Œλ¬Έμ— νƒ€μž… 정보λ₯Ό 싀체화 ν•΄μ£ΌκΈ° μœ„ν•΄ reified ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€. ν•΄λ‹Ή ν‚€μ›Œλ“œλŠ” inline ν•¨μˆ˜μ—μ„œ μ‚¬μš©μ΄ κ°€λŠ₯ν•˜λ‹€. λ˜ν•œ, TλŠ” 기본적으둜 Any?λ₯Ό μƒν•œμœΌλ‘œ μ§€μ •ν•˜κΈ° λ•Œλ¬Έμ— Detectable을 κ΅¬ν˜„ν•œ κ΅¬ν˜„μ²΄λ§Œ λ“€μ–΄μ˜¬ 수 μžˆλ„λ‘ ν•œλ‹€.

Kotlin in action 8μž₯(κ³ μ°¨ ν•¨μˆ˜; νŒŒλΌλ―Έν„°μ™€ λ°˜ν™˜ κ°’μœΌλ‘œ λžŒλ‹€ μ‚¬μš©)에 λŒ€ν•œ λ‚΄μš©μ„ 보면, inline ν•¨μˆ˜λŠ” λžŒλ‹€μ™€ μ‚¬μš©ν•  λ•Œλ§Œ μ‚¬μš©ν•˜λŠ” 것을 μΆ”μ²œν•œλ‹€κ³  ν•œλ‹€. ν•˜μ§€λ§Œ 클래슀둜 λ§Œλ“€κΈ°μ—” μ• λ§€ν•˜λ‹ˆ μš°μ„  μ§„ν–‰ν•˜λ„λ‘ ν•˜κ² λ‹€.

이제 ...에 λŒ€ν•œ λ‚΄μš©μ„ κ΅¬ν˜„ν•΄λ³΄μž.

// μ‹€μ²΄ν™”λœ T νƒ€μž…μ˜ 클래슀 정보 λ°›μ•„μ˜€κΈ°
val clazz = T::class // Type: KClass<T>

// κ²°κ³Ό 리슀트λ₯Ό 담을 Item 리슀트
val result = mutableListOf<Item>()

for (property in clazz.memberProperties) {  
    val propertyName = property.name
    itemList.find { it.propertyName == propertyName }?.let {  
        ...
    }  
}

μœ„μ™€ 같이 clazzκ°€ κ°–κ³ μžˆλŠ” 멀버 ν”„λ‘œνΌν‹°λ“€μ„ 가져와 iteratorλ₯Ό 돌렀주고, ν•΄λ‹Ή ν”„λ‘œνΌν‹°μ˜ 이름이 ν΄λΌμ΄μ–ΈνŠΈκ°€ 보내쀀 itemList에 ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ 검사해쀀닀. λ§Œμ•½ 포함이 λ˜μ–΄μžˆμ„ 경우 값에 λŒ€ν•œ 비ꡐλ₯Ό μ§„ν–‰ν•΄μ£ΌκΈ°λ§Œ ν•˜λ©΄ λλ‚œλ‹€.

// κΈ°μ‘΄ 객체의 κ°’
val originValue = property.get(origin).toString()  
// νƒ€κ²Ÿ 객체의 κ°’
val targetValue = property.get(target).toString()  

// 두 값이 λ‹€λ₯Έμ§€ 비ꡐ
if (originValue != targetValue) {  
    // item 객체의 v1이 λΉ„μ–΄μžˆμ„ 경우 originValueλ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©
    val v1 = it.v1.ifEmpty { originValue }  
    // item 객체의 v2κ°€ λΉ„μ–΄μžˆμ„ 경우 targetValueλ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©
    val v2 = it.v2.ifEmpty { targetValue }  

    // κ²°κ³Ό λ¦¬μŠ€νŠΈμ— μΆ”κ°€
    result += Item(it.order, it.propertyName, it.label, v1, v2)  
}

κΈ°μ‘΄μ—λŠ” λ‚΄κ°€ λͺ¨λ“  ν•„λ“œλ₯Ό 비ꡐ해야 ν–ˆμ§€λ§Œ, μ΄μ œλŠ” λ‹€μŒκ³Ό 같이 κ°„λ‹¨ν•˜κ²Œ 리슀트만 λ§Œλ“€μ–΄μ„œ ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜κΈ°λ§Œ ν•˜λ©΄ 두 객체의 값을 μ‰½κ²Œ 비ꡐ할 수 μžˆλ‹€.

val items = listOf(  
   Item(1, "title", "제λͺ©"),  
   Item(2, "writer", "μž‘μ„±μž", origin.writer.username, changed.writer.username),
   Item(3, "category", "μΉ΄ν…Œκ³ λ¦¬"),  
   Item(4, "isDeleted", "μ‚­μ œμ—¬λΆ€", "μ‚­μ œλ¨", "μ‚­μ œλ˜μ§€ μ•ŠμŒ")  
)

전체 μ½”λ“œμ™€ κ²°κ³Ό 값은 μ•„λž˜μ™€ κ°™λ‹€.

/**  
 * 두 객체λ₯Ό 비ꡐ해 λ³€κ²½λœ 뢀뢄을 μ°ΎλŠ” ν•¨μˆ˜  
 * @param origin 기쑴 객체  
 * @param target 비ꡐ λŒ€μƒ 객체  
 * @param itemList 변경을 감지할 μ•„μ΄ν…œ 리슀트  
* */  
inline fun <reified T : Detectable> findChanges(  
    origin: T,  
    target: T,  
    itemList: List<Item>  
): List<Item> {  
    val clazz = T::class  

    val result = mutableListOf<Item>()  

    for (property in clazz.memberProperties) {  
        val propertyName = property.name  

        itemList.find { it.propertyName == propertyName }?.let {  
            val originValue = property.get(origin).toString()  
            val targetValue = property.get(target).toString()  

            if (originValue != targetValue) {  
            val v1 = it.v1.ifEmpty { originValue }  
            val v2 = it.v2.ifEmpty { targetValue }  

            result += Item(it.order, it.propertyName, it.label, v1, v2)  
            }  
        }  
    }  
    return result.sortedBy { it.order }  
}
// κ²°κ³Ό (title은 값이 λ™μΌν•˜κΈ° λ•Œλ¬Έμ— κ²°κ³Ό λ¦¬μŠ€νŠΈμ— μΆ”κ°€λ˜μ§€ μ•ŠμŒ)
[Item(order=2, field=category, label=μΉ΄ν…Œκ³ λ¦¬, v1=자유 κ²Œμ‹œνŒ, v2=정보 κ²Œμ‹œνŒ),
Item(order=3, field=isDeleted, label=μ‚­μ œμ—¬λΆ€, v1=μ‚­μ œλ¨, v2=μ‚­μ œλ˜μ§€ μ•ŠμŒ),
Item(order=4, field=writer, label=μž‘μ„±μž, v1=admin, v2=tester)]

πŸ€” 회고

코틀린을 μ‹œμž‘ν•œμ§€ 2달 정도인지라 많이 λ―Έμˆ™ν•˜λ‹€. 사싀 μ²˜μŒμ— λΉ„κ΅ν•œ 방식은 O(N) 만큼의 λ³΅μž‘λ„λ₯Ό κ°€μ§€μ§€λ§Œ, λ³€κ²½ν•œ μ½”λ“œλŠ” O(N^2) 만큼의 λ³΅μž‘λ„λ₯Ό κ°€μ§„λ‹€. λ•Œλ¬Έμ— μ²˜μŒμ— λΉ„κ΅ν•œ 방식이 μ„±λŠ₯적으둜 더 μ’‹μ§€λ§Œ, ν•˜λ‚˜ν•˜λ‚˜ 비ꡐλ₯Ό ν•œλ‹€λŠ” 것은 λΆˆνŽΈν•˜κΈ° λ•Œλ¬Έμ— μ΄μ •λ„μ˜ μ„±λŠ₯ μ°¨μ΄λŠ” κ°μˆ˜ν•΄μ•Όν•  것 κ°™λ‹€. 이보닀 더 효율적으둜 λ§Œλ“€ 수 μžˆμ„μ§€ 더 고민해보고, λ¦¬νŒ©ν„°λ§ν•˜λŠ” μ‹œκ°„μ„ κ°€μ Έμ•Όκ² λ‹€!

레퍼런슀

  • Kotlin In Action - Chapter08; κ³ μ°¨ ν•¨μˆ˜; νŒŒλΌλ―Έν„°μ™€ λ°˜ν™˜ κ°’μœΌλ‘œ λžŒλ‹€ μ‚¬μš©
  • Kotlin In Action - Chapter09; μ œλ„€λ¦­μŠ€