
안녕하세요.
회사에서 진행한 프로젝트에 적용했던, ISP, LSP와 관련된 사례를 정리해보겠습니다.
ISP & LSP
ISP (Interface Segregation Principle)
인터페이스는 클라이언트에 맞게 분리하라
- 하나의 커다란 인터페이스 보다는, 역할에 맞는 작은 인터페이스 여러 개로 나누는 것이 좋다
LSP (Liskov Substitution Principle)
하위 타입은 상위 타입을 대체할 수 있어야 한다
- 자식 클래스는 부모 클래스를 대체할 수 있어야 한다
- 상위 클래스: Bird // 하위 클래스: 날지 못하는 새 → LSP 위반
LSP 위반
유관 부서에서 기능 추가를 요청했습니다
- JsonTimeUpdate
- SharedPreferenceTimeUpdate
이와 관련된 클래스가 아래와 같다고 가정해보겠습니다
open class Event {
open fun trigger() {
println("Event triggered")
}
}
class A : Event()
class B : Event()
class C : Event()
class D : Event()
class E : Event()
그리고, 클래스마다 필요한 기능이 아래와 같다고 가정해보겠습니다
- JsonTimeUpdate - Class A, B, C, D 사용
- SharedPreferenceTimeUpdate - Class A, B 사용
방안 1)
먼저 떠오른 방안은 Super Class인 Event Class에 기능을 선언하고,
불필요한 하위 클래스에는 구현을 지우자 라는 것 이었습니다
open class Event {
open fun trigger() {
println("Event triggered")
}
open fun jsonTimeUpdate() {
println("JsonTimeUpdate 실행됨")
}
open fun sharedPreferenceTimeUpdate() {
println("SharedPreferenceTimeUpdate 실행됨")
}
}
class A : Event()
class B : Event()
class C : Event() {
override fun sharedPreferenceTimeUpdate() {
// C에서는 SharedPreferenceTimeUpdate가 필요 없지만 강제로 구현됨
}
}
class D : Event() {
override fun sharedPreferenceTimeUpdate() {
// D에서는 SharedPreferenceTimeUpdate가 필요 없지만 강제로 구현됨
}
}
class E : Event() {
override fun jsonTimeUpdate() {
// E에서는 JsonTimeUpdate가 필요 없지만 강제로 구현됨
}
override fun sharedPreferenceTimeUpdate() {
// E에서는 SharedPreferenceTimeUpdate가 필요 없지만 강제로 구현됨
}
}
이는 LSP 위반 입니다
부모 클래스인 Event 클래스가 할 수 있는 일을 자식 클래스가 하지 못합니다
→ 하위 클래스가 상위 클래스를 대체할 수 없다
인터페이스 분리 적용
먼저, 각 기능을 인터페이스로 선언해보겠습니다
- JsonTimeUpdate → JsonTimeUpdatable
- SharedPreferenceTimeUpdate → SharedPreferenceTimeUpdatable
interface JsonTimeUpdatable {
fun updateJsonTime() {
println("[$timestamp] JsonTimeUpdate 실행됨")
}
val timestamp: LocalDateTime
get() = LocalDateTime.now()
}
interface SharedPreferenceTimeUpdatable {
fun updateSharedPreferenceTime(context: Context) {
val sharedPref = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
sharedPref.edit().putLong("last_update", System.currentTimeMillis()).apply()
}
}
이렇게 선언한 인터페이스를 필요한 클래스에만 붙여주면 됩니다
open class Event {
open fun trigger() {
println("Event triggered")
}
}
class A : Event(), JsonTimeUpdatable, SharedPreferenceTimeUpdatable
class B : Event(), JsonTimeUpdatable, SharedPreferenceTimeUpdatable
class C : Event(), JsonTimeUpdatable
class D : Event(), JsonTimeUpdatable
class E : Event()
But) 인터페이스가 구체 클래스에 의존하는 문제가 발생합니다
- JsonTimeUpdatable 인터페이스가 LocalDateTime 클래스에 의존
- SharedPreferenceTimeUpdatable 인터페이스가 Context에 의존
- 고수준이 저수준에 의존 → 의존성 역전 원칙 위배
Composition 사용
각 인터페이스를 상속 받는 Concrete Class를 만듭니다.
이 class를 각 Event Class들에 주입해서 문제를 해결할 수 있습니다.
→ 상속 관계보다 덜 의존적이게 되며, 외부에서 주입 가능하여 테스트가 용이합니다.
interface JsonDateUpdatable {
fun updateJsonTime()
}
class JsonTimeUpdater : JsonDateUpdatable {
fun updateJsonTime() {
println("[$timestamp] JsonTimeUpdate 실행됨")
}
val timestamp: LocalDateTime
get() = LocalDateTime.now()
}
interface SharedPreferenceTimeUpdatable {
fun updateSharedPreferenceTime(context: Context)
}
class SharedPreferenceDateUpdater() : SharedPreferenceTimeUpdatable {
override fun updateSharedPreferenceTime(context: Context) {
val sharedPref = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
sharedPref.edit().putLong("last_update", System.currentTimeMillis()).apply()
}
}
이렇게 만들어진 Concrete class를 필요한 클래스에 주입해줍니다.
class A(
private val jsonDateUpdatable: JsonDateUpdatable,
private val sharedPreferenceTimeUpdatable: SharedPreferenceTimeUpdatable
) : Event(),
JsonDateUpdatable,
SharedPreferenceTimeUpdatable {
override fun updateJsonTime() {
jsonDateUpdatable.updateJsonTime()
}
override fun updateSharedPreferenceTime(context: Context) {
sharedPreferenceTimeUpdatable.updateSharedPreferenceTime(context)
}
}
class B(
private val jsonDateUpdatable: JsonDateUpdatable,
private val sharedPreferenceTimeUpdatable: SharedPreferenceTimeUpdatable
) : Event(),
JsonDateUpdatable,
SharedPreferenceTimeUpdatable {
override fun updateJsonTime() {
jsonDateUpdatable.updateJsonTime()
}
override fun updateSharedPreferenceTime(context: Context) {
sharedPreferenceTimeUpdatable.updateSharedPreferenceTime(context)
}
}
class C(
private val jsonDateUpdatable: JsonDateUpdatable
) : Event(),
JsonDateUpdatable {
override fun updateJsonTime() {
jsonDateUpdatable.updateJsonTime()
}
}
class D(
private val jsonDateUpdatable: JsonDateUpdatable
) : Event(),
JsonDateUpdatable {
override fun updateJsonTime() {
jsonDateUpdatable.updateJsonTime()
}
}
class E : Event()
하지만 오히려 보일러 플레이트 코드가 늘어남에 따라 더 복잡해졌습니다
이를 더 줄일 수 있는 방법이 있습니다
by를 통한 위임 사용
by 키워드를 사용하면 이를 줄일 수 있습니다
다른 클래스로 구현을 위임하는 기능입니다
https://kdr06006.tistory.com/63
[Kotlin] 위임 (Delegation)
안녕하세요. 오늘은 위임 (Delegation) 에 대해 알아보겠습니다. 다른 부서의 코드를 참고하다가 아래와 같은 코드를 봤습니다.class UserRepository(userDao: UserDao) : UserDao by userDao 오늘은 이 코드를 이해
kdr06006.tistory.com
class A(
private val jsonDateUpdatable: JsonDateUpdatable,
private val sharedPreferenceTimeUpdatable: SharedPreferenceTimeUpdatable
) : Event(),
JsonDateUpdatable by jsonDateUpdatable,
SharedPreferenceTimeUpdatable by sharedPreferenceTimeUpdatable
class B(
private val jsonDateUpdatable: JsonDateUpdatable,
private val sharedPreferenceTimeUpdatable: SharedPreferenceTimeUpdatable
) : Event(),
JsonDateUpdatable by jsonDateUpdatable,
SharedPreferenceTimeUpdatable by sharedPreferenceTimeUpdatable
class C(
private val jsonDateUpdatable: JsonDateUpdatable
) : Event(),
JsonDateUpdatable by jsonDateUpdatable
class D(
private val jsonDateUpdatable: JsonDateUpdatable
) : Event(),
JsonDateUpdatable by jsonDateUpdatable
class E : Event()
+)
위 예제에서는 TimeProvider 인터페이스를 사용해 JsonTimeUpdatable에 제공해주면 됩니다
- 실제 프로젝트에서는 아직 Clean Architecture 도입 초기 단계라, 의존하는 클래스가 이보다 더 많이 있어서 해당 방법으로 적용하진 못했습니다
interface TimeProvider {
fun getCurrentTime(): String
}
class DefaultTimeProvider : TimeProvider {
override fun getCurrentTime(): String {
return LocalDateTime.now().toString()
}
}
interface JsonTimeUpdatable {
fun jsonTimeUpdate(timeProvider: TimeProvider) {
println("[${timeProvider.getCurrentTime()}] JsonTimeUpdate 실행됨")
}
}
마무리
해당 방법은 분명한 trade-off가 존재합니다
클래스에 필요한 기능이 많아지면
→ 클래스에 주입되는 클래스, 인터페이스가 너무 많아집니다
→ 이에 따라 클래스 선언부가 비대해집니다
하지만 이런한 클래스 기능이 많다는 것은
→ 높은 확률로 클래스가 가지는 책임이 많다는 뜻입니다
→ SRP에 충족하게끔 클래스 분리가 필요하며, 이를 통해 선언부가 많이 가벼워질 것이라 생각합니다
감사합니다.