본문 바로가기

Architecture

[Architecture] ISP, LSP 적용하기

powered by pixabay on www.picjumbo.com

 

안녕하세요.

회사에서 진행한 프로젝트에 적용했던, 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에 충족하게끔 클래스 분리가 필요하며, 이를 통해 선언부가 많이 가벼워질 것이라 생각합니다

 

감사합니다.