๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ
- 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; ์ ๋ค๋ฆญ์ค