본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item 27. 변화로부터 코드를 보호하려면 추상화를 사용하라

  • 추상화를 잘하면 세부 사항을 잘 알지 못하더라도 변경에 자유로워짐

상수

  • 리터럴을 상수 프로퍼티로 변경하게 되면 해당 값을 이름을 붙일 수 있고 값을 변경할때에 훨씬 쉽고 안전하게 변경이 가능하게됨

Before

fun isPasswordValid(text: String): Boolean {
    if (text.length < 7) return false
}

After

const val MIN_PASSWORD_LENGTH = 7

fun isPasswordValid(text: String): Boolean {
    if (text.length < MIN_PASSWORD_LENGTH) return false
}
  • 이런식으로 상수로 빼면 장점
    • 의미있는 이름 부여
    • 값 변경에 안전함

함수

  • 사용자에게 토스트 메시지를 자주 출력해야하는 상황이 발생하는 경우 아래와 같이 간단한 확장 함수를 만들어 사용할 수 있음

Before

fun Context.toast(
    message: String,
    duration: Int = Toast.LENGTH_LONG
) {
    Toast.makeText(this, message, duration).show()
}
  • 여기서 스낵바라는 다른 형태의 방식으로 출력해야하는 요구사항 발생
fun Context.snackbar(
    message: String,
    duration: Int = Snackbar.LENGTH_LONG
) {
   //
}
  • 위와 같이 스낵바를 출력하는 확장함수를 만들고 기존의 Context.toast를 Context.snackbar로 변경
  • 그러나 위와 같으 방법은 다른 모듈이 해당 함수를 의존하고 있을때 영향을 줄 수 있기 때문에 좋지 않음

After

fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG,
) {
    val toastDuration = when (duration) {
        MessageLength.SHORT -> Length.LENGTH_LONG
        MessageLength.LONG -> Length.LENGTH_LONG
    }
    
    Toast.makeText(this, message, toastDuration).show()
}
  • 위 요구사항의 경우 메시지 출력하고 싶다는 의도 자체가 중요하기 duration을 enum으로 관리 및 입력을 받고 함수명을 메시지 출력만 하는 역할로써 높은 레벨의 함수로 옮김

클래스

  • 클래스는 상태를 가질 수 있고 많은 함수를 가질 수 있기 때문에 함수보다 더욱 다양한 기능의 추상화 도구로 사용할 수 있음
class MessageDisplay(
    val context: Context
) {
    
    fun show(
        message: String,
        duration: MessageLength = MessageLength.LONG
    )  {
        val toastDuration = when (duration) {
            MessageLength.SHORT -> Length.LENGTH_LONG
            MessageLength.LONG -> Length.LENGTH_LONG
        }

        Toast.makeText(this.context, message, toastDuration).show()
    }
}

fun main() {
    val messageDisplay = MessageDisplay(Context())
    messageDisplay.show("message")
}
  • 의존성 주입 프레임 워크를 사용해 클래스 생성 위임할 수 있음
@Inject lateinit var messageDisplay: MessageDisplay
  • mock 객체를 활용해 해당 클래스에 의존하는 다른 클래스의 기능을 테스트 할 수 있음
val messageDisplay: MessageDisplay = mockk()
messageDisplay.setChristmasMode(true)
  • 하지만 더 많은 확장성을 얻기 위해서는 인터페이스를 활용하는 것이 좋음

인터페이스

  • 라이브러리의 경우 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 노출하는 코드를 많이 사용
  • 이렇게 하면 사용자가 클래스를 직접 사용하지 못해 라이브러리를 만드는 사람은 인터페이스만 유지하면 되기 때문에 사용자 측과의 결합을 줄일 수 있음
  • 참고로 코틀린은 멀티 플랫폼 언어라 환경에 따라 다른 리스트를 리턴 및 플랫폼에 최적화를 시키고 있음
interface MessageDisplay {
    fun show(
        message: String,
        duration: MessageLength = MessageLength.LONG
    )
}

class ToastDisplay(
    val context: Context,
): MessageDisplay {
    override fun show(message: String, duration: MessageLength) {
        val toastDuration = when (duration) {
            MessageLength.SHORT -> Length.LENGTH_LONG
            MessageLength.LONG -> Length.LENGTH_LONG
        }

        Toast.makeText(this.context, message, toastDuration).show()
    }
}
  • 인터페이스로 구성하면 해당 인터페이스의 의도만 제공하고 다양한 플랫폼에서 의도에 맞는 다양한 기능 직접 구현해 사용할 수 있음
  • 추가로 테스트시에 인터페이스 faking이 클래스 mocking보다 간단하기 때문에 별도의 mocking 라이브러리를 사용하지 않아도됨
  • 추가로 선언과 사용이 분리되어 있기 때문에 ToastDisplay 등의 실제 클래스를 자유롭게 변경할 수 있음

추상화가 주는 자유

  • 아래와 같이 추상화하는 방법을 통해 추상화를 진행하게 되면 변화에 대한 자유를 제공함
    • 상수로 추출
    • 동작을 함수로 래핑
    • 함수를 클래스로 래핑
    • 인터페이스 뒤에 클래스를 숨김
    • 보편적인 객체를 특수한 객체로 래핑

추상화의 문제

  • 모든 것을 추상화하게 되면 불필요하게 너무 많은 것을 숨기게 되고 간단한 의도나 동작을 이해하기 더욱 어려워질 수 있음
  • 그래서 팀의 크기나 도메인 복잡도 등을 고려해 적절한 추상화를 진행해야함
  • 적절한 균형을 찾는 것은 거의 예술에 가까운 것이기 때문에 많은 경험이 필요한 영역

정리

  • 추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것이 아니고 코드를 변경해야할때 자유를 주는 것
  • 하지만 너무 과한 추상화는 오히려 이해하기 힘든 구조로 변질될 수 있음