Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PoC] Abstract Domain Model 정의 #448

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.time.Instant
@Entity
class ClosedPlaceCandidate(
@Id
val id: String,
override val id: String,

@Column(nullable = false)
val placeId: String,
Expand Down Expand Up @@ -40,22 +40,4 @@ class ClosedPlaceCandidate(
fun ignore() {
ignoredAt = SccClock.instant()
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ClosedPlaceCandidate

return id == other.id
}

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

override fun toString(): String {
return "ClosedPlaceCandidate(id='$id', placeId='$placeId', externalId='$externalId', " +
"acceptedAt='$acceptedAt', ignoredAt='$ignoredAt' createdAt=$createdAt, updatedAt=$updatedAt)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import org.locationtech.jts.geom.Point
@Entity
class Place private constructor(
@Id
val id: String,
override val id: String,
val name: String,
@AttributeOverrides(
AttributeOverride(name = "lng", column = Column(name = "location_x")),
Expand Down Expand Up @@ -61,24 +61,6 @@ class Place private constructor(
return this.locationForQuery == null && maybeUpdated.locationForQuery != null
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Place

return id == other.id
}

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

override fun toString(): String {
return "Place(id='$id', name='$name', location=$location, building=$building, siGunGuId=$siGunGuId, " +
"eupMyeonDongId=$eupMyeonDongId, category=$category, isClosed=$isClosed, isNotAccessible=$isNotAccessible)"
}

companion object {
private val geometryFactory = GeometryFactory()
fun of(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package club.staircrusher.stdlib.persistence

import org.hibernate.Hibernate
import org.hibernate.proxy.HibernateProxy
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

abstract class AbstractDomainModel {
abstract val id: String

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as AbstractDomainModel

return this.id == other.id
}

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


override fun toString(): String {
val properties = this::class.memberProperties
.filter { it.name != "id" }
.mapNotNull { prop ->
try {
prop.isAccessible = true
val value = prop.getter.call(this)

// Exclude uninitialized Hibernate proxy collections
if (!isInitialized(value)) return@mapNotNull null

val displayValue = if (prop.findAnnotation<Sensitive>() != null || prop.name in sensitiveFields) {
"<redacted>"
} else {
value.toString()
}

"${prop.name}='$displayValue'"
} catch (t: Throwable) {
null
}
}

return "${this::class.simpleName}(id='$id', ${properties.joinToString(", ")})"
}

private fun isInitialized(value: Any?): Boolean {
return when (value) {
null -> true
is Collection<*> -> Hibernate.isInitialized(value)
is HibernateProxy -> !value.hibernateLazyInitializer.isUninitialized
else -> true
}
}

companion object {
private val sensitiveFields = setOf("password", "secretKey", "apiKey", "token")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package club.staircrusher.stdlib.persistence

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Sensitive
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package club.staircrusher.stdlib.persistence.jpa

import club.staircrusher.stdlib.persistence.AbstractDomainModel
import jakarta.persistence.Column
import jakarta.persistence.MappedSuperclass
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.time.Instant

@MappedSuperclass
class TimeAuditingBaseEntity {
abstract class TimeAuditingBaseEntity : AbstractDomainModel() {
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
lateinit var createdAt: Instant
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package club.staircrusher.stdlib.persistence

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class AbstractDomainModelUT {
@Test
fun `정의된 필드가 toString 메소드에 포함된다`() {
val some = SomeDomainModel(
id = "id",
name = "name",
password = "password",
age = 10,
someSensitiveField = "sensitive",
isClosed = false
)

val str = some.toString()
assertEquals(true, str.contains("id="))
assertEquals(true, str.contains("name="))
assertEquals(true, str.contains("password="))
assertEquals(true, str.contains("age="))
assertEquals(true, str.contains("someSensitiveField="))
}

@Test
fun `확실한 민감정보는 toString 에서 마스킹된다`() {
val some = SomeDomainModel(
id = "id",
name = "name",
password = "password",
age = 10,
someSensitiveField = "sensitive",
isClosed = false,
)

val str = some.toString()
assertEquals(true, str.contains("password='<redacted>'"))
}

@Test
fun `Sensitive 어노테이션이 붙은 필드는 toString 에서 마스킹된다`() {
val some = SomeDomainModel(
id = "id",
name = "name",
password = "password",
age = 10,
someSensitiveField = "sensitive",
isClosed = false,
)

val str = some.toString()
assertEquals(true, str.contains("someSensitiveField='<redacted>'"))
}

@Test
fun `contructor 에 정의되지 않은 필드도 toString 에 포함된다`() {
val some = SomeDomainModel(
id = "id",
name = "name",
password = "password",
age = 10,
someSensitiveField = "sensitive",
isClosed = false
)

val str = some.toString()
assertEquals(true, str.contains("isClosed='false'"))
}

class SomeDomainModel(
override val id: String,
val name: String,
val password: String,
val age: Int,
@Sensitive
val someSensitiveField: String,
isClosed: Boolean
) : AbstractDomainModel() {
var isClosed: Boolean = isClosed
private set
}
}